SwingTrajectoryPlot.java
package org.opentrafficsim.swing.graphs;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Optional;
import javax.swing.ButtonGroup;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import org.djutils.draw.point.Point2d;
import org.djutils.exceptions.Throw;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.annotations.XYLineAnnotation;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.entity.PlotEntity;
import org.jfree.chart.plot.XYPlot;
import org.opentrafficsim.draw.colorer.trajectory.AccelerationTrajectoryColorer;
import org.opentrafficsim.draw.colorer.trajectory.FixedTrajectoryColorer;
import org.opentrafficsim.draw.colorer.trajectory.IdTrajectoryColorer;
import org.opentrafficsim.draw.colorer.trajectory.SpeedTrajectoryColorer;
import org.opentrafficsim.draw.colorer.trajectory.TrajectoryColorer;
import org.opentrafficsim.draw.graphs.GraphUtil;
import org.opentrafficsim.draw.graphs.TrajectoryPlot;
/**
* Embed a TrajectoryPlot in a Swing JPanel.
* <p>
* Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public class SwingTrajectoryPlot extends SwingSpaceTimePlot
{
/** */
private static final long serialVersionUID = 20190823L;
/** Calculate density (vertical line). */
private boolean density;
/** Calculate flow ((horizontal line). */
private boolean flow;
/** From point for line statistics. */
private Point2D.Double from;
/** From point for line statistics. */
private Point2D.Double to;
/** Line annotation for line statistics. */
private XYLineAnnotation lineAnnotation;
/** Text annotation for line statistics. */
private XYTextAnnotation textAnnotation;
/** Menu to select color. */
private JMenu colorMenu;
/** Color button group (so one is selected at a time). */
private ButtonGroup colorButtonGroup = new ButtonGroup();
/**
* Construct a new Swing container for a TrajectoryPlot. Default colorers for blue, speed, id and acceleration are used if
* the plot has a single lane.
* @param plot the plot to embed
*/
public SwingTrajectoryPlot(final TrajectoryPlot plot)
{
this(plot, true);
}
/**
* Constructor. Default colorers might be set based on the input, but only when the plot has a single lane.
* @param plot the plot to embed
* @param defaultColorers whether to use default colorers for blue, speed, id and acceleration
*/
public SwingTrajectoryPlot(final TrajectoryPlot plot, final boolean defaultColorers)
{
super(plot);
if (plot.getLaneCount() == 1)
{
if (defaultColorers)
{
addColorer(new FixedTrajectoryColorer(Color.BLUE, "Blue"), true);
addColorer(new IdTrajectoryColorer(), false);
addColorer(new SpeedTrajectoryColorer(), false);
addColorer(new AccelerationTrajectoryColorer(), false);
}
else
{
// Make sure a single-lane plot has some colorer, even if non will be set through addColorer()
plot.setColorer(new FixedTrajectoryColorer(Color.BLUE, "Blue"));
}
}
}
/**
* Add colorer.
* @param colorer colorer
* @param selected whether the colorer should be the selected one
* @throws IllegalStateException when the plot has multiple lanes, in which case colorers are not supported
*/
public void addColorer(final TrajectoryColorer colorer, final boolean selected)
{
Throw.when(getPlot().getLaneCount() > 1, IllegalStateException.class,
"Trajectory plots of multiple lanes do not support colorers.");
if (this.colorMenu == null)
{
// a sub-class may override addPopUpMenuItems() in which case there is perhaps no color menu
return;
}
JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(colorer.getName());
menuItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(final ActionEvent e)
{
SwingTrajectoryPlot.this.getPlot().setColorer(colorer);
SwingTrajectoryPlot.this.getPlot().update();
}
});
this.colorButtonGroup.add(menuItem);
if (selected || this.colorButtonGroup.getButtonCount() == 1)
{
menuItem.setSelected(true);
SwingTrajectoryPlot.this.getPlot().setColorer(colorer);
}
menuItem.setFont(this.colorMenu.getFont());
this.colorMenu.add(menuItem);
this.colorMenu.setVisible(true);
}
@Override
protected void addPopUpMenuItems(final JPopupMenu popupMenu)
{
super.addPopUpMenuItems(popupMenu);
if (getPlot().getLaneCount() == 1)
{
this.colorMenu = new JMenu("Color");
this.colorMenu.setVisible(false);
popupMenu.insert(this.colorMenu, 0);
}
}
/**
* {@inheritDoc} This implementation creates a listener to disable and enable lanes through the legend, and to display
* density, flow of speed of a line.
*/
@Override
protected Optional<ChartMouseListener> getChartMouseListener()
{
// Second listener for legend clicks
ChartMouseListener toggle = getPlot().getPath().getNumberOfSeries() < 2 ? null
: GraphUtil.getToggleSeriesByLegendListener(getPlot().getLegend(), getPlot().getLaneVisible());
return Optional.of(new ChartMouseListener()
{
@Override
public void chartMouseClicked(final ChartMouseEvent event)
{
if (toggle != null)
{
toggle.chartMouseClicked(event); // forward to second listener
}
if (event.getEntity() instanceof PlotEntity)
{
removeAnnotations();
if (SwingTrajectoryPlot.this.from == null)
{
if (event.getTrigger().isControlDown())
{
SwingTrajectoryPlot.this.density = false;
SwingTrajectoryPlot.this.flow = false;
}
else if (event.getTrigger().isShiftDown())
{
SwingTrajectoryPlot.this.density = true;
SwingTrajectoryPlot.this.flow = false;
}
else if (event.getTrigger().isAltDown())
{
SwingTrajectoryPlot.this.density = false;
SwingTrajectoryPlot.this.flow = true;
}
else
{
SwingTrajectoryPlot.this.from = null;
SwingTrajectoryPlot.this.to = null;
return;
}
SwingTrajectoryPlot.this.from = getValuePoint(event);
SwingTrajectoryPlot.this.to = null;
}
else
{
SwingTrajectoryPlot.this.to = getValuePoint(event);
removeAnnotations();
snap(SwingTrajectoryPlot.this.to);
drawLine(SwingTrajectoryPlot.this.to);
drawStatistics();
SwingTrajectoryPlot.this.from = null;
SwingTrajectoryPlot.this.to = null;
}
}
}
@Override
public void chartMouseMoved(final ChartMouseEvent event)
{
if (toggle != null)
{
toggle.chartMouseMoved(event); // forward to second listener
}
if (event.getEntity() instanceof PlotEntity && SwingTrajectoryPlot.this.from != null
&& SwingTrajectoryPlot.this.to == null)
{
removeAnnotations();
Point2D.Double toPoint = getValuePoint(event);
snap(toPoint);
drawLine(toPoint);
}
}
});
}
/**
* Returns point in data coordinates based on mouse coordinates.
* @param event event.
* @return point in data coordinates
*/
private Point2D.Double getValuePoint(final ChartMouseEvent event)
{
Point2D p = getChartPanel().translateScreenToJava2D(new Point(event.getTrigger().getX(), event.getTrigger().getY()));
XYPlot plot = getChartPanel().getChart().getXYPlot();
Rectangle2D dataArea = getChartPanel().getChartRenderingInfo().getPlotInfo().getDataArea();
double x = plot.getDomainAxis().java2DToValue(p.getX(), dataArea, plot.getDomainAxisEdge());
double y = plot.getRangeAxis().java2DToValue(p.getY(), dataArea, plot.getRangeAxisEdge());
return new Point2D.Double(x, y);
}
/**
* Draw line towards point.
* @param toPoint Point2D.Double; to point.
*/
private void drawLine(final Point2D.Double toPoint)
{
this.lineAnnotation =
new XYLineAnnotation(this.from.x, this.from.y, toPoint.x, toPoint.y, new BasicStroke(2.0f), Color.WHITE);
getPlot().getChart().getXYPlot().addAnnotation(this.lineAnnotation);
}
/**
* Draw statistics label.
*/
private void drawStatistics()
{
double dx = this.to.x - this.from.x;
double dy = this.to.y - this.from.y;
double v = 3.6 * dy / dx;
String label;
if (this.density || this.flow)
{
int n = 0;
for (int i = 0; i < getPlot().getSeriesCount(); i++)
{
// quick filter
int k = getPlot().getItemCount(i) - 1;
double x1 = Math.min(this.from.x, this.to.x);
double y1 = Math.min(this.from.y, this.to.y);
double x2 = Math.max(this.from.x, this.to.x);
double y2 = Math.max(this.from.y, this.to.y);
double x3 = Math.min(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
double y3 = Math.min(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
double x4 = Math.max(getPlot().getXValue(i, 0), getPlot().getXValue(i, k));
double y4 = Math.max(getPlot().getYValue(i, 0), getPlot().getYValue(i, k));
if (x3 <= x2 && y3 <= y2 && x1 <= x4 && y1 <= y4)
{
for (int j = 0; j < k; j++)
{
if (Point2d.intersectionOfLineSegments(this.from.x, this.from.y, this.to.x, this.to.y,
getPlot().getXValue(i, j), getPlot().getYValue(i, j), getPlot().getXValue(i, j + 1),
getPlot().getYValue(i, j + 1)) != null)
{
n++;
break;
}
}
}
}
if (this.density)
{
label = String.format("%.1f veh/km", Math.abs(1000.0 * n / dy));
}
else
{
label = String.format("%.1f veh/h", Math.abs(3600.0 * n / dx));
}
}
else
{
label = String.format("%.1f km/h", v);
}
this.textAnnotation = new XYTextAnnotation(label, this.from.x, this.from.y);
getPlot().getChart().getXYPlot().addAnnotation(this.textAnnotation);
}
/**
* Remove line and statistic annotations, if any.
*/
private void removeAnnotations()
{
if (SwingTrajectoryPlot.this.lineAnnotation != null)
{
getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.lineAnnotation);
}
if (SwingTrajectoryPlot.this.textAnnotation != null)
{
getPlot().getChart().getXYPlot().removeAnnotation(SwingTrajectoryPlot.this.textAnnotation);
}
}
/**
* Snap to point for density or flow.
* @param toPoint Point2D.Double; to point
*/
private void snap(final Point2D.Double toPoint)
{
if (this.density)
{
toPoint.x = this.from.x;
}
if (this.flow)
{
toPoint.y = this.from.y;
}
}
/**
* Retrieve the plot.
* @return the plot
*/
@Override
public TrajectoryPlot getPlot()
{
return (TrajectoryPlot) super.getPlot();
}
}