View Javadoc
1   package org.opentrafficsim.swing.graphs;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Point;
6   import java.awt.event.ActionEvent;
7   import java.awt.event.ActionListener;
8   import java.awt.geom.Point2D;
9   import java.awt.geom.Rectangle2D;
10  import java.util.Optional;
11  
12  import javax.swing.ButtonGroup;
13  import javax.swing.JMenu;
14  import javax.swing.JPopupMenu;
15  import javax.swing.JRadioButtonMenuItem;
16  
17  import org.djutils.draw.point.Point2d;
18  import org.djutils.exceptions.Throw;
19  import org.jfree.chart.ChartMouseEvent;
20  import org.jfree.chart.ChartMouseListener;
21  import org.jfree.chart.annotations.XYLineAnnotation;
22  import org.jfree.chart.annotations.XYTextAnnotation;
23  import org.jfree.chart.entity.PlotEntity;
24  import org.jfree.chart.plot.XYPlot;
25  import org.opentrafficsim.animation.Colors;
26  import org.opentrafficsim.animation.colorer.trajectory.AccelerationTrajectoryColorer;
27  import org.opentrafficsim.animation.colorer.trajectory.FixedTrajectoryColorer;
28  import org.opentrafficsim.animation.colorer.trajectory.IdTrajectoryColorer;
29  import org.opentrafficsim.animation.colorer.trajectory.SpeedTrajectoryColorer;
30  import org.opentrafficsim.animation.colorer.trajectory.TrajectoryColorer;
31  import org.opentrafficsim.animation.graphs.GraphUtil;
32  import org.opentrafficsim.animation.graphs.TrajectoryPlot;
33  
34  /**
35   * Embed a TrajectoryPlot in a Swing JPanel.
36   * <p>
37   * Copyright (c) 2023-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
38   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
39   * </p>
40   * @author Alexander Verbraeck
41   * @author Peter Knoppers
42   * @author Wouter Schakel
43   */
44  public class SwingTrajectoryPlot extends SwingSpaceTimePlot
45  {
46      /** Serialization version UID. */
47      private static final long serialVersionUID = 20190823L;
48  
49      /** Calculate density (vertical line). */
50      private boolean density;
51  
52      /** Calculate flow ((horizontal line). */
53      private boolean flow;
54  
55      /** From point for line statistics. */
56      private Point2D.Double from;
57  
58      /** From point for line statistics. */
59      private Point2D.Double to;
60  
61      /** Line annotation for line statistics. */
62      private XYLineAnnotation lineAnnotation;
63  
64      /** Text annotation for line statistics. */
65      private XYTextAnnotation textAnnotation;
66  
67      /** Menu to select color. */
68      private JMenu colorMenu;
69  
70      /** Color button group (so one is selected at a time). */
71      private ButtonGroup colorButtonGroup = new ButtonGroup();
72  
73      /**
74       * Construct a new Swing container for a TrajectoryPlot. Default colorers for blue, speed, id and acceleration are used if
75       * the plot has a single lane.
76       * @param plot the plot to embed
77       */
78      public SwingTrajectoryPlot(final TrajectoryPlot plot)
79      {
80          this(plot, true);
81      }
82  
83      /**
84       * Constructor. Default colorers might be set based on the input, but only when the plot has a single lane.
85       * @param plot the plot to embed
86       * @param defaultColorers whether to use default colorers for blue, speed, id and acceleration
87       */
88      public SwingTrajectoryPlot(final TrajectoryPlot plot, final boolean defaultColorers)
89      {
90          super(plot);
91          if (plot.getLaneCount() == 1)
92          {
93              if (defaultColorers)
94              {
95                  addColorer(new FixedTrajectoryColorer(Colors.OTS_BLUE, "Blue"), true);
96                  addColorer(new IdTrajectoryColorer(), false);
97                  addColorer(new SpeedTrajectoryColorer(), false);
98                  addColorer(new AccelerationTrajectoryColorer(), false);
99              }
100             else
101             {
102                 // Make sure a single-lane plot has some colorer, even if none will be set through addColorer()
103                 plot.setColorer(new FixedTrajectoryColorer(Colors.OTS_BLUE, "Blue"));
104             }
105         }
106     }
107 
108     /**
109      * Add colorer.
110      * @param colorer colorer
111      * @param selected whether the colorer should be the selected one
112      * @throws IllegalStateException when the plot has multiple lanes, in which case colorers are not supported
113      */
114     public void addColorer(final TrajectoryColorer colorer, final boolean selected)
115     {
116         Throw.when(getPlot().getLaneCount() > 1, IllegalStateException.class,
117                 "Trajectory plots of multiple lanes do not support colorers.");
118         if (this.colorMenu == null)
119         {
120             // a sub-class may override addPopUpMenuItems() in which case there is perhaps no color menu
121             return;
122         }
123         JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(colorer.getName());
124         menuItem.addActionListener(new ActionListener()
125         {
126             @Override
127             public void actionPerformed(final ActionEvent e)
128             {
129                 SwingTrajectoryPlot.this.getPlot().setColorer(colorer);
130             }
131         });
132         this.colorButtonGroup.add(menuItem);
133         if (selected || this.colorButtonGroup.getButtonCount() == 1)
134         {
135             menuItem.setSelected(true);
136             SwingTrajectoryPlot.this.getPlot().setColorer(colorer);
137         }
138         menuItem.setFont(this.colorMenu.getFont());
139         this.colorMenu.add(menuItem);
140         this.colorMenu.setVisible(true);
141     }
142 
143     @Override
144     protected void addPopUpMenuItems(final JPopupMenu popupMenu)
145     {
146         super.addPopUpMenuItems(popupMenu);
147         if (getPlot().getLaneCount() == 1)
148         {
149             this.colorMenu = new JMenu("Color");
150             this.colorMenu.setVisible(false);
151             popupMenu.insert(this.colorMenu, 0);
152         }
153     }
154 
155     /**
156      * {@inheritDoc} This implementation creates a listener to disable and enable lanes through the legend, and to display
157      * density, flow of speed of a line.
158      */
159     @Override
160     protected Optional<ChartMouseListener> getChartMouseListener()
161     {
162         // Second listener for legend clicks
163         ChartMouseListener toggle = getPlot().getLaneCount() < 2 ? null
164                 : GraphUtil.getToggleSeriesByLegendListener(getPlot(), getPlot().getLegend(), getPlot().getLaneVisible());
165         return Optional.of(new ChartMouseListener()
166         {
167             @Override
168             public void chartMouseClicked(final ChartMouseEvent event)
169             {
170                 if (toggle != null)
171                 {
172                     toggle.chartMouseClicked(event); // forward to second listener
173                 }
174                 if (event.getEntity() instanceof PlotEntity)
175                 {
176                     removeAnnotations();
177                     if (SwingTrajectoryPlot.this.from == null)
178                     {
179                         if (event.getTrigger().isControlDown())
180                         {
181                             SwingTrajectoryPlot.this.density = false;
182                             SwingTrajectoryPlot.this.flow = false;
183                         }
184                         else if (event.getTrigger().isShiftDown())
185                         {
186                             SwingTrajectoryPlot.this.density = true;
187                             SwingTrajectoryPlot.this.flow = false;
188                         }
189                         else if (event.getTrigger().isAltDown())
190                         {
191                             SwingTrajectoryPlot.this.density = false;
192                             SwingTrajectoryPlot.this.flow = true;
193                         }
194                         else
195                         {
196                             SwingTrajectoryPlot.this.from = null;
197                             SwingTrajectoryPlot.this.to = null;
198                             return;
199                         }
200                         SwingTrajectoryPlot.this.from = getValuePoint(event);
201                         SwingTrajectoryPlot.this.to = null;
202                     }
203                     else
204                     {
205                         SwingTrajectoryPlot.this.to = getValuePoint(event);
206                         removeAnnotations();
207                         snap(SwingTrajectoryPlot.this.to);
208                         drawLine(SwingTrajectoryPlot.this.to);
209                         drawStatistics();
210                         SwingTrajectoryPlot.this.from = null;
211                         SwingTrajectoryPlot.this.to = null;
212                     }
213                 }
214             }
215 
216             @Override
217             public void chartMouseMoved(final ChartMouseEvent event)
218             {
219                 if (toggle != null)
220                 {
221                     toggle.chartMouseMoved(event); // forward to second listener
222                 }
223                 if (event.getEntity() instanceof PlotEntity && SwingTrajectoryPlot.this.from != null
224                         && SwingTrajectoryPlot.this.to == null)
225                 {
226                     removeAnnotations();
227                     Point2D.Double toPoint = getValuePoint(event);
228                     snap(toPoint);
229                     drawLine(toPoint);
230                 }
231             }
232 
233         });
234     }
235 
236     /**
237      * Returns point in data coordinates based on mouse coordinates.
238      * @param event event.
239      * @return point in data coordinates
240      */
241     private Point2D.Double getValuePoint(final ChartMouseEvent event)
242     {
243         Point2D p = getChartPanel().translateScreenToJava2D(new Point(event.getTrigger().getX(), event.getTrigger().getY()));
244         XYPlot plot = getChartPanel().getChart().getXYPlot();
245         Rectangle2D dataArea = getChartPanel().getChartRenderingInfo().getPlotInfo().getDataArea();
246         double x = plot.getDomainAxis().java2DToValue(p.getX(), dataArea, plot.getDomainAxisEdge());
247         double y = plot.getRangeAxis().java2DToValue(p.getY(), dataArea, plot.getRangeAxisEdge());
248         return new Point2D.Double(x, y);
249     }
250 
251     /**
252      * Draw line towards point.
253      * @param toPoint Point2D.Double; to point.
254      */
255     private void drawLine(final Point2D.Double toPoint)
256     {
257         this.lineAnnotation =
258                 new XYLineAnnotation(this.from.x, this.from.y, toPoint.x, toPoint.y, new BasicStroke(2.0f), Color.WHITE);
259         getPlot().getChart().getXYPlot().addAnnotation(this.lineAnnotation);
260     }
261 
262     /**
263      * Draw statistics label.
264      */
265     private void drawStatistics()
266     {
267         double dx = this.to.x - this.from.x;
268         double dy = this.to.y - this.from.y;
269         double v = 3.6 * dy / dx;
270 
271         String label;
272         if (this.density || this.flow)
273         {
274             int n = 0;
275             for (int i = 0; i < getPlot().getSeriesCount(); i++)
276             {
277                 // quick filter
278                 int k = getPlot().getItemCount(i) - 1;
279                 double x1 = Math.min(this.from.x, this.to.x);
280                 double y1 = Math.min(this.from.y, this.to.y);
281                 double x2 = Math.max(this.from.x, this.to.x);
282                 double y2 = Math.max(this.from.y, this.to.y);
283                 double x3 = Math.min(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
284                 double y3 = Math.min(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
285                 double x4 = Math.max(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
286                 double y4 = Math.max(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
287                 if (x3 <= x2 && y3 <= y2 && x1 <= x4 && y1 <= y4)
288                 {
289                     for (int j = 0; j < k; j++)
290                     {
291                         if (Point2d.intersectionOfLineSegments(this.from.x, this.from.y, this.to.x, this.to.y,
292                                 getPlot().getXValue(i, j), getPlot().getYValue(i, j), getPlot().getXValue(i, j + 1),
293                                 getPlot().getYValue(i, j + 1)) != null)
294                         {
295                             n++;
296                             break;
297                         }
298                     }
299                 }
300             }
301             if (this.density)
302             {
303                 label = String.format("%.1f veh/km", Math.abs(1000.0 * n / dy));
304             }
305             else
306             {
307                 label = String.format("%.1f veh/h", Math.abs(3600.0 * n / dx));
308             }
309         }
310         else
311         {
312             label = String.format("%.1f km/h", v);
313         }
314 
315         this.textAnnotation = new XYTextAnnotation(label, this.from.x, this.from.y);
316         getPlot().getChart().getXYPlot().addAnnotation(this.textAnnotation);
317 
318     }
319 
320     /**
321      * Remove line and statistic annotations, if any.
322      */
323     private void removeAnnotations()
324     {
325         if (SwingTrajectoryPlot.this.lineAnnotation != null)
326         {
327             getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.lineAnnotation);
328         }
329         if (SwingTrajectoryPlot.this.textAnnotation != null)
330         {
331             getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.textAnnotation);
332         }
333     }
334 
335     /**
336      * Snap to point for density or flow.
337      * @param toPoint Point2D.Double; to point
338      */
339     private void snap(final Point2D.Double toPoint)
340     {
341         if (this.density)
342         {
343             toPoint.x = this.from.x;
344         }
345         if (this.flow)
346         {
347             toPoint.y = this.from.y;
348         }
349     }
350 
351     /**
352      * Retrieve the plot.
353      * @return the plot
354      */
355     @Override
356     public TrajectoryPlot getPlot()
357     {
358         return (TrajectoryPlot) super.getPlot();
359     }
360 
361 }