1 package org.opentrafficsim.graphs;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.event.ActionEvent;
6 import java.awt.event.ActionListener;
7 import java.io.Serializable;
8 import java.rmi.RemoteException;
9 import java.text.NumberFormat;
10 import java.text.ParseException;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Locale;
14 import java.util.Set;
15
16 import javax.swing.ButtonGroup;
17 import javax.swing.JFrame;
18 import javax.swing.JLabel;
19 import javax.swing.JMenu;
20 import javax.swing.JPopupMenu;
21 import javax.swing.JRadioButtonMenuItem;
22 import javax.swing.SwingConstants;
23
24 import org.djunits.unit.LengthUnit;
25 import org.djunits.unit.TimeUnit;
26 import org.djunits.value.StorageType;
27 import org.djunits.value.ValueException;
28 import org.djunits.value.vdouble.scalar.DoubleScalarInterface;
29 import org.djunits.value.vdouble.scalar.Length;
30 import org.djunits.value.vdouble.scalar.Time;
31 import org.djunits.value.vdouble.vector.LengthVector;
32 import org.jfree.chart.ChartPanel;
33 import org.jfree.chart.JFreeChart;
34 import org.jfree.chart.LegendItem;
35 import org.jfree.chart.LegendItemCollection;
36 import org.jfree.chart.axis.NumberAxis;
37 import org.jfree.chart.event.PlotChangeEvent;
38 import org.jfree.chart.plot.XYPlot;
39 import org.jfree.chart.renderer.xy.XYBlockRenderer;
40 import org.jfree.data.DomainOrder;
41 import org.jfree.data.general.DatasetChangeEvent;
42 import org.jfree.data.general.DatasetChangeListener;
43 import org.jfree.data.general.DatasetGroup;
44 import org.jfree.data.xy.XYZDataset;
45 import org.opentrafficsim.core.gtu.GTUException;
46 import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
47 import org.opentrafficsim.road.network.lane.Lane;
48 import org.opentrafficsim.simulationengine.OTSSimulationException;
49
50 import nl.tudelft.simulation.event.EventInterface;
51 import nl.tudelft.simulation.event.EventListenerInterface;
52 import nl.tudelft.simulation.event.TimedEvent;
53
54
55
56
57
58
59
60
61
62
63
64
65 public abstract class ContourPlot extends AbstractOTSPlot
66 implements ActionListener, XYZDataset, MultipleViewerChart, LaneBasedGTUSampler, EventListenerInterface, Serializable
67 {
68
69 private static final long serialVersionUID = 20140716L;
70
71
72 private final ContinuousColorPaintScale paintScale;
73
74
75 @SuppressWarnings("visibilitymodifier")
76 protected final Axis xAxis;
77
78
79 @SuppressWarnings("visibilitymodifier")
80 protected final Axis yAxis;
81
82
83 private final double legendStep;
84
85
86 private final String legendFormat;
87
88
89 protected static final double[] STANDARDTIMEGRANULARITIES = { 1, 2, 5, 10, 20, 30, 60, 120, 300, 600 };
90
91
92 protected static final int STANDARDINITIALTIMEGRANULARITYINDEX = 3;
93
94
95 protected static final double[] STANDARDDISTANCEGRANULARITIES = { 10, 20, 50, 100, 200, 500, 1000 };
96
97
98 protected static final int STANDARDINITIALDISTANCEGRANULARITYINDEX = 3;
99
100
101 protected static final Time INITIALLOWERTIMEBOUND = new Time(0, TimeUnit.BASE);
102
103
104 protected static final Time INITIALUPPERTIMEBOUND = new Time(300, TimeUnit.BASE);
105
106
107 private final LengthVector cumulativeLengths;
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122 public ContourPlot(final String caption, final Axis xAxis, final List<Lane> path, final double redValue,
123 final double yellowValue, final double greenValue, final String valueFormat, final String legendFormat,
124 final double legendStep) throws OTSSimulationException
125 {
126 super(caption, path);
127 double[] endLengths = new double[path.size()];
128 double cumulativeLength = 0;
129 LengthVector lengths = null;
130 for (int i = 0; i < path.size(); i++)
131 {
132 Lane lane = path.get(i);
133 lane.addListener(this, Lane.GTU_ADD_EVENT, true);
134 lane.addListener(this, Lane.GTU_REMOVE_EVENT, true);
135 try
136 {
137
138 for (LaneBasedGTU gtu : lane.getGtuList())
139 {
140 notify(new TimedEvent<Time>(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu },
141 gtu.getSimulator().getSimulatorTime()));
142 }
143 }
144 catch (RemoteException exception)
145 {
146 exception.printStackTrace();
147 }
148 cumulativeLength += lane.getLength().getSI();
149 endLengths[i] = cumulativeLength;
150 }
151 try
152 {
153 lengths = new LengthVector(endLengths, LengthUnit.SI, StorageType.DENSE);
154 }
155 catch (ValueException exception)
156 {
157 exception.printStackTrace();
158 }
159 this.cumulativeLengths = lengths;
160 this.xAxis = xAxis;
161 this.yAxis = new Axis(new Length(0, LengthUnit.METER), getCumulativeLength(-1), STANDARDDISTANCEGRANULARITIES,
162 STANDARDDISTANCEGRANULARITIES[STANDARDINITIALDISTANCEGRANULARITYINDEX], "", "Distance", "%.0fm");
163 this.legendStep = legendStep;
164 this.legendFormat = legendFormat;
165 extendXRange(xAxis.getMaximumValue());
166 double[] boundaries = { redValue, yellowValue, greenValue };
167 final Color[] colorValues = { Color.RED, Color.YELLOW, Color.GREEN };
168 this.paintScale = new ContinuousColorPaintScale(valueFormat, boundaries, colorValues);
169 setChart(createChart(this));
170 reGraph();
171 }
172
173
174 private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();
175
176
177 @Override
178 @SuppressWarnings("checkstyle:designforextension")
179 public void notify(final EventInterface event) throws RemoteException
180 {
181 if (event.getType().equals(Lane.GTU_ADD_EVENT))
182 {
183 Object[] content = (Object[]) event.getContent();
184 LaneBasedGTU gtu = (LaneBasedGTU) content[1];
185 if (!this.gtusOfInterest.contains(gtu))
186 {
187
188 this.gtusOfInterest.add(gtu);
189 gtu.addListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
190 }
191 }
192 else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
193 {
194 Object[] content = (Object[]) event.getContent();
195 LaneBasedGTU gtu = (LaneBasedGTU) content[1];
196
197
198
199
200
201
202 Lane lane = null;
203 try
204 {
205 lane = gtu.getReferencePosition().getLane();
206 }
207 catch (GTUException exception)
208 {
209
210 }
211 if (lane == null || !getPath().contains(lane))
212 {
213 this.gtusOfInterest.remove(gtu);
214 gtu.removeListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
215 }
216 }
217 else if (event.getType().equals(LaneBasedGTU.LANEBASED_MOVE_EVENT))
218 {
219 Object[] content = (Object[]) event.getContent();
220 Lane lane = (Lane) content[6];
221 LaneBasedGTU gtu = (LaneBasedGTU) event.getSource();
222
223 addData(gtu, lane);
224 }
225 }
226
227
228
229
230
231
232 public final Length getCumulativeLength(final int index)
233 {
234 int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index;
235 try
236 {
237 return new Length(this.cumulativeLengths.get(useIndex));
238 }
239 catch (ValueException exception)
240 {
241 exception.printStackTrace();
242 }
243 return null;
244 }
245
246
247
248
249
250
251
252
253
254
255 private JMenu buildMenu(final String menuName, final String format, final String commandPrefix, final double[] values,
256 final double currentValue)
257 {
258 final JMenu result = new JMenu(menuName);
259
260 final ButtonGroup group = new ButtonGroup();
261 for (double value : values)
262 {
263 final JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, value));
264 item.setSelected(value == currentValue);
265 item.setActionCommand(commandPrefix + String.format(Locale.US, " %f", value));
266 item.addActionListener(this);
267 result.add(item);
268 group.add(item);
269 }
270 return result;
271 }
272
273
274 @Override
275 protected final JFreeChart createChart(final JFrame container)
276 {
277 final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
278 container.add(statusLabel, BorderLayout.SOUTH);
279 final NumberAxis xAxis1 = new NumberAxis("\u2192 " + "time [s]");
280 xAxis1.setLowerMargin(0.0);
281 xAxis1.setUpperMargin(0.0);
282 final NumberAxis yAxis1 = new NumberAxis("\u2192 " + "Distance [m]");
283 yAxis1.setAutoRangeIncludesZero(false);
284 yAxis1.setLowerMargin(0.0);
285 yAxis1.setUpperMargin(0.0);
286 yAxis1.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
287 XYBlockRenderer renderer = new XYBlockRenderer();
288 renderer.setPaintScale(this.paintScale);
289 final XYPlot plot = new XYPlot(this, xAxis1, yAxis1, renderer);
290 final LegendItemCollection legend = new LegendItemCollection();
291 for (int i = 0;; i++)
292 {
293 double value = this.paintScale.getLowerBound() + i * this.legendStep;
294 if (value > this.paintScale.getUpperBound())
295 {
296 break;
297 }
298 legend.add(new LegendItem(String.format(this.legendFormat, value), this.paintScale.getPaint(value)));
299 }
300 legend.add(new LegendItem("No data", Color.BLACK));
301 plot.setFixedLegendItems(legend);
302 plot.setBackgroundPaint(Color.lightGray);
303 plot.setDomainGridlinePaint(Color.white);
304 plot.setRangeGridlinePaint(Color.white);
305 final JFreeChart chart = new JFreeChart(getCaption(), plot);
306 FixCaption.fixCaption(chart);
307 chart.setBackgroundPaint(Color.white);
308 final ChartPanel cp = new ChartPanel(chart);
309 final PointerHandler ph = new PointerHandler()
310 {
311
312 @Override
313 void updateHint(final double domainValue, final double rangeValue)
314 {
315 if (Double.isNaN(domainValue))
316 {
317 statusLabel.setText(" ");
318 return;
319 }
320
321 XYZDataset dataset = (XYZDataset) plot.getDataset();
322 String value = "";
323 double roundedTime = domainValue;
324 double roundedDistance = rangeValue;
325 for (int item = dataset.getItemCount(0); --item >= 0;)
326 {
327 double x = dataset.getXValue(0, item);
328 if (x + ContourPlot.this.xAxis.getCurrentGranularity() / 2 < domainValue
329 || x - ContourPlot.this.xAxis.getCurrentGranularity() / 2 >= domainValue)
330 {
331 continue;
332 }
333 double y = dataset.getYValue(0, item);
334 if (y + ContourPlot.this.yAxis.getCurrentGranularity() / 2 < rangeValue
335 || y - ContourPlot.this.yAxis.getCurrentGranularity() / 2 >= rangeValue)
336 {
337 continue;
338 }
339 roundedTime = x;
340 roundedDistance = y;
341 double valueUnderMouse = dataset.getZValue(0, item);
342
343 if (Double.isNaN(valueUnderMouse))
344 {
345 break;
346 }
347 String format = ((ContinuousColorPaintScale) (((XYBlockRenderer) (plot.getRenderer(0))).getPaintScale()))
348 .getFormat();
349 value = String.format(format, valueUnderMouse);
350 }
351 statusLabel.setText(String.format("time %.0fs, distance %.0fm, %s", roundedTime, roundedDistance, value));
352 }
353
354 };
355 cp.addMouseMotionListener(ph);
356 cp.addMouseListener(ph);
357 container.add(cp, BorderLayout.CENTER);
358 cp.setMouseWheelEnabled(true);
359 JPopupMenu popupMenu = cp.getPopupMenu();
360 popupMenu.add(new JPopupMenu.Separator());
361 popupMenu.add(StandAloneChartWindow.createMenuItem(this));
362 popupMenu.insert(buildMenu("Distance granularity", "%.0f m", "setDistanceGranularity", this.yAxis.getGranularities(),
363 this.yAxis.getCurrentGranularity()), 0);
364 popupMenu.insert(buildMenu("Time granularity", "%.0f s", "setTimeGranularity", this.xAxis.getGranularities(),
365 this.xAxis.getCurrentGranularity()), 1);
366 return chart;
367 }
368
369
370 @Override
371 public final void actionPerformed(final ActionEvent actionEvent)
372 {
373 final String command = actionEvent.getActionCommand();
374
375 String[] fields = command.split("[ ]");
376 if (fields.length == 2)
377 {
378 final NumberFormat nf = NumberFormat.getInstance(Locale.US);
379 double value;
380 try
381 {
382 value = nf.parse(fields[1]).doubleValue();
383 }
384 catch (ParseException e)
385 {
386 throw new RuntimeException("Bad value: " + fields[1]);
387 }
388 if (fields[0].equalsIgnoreCase("setDistanceGranularity"))
389 {
390 this.getYAxis().setCurrentGranularity(value);
391 clearCachedValues();
392 }
393 else if (fields[0].equalsIgnoreCase("setTimeGranularity"))
394 {
395 this.getXAxis().setCurrentGranularity(value);
396 clearCachedValues();
397 }
398 else
399 {
400 throw new RuntimeException("Unknown ActionEvent");
401 }
402 reGraph();
403 }
404 else
405 {
406 throw new RuntimeException("Unknown ActionEvent: " + command);
407 }
408 }
409
410
411
412
413 @Override
414 public final void reGraph()
415 {
416 for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
417 {
418 if (dcl instanceof XYPlot)
419 {
420 final XYPlot plot = (XYPlot) dcl;
421 final XYBlockRenderer blockRenderer = (XYBlockRenderer) plot.getRenderer();
422 blockRenderer.setBlockHeight(this.getYAxis().getCurrentGranularity());
423 blockRenderer.setBlockWidth(this.getXAxis().getCurrentGranularity());
424 plot.notifyListeners(new PlotChangeEvent(plot));
425
426 }
427 }
428 notifyListeners(new DatasetChangeEvent(this, null));
429 }
430
431
432 @Override
433 public final int getSeriesCount()
434 {
435 return 1;
436 }
437
438
439 private int cachedYAxisBins = -1;
440
441
442
443
444
445 protected final int yAxisBins()
446 {
447 if (this.cachedYAxisBins >= 0)
448 {
449 return this.cachedYAxisBins;
450 }
451 this.cachedYAxisBins = this.getYAxis().getAggregatedBinCount();
452 return this.cachedYAxisBins;
453 }
454
455
456
457
458
459
460
461 protected final int yAxisBin(final int item)
462 {
463 int maxItem = getItemCount(0);
464 if (item < 0 || item >= maxItem)
465 {
466 throw new RuntimeException("yAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
467 }
468 return item % yAxisBins();
469 }
470
471
472
473
474
475
476
477 protected final int xAxisBin(final int item)
478 {
479 int maxItem = getItemCount(0);
480 if (item < 0 || item >= maxItem)
481 {
482 throw new RuntimeException("xAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
483 }
484 return item / yAxisBins();
485 }
486
487
488 private int cachedXAxisBins = -1;
489
490
491
492
493
494 protected final int xAxisBins()
495 {
496 if (this.cachedXAxisBins >= 0)
497 {
498 return this.cachedXAxisBins;
499 }
500 this.cachedXAxisBins = this.getXAxis().getAggregatedBinCount();
501 return this.cachedXAxisBins;
502 }
503
504
505 private int cachedItemCount = -1;
506
507
508 @Override
509 public final int getItemCount(final int series)
510 {
511 if (this.cachedItemCount >= 0)
512 {
513 return this.cachedItemCount;
514 }
515 this.cachedItemCount = yAxisBins() * xAxisBins();
516 return this.cachedItemCount;
517 }
518
519
520 @Override
521 public final Number getX(final int series, final int item)
522 {
523 return getXValue(series, item);
524 }
525
526
527 @Override
528 public final double getXValue(final int series, final int item)
529 {
530 double result = this.getXAxis().getValue(xAxisBin(item));
531
532
533 return result;
534 }
535
536
537 @Override
538 public final Number getY(final int series, final int item)
539 {
540 return getYValue(series, item);
541 }
542
543
544 @Override
545 public final double getYValue(final int series, final int item)
546 {
547 return this.getYAxis().getValue(yAxisBin(item));
548 }
549
550
551 @Override
552 public final Number getZ(final int series, final int item)
553 {
554 return getZValue(series, item);
555 }
556
557
558 @Override
559 public final DatasetGroup getGroup()
560 {
561 return null;
562 }
563
564
565 @Override
566 public void setGroup(final DatasetGroup group)
567 {
568
569 }
570
571
572 @SuppressWarnings("rawtypes")
573 @Override
574 public final int indexOf(final Comparable seriesKey)
575 {
576 return 0;
577 }
578
579
580 @Override
581 public final DomainOrder getDomainOrder()
582 {
583 return DomainOrder.ASCENDING;
584 }
585
586
587
588
589 private void clearCachedValues()
590 {
591 this.cachedItemCount = -1;
592 this.cachedXAxisBins = -1;
593 this.cachedYAxisBins = -1;
594 }
595
596
597
598
599
600
601 protected final void addData(final LaneBasedGTU gtu, final Lane lane)
602 {
603
604
605
606 double lengthOffset = 0;
607 int index = getPath().indexOf(lane);
608 if (index >= 0)
609 {
610 if (index > 0)
611 {
612 try
613 {
614 lengthOffset = this.cumulativeLengths.getSI(index - 1);
615 }
616 catch (ValueException exception)
617 {
618
619 System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
620 + exception.getMessage());
621 }
622 }
623 }
624 else
625 {
626
627 return;
628
629
630 }
631
632 try
633 {
634 final Time fromTime = gtu.getOperationalPlan().getStartTime();
635 if (gtu.position(lane, gtu.getRear(), fromTime).getSI() < 0 && lengthOffset > 0)
636 {
637 return;
638 }
639 final Time toTime = gtu.getOperationalPlan().getEndTime();
640 if (toTime.getSI() > this.getXAxis().getMaximumValue().getSI())
641 {
642 extendXRange(toTime);
643 clearCachedValues();
644 this.getXAxis().adjustMaximumValue(toTime);
645 }
646 if (toTime.le(fromTime))
647 {
648 return;
649 }
650
651 final double relativeFromDistance = (gtu.position(lane, gtu.getRear(), fromTime).getSI() + lengthOffset)
652 / this.getYAxis().getGranularities()[0];
653 final double relativeToDistance =
654 (gtu.position(lane, gtu.getRear(), toTime).getSI() + lengthOffset) / this.getYAxis().getGranularities()[0];
655 double relativeFromTime =
656 (fromTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
657 final double relativeToTime =
658 (toTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
659 final int fromTimeBin = (int) Math.floor(relativeFromTime);
660 final int toTimeBin = (int) Math.floor(relativeToTime) + 1;
661 double relativeMeanSpeed = (relativeToDistance - relativeFromDistance) / (relativeToTime - relativeFromTime);
662
663
664 double acceleration = gtu.getAcceleration().getSI();
665 for (int timeBin = fromTimeBin; timeBin < toTimeBin; timeBin++)
666 {
667 if (timeBin < 0)
668 {
669 continue;
670 }
671 double binEndTime = timeBin + 1;
672 if (binEndTime > relativeToTime)
673 {
674 binEndTime = relativeToTime;
675 }
676 if (binEndTime <= relativeFromTime)
677 {
678 continue;
679 }
680 double binDistanceStart = (gtu
681 .position(lane, gtu.getRear(),
682 new Time(relativeFromTime * this.getXAxis().getGranularities()[0], TimeUnit.BASE))
683 .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
684 / this.getYAxis().getGranularities()[0];
685 double binDistanceEnd = (gtu
686 .position(lane, gtu.getRear(),
687 new Time(binEndTime * this.getXAxis().getGranularities()[0], TimeUnit.BASE))
688 .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
689 / this.getYAxis().getGranularities()[0];
690
691
692 for (int distanceBin = (int) Math.floor(binDistanceStart); distanceBin <= binDistanceEnd; distanceBin++)
693 {
694 double relativeDuration = 1;
695 if (relativeFromTime > timeBin)
696 {
697 relativeDuration -= relativeFromTime - timeBin;
698 }
699 if (distanceBin == (int) Math.floor(binDistanceEnd))
700 {
701
702 if (binEndTime < timeBin + 1)
703 {
704 relativeDuration -= timeBin + 1 - binEndTime;
705 }
706 }
707 else
708 {
709
710
711
712 double timeToBinBoundary = (distanceBin + 1 - binDistanceStart) / relativeMeanSpeed;
713 double endTime = relativeFromTime + timeToBinBoundary;
714 relativeDuration -= timeBin + 1 - endTime;
715 }
716 final double duration = relativeDuration * this.getXAxis().getGranularities()[0];
717 final double distance = duration * relativeMeanSpeed * this.getYAxis().getGranularities()[0];
718
719
720
721
722 incrementBinData(timeBin, distanceBin, duration, distance, acceleration);
723 relativeFromTime += relativeDuration;
724 binDistanceStart = distanceBin + 1;
725 }
726 relativeFromTime = timeBin + 1;
727 }
728 }
729 catch (GTUException exception)
730 {
731
732 System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
733 + exception.getMessage());
734 }
735 }
736
737
738
739
740
741
742 public abstract void extendXRange(DoubleScalarInterface newUpperLimit);
743
744
745
746
747
748
749
750
751
752 public abstract void incrementBinData(int timeBin, int distanceBin, double duration, double distanceCovered,
753 double acceleration);
754
755
756 @Override
757 public final double getZValue(final int series, final int item)
758 {
759 final int timeBinGroup = xAxisBin(item);
760 final int distanceBinGroup = yAxisBin(item);
761
762
763 final int timeGroupSize = (int) (this.getXAxis().getCurrentGranularity() / this.getXAxis().getGranularities()[0]);
764 final int firstTimeBin = timeBinGroup * timeGroupSize;
765 final int distanceGroupSize = (int) (this.getYAxis().getCurrentGranularity() / this.getYAxis().getGranularities()[0]);
766 final int firstDistanceBin = distanceBinGroup * distanceGroupSize;
767 final int endTimeBin = Math.min(firstTimeBin + timeGroupSize, this.getXAxis().getBinCount());
768 final int endDistanceBin = Math.min(firstDistanceBin + distanceGroupSize, this.getYAxis().getBinCount());
769 return computeZValue(firstTimeBin, endTimeBin, firstDistanceBin, endDistanceBin);
770 }
771
772
773
774
775
776
777
778
779
780 public abstract double computeZValue(int firstTimeBin, int endTimeBin, int firstDistanceBin, int endDistanceBin);
781
782
783
784
785
786 public final Axis getXAxis()
787 {
788 return this.xAxis;
789 }
790
791
792
793
794
795 public final Axis getYAxis()
796 {
797 return this.yAxis;
798 }
799
800 }