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