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