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