1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Paint;
6   import java.awt.Shape;
7   import java.awt.Stroke;
8   import java.awt.geom.CubicCurve2D;
9   import java.awt.geom.Line2D;
10  import java.util.ArrayList;
11  import java.util.LinkedHashMap;
12  import java.util.List;
13  import java.util.Map;
14  
15  import org.djunits.value.vdouble.scalar.Duration;
16  import org.djunits.value.vdouble.scalar.Length;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djutils.exceptions.Try;
19  import org.jfree.chart.JFreeChart;
20  import org.jfree.chart.LegendItem;
21  import org.jfree.chart.LegendItemCollection;
22  import org.jfree.chart.axis.NumberAxis;
23  import org.jfree.chart.entity.EntityCollection;
24  import org.jfree.chart.entity.XYItemEntity;
25  import org.jfree.chart.labels.XYToolTipGenerator;
26  import org.jfree.chart.plot.XYPlot;
27  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
28  import org.jfree.data.DomainOrder;
29  import org.jfree.data.xy.XYDataset;
30  import org.opentrafficsim.core.animation.gtu.colorer.IDGTUColorer;
31  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
32  import org.opentrafficsim.draw.core.BoundsPaintScale;
33  import org.opentrafficsim.draw.graphs.GraphPath.Section;
34  import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
35  import org.opentrafficsim.kpi.sampling.SamplerData;
36  import org.opentrafficsim.kpi.sampling.SamplingException;
37  import org.opentrafficsim.kpi.sampling.Trajectory;
38  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
52  {
53      
54      private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
55  
56      
57      private static final Color[] COLORMAP;
58  
59      
60      private static final BasicStroke[] STROKES;
61  
62      
63      private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
64  
65      
66      private final GraphUpdater<Time> graphUpdater;
67  
68      
69      private final Map<KpiLaneDirection, Integer> knownTrajectories = new LinkedHashMap<>();
70  
71      
72      private List<List<OffsetTrajectory>> curves = new ArrayList<>();
73  
74      
75      private List<List<Stroke>> strokes = new ArrayList<>();
76  
77      
78      private List<Integer> curvesPerLane = new ArrayList<>();
79  
80      
81      private LegendItemCollection legend;
82  
83      
84      private final List<Boolean> laneVisible = new ArrayList<>();
85  
86      static
87      {
88          Color[] c = BoundsPaintScale.hue(6);
89          COLORMAP = new Color[] {c[0], c[4], c[2], c[1], c[3], c[5]};
90          float lw = 0.4f;
91          STROKES = new BasicStroke[] {new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
92                  new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {13f, 4f}, 0.0f),
93                  new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {11f, 3f, 2f, 3f}, 0.0f)};
94      }
95  
96      
97  
98  
99  
100 
101 
102 
103 
104     public TrajectoryPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator,
105             final SamplerData<?> samplerData, final GraphPath<KpiLaneDirection> path)
106     {
107         super(caption, updateInterval, simulator, samplerData, path, Duration.ZERO);
108         for (int i = 0; i < path.getNumberOfSeries(); i++)
109         {
110             this.curves.add(new ArrayList<>());
111             this.strokes.add(new ArrayList<>());
112             this.curvesPerLane.add(0);
113             this.laneVisible.add(true);
114         }
115         setChart(createChart());
116 
117         
118         this.graphUpdater = new GraphUpdater<>("Trajectories worker", Thread.currentThread(), (t) ->
119         {
120             for (Section<KpiLaneDirection> section : path.getSections())
121             {
122                 Length startDistance = path.getStartDistance(section);
123                 for (int i = 0; i < path.getNumberOfSeries(); i++)
124                 {
125                     KpiLaneDirection lane = section.getSource(i);
126                     if (lane == null)
127                     {
128                         continue; 
129                     }
130                     TrajectoryGroup<?> trajectoryGroup = getSamplerData().getTrajectoryGroup(lane);
131                     int from = this.knownTrajectories.getOrDefault(lane, 0);
132                     int to = trajectoryGroup.size();
133                     double scaleFactor = section.getLength().si / lane.getLaneData().getLength().si;
134                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
135                     {
136                         if (getPath().getNumberOfSeries() > 1)
137                         {
138                             
139                             BasicStroke stroke = STROKES[i % STROKES.length];
140                             if (stroke.getDashArray() != null)
141                             {
142                                 float dashLength = 0.0f;
143                                 for (float d : stroke.getDashArray())
144                                 {
145                                     dashLength += d;
146                                 }
147                                 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
148                                         stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
149                             }
150                             this.strokes.get(i).add(stroke);
151                         }
152                         this.curves.get(i).add(
153                                 new OffsetTrajectory(trajectory, startDistance, scaleFactor, lane.getLaneData().getLength()));
154                     }
155                     this.knownTrajectories.put(lane, to);
156                 }
157             }
158         });
159     }
160 
161     
162 
163 
164 
165     private JFreeChart createChart()
166     {
167         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
168         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
169         XYLineAndShapeRendererID renderer = new XYLineAndShapeRendererID();
170         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
171         boolean showLegend;
172         if (getPath().getNumberOfSeries() < 2)
173         {
174             plot.setFixedLegendItems(null);
175             showLegend = false;
176         }
177         else
178         {
179             this.legend = new LegendItemCollection();
180             for (int i = 0; i < getPath().getNumberOfSeries(); i++)
181             {
182                 LegendItem li = new LegendItem(getPath().getName(i));
183                 li.setSeriesKey(i); 
184                 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
185                 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
186                 this.legend.add(li);
187             }
188             plot.setFixedLegendItems(this.legend);
189             showLegend = true;
190         }
191         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
192     }
193 
194     
195     @Override
196     public GraphType getGraphType()
197     {
198         return GraphType.TRAJECTORY;
199     }
200 
201     
202     @Override
203     public String getStatusLabel(final double domainValue, final double rangeValue)
204     {
205         return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
206     }
207 
208     
209     @Override
210     protected void increaseTime(final Time time)
211     {
212         if (this.graphUpdater != null) 
213         {
214             this.graphUpdater.offer(time);
215         }
216     }
217 
218     
219     @Override
220     public int getSeriesCount()
221     {
222         int n = 0;
223         for (int i = 0; i < this.curves.size(); i++)
224         {
225             List<OffsetTrajectory> list = this.curves.get(i);
226             int m = list.size();
227             this.curvesPerLane.set(i, m);
228             n += m;
229         }
230         return n;
231     }
232 
233     
234     @Override
235     public Comparable<Integer> getSeriesKey(final int series)
236     {
237         return series;
238     }
239 
240     
241     @SuppressWarnings("rawtypes")
242     @Override
243     public int indexOf(final Comparable seriesKey)
244     {
245         return 0;
246     }
247 
248     
249     @Override
250     public DomainOrder getDomainOrder()
251     {
252         return DomainOrder.ASCENDING;
253     }
254 
255     
256     @Override
257     public int getItemCount(final int series)
258     {
259         OffsetTrajectory trajectory = getTrajectory(series);
260         return trajectory == null ? 0 : trajectory.size();
261     }
262 
263     
264     @Override
265     public Number getX(final int series, final int item)
266     {
267         return getXValue(series, item);
268     }
269 
270     
271     @Override
272     public double getXValue(final int series, final int item)
273     {
274         return getTrajectory(series).getT(item);
275     }
276 
277     
278     @Override
279     public Number getY(final int series, final int item)
280     {
281         return getYValue(series, item);
282     }
283 
284     
285     @Override
286     public double getYValue(final int series, final int item)
287     {
288         return getTrajectory(series).getX(item);
289     }
290 
291     
292 
293 
294 
295 
296     private OffsetTrajectory getTrajectory(final int series)
297     {
298         int[] n = getLaneAndSeriesNumber(series);
299         return this.curves.get(n[0]).get(n[1]);
300     }
301 
302     
303 
304 
305 
306 
307     private int[] getLaneAndSeriesNumber(final int series)
308     {
309         int n = series;
310         for (int i = 0; i < this.curves.size(); i++)
311         {
312             int m = this.curvesPerLane.get(i);
313             if (n < m)
314             {
315                 return new int[] {i, n};
316             }
317             n -= m;
318         }
319         throw new RuntimeException("Discrepancy between series number and available data.");
320     }
321 
322     
323 
324 
325 
326 
327 
328 
329 
330 
331 
332 
333 
334     private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
335     {
336         
337         private static final long serialVersionUID = 20181014L;
338 
339         
340 
341 
342         XYLineAndShapeRendererID()
343         {
344             super(false, true);
345             setDefaultLinesVisible(true);
346             setDefaultShapesVisible(false);
347             setDrawSeriesLineAsPath(true);
348         }
349 
350         
351         @SuppressWarnings("synthetic-access")
352         @Override
353         public boolean isSeriesVisible(final int series)
354         {
355             int[] n = getLaneAndSeriesNumber(series);
356             return TrajectoryPlot.this.laneVisible.get(n[0]);
357         }
358 
359         
360         @SuppressWarnings("synthetic-access")
361         @Override
362         public Stroke getSeriesStroke(final int series)
363         {
364             if (TrajectoryPlot.this.curves.size() == 1)
365             {
366                 return STROKES[0];
367             }
368             int[] n = getLaneAndSeriesNumber(series);
369             return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
370         }
371 
372         
373         @SuppressWarnings("synthetic-access")
374         @Override
375         public Paint getSeriesPaint(final int series)
376         {
377             if (TrajectoryPlot.this.curves.size() == 1)
378             {
379                 String gtuId = getTrajectory(series).getGtuId();
380                 for (int pos = gtuId.length(); --pos >= 0;)
381                 {
382                     Character c = gtuId.charAt(pos);
383                     if (Character.isDigit(c))
384                     {
385                         return IDGTUColorer.LEGEND.get(c - '0').getColor();
386                     }
387                 }
388             }
389             int[] n = getLaneAndSeriesNumber(series);
390             return COLORMAP[n[0] % COLORMAP.length];
391         }
392 
393         
394 
395 
396 
397         @SuppressWarnings("synthetic-access")
398         @Override
399         protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
400                 final int series, final int item, final double entityX, final double entityY)
401         {
402 
403             if (!getItemCreateEntity(series, item))
404             {
405                 return;
406             }
407 
408             
409             
410             Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
411             String tip = null;
412             XYToolTipGenerator generator = getToolTipGenerator(series, item);
413             if (generator != null)
414             {
415                 tip = generator.generateToolTip(dataset, series, item);
416             }
417             String url = null;
418             if (getURLGenerator() != null)
419             {
420                 url = getURLGenerator().generateURL(dataset, series, item);
421             }
422             XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
423             entities.add(entity);
424         }
425 
426         
427         @Override
428         public String toString()
429         {
430             return "XYLineAndShapeRendererID []";
431         }
432 
433     }
434 
435     
436 
437 
438 
439 
440 
441 
442 
443 
444 
445 
446 
447 
448     private class OffsetTrajectory
449     {
450         
451         private final Trajectory<?> trajectory;
452 
453         
454         private final double offset;
455 
456         
457         private final double scaleFactor;
458 
459         
460         private int first;
461 
462         
463         private int size;
464 
465         
466         private final Length laneLength;
467 
468         
469 
470 
471 
472 
473 
474 
475 
476         OffsetTrajectory(final Trajectory<?> trajectory, final Length offset, final double scaleFactor, final Length laneLength)
477         {
478             this.trajectory = trajectory;
479             this.offset = offset.si;
480             this.scaleFactor = scaleFactor;
481             this.laneLength = laneLength;
482         }
483 
484         
485 
486 
487 
488         public final int size()
489         {
490             
491             try
492             {
493                 
494 
495 
496 
497 
498 
499 
500 
501 
502 
503 
504 
505 
506 
507 
508 
509 
510 
511 
512 
513 
514 
515 
516                 int f = 0;
517                 while (f < this.trajectory.size() - 1 && this.trajectory.getX(f + 1) < 0.0)
518                 {
519                     f++;
520                 }
521                 this.first = f;
522                 int s = this.trajectory.size() - 1;
523                 while (s > 1 && this.trajectory.getX(s - 1) > this.laneLength.si)
524                 {
525                     s--;
526                 }
527                 this.size = s - f + 1;
528             }
529             catch (SamplingException exception)
530             {
531                 throw new RuntimeException("Unexpected exception while obtaining location value from trajectory for plotting.",
532                         exception);
533             }
534             return this.size;
535         }
536 
537         
538 
539 
540 
541 
542         public final double getX(final int item)
543         {
544             return Try.assign(() -> this.offset + this.trajectory.getX(this.first + item) * this.scaleFactor,
545                     "Unexpected exception while obtaining location value from trajectory for plotting.");
546         }
547 
548         
549 
550 
551 
552 
553         public final double getT(final int item)
554         {
555             return Try.assign(() -> (double) this.trajectory.getT(this.first + item),
556                     "Unexpected exception while obtaining time value from trajectory for plotting.");
557         }
558 
559         
560 
561 
562 
563         public final String getGtuId()
564         {
565             return this.trajectory.getGtuId();
566         }
567 
568         
569         @Override
570         public final String toString()
571         {
572             return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
573         }
574 
575     }
576 
577     
578     @Override
579     public String toString()
580     {
581         return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
582                 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
583                 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
584     }
585 
586     
587 
588 
589 
590     public LegendItemCollection getLegend()
591     {
592         return this.legend;
593     }
594 
595     
596 
597 
598 
599     public List<Boolean> getLaneVisible()
600     {
601         return this.laneVisible;
602     }
603 
604 }