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