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.geom.Point2D;
7   import java.awt.geom.Rectangle2D;
8   
9   import org.djutils.draw.point.Point2d;
10  import org.jfree.chart.ChartMouseEvent;
11  import org.jfree.chart.ChartMouseListener;
12  import org.jfree.chart.annotations.XYLineAnnotation;
13  import org.jfree.chart.annotations.XYTextAnnotation;
14  import org.jfree.chart.entity.PlotEntity;
15  import org.jfree.chart.plot.XYPlot;
16  import org.opentrafficsim.draw.graphs.GraphUtil;
17  import org.opentrafficsim.draw.graphs.TrajectoryPlot;
18  
19  /**
20   * Embed a TrajectoryPlot in a Swing JPanel.
21   * <p>
22   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
23   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
24   * </p>
25   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
26   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
27   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
28   */
29  public class SwingTrajectoryPlot extends SwingSpaceTimePlot
30  {
31      /** */
32      private static final long serialVersionUID = 20190823L;
33  
34      /** Calculate density (vertical line). */
35      private boolean density;
36  
37      /** Calculate flow ((horizontal line). */
38      private boolean flow;
39  
40      /** From point for line statistics. */
41      private Point2D.Double from;
42  
43      /** From point for line statistics. */
44      private Point2D.Double to;
45  
46      /** Line annotation for line statistics. */
47      private XYLineAnnotation lineAnnotation;
48  
49      /** Text annotation for line statistics. */
50      private XYTextAnnotation textAnnotation;
51  
52      /**
53       * Construct a new Swing container for a TrajectoryPlot.
54       * @param plot the plot to embed
55       */
56      public SwingTrajectoryPlot(final TrajectoryPlot plot)
57      {
58          super(plot);
59      }
60  
61      /**
62       * {@inheritDoc} This implementation creates a listener to disable and enable lanes through the legend, and to display
63       * density, flow of speed of a line.
64       */
65      @Override
66      protected ChartMouseListener getChartMouseListener()
67      {
68          // Second listener for legend clicks
69          ChartMouseListener toggle = getPlot().getPath().getNumberOfSeries() < 2 ? null
70                  : GraphUtil.getToggleSeriesByLegendListener(getPlot().getLegend(), getPlot().getLaneVisible());
71          return new ChartMouseListener()
72          {
73              @Override
74              public void chartMouseClicked(final ChartMouseEvent event)
75              {
76                  if (toggle != null)
77                  {
78                      toggle.chartMouseClicked(event); // forward to second listener
79                  }
80                  if (event.getEntity() instanceof PlotEntity)
81                  {
82                      removeAnnotations();
83                      if (SwingTrajectoryPlot.this.from == null)
84                      {
85                          if (event.getTrigger().isControlDown())
86                          {
87                              SwingTrajectoryPlot.this.density = false;
88                              SwingTrajectoryPlot.this.flow = false;
89                          }
90                          else if (event.getTrigger().isShiftDown())
91                          {
92                              SwingTrajectoryPlot.this.density = true;
93                              SwingTrajectoryPlot.this.flow = false;
94                          }
95                          else if (event.getTrigger().isAltDown())
96                          {
97                              SwingTrajectoryPlot.this.density = false;
98                              SwingTrajectoryPlot.this.flow = true;
99                          }
100                         else
101                         {
102                             SwingTrajectoryPlot.this.from = null;
103                             SwingTrajectoryPlot.this.to = null;
104                             return;
105                         }
106                         SwingTrajectoryPlot.this.from = getValuePoint(event);
107                         SwingTrajectoryPlot.this.to = null;
108                     }
109                     else
110                     {
111                         SwingTrajectoryPlot.this.to = getValuePoint(event);
112                         removeAnnotations();
113                         snap(SwingTrajectoryPlot.this.to);
114                         drawLine(SwingTrajectoryPlot.this.to);
115                         drawStatistics();
116                         SwingTrajectoryPlot.this.from = null;
117                         SwingTrajectoryPlot.this.to = null;
118                     }
119                 }
120             }
121 
122             @Override
123             public void chartMouseMoved(final ChartMouseEvent event)
124             {
125                 if (toggle != null)
126                 {
127                     toggle.chartMouseClicked(event); // forward to second listener
128                 }
129                 if (event.getEntity() instanceof PlotEntity && SwingTrajectoryPlot.this.from != null
130                         && SwingTrajectoryPlot.this.to == null)
131                 {
132                     removeAnnotations();
133                     Point2D.Double toPoint = getValuePoint(event);
134                     snap(toPoint);
135                     drawLine(toPoint);
136                 }
137             }
138 
139         };
140     }
141 
142     /**
143      * Returns point in data coordinates based on mouse coordinates.
144      * @param event event.
145      * @return point in data coordinates
146      */
147     private Point2D.Double getValuePoint(final ChartMouseEvent event)
148     {
149         Point2D p = getChartPanel().translateScreenToJava2D(new Point(event.getTrigger().getX(), event.getTrigger().getY()));
150         XYPlot plot = getChartPanel().getChart().getXYPlot();
151         Rectangle2D dataArea = getChartPanel().getChartRenderingInfo().getPlotInfo().getDataArea();
152         double x = plot.getDomainAxis().java2DToValue(p.getX(), dataArea, plot.getDomainAxisEdge());
153         double y = plot.getRangeAxis().java2DToValue(p.getY(), dataArea, plot.getRangeAxisEdge());
154         return new Point2D.Double(x, y);
155     }
156 
157     /**
158      * Draw line towards point.
159      * @param toPoint Point2D.Double; to point.
160      */
161     private void drawLine(final Point2D.Double toPoint)
162     {
163         this.lineAnnotation =
164                 new XYLineAnnotation(this.from.x, this.from.y, toPoint.x, toPoint.y, new BasicStroke(2.0f), Color.WHITE);
165         getPlot().getChart().getXYPlot().addAnnotation(this.lineAnnotation);
166     }
167 
168     /**
169      * Draw statistics label.
170      */
171     private void drawStatistics()
172     {
173         double dx = this.to.x - this.from.x;
174         double dy = this.to.y - this.from.y;
175         double v = 3.6 * dy / dx;
176 
177         String label;
178         if (this.density || this.flow)
179         {
180             int n = 0;
181             for (int i = 0; i < getPlot().getSeriesCount(); i++)
182             {
183                 // quick filter
184                 int k = getPlot().getItemCount(i) - 1;
185                 double x1 = Math.min(this.from.x, this.to.x);
186                 double y1 = Math.min(this.from.y, this.to.y);
187                 double x2 = Math.max(this.from.x, this.to.x);
188                 double y2 = Math.max(this.from.y, this.to.y);
189                 double x3 = Math.min(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
190                 double y3 = Math.min(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
191                 double x4 = Math.max(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
192                 double y4 = Math.max(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
193                 if (x3 <= x2 && y3 <= y2 && x1 <= x4 && y1 <= y4)
194                 {
195                     for (int j = 0; j < k; j++)
196                     {
197                         if (Point2d.intersectionOfLineSegments(this.from.x, this.from.y, this.to.x, this.to.y,
198                                 getPlot().getXValue(i, j), getPlot().getYValue(i, j), getPlot().getXValue(i, j + 1),
199                                 getPlot().getYValue(i, j + 1)) != null)
200                         {
201                             n++;
202                             break;
203                         }
204                     }
205                 }
206             }
207             if (this.density)
208             {
209                 label = String.format("%.1f veh/km", Math.abs(1000.0 * n / dy));
210             }
211             else
212             {
213                 label = String.format("%.1f veh/h", Math.abs(3600.0 * n / dx));
214             }
215         }
216         else
217         {
218             label = String.format("%.1f km/h", v);
219         }
220 
221         this.textAnnotation = new XYTextAnnotation(label, this.from.x, this.from.y);
222         getPlot().getChart().getXYPlot().addAnnotation(this.textAnnotation);
223 
224     }
225 
226     /**
227      * Remove line and statistic annotations, if any.
228      */
229     private void removeAnnotations()
230     {
231         if (SwingTrajectoryPlot.this.lineAnnotation != null)
232         {
233             getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.lineAnnotation);
234         }
235         if (SwingTrajectoryPlot.this.textAnnotation != null)
236         {
237             getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.textAnnotation);
238         }
239     }
240 
241     /**
242      * Snap to point for density or flow.
243      * @param toPoint Point2D.Double; to point
244      */
245     private void snap(final Point2D.Double toPoint)
246     {
247         if (this.density)
248         {
249             toPoint.x = this.from.x;
250         }
251         if (this.flow)
252         {
253             toPoint.y = this.from.y;
254         }
255     }
256 
257     /**
258      * Retrieve the plot.
259      * @return the plot
260      */
261     @Override
262     public TrajectoryPlot getPlot()
263     {
264         return (TrajectoryPlot) super.getPlot();
265     }
266 
267 }