1 package org.opentrafficsim.road.network.lane.object.sensor;
2
3 import java.awt.Color;
4 import java.io.BufferedWriter;
5 import java.io.IOException;
6 import java.util.ArrayList;
7 import java.util.Comparator;
8 import java.util.LinkedHashMap;
9 import java.util.LinkedHashSet;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Set;
13 import java.util.TreeSet;
14
15 import org.djunits.unit.FrequencyUnit;
16 import org.djunits.unit.SpeedUnit;
17 import org.djunits.value.vdouble.scalar.Duration;
18 import org.djunits.value.vdouble.scalar.Frequency;
19 import org.djunits.value.vdouble.scalar.Length;
20 import org.djunits.value.vdouble.scalar.Speed;
21 import org.djunits.value.vdouble.scalar.Time;
22 import org.opentrafficsim.base.CompressedFileWriter;
23 import org.opentrafficsim.core.compatibility.Compatible;
24 import org.opentrafficsim.core.gtu.GTUDirectionality;
25 import org.opentrafficsim.core.gtu.GTUType;
26 import org.opentrafficsim.core.gtu.RelativePosition;
27 import org.opentrafficsim.core.gtu.Try;
28 import org.opentrafficsim.core.network.NetworkException;
29 import org.opentrafficsim.core.network.OTSNetwork;
30 import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
31 import org.opentrafficsim.road.network.animation.SensorAnimation;
32 import org.opentrafficsim.road.network.lane.CrossSectionElement;
33 import org.opentrafficsim.road.network.lane.Lane;
34
35 import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
36 import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
37 import nl.tudelft.simulation.event.EventType;
38 import nl.tudelft.simulation.language.Throw;
39
40
41
42
43
44
45
46
47
48
49
50
51 public class Detector extends AbstractSensor
52 {
53
54
55 private static final long serialVersionUID = 20180312L;
56
57
58 public static final EventType DETECTOR_TRIGGERED = new EventType("DUAL_LOOP_DETECTOR.TRIGGER");
59
60
61 public static final EventType DETECTOR_AGGREGATE = new EventType("DUAL_LOOP_DETECTOR.AGGREGATE");
62
63
64 private static Compatible compatible = new Compatible()
65 {
66
67 @Override
68 public boolean isCompatible(final GTUType gtuType, final GTUDirectionality directionality)
69 {
70 return gtuType.isOfType(GTUType.VEHICLE);
71 }
72 };
73
74
75 public static final DetectorMeasurement<Double, Speed> MEAN_SPEED = new DetectorMeasurement<Double, Speed>()
76 {
77 @Override
78 public Double identity()
79 {
80 return 0.0;
81 }
82
83 @Override
84 public Double accumulateEntry(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
85 {
86 return cumulative + gtu.getSpeed().si;
87 }
88
89 @Override
90 public Double accumulateExit(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
91 {
92 return cumulative;
93 }
94
95 @Override
96 public boolean isPeriodic()
97 {
98 return true;
99 }
100
101 @Override
102 public Speed aggregate(final Double cumulative, final int count, final Duration aggregation)
103 {
104 return Speed.createSI(cumulative / count);
105 }
106
107 @Override
108 public String getName()
109 {
110 return "v[km/h]";
111 }
112
113 @Override
114 public String stringValue(final Speed aggregate, final String format)
115 {
116 return String.format(format, aggregate.getInUnit(SpeedUnit.KM_PER_HOUR));
117 }
118
119 @Override
120 public String toString()
121 {
122 return getName();
123 }
124 };
125
126
127 public static final DetectorMeasurement<Double, Speed> HARMONIC_MEAN_SPEED = new DetectorMeasurement<Double, Speed>()
128 {
129 @Override
130 public Double identity()
131 {
132 return 0.0;
133 }
134
135 @Override
136 public Double accumulateEntry(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
137 {
138 return cumulative + (1.0 / gtu.getSpeed().si);
139 }
140
141 @Override
142 public Double accumulateExit(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
143 {
144 return cumulative;
145 }
146
147 @Override
148 public boolean isPeriodic()
149 {
150 return true;
151 }
152
153 @Override
154 public Speed aggregate(final Double cumulative, final int count, final Duration aggregation)
155 {
156 return Speed.createSI(count / cumulative);
157 }
158
159 @Override
160 public String getName()
161 {
162 return "vHarm[km/h]";
163 }
164
165 @Override
166 public String stringValue(final Speed aggregate, final String format)
167 {
168 return String.format(format, aggregate.getInUnit(SpeedUnit.KM_PER_HOUR));
169 }
170
171 @Override
172 public String toString()
173 {
174 return getName();
175 }
176 };
177
178
179 public static final DetectorMeasurement<Double, Double> OCCUPANCY = new DetectorMeasurement<Double, Double>()
180 {
181 @Override
182 public Double identity()
183 {
184 return 0.0;
185 }
186
187 @Override
188 public Double accumulateEntry(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
189 {
190 return cumulative + ((gtu.getLength().si + loopDetector.getLength().si) / gtu.getSpeed().si);
191 }
192
193 @Override
194 public Double accumulateExit(final Double cumulative, final LaneBasedGTU gtu, final Detector loopDetector)
195 {
196 return cumulative;
197 }
198
199 @Override
200 public boolean isPeriodic()
201 {
202 return true;
203 }
204
205 @Override
206 public Double aggregate(final Double cumulative, final int count, final Duration aggregation)
207 {
208 return cumulative / aggregation.si;
209 }
210
211 @Override
212 public String getName()
213 {
214 return "occupancy";
215 }
216
217 @Override
218 public String stringValue(final Double aggregate, final String format)
219 {
220 return String.format(format, aggregate);
221 }
222
223 @Override
224 public String toString()
225 {
226 return getName();
227 }
228 };
229
230
231 public static final DetectorMeasurement<List<Double>, List<Double>> PASSAGES =
232 new DetectorMeasurement<List<Double>, List<Double>>()
233 {
234 @Override
235 public List<Double> identity()
236 {
237 return new ArrayList<>();
238 }
239
240 @Override
241 public List<Double> accumulateEntry(final List<Double> cumulative, final LaneBasedGTU gtu,
242 final Detector loopDetector)
243 {
244 cumulative.add(gtu.getSimulator().getSimulatorTime().si);
245 return cumulative;
246 }
247
248 @Override
249 public List<Double> accumulateExit(final List<Double> cumulative, final LaneBasedGTU gtu,
250 final Detector loopDetector)
251 {
252 return cumulative;
253 }
254
255 @Override
256 public boolean isPeriodic()
257 {
258 return false;
259 }
260
261 @Override
262 public List<Double> aggregate(final List<Double> cumulative, final int count, final Duration aggregation)
263 {
264 return cumulative;
265 }
266
267 @Override
268 public String getName()
269 {
270 return "passage times";
271 }
272
273 @Override
274 public String stringValue(final List<Double> aggregate, final String format)
275 {
276 return printListDouble(aggregate, format);
277 }
278
279 @Override
280 public String toString()
281 {
282 return getName();
283 }
284 };
285
286
287 private final Duration aggregation;
288
289
290 private final List<Frequency> count = new ArrayList<>();
291
292
293 private final Map<DetectorMeasurement<?, ?>, List<?>> dataMap = new LinkedHashMap<>();
294
295
296 private final Length length;
297
298
299 private int period = 1;
300
301
302 private int periodCount = 0;
303
304
305 private int overallCount = 0;
306
307
308 private final Map<DetectorMeasurement<?, ?>, Object> cumulDataMap = new LinkedHashMap<>();
309
310
311
312
313
314
315
316
317
318 public Detector(final String id, final Lane lane, final Length longitudinalPosition,
319 final DEVSSimulatorInterface.TimeDoubleUnit simulator) throws NetworkException
320 {
321
322 this(id, lane, longitudinalPosition, Length.ZERO, simulator, Duration.createSI(60.0), MEAN_SPEED);
323 }
324
325
326
327
328
329
330
331
332
333
334
335
336 public Detector(final String id, final Lane lane, final Length longitudinalPosition, final Length length,
337 final DEVSSimulatorInterface.TimeDoubleUnit simulator, final Duration aggregation,
338 final DetectorMeasurement<?, ?>... measurements) throws NetworkException
339 {
340 super(id, lane, longitudinalPosition, RelativePosition.FRONT, simulator, compatible);
341 Throw.when(aggregation.si <= 0.0, IllegalArgumentException.class, "Aggregation time should be positive.");
342 this.length = length;
343 this.aggregation = aggregation;
344 Try.execute(() -> simulator.scheduleEventAbs(Time.createSI(aggregation.si), this, this, "aggregate", null), "");
345 for (DetectorMeasurement<?, ?> measurement : measurements)
346 {
347 this.cumulDataMap.put(measurement, measurement.identity());
348 if (measurement.isPeriodic())
349 {
350 this.dataMap.put(measurement, new ArrayList<>());
351 }
352 }
353
354 Try.execute(() -> new SensorAnimation(this, longitudinalPosition, simulator, Color.BLUE), "Unable to create animation");
355
356
357
358 class RearDetector extends AbstractSensor
359 {
360
361 private static final long serialVersionUID = 20180315L;
362
363
364
365
366
367
368
369
370
371 @SuppressWarnings("synthetic-access")
372 RearDetector(final String idRear, final Lane laneRear, final Length longitudinalPositionRear,
373 final DEVSSimulatorInterface.TimeDoubleUnit simulatorRear) throws NetworkException
374 {
375 super(idRear, laneRear, longitudinalPositionRear, RelativePosition.REAR, simulatorRear, compatible);
376 }
377
378
379 @SuppressWarnings("synthetic-access")
380 @Override
381 protected void triggerResponse(final LaneBasedGTU gtu)
382 {
383 for (DetectorMeasurement<?, ?> measurement : Detector.this.cumulDataMap.keySet())
384 {
385 accumulate(measurement, gtu, false);
386 }
387 }
388
389
390 @Override
391 public AbstractSensor clone(final CrossSectionElement newCSE, final SimulatorInterface.TimeDoubleUnit newSimulator,
392 final boolean animation) throws NetworkException
393 {
394 return null;
395 }
396 }
397 Length position = longitudinalPosition.plus(length);
398 Throw.when(position.gt(lane.getLength()), IllegalStateException.class,
399 "A Detector can not be placed at a lane boundary");
400 new RearDetector(id + "_rear", lane, position, simulator);
401 }
402
403
404
405
406
407 public Length getLength()
408 {
409 return this.length;
410 }
411
412
413 @Override
414 protected void triggerResponse(final LaneBasedGTU gtu)
415 {
416 this.periodCount++;
417 this.overallCount++;
418 for (DetectorMeasurement<?, ?> measurement : this.cumulDataMap.keySet())
419 {
420 accumulate(measurement, gtu, true);
421 }
422 if (this.listeners.containsKey(DETECTOR_TRIGGERED))
423 {
424 this.fireTimedEvent(DETECTOR_TRIGGERED, new Object[] { gtu }, getSimulator().getSimulatorTime());
425 }
426 }
427
428
429
430
431
432
433
434
435 @SuppressWarnings("unchecked")
436 <C> void accumulate(final DetectorMeasurement<C, ?> measurement, final LaneBasedGTU gtu, final boolean front)
437 {
438 if (front)
439 {
440 this.cumulDataMap.put(measurement, measurement.accumulateEntry((C) this.cumulDataMap.get(measurement), gtu, this));
441 }
442 else
443 {
444 this.cumulDataMap.put(measurement, measurement.accumulateExit((C) this.cumulDataMap.get(measurement), gtu, this));
445 }
446 }
447
448
449
450
451 @SuppressWarnings("unused")
452 private void aggregate()
453 {
454 Frequency frequency = Frequency.createSI(this.periodCount / this.aggregation.si);
455 this.count.add(frequency);
456 for (DetectorMeasurement<?, ?> measurement : this.dataMap.keySet())
457 {
458 aggregate(measurement, this.periodCount, this.aggregation);
459 this.cumulDataMap.put(measurement, measurement.identity());
460 }
461 this.periodCount = 0;
462 if (this.listeners.containsKey(DETECTOR_AGGREGATE))
463 {
464 Object[] data = new Object[this.dataMap.size() + 1];
465 data[0] = frequency;
466 int i = 1;
467 for (DetectorMeasurement<?, ?> measurement : this.dataMap.keySet())
468 {
469 List<?> list = this.dataMap.get(measurement);
470 data[i] = list.get(list.size() - 1);
471 i++;
472 }
473 this.fireTimedEvent(DETECTOR_AGGREGATE, data, getSimulator().getSimulatorTime());
474 }
475 this.period++;
476 double t = this.aggregation.si * this.period;
477 Time time = Time.createSI(t);
478 Try.execute(() -> getSimulator().scheduleEventAbs(time, this, this, "aggregate", null), "");
479 }
480
481
482
483
484
485
486
487
488
489 @SuppressWarnings("unchecked")
490 private <C, A> void aggregate(final DetectorMeasurement<C, A> measurement, final int cnt, final Duration agg)
491 {
492 ((List<A>) this.dataMap.get(measurement)).add(getAggregateValue(measurement, cnt, agg));
493 }
494
495
496
497
498
499
500
501
502
503
504 @SuppressWarnings("unchecked")
505 private <C, A> A getAggregateValue(final DetectorMeasurement<C, A> measurement, final int cnt, final Duration agg)
506 {
507 return measurement.aggregate((C) this.cumulDataMap.get(measurement), cnt, agg);
508 }
509
510
511
512
513
514 public Map<DetectorMeasurement<?, ?>, Object> getMesoMeasurements()
515 {
516 Map<DetectorMeasurement<?, ?>, Object> map = new LinkedHashMap<>();
517 for (DetectorMeasurement<?, ?> measurement : this.cumulDataMap.keySet())
518 {
519 if (!measurement.isPeriodic())
520 {
521 map.put(measurement, getAggregateValue(measurement, this.overallCount,
522 this.getSimulator().getSimulatorTime().minus(Time.ZERO)));
523 }
524 }
525 return map;
526 }
527
528
529 @Override
530 public AbstractSensor clone(final CrossSectionElement newCSE, final SimulatorInterface.TimeDoubleUnit newSimulator,
531 final boolean animation) throws NetworkException
532 {
533 return null;
534 }
535
536
537
538
539
540
541
542 public static final void writeToFile(final OTSNetwork network, final String file, final boolean periodic)
543 {
544 writeToFile(network, file, periodic, "%.3f", CompressionMethod.ZIP);
545 }
546
547
548
549
550
551
552
553
554
555
556 @SuppressWarnings("unchecked")
557 public static final <C> void writeToFile(final OTSNetwork network, final String file, final boolean periodic,
558 final String format, final CompressionMethod compression)
559 {
560 BufferedWriter bw = CompressedFileWriter.create(file, compression.equals(CompressionMethod.ZIP));
561 try
562 {
563
564 Set<DetectorMeasurement<?, ?>> measurements = new LinkedHashSet<>();
565 Set<Detector> detectors = new TreeSet<>(new Comparator<Detector>()
566 {
567 @Override
568 public int compare(final Detector o1, final Detector o2)
569 {
570 return o1.getId().compareTo(o2.getId());
571 }
572 });
573 for (Detector detector : network.getObjectMap(Detector.class).values())
574 {
575 detectors.add(detector);
576 }
577 for (Detector detector : detectors)
578 {
579 for (DetectorMeasurement<?, ?> measurement : detector.cumulDataMap.keySet())
580 {
581 if (measurement.isPeriodic() == periodic)
582 {
583 measurements.add(measurement);
584 }
585 }
586 }
587
588 StringBuilder str = periodic ? new StringBuilder("id,t[s],q[veh/h]") : new StringBuilder("id,measurement,data");
589 if (periodic)
590 {
591 for (DetectorMeasurement<?, ?> measurement : measurements)
592 {
593 str.append(",");
594 str.append(measurement.getName());
595 }
596 }
597 bw.write(str.toString());
598 bw.newLine();
599
600 for (Detector detector : detectors)
601 {
602 String id = detector.getId();
603
604 if (!periodic)
605 {
606 Map<DetectorMeasurement<?, ?>, Object> map = detector.getMesoMeasurements();
607 for (DetectorMeasurement<?, ?> measurement : measurements)
608 {
609 if (map.containsKey(measurement))
610 {
611 bw.write(id + "," + measurement.getName() + ","
612 + ((DetectorMeasurement<?, C>) measurement).stringValue((C) map.get(measurement), format));
613 bw.newLine();
614 }
615 }
616 }
617 else
618 {
619
620 double t = 0.0;
621 for (int i = 0; i < detector.count.size(); i++)
622 {
623 str = new StringBuilder(id + "," + removeTrailingZeros(String.format(format, t)) + ",");
624 str.append(removeTrailingZeros(
625 String.format(format, detector.count.get(i).getInUnit(FrequencyUnit.PER_HOUR))));
626 for (DetectorMeasurement<?, ?> measurement : measurements)
627 {
628 str.append(",");
629 List<?> list = detector.dataMap.get(measurement);
630 if (list != null)
631 {
632 str.append(removeTrailingZeros(
633 ((DetectorMeasurement<?, C>) measurement).stringValue((C) list.get(i), format)));
634 }
635 }
636 bw.write(str.toString());
637 bw.newLine();
638 t += detector.aggregation.si;
639 }
640 }
641 }
642 }
643 catch (IOException exception)
644 {
645 throw new RuntimeException("Could not write to file.", exception);
646 }
647
648 finally
649 {
650 try
651 {
652 if (bw != null)
653 {
654 bw.close();
655 }
656 }
657 catch (IOException ex)
658 {
659 ex.printStackTrace();
660 }
661 }
662 }
663
664
665
666
667
668
669 public static final String removeTrailingZeros(final String string)
670 {
671 return string.replaceFirst("\\.0*$|(\\.\\d*?)0+$", "$1");
672 }
673
674
675
676
677
678
679
680 public static final String printListDouble(final List<Double> list, final String format)
681 {
682 StringBuilder str = new StringBuilder("[");
683 String sep = "";
684 for (double t : list)
685 {
686 str.append(sep);
687 str.append(removeTrailingZeros(String.format(format, t)));
688 sep = ", ";
689 }
690 str.append("]");
691 return str.toString();
692 }
693
694
695
696
697
698
699
700
701
702
703
704
705
706 public enum CompressionMethod
707 {
708
709 NONE,
710
711
712 ZIP,
713 }
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729 public interface DetectorMeasurement<C, A>
730 {
731
732
733
734
735 C identity();
736
737
738
739
740
741
742
743
744 C accumulateEntry(C cumulative, LaneBasedGTU gtu, Detector loopDetector);
745
746
747
748
749
750
751
752
753
754 C accumulateExit(C cumulative, LaneBasedGTU gtu, Detector loopDetector);
755
756
757
758
759
760 boolean isPeriodic();
761
762
763
764
765
766
767
768
769 A aggregate(C cumulative, int count, Duration aggregation);
770
771
772
773
774
775 String getName();
776
777
778
779
780
781
782
783 String stringValue(A aggregate, String format);
784 }
785
786
787
788
789
790
791
792
793
794
795
796
797
798 public static class PlatoonSizes implements DetectorMeasurement<PlatoonMeasurement, List<Integer>>
799 {
800
801
802 private final Duration threshold;
803
804
805
806
807
808 public PlatoonSizes(final Duration threshold)
809 {
810 this.threshold = threshold;
811 }
812
813
814 @Override
815 public PlatoonMeasurement identity()
816 {
817 return new PlatoonMeasurement();
818 }
819
820
821 @SuppressWarnings("synthetic-access")
822 @Override
823 public PlatoonMeasurement accumulateEntry(final PlatoonMeasurement cumulative, final LaneBasedGTU gtu,
824 final Detector loopDetector)
825 {
826 Time now = gtu.getSimulator().getSimulatorTime();
827 if (now.si - cumulative.lastExitTime.si < this.threshold.si)
828 {
829 cumulative.count++;
830 }
831 else
832 {
833 if (cumulative.count > 0)
834 {
835 cumulative.platoons.add(cumulative.count);
836 }
837 cumulative.count = 1;
838 }
839 cumulative.enteredGTUs.add(gtu);
840 cumulative.lastExitTime = now;
841 return cumulative;
842 }
843
844
845 @SuppressWarnings("synthetic-access")
846 @Override
847 public PlatoonMeasurement accumulateExit(final PlatoonMeasurement cumulative, final LaneBasedGTU gtu,
848 final Detector loopDetector)
849 {
850 int index = cumulative.enteredGTUs.indexOf(gtu);
851 if (index >= 0)
852 {
853 cumulative.lastExitTime = gtu.getSimulator().getSimulatorTime();
854
855
856 cumulative.enteredGTUs.subList(0, index).clear();
857 }
858 return cumulative;
859 }
860
861
862 @Override
863 public boolean isPeriodic()
864 {
865 return false;
866 }
867
868
869 @SuppressWarnings("synthetic-access")
870 @Override
871 public List<Integer> aggregate(final PlatoonMeasurement cumulative, final int count, final Duration aggregation)
872 {
873 if (cumulative.count > 0)
874 {
875 cumulative.platoons.add(cumulative.count);
876 cumulative.count = 0;
877 }
878 return cumulative.platoons;
879 }
880
881
882 @Override
883 public String getName()
884 {
885 return "platoon sizes";
886 }
887
888
889 @Override
890 public String stringValue(final List<Integer> aggregate, final String format)
891 {
892 return aggregate.toString();
893 }
894
895
896 @Override
897 public String toString()
898 {
899 return getName();
900 }
901
902 }
903
904
905
906
907
908
909
910
911
912
913
914
915
916 static class PlatoonMeasurement
917 {
918
919 private int count = 0;
920
921
922 private Time lastExitTime = Time.createSI(Double.NEGATIVE_INFINITY);
923
924
925 private List<Integer> platoons = new ArrayList<>();
926
927
928 private List<LaneBasedGTU> enteredGTUs = new ArrayList<>();
929 }
930
931 }