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.Sampler;
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 Sampler<?> sampler, final GraphPath<KpiLaneDirection> path)
106     {
107         super(caption, updateInterval, simulator, sampler, 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                     TrajectoryGroup<?> trajectoryGroup = getSampler().getTrajectoryGroup(lane);
127                     int from = this.knownTrajectories.getOrDefault(lane, 0);
128                     int to = trajectoryGroup.size();
129                     double scaleFactor = section.getLength().si / lane.getLaneData().getLength().si;
130                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
131                     {
132                         if (getPath().getNumberOfSeries() > 1)
133                         {
134                             
135                             BasicStroke stroke = STROKES[i % STROKES.length];
136                             if (stroke.getDashArray() != null)
137                             {
138                                 float dashLength = 0.0f;
139                                 for (float d : stroke.getDashArray())
140                                 {
141                                     dashLength += d;
142                                 }
143                                 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
144                                         stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
145                             }
146                             this.strokes.get(i).add(stroke);
147                         }
148                         this.curves.get(i).add(
149                                 new OffsetTrajectory(trajectory, startDistance, scaleFactor, lane.getLaneData().getLength()));
150                     }
151                     this.knownTrajectories.put(lane, to);
152                 }
153             }
154         });
155     }
156 
157     
158 
159 
160 
161     private JFreeChart createChart()
162     {
163         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
164         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
165         XYLineAndShapeRendererID renderer = new XYLineAndShapeRendererID();
166         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
167         boolean showLegend;
168         if (getPath().getNumberOfSeries() < 2)
169         {
170             plot.setFixedLegendItems(null);
171             showLegend = false;
172         }
173         else
174         {
175             this.legend = new LegendItemCollection();
176             for (int i = 0; i < getPath().getNumberOfSeries(); i++)
177             {
178                 LegendItem li = new LegendItem(getPath().getName(i));
179                 li.setSeriesKey(i); 
180                 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
181                 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
182                 this.legend.add(li);
183             }
184             plot.setFixedLegendItems(this.legend);
185             showLegend = true;
186         }
187         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
188     }
189 
190     
191     @Override
192     public GraphType getGraphType()
193     {
194         return GraphType.TRAJECTORY;
195     }
196 
197     
198     @Override
199     public String getStatusLabel(final double domainValue, final double rangeValue)
200     {
201         return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
202     }
203 
204     
205     @Override
206     protected void increaseTime(final Time time)
207     {
208         if (this.graphUpdater != null) 
209         {
210             this.graphUpdater.offer(time);
211         }
212     }
213 
214     
215     @Override
216     public int getSeriesCount()
217     {
218         int n = 0;
219         for (int i = 0; i < this.curves.size(); i++)
220         {
221             List<OffsetTrajectory> list = this.curves.get(i);
222             int m = list.size();
223             this.curvesPerLane.set(i, m);
224             n += m;
225         }
226         return n;
227     }
228 
229     
230     @Override
231     public Comparable<Integer> getSeriesKey(final int series)
232     {
233         return series;
234     }
235 
236     
237     @SuppressWarnings("rawtypes")
238     @Override
239     public int indexOf(final Comparable seriesKey)
240     {
241         return 0;
242     }
243 
244     
245     @Override
246     public DomainOrder getDomainOrder()
247     {
248         return DomainOrder.ASCENDING;
249     }
250 
251     
252     @Override
253     public int getItemCount(final int series)
254     {
255         OffsetTrajectory trajectory = getTrajectory(series);
256         return trajectory == null ? 0 : trajectory.size();
257     }
258 
259     
260     @Override
261     public Number getX(final int series, final int item)
262     {
263         return getXValue(series, item);
264     }
265 
266     
267     @Override
268     public double getXValue(final int series, final int item)
269     {
270         return getTrajectory(series).getT(item);
271     }
272 
273     
274     @Override
275     public Number getY(final int series, final int item)
276     {
277         return getYValue(series, item);
278     }
279 
280     
281     @Override
282     public double getYValue(final int series, final int item)
283     {
284         return getTrajectory(series).getX(item);
285     }
286 
287     
288 
289 
290 
291 
292     private OffsetTrajectory getTrajectory(final int series)
293     {
294         int[] n = getLaneAndSeriesNumber(series);
295         return this.curves.get(n[0]).get(n[1]);
296     }
297 
298     
299 
300 
301 
302 
303     private int[] getLaneAndSeriesNumber(final int series)
304     {
305         int n = series;
306         for (int i = 0; i < this.curves.size(); i++)
307         {
308             int m = this.curvesPerLane.get(i);
309             if (n < m)
310             {
311                 return new int[] {i, n};
312             }
313             n -= m;
314         }
315         throw new RuntimeException("Discrepancy between series number and available data.");
316     }
317 
318     
319 
320 
321 
322 
323 
324 
325 
326 
327 
328 
329 
330     private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
331     {
332         
333         private static final long serialVersionUID = 20181014L;
334 
335         
336 
337 
338         XYLineAndShapeRendererID()
339         {
340             super(false, true);
341             setDefaultLinesVisible(true);
342             setDefaultShapesVisible(false);
343             setDrawSeriesLineAsPath(true);
344         }
345 
346         
347         @SuppressWarnings("synthetic-access")
348         @Override
349         public boolean isSeriesVisible(final int series)
350         {
351             int[] n = getLaneAndSeriesNumber(series);
352             return TrajectoryPlot.this.laneVisible.get(n[0]);
353         }
354 
355         
356         @SuppressWarnings("synthetic-access")
357         @Override
358         public Stroke getSeriesStroke(final int series)
359         {
360             if (TrajectoryPlot.this.curves.size() == 1)
361             {
362                 return STROKES[0];
363             }
364             int[] n = getLaneAndSeriesNumber(series);
365             return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
366         }
367 
368         
369         @SuppressWarnings("synthetic-access")
370         @Override
371         public Paint getSeriesPaint(final int series)
372         {
373             if (TrajectoryPlot.this.curves.size() == 1)
374             {
375                 String gtuId = getTrajectory(series).getGtuId();
376                 for (int pos = gtuId.length(); --pos >= 0;)
377                 {
378                     Character c = gtuId.charAt(pos);
379                     if (Character.isDigit(c))
380                     {
381                         return IDGTUColorer.LEGEND.get(c - '0').getColor();
382                     }
383                 }
384             }
385             int[] n = getLaneAndSeriesNumber(series);
386             return COLORMAP[n[0] % COLORMAP.length];
387         }
388 
389         
390 
391 
392 
393         @SuppressWarnings("synthetic-access")
394         @Override
395         protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
396                 final int series, final int item, final double entityX, final double entityY)
397         {
398 
399             if (!getItemCreateEntity(series, item))
400             {
401                 return;
402             }
403 
404             
405             
406             Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
407             String tip = null;
408             XYToolTipGenerator generator = getToolTipGenerator(series, item);
409             if (generator != null)
410             {
411                 tip = generator.generateToolTip(dataset, series, item);
412             }
413             String url = null;
414             if (getURLGenerator() != null)
415             {
416                 url = getURLGenerator().generateURL(dataset, series, item);
417             }
418             XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
419             entities.add(entity);
420         }
421 
422         
423         @Override
424         public String toString()
425         {
426             return "XYLineAndShapeRendererID []";
427         }
428 
429     }
430 
431     
432 
433 
434 
435 
436 
437 
438 
439 
440 
441 
442 
443 
444     private class OffsetTrajectory
445     {
446         
447         private final Trajectory<?> trajectory;
448 
449         
450         private final double offset;
451 
452         
453         private final double scaleFactor;
454 
455         
456         private int first;
457 
458         
459         private int size;
460 
461         
462         private final Length laneLength;
463 
464         
465 
466 
467 
468 
469 
470 
471 
472         OffsetTrajectory(final Trajectory<?> trajectory, final Length offset, final double scaleFactor, final Length laneLength)
473         {
474             this.trajectory = trajectory;
475             this.offset = offset.si;
476             this.scaleFactor = scaleFactor;
477             this.laneLength = laneLength;
478         }
479 
480         
481 
482 
483 
484         public final int size()
485         {
486             
487             try
488             {
489                 
490 
491 
492 
493 
494 
495 
496 
497 
498 
499 
500 
501 
502 
503 
504 
505 
506 
507 
508 
509 
510 
511 
512                 int f = 0;
513                 while (f < this.trajectory.size() - 1 && this.trajectory.getX(f + 1) < 0.0)
514                 {
515                     f++;
516                 }
517                 this.first = f;
518                 int s = this.trajectory.size() - 1;
519                 while (s > 1 && this.trajectory.getX(s - 1) > this.laneLength.si)
520                 {
521                     s--;
522                 }
523                 this.size = s - f + 1;
524             }
525             catch (SamplingException exception)
526             {
527                 throw new RuntimeException("Unexpected exception while obtaining location value from trajectory for plotting.",
528                         exception);
529             }
530             return this.size;
531         }
532 
533         
534 
535 
536 
537 
538         public final double getX(final int item)
539         {
540             return Try.assign(() -> this.offset + this.trajectory.getX(this.first + item) * this.scaleFactor,
541                     "Unexpected exception while obtaining location value from trajectory for plotting.");
542         }
543 
544         
545 
546 
547 
548 
549         public final double getT(final int item)
550         {
551             return Try.assign(() -> (double) this.trajectory.getT(this.first + item),
552                     "Unexpected exception while obtaining time value from trajectory for plotting.");
553         }
554 
555         
556 
557 
558 
559         public final String getGtuId()
560         {
561             return this.trajectory.getGtuId();
562         }
563 
564         
565         @Override
566         public final String toString()
567         {
568             return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
569         }
570 
571     }
572 
573     
574     @Override
575     public String toString()
576     {
577         return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
578                 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
579                 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
580     }
581 
582     
583 
584 
585 
586     public LegendItemCollection getLegend()
587     {
588         return legend;
589     }
590 
591     
592 
593 
594 
595     public List<Boolean> getLaneVisible()
596     {
597         return laneVisible;
598     }
599 
600 }