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.draw.colorer.trajectory.AccelerationTrajectoryColorer;
26  import org.opentrafficsim.draw.colorer.trajectory.FixedTrajectoryColorer;
27  import org.opentrafficsim.draw.colorer.trajectory.IdTrajectoryColorer;
28  import org.opentrafficsim.draw.colorer.trajectory.SpeedTrajectoryColorer;
29  import org.opentrafficsim.draw.colorer.trajectory.TrajectoryColorer;
30  import org.opentrafficsim.draw.graphs.GraphUtil;
31  import org.opentrafficsim.draw.graphs.TrajectoryPlot;
32  
33  /**
34   * Embed a TrajectoryPlot in a Swing JPanel.
35   * <p>
36   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
37   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
38   * </p>
39   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
40   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
41   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
42   */
43  public class SwingTrajectoryPlot extends SwingSpaceTimePlot
44  {
45      /** */
46      private static final long serialVersionUID = 20190823L;
47  
48      /** Calculate density (vertical line). */
49      private boolean density;
50  
51      /** Calculate flow ((horizontal line). */
52      private boolean flow;
53  
54      /** From point for line statistics. */
55      private Point2D.Double from;
56  
57      /** From point for line statistics. */
58      private Point2D.Double to;
59  
60      /** Line annotation for line statistics. */
61      private XYLineAnnotation lineAnnotation;
62  
63      /** Text annotation for line statistics. */
64      private XYTextAnnotation textAnnotation;
65  
66      /** Menu to select color. */
67      private JMenu colorMenu;
68  
69      /** Color button group (so one is selected at a time). */
70      private ButtonGroup colorButtonGroup = new ButtonGroup();
71  
72      /**
73       * Construct a new Swing container for a TrajectoryPlot. Default colorers for blue, speed, id and acceleration are used if
74       * the plot has a single lane.
75       * @param plot the plot to embed
76       */
77      public SwingTrajectoryPlot(final TrajectoryPlot plot)
78      {
79          this(plot, true);
80      }
81  
82      /**
83       * Constructor. Default colorers might be set based on the input, but only when the plot has a single lane.
84       * @param plot the plot to embed
85       * @param defaultColorers whether to use default colorers for blue, speed, id and acceleration
86       */
87      public SwingTrajectoryPlot(final TrajectoryPlot plot, final boolean defaultColorers)
88      {
89          super(plot);
90          if (plot.getLaneCount() == 1)
91          {
92              if (defaultColorers)
93              {
94                  addColorer(new FixedTrajectoryColorer(Color.BLUE, "Blue"), true);
95                  addColorer(new IdTrajectoryColorer(), false);
96                  addColorer(new SpeedTrajectoryColorer(), false);
97                  addColorer(new AccelerationTrajectoryColorer(), false);
98              }
99              else
100             {
101                 // Make sure a single-lane plot has some colorer, even if non will be set through addColorer()
102                 plot.setColorer(new FixedTrajectoryColorer(Color.BLUE, "Blue"));
103             }
104         }
105     }
106 
107     /**
108      * Add colorer.
109      * @param colorer colorer
110      * @param selected whether the colorer should be the selected one
111      * @throws IllegalStateException when the plot has multiple lanes, in which case colorers are not supported
112      */
113     public void addColorer(final TrajectoryColorer colorer, final boolean selected)
114     {
115         Throw.when(getPlot().getLaneCount() > 1, IllegalStateException.class,
116                 "Trajectory plots of multiple lanes do not support colorers.");
117         if (this.colorMenu == null)
118         {
119             // a sub-class may override addPopUpMenuItems() in which case there is perhaps no color menu
120             return;
121         }
122         JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(colorer.getName());
123         menuItem.addActionListener(new ActionListener()
124         {
125             @Override
126             public void actionPerformed(final ActionEvent e)
127             {
128                 SwingTrajectoryPlot.this.getPlot().setColorer(colorer);
129                 SwingTrajectoryPlot.this.getPlot().update();
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().getPath().getNumberOfSeries() < 2 ? null
164                 : GraphUtil.getToggleSeriesByLegendListener(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 }