View Javadoc
1   package org.opentrafficsim.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.Paint;
6   import java.awt.event.ActionEvent;
7   import java.awt.geom.Line2D;
8   import java.util.ArrayList;
9   import java.util.List;
10  
11  import javax.swing.JFrame;
12  import javax.swing.JLabel;
13  import javax.swing.JPopupMenu;
14  import javax.swing.SwingConstants;
15  import javax.swing.SwingUtilities;
16  
17  import org.djunits.unit.LengthUnit;
18  import org.djunits.unit.TimeUnit;
19  import org.djunits.value.vdouble.scalar.DoubleScalar;
20  import org.djunits.value.vdouble.scalar.Duration;
21  import org.djunits.value.vdouble.scalar.Frequency;
22  import org.djunits.value.vdouble.scalar.Length;
23  import org.djunits.value.vdouble.scalar.Time;
24  import org.jfree.chart.ChartFactory;
25  import org.jfree.chart.ChartPanel;
26  import org.jfree.chart.JFreeChart;
27  import org.jfree.chart.StandardChartTheme;
28  import org.jfree.chart.axis.NumberAxis;
29  import org.jfree.chart.axis.ValueAxis;
30  import org.jfree.chart.plot.PlotOrientation;
31  import org.jfree.chart.plot.XYPlot;
32  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
33  import org.jfree.data.DomainOrder;
34  import org.jfree.data.general.DatasetChangeEvent;
35  import org.jfree.data.general.DatasetChangeListener;
36  import org.jfree.data.general.DatasetGroup;
37  import org.jfree.data.xy.XYDataset;
38  import org.opentrafficsim.core.gtu.animation.IDGTUColorer;
39  import org.opentrafficsim.kpi.sampling.KpiGtuDirectionality;
40  import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
41  import org.opentrafficsim.kpi.sampling.SamplingException;
42  import org.opentrafficsim.kpi.sampling.SpaceTimeRegion;
43  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
44  import org.opentrafficsim.road.network.lane.Lane;
45  import org.opentrafficsim.road.network.sampling.LaneData;
46  import org.opentrafficsim.road.network.sampling.RoadSampler;
47  
48  import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
49  
50  /**
51   * Trajectory plot.
52   * <p>
53   * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
54   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
55   * <p>
56   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
57   * initial version Jul 24, 2014 <br>
58   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
59   */
60  public class TrajectoryPlot extends AbstractOTSPlot implements XYDataset, LaneBasedGTUSampler// , EventListenerInterface
61  {
62      /** */
63      private static final long serialVersionUID = 20140724L;
64  
65      /** Sample interval of this TrajectoryPlot. */
66      private final Duration sampleInterval;
67  
68      /** The simulator. */
69      private final DEVSSimulatorInterface.TimeDoubleUnit simulator;
70  
71      /**
72       * @return sampleInterval if this TrajectoryPlot samples at a fixed rate, or null if this TrajectoryPlot samples on the GTU
73       *         move events
74       */
75      public final Duration getSampleInterval()
76      {
77          return this.sampleInterval;
78      }
79  
80      /** The cumulative lengths of the elements of path. */
81      private final double[] cumulativeLengths;
82  
83      /**
84       * Retrieve the cumulative length of the sampled path at the end of a path element.
85       * @param index int; the index of the path element; if -1, the total length of the path is returned
86       * @return double; the cumulative length at the end of the specified path element in meters (si)
87       */
88      public final double getCumulativeLength(final int index)
89      {
90          return index == -1 ? this.cumulativeLengths[this.cumulativeLengths.length - 1] : this.cumulativeLengths[index];
91      }
92  
93      /** Maximum of the time axis. */
94      private Time maximumTime = new Time(300, TimeUnit.BASE);
95  
96      /**
97       * Retrieve the maximum time.
98       * @return Time; the maximum time
99       */
100     public final Time getMaximumTime()
101     {
102         return this.maximumTime;
103     }
104 
105     /**
106      * Set the maximum time.
107      * @param maximumTime Time; set the maximum time
108      */
109     public final void setMaximumTime(final Time maximumTime)
110     {
111         this.maximumTime = maximumTime;
112     }
113 
114     /** Not used internally. */
115     private DatasetGroup datasetGroup = null;
116 
117     /** The underlying sampler. */
118     private RoadSampler roadSampler;
119 
120     /** The lanes that make up the path. */
121     private List<KpiLaneDirection> lanes;
122 
123     /** Mapping from series rank number to trajectory. */
124     private List<TrajectoryAndLengthOffset> curves = null;
125 
126     /** Re generate the mapping on the next call to getSeriesCount. */
127     private boolean shouldGenerateNewCurves = true;
128 
129     /**
130      * Create a new TrajectoryPlot.
131      * @param caption String; the text to show above the TrajectoryPlot
132      * @param sampleInterval DoubleScalarRel&lt;TimeUnit&gt;; the time between samples of this TrajectoryPlot, or null in which
133      *            case the GTUs are sampled whenever they fire a MOVE_EVENT
134      * @param path ArrayList&lt;Lane&gt;; the series of Lanes that will provide the data for this TrajectoryPlot
135      * @param simulator DEVSSimulatorInterface.TimeDoubleUnit; the simulator
136      */
137     public TrajectoryPlot(final String caption, final Duration sampleInterval, final List<Lane> path,
138             final DEVSSimulatorInterface.TimeDoubleUnit simulator)
139     {
140         super(caption, path);
141         this.roadSampler = null == sampleInterval ? new RoadSampler(simulator)
142                 : new RoadSampler(simulator, Frequency.createSI(1 / sampleInterval.si));
143         this.lanes = new ArrayList<>();
144         for (Lane lane : path)
145         {
146             KpiLaneDirection kpiLaneDirection =
147                     new KpiLaneDirection(new LaneData(lane), KpiGtuDirectionality.DIR_PLUS);
148             SpaceTimeRegion spaceTimeRegion = new SpaceTimeRegion(kpiLaneDirection, Length.ZERO, lane.getLength(),
149                     Time.ZERO, Time.createSI(Double.MAX_VALUE));
150             this.roadSampler.registerSpaceTimeRegion(spaceTimeRegion);
151             this.lanes.add(kpiLaneDirection);
152         }
153         this.sampleInterval = sampleInterval;
154         this.simulator = simulator;
155         double[] endLengths = new double[path.size()];
156         double cumulativeLength = 0;
157         for (int i = 0; i < path.size(); i++)
158         {
159             Lane lane = path.get(i);
160             cumulativeLength += lane.getLength().getSI();
161             endLengths[i] = cumulativeLength;
162         }
163         this.cumulativeLengths = endLengths;
164         setChart(createChart(this));
165         this.reGraph(); // fixes the domain axis
166     }
167 
168     /**
169      * Derived from example on stackoverflow.
170      * http://stackoverflow.com/questions/7283902/setting-different-color-to-particular-row-in-series-jfreechart/7285922#7285922
171      */
172     private class MyRenderer extends XYLineAndShapeRenderer
173     {
174 
175         /** */
176         private static final long serialVersionUID = 20170503L;
177 
178         /**
179          * Construct a new MyRenderer.
180          * @param lines boolean; draw connecting lines
181          * @param shapes boolean; draw shapes at the points that define the lines
182          */
183         MyRenderer(final boolean lines, final boolean shapes)
184         {
185             super(lines, shapes);
186         }
187 
188         @Override
189         public Paint getItemPaint(final int row, final int col)
190         {
191             @SuppressWarnings("synthetic-access")
192             TrajectoryAndLengthOffset tal = getTrajectory(row);
193             String gtuId = tal.getTrajectory().getGtuId();
194             int colorIndex = 0;
195             for (int pos = gtuId.length(); --pos >= 0;)
196             {
197                 Character c = gtuId.charAt(pos);
198                 if (Character.isDigit(c))
199                 {
200                     colorIndex = c - '0';
201                     break;
202                 }
203             }
204             return IDGTUColorer.LEGEND.get(colorIndex).getColor();
205         }
206 
207         /** {@inheritDoc} */
208         @Override
209         public final String toString()
210         {
211             return "MyRenderer []";
212         }
213     }
214 
215     /** {@inheritDoc} */
216     @Override
217     public final GraphType getGraphType()
218     {
219         return GraphType.TRAJECTORY;
220     }
221 
222     /** {@inheritDoc} */
223     @Override
224     protected final JFreeChart createChart(final JFrame container)
225     {
226         final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
227         container.add(statusLabel, BorderLayout.SOUTH);
228         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
229         final JFreeChart result =
230                 ChartFactory.createXYLineChart(getCaption(), "", "", this, PlotOrientation.VERTICAL, false, false, false);
231         // Overrule the default background paint because some of the lines are invisible on top of this default.
232         result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
233         FixCaption.fixCaption(result);
234         NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
235         xAxis.setLowerMargin(0.0);
236         xAxis.setUpperMargin(0.0);
237         NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
238         yAxis.setAutoRangeIncludesZero(false);
239         yAxis.setLowerMargin(0.0);
240         yAxis.setUpperMargin(0.0);
241         yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
242         result.getXYPlot().setDomainAxis(xAxis);
243         result.getXYPlot().setRangeAxis(yAxis);
244         Length minimumPosition = Length.ZERO;
245         Length maximumPosition = new Length(getCumulativeLength(-1), LengthUnit.SI);
246         configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
247         // final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer();
248         MyRenderer renderer = new MyRenderer(false, true);
249         result.getXYPlot().setRenderer(renderer);
250         renderer.setDefaultLinesVisible(true);
251         renderer.setDefaultShapesVisible(false);
252         renderer.setDefaultShape(new Line2D.Float(0, 0, 0, 0));
253         final ChartPanel cp = new ChartPanel(result);
254         cp.setMouseWheelEnabled(true);
255         final PointerHandler ph = new PointerHandler()
256         {
257             /** {@inheritDoc} */
258             @Override
259             void updateHint(final double domainValue, final double rangeValue)
260             {
261                 if (Double.isNaN(domainValue))
262                 {
263                     statusLabel.setText(" ");
264                     return;
265                 }
266                 String value = "";
267                 /*-
268                 XYDataset dataset = plot.getDataset();
269                 double bestDistance = Double.MAX_VALUE;
270                 Trajectory bestTrajectory = null;
271                 final int mousePrecision = 5;
272                 java.awt.geom.Point2D.Double mousePoint = new java.awt.geom.Point2D.Double(t, distance);
273                 double lowTime =
274                         plot.getDomainAxis().java2DToValue(p.getX() - mousePrecision, pi.getDataArea(),
275                                 plot.getDomainAxisEdge()) - 1;
276                 double highTime =
277                         plot.getDomainAxis().java2DToValue(p.getX() + mousePrecision, pi.getDataArea(),
278                                 plot.getDomainAxisEdge()) + 1;
279                 double lowDistance =
280                         plot.getRangeAxis().java2DToValue(p.getY() + mousePrecision, pi.getDataArea(),
281                                 plot.getRangeAxisEdge()) - 20;
282                 double highDistance =
283                         plot.getRangeAxis().java2DToValue(p.getY() - mousePrecision, pi.getDataArea(),
284                                 plot.getRangeAxisEdge()) + 20;
285                 // System.out.println(String.format("Searching area t[%.1f-%.1f], x[%.1f,%.1f]", lowTime, highTime,
286                 // lowDistance, highDistance));
287                 for (Trajectory trajectory : this.trajectories)
288                 {
289                     java.awt.geom.Point2D.Double[] clippedTrajectory =
290                             trajectory.clipTrajectory(lowTime, highTime, lowDistance, highDistance);
291                     if (null == clippedTrajectory)
292                         continue;
293                     java.awt.geom.Point2D.Double prevPoint = null;
294                     for (java.awt.geom.Point2D.Double trajectoryPoint : clippedTrajectory)
295                     {
296                         if (null != prevPoint)
297                         {
298                             double thisDistance = Planar.distancePolygonToPoint(clippedTrajectory, mousePoint);
299                             if (thisDistance < bestDistance)
300                             {
301                                 bestDistance = thisDistance;
302                                 bestTrajectory = trajectory;
303                             }
304                         }
305                         prevPoint = trajectoryPoint;
306                     }
307                 }
308                 if (null != bestTrajectory)
309                 {
310                     for (SimulatedObject so : indices.keySet())
311                         if (this.trajectories.get(indices.get(so)) == bestTrajectory)
312                         {
313                             Point2D.Double bestPosition = bestTrajectory.getEstimatedPosition(t);
314                             if (null == bestPosition)
315                                 continue;
316                             value =
317                                     String.format(
318                                             Main.locale,
319                                             ": vehicle %s; location on measurement path at t=%.1fs: "
320                                             + "longitudinal %.1fm, lateral %.1fm",
321                                             so.toString(), t, bestPosition.x, bestPosition.y);
322                         }
323                 }
324                 else
325                     value = "";
326                  */
327                 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
328             }
329         };
330         cp.addMouseMotionListener(ph);
331         cp.addMouseListener(ph);
332         container.add(cp, BorderLayout.CENTER);
333         // TODO ensure that shapes for all the data points don't get allocated.
334         // Currently JFreeChart allocates many megabytes of memory for Ellipses that are never drawn.
335         JPopupMenu popupMenu = cp.getPopupMenu();
336         popupMenu.add(new JPopupMenu.Separator());
337         popupMenu.add(StandAloneChartWindow.createMenuItem(this));
338         return result;
339     }
340 
341     /** {@inheritDoc} */
342     @Override
343     public final void reGraph()
344     {
345 
346         SwingUtilities.invokeLater(new Runnable()
347         {
348 
349             @SuppressWarnings({ "synthetic-access", "unqualified-field-access" })
350             @Override
351             public void run()
352             {
353                 for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
354                 {
355                     if (dcl instanceof XYPlot)
356                     {
357                         Time simulatorTime = simulator.getSimulatorTime();
358                         if (getMaximumTime().lt(simulatorTime))
359                         {
360                             setMaximumTime(simulatorTime);
361                         }
362                         configureAxis(((XYPlot) dcl).getDomainAxis(), maximumTime.getSI());
363                     }
364                 }
365                 shouldGenerateNewCurves = true;
366                 notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
367             }
368         });
369     }
370 
371     /**
372      * Configure the range of an axis.
373      * @param valueAxis ValueAxis
374      * @param range double; the upper bound of the axis
375      */
376     static void configureAxis(final ValueAxis valueAxis, final double range)
377     {
378         valueAxis.setUpperBound(range);
379         valueAxis.setLowerMargin(0);
380         valueAxis.setUpperMargin(0);
381         valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
382         valueAxis.setAutoRange(true);
383         valueAxis.setAutoRangeMinimumSize(range);
384         valueAxis.centerRange(range / 2);
385         // System.out.println("centerRange is " + (range / 2));
386     }
387 
388     /** {@inheritDoc} */
389     @Override
390     public void actionPerformed(final ActionEvent e)
391     {
392         // not yet
393     }
394 
395     /** {@inheritDoc} */
396     @Override
397     public final int getSeriesCount()
398     {
399         if (null == this.curves || this.shouldGenerateNewCurves)
400         {
401             List<TrajectoryAndLengthOffset> newCurves = new ArrayList<>();
402             double cumulativeLength = 0;
403             for (KpiLaneDirection kld : this.lanes)
404             {
405                 TrajectoryGroup tg = this.roadSampler.getTrajectoryGroup(kld);
406                 if (null == tg)
407                 {
408                     continue;
409                 }
410                 for (org.opentrafficsim.kpi.sampling.Trajectory trajectory : tg.getTrajectories())
411                 {
412                     newCurves.add(new TrajectoryAndLengthOffset(trajectory, cumulativeLength));
413                 }
414                 cumulativeLength += kld.getLaneData().getLength().si;
415             }
416             this.curves = newCurves;
417             this.shouldGenerateNewCurves = false;
418         }
419         return this.curves.size();
420     }
421 
422     /** {@inheritDoc} */
423     @Override
424     public final Comparable<Integer> getSeriesKey(final int series)
425     {
426         return series;
427     }
428 
429     /** {@inheritDoc} */
430     @SuppressWarnings("rawtypes")
431     @Override
432     public final int indexOf(final Comparable seriesKey)
433     {
434         if (seriesKey instanceof Integer)
435         {
436             return (Integer) seriesKey;
437         }
438         return -1;
439     }
440 
441     /** {@inheritDoc} */
442     @Override
443     public final DatasetGroup getGroup()
444     {
445         return this.datasetGroup;
446     }
447 
448     /** {@inheritDoc} */
449     @Override
450     public final void setGroup(final DatasetGroup group)
451     {
452         this.datasetGroup = group;
453     }
454 
455     /** {@inheritDoc} */
456     @Override
457     public final DomainOrder getDomainOrder()
458     {
459         return DomainOrder.ASCENDING;
460     }
461 
462     /**
463      * Storage for a trajectory and a length.
464      */
465     class TrajectoryAndLengthOffset
466     {
467         /** The trajectory. */
468         private final org.opentrafficsim.kpi.sampling.Trajectory trajectory;
469 
470         /** The length. */
471         private final double lengthOffset;
472 
473         /**
474          * Construct a new TrajectoryAndLengthOffset object.
475          * @param trajectory org.opentrafficsim.kpi.sampling.Trajectory; the trajectory
476          * @param lengthOffset double; the length from the beginning of the sampled path to the start of the lane to which the
477          *            trajectory belongs
478          */
479         TrajectoryAndLengthOffset(final org.opentrafficsim.kpi.sampling.Trajectory trajectory,
480                 final double lengthOffset)
481         {
482             this.trajectory = trajectory;
483             this.lengthOffset = lengthOffset;
484         }
485 
486         /**
487          * Retrieve the trajectory.
488          * @return org.opentrafficsim.kpi.sampling.Trajectory; the trajectory
489          */
490         public org.opentrafficsim.kpi.sampling.Trajectory getTrajectory()
491         {
492             return this.trajectory;
493         }
494 
495         /**
496          * Retrieve the lengthOffset.
497          * @return double; the lengthOffset
498          */
499         public double getLengthOffset()
500         {
501             return this.lengthOffset;
502         }
503 
504         /** {@inheritDoc} */
505         @Override
506         public final String toString()
507         {
508             return "TrajectoryAndLengthOffset [trajectory=" + this.trajectory + ", lengthOffset=" + this.lengthOffset + "]";
509         }
510 
511     }
512 
513     /**
514      * Retrieve the Nth trajectory.
515      * @param index int; the index of the requested trajectory
516      * @return org.opentrafficsim.kpi.sampling.Trajectory; the Nth trajectory, or null if the provided index is out of range
517      */
518     private TrajectoryAndLengthOffset getTrajectory(final int index)
519     {
520         if (index < 0)
521         {
522             System.err.println("Negative index (" + index + ")");
523             return null;
524         }
525         while (null == this.curves)
526         {
527             getSeriesCount();
528         }
529         if (index >= this.curves.size())
530         {
531             System.err.println("index out of range (" + index + " >= " + this.curves.size() + ")");
532             return null;
533         }
534         return this.curves.get(index);
535     }
536 
537     /** {@inheritDoc} */
538     @Override
539     public final int getItemCount(final int series)
540     {
541         return getTrajectory(series).getTrajectory().size();
542     }
543 
544     /** {@inheritDoc} */
545     @Override
546     public final Number getX(final int series, final int item)
547     {
548         double v = getXValue(series, item);
549         if (Double.isNaN(v))
550         {
551             return null;
552         }
553         return v;
554     }
555 
556     /** {@inheritDoc} */
557     @Override
558     public final double getXValue(final int series, final int item)
559     {
560         TrajectoryAndLengthOffset tal = getTrajectory(series);
561         try
562         {
563             return tal.getTrajectory().getT(item);
564         }
565         catch (SamplingException exception)
566         {
567             exception.printStackTrace();
568             System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
569             return Double.NaN;
570         }
571     }
572 
573     /** {@inheritDoc} */
574     @Override
575     public final Number getY(final int series, final int item)
576     {
577         double v = getYValue(series, item);
578         if (Double.isNaN(v))
579         {
580             return null;
581         }
582         return v;
583     }
584 
585     /** {@inheritDoc} */
586     @Override
587     public final double getYValue(final int series, final int item)
588     {
589         TrajectoryAndLengthOffset tal = getTrajectory(series);
590         try
591         {
592             return tal.getTrajectory().getX(item) + tal.getLengthOffset();
593         }
594         catch (SamplingException exception)
595         {
596             exception.printStackTrace();
597             System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
598             return Double.NaN;
599         }
600     }
601 
602     /** {@inheritDoc} */
603     @Override
604     public final String toString()
605     {
606         return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length="
607                 + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption() + "]";
608     }
609 
610 }