1 package net.sf.openrocket.gui.plot;
3 import java.awt.AlphaComposite;
4 import java.awt.BasicStroke;
6 import java.awt.Composite;
8 import java.awt.Graphics2D;
10 import java.awt.Window;
11 import java.awt.event.ActionEvent;
12 import java.awt.event.ActionListener;
13 import java.awt.geom.Line2D;
14 import java.awt.geom.Point2D;
15 import java.awt.geom.Rectangle2D;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
25 import javax.imageio.ImageIO;
26 import javax.swing.BorderFactory;
27 import javax.swing.JButton;
28 import javax.swing.JCheckBox;
29 import javax.swing.JDialog;
30 import javax.swing.JLabel;
31 import javax.swing.JPanel;
33 import net.miginfocom.swing.MigLayout;
34 import net.sf.openrocket.document.Simulation;
35 import net.sf.openrocket.gui.components.StyledLabel;
36 import net.sf.openrocket.gui.util.GUIUtil;
37 import net.sf.openrocket.l10n.Translator;
38 import net.sf.openrocket.simulation.FlightDataBranch;
39 import net.sf.openrocket.simulation.FlightDataType;
40 import net.sf.openrocket.simulation.FlightEvent;
41 import net.sf.openrocket.startup.Application;
42 import net.sf.openrocket.startup.Preferences;
43 import net.sf.openrocket.unit.Unit;
44 import net.sf.openrocket.unit.UnitGroup;
45 import net.sf.openrocket.util.BugException;
46 import net.sf.openrocket.util.MathUtil;
48 import org.jfree.chart.ChartFactory;
49 import org.jfree.chart.ChartPanel;
50 import org.jfree.chart.JFreeChart;
51 import org.jfree.chart.annotations.XYImageAnnotation;
52 import org.jfree.chart.axis.NumberAxis;
53 import org.jfree.chart.axis.ValueAxis;
54 import org.jfree.chart.plot.Marker;
55 import org.jfree.chart.plot.PlotOrientation;
56 import org.jfree.chart.plot.ValueMarker;
57 import org.jfree.chart.plot.XYPlot;
58 import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
59 import org.jfree.chart.title.TextTitle;
60 import org.jfree.data.Range;
61 import org.jfree.data.xy.XYSeries;
62 import org.jfree.data.xy.XYSeriesCollection;
63 import org.jfree.text.TextUtilities;
64 import org.jfree.ui.LengthAdjustmentType;
65 import org.jfree.ui.RectangleAnchor;
66 import org.jfree.ui.TextAnchor;
69 * Dialog that shows a plot of a simulation results based on user options.
71 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
73 public class SimulationPlotDialog extends JDialog {
75 private static final float PLOT_STROKE_WIDTH = 1.5f;
76 private static final Translator trans = Application.getTranslator();
78 private static final Color DEFAULT_EVENT_COLOR = new Color(0, 0, 0);
79 private static final Map<FlightEvent.Type, Color> EVENT_COLORS =
80 new HashMap<FlightEvent.Type, Color>();
82 EVENT_COLORS.put(FlightEvent.Type.LAUNCH, new Color(255, 0, 0));
83 EVENT_COLORS.put(FlightEvent.Type.LIFTOFF, new Color(0, 80, 196));
84 EVENT_COLORS.put(FlightEvent.Type.LAUNCHROD, new Color(0, 100, 80));
85 EVENT_COLORS.put(FlightEvent.Type.IGNITION, new Color(230, 130, 15));
86 EVENT_COLORS.put(FlightEvent.Type.BURNOUT, new Color(80, 55, 40));
87 EVENT_COLORS.put(FlightEvent.Type.EJECTION_CHARGE, new Color(80, 55, 40));
88 EVENT_COLORS.put(FlightEvent.Type.STAGE_SEPARATION, new Color(80, 55, 40));
89 EVENT_COLORS.put(FlightEvent.Type.APOGEE, new Color(15, 120, 15));
90 EVENT_COLORS.put(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, new Color(0, 0, 128));
91 EVENT_COLORS.put(FlightEvent.Type.GROUND_HIT, new Color(0, 0, 0));
92 EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128, 0, 0));
95 private static final Map<FlightEvent.Type, Image> EVENT_IMAGES =
96 new HashMap<FlightEvent.Type, Image>();
98 loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
99 loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
100 loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
101 loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
102 loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
103 loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png");
104 loadImage(FlightEvent.Type.STAGE_SEPARATION,
105 "pix/eventicons/event-stage-separation.png");
106 loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
107 loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
108 "pix/eventicons/event-recovery-device-deployment.png");
109 loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
110 loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
113 private static void loadImage(FlightEvent.Type type, String file) {
116 is = ClassLoader.getSystemResourceAsStream(file);
118 System.out.println("ERROR: File " + file + " not found!");
123 Image image = ImageIO.read(is);
124 EVENT_IMAGES.put(type, image);
125 } catch (IOException ignore) {
126 ignore.printStackTrace();
133 private final List<ModifiedXYItemRenderer> renderers =
134 new ArrayList<ModifiedXYItemRenderer>();
136 private SimulationPlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
137 //// Flight data plot
138 super(parent, trans.get("PlotDialog.title.Flightdataplot"));
139 this.setModalityType(ModalityType.DOCUMENT_MODAL);
141 final boolean initialShowPoints = Application.getPreferences().getBoolean(Preferences.PLOT_SHOW_POINTS, false);
144 // Fill the auto-selections
145 FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
146 PlotConfiguration filled = config.fillAutoAxes(branch);
147 List<Axis> axes = filled.getAllAxes();
150 // Create the data series for both axes
151 XYSeriesCollection[] data = new XYSeriesCollection[2];
152 data[0] = new XYSeriesCollection();
153 data[1] = new XYSeriesCollection();
156 // Get the domain axis type
157 final FlightDataType domainType = filled.getDomainAxisType();
158 final Unit domainUnit = filled.getDomainAxisUnit();
159 if (domainType == null) {
160 throw new IllegalArgumentException("Domain axis type not specified.");
162 List<Double> x = branch.get(domainType);
165 // Get plot length (ignore trailing NaN's)
166 int typeCount = filled.getTypeCount();
168 for (int i = 0; i < typeCount; i++) {
169 FlightDataType type = filled.getType(i);
170 List<Double> y = branch.get(type);
172 for (int j = dataLength; j < y.size(); j++) {
173 if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j)))
177 dataLength = Math.min(dataLength, x.size());
180 // Create the XYSeries objects from the flight data and store into the collections
181 String[] axisLabel = new String[2];
182 for (int i = 0; i < typeCount; i++) {
184 FlightDataType type = filled.getType(i);
185 Unit unit = filled.getUnit(i);
186 int axis = filled.getAxis(i);
187 String name = getLabel(type, unit);
189 // Store data in provided units
190 List<Double> y = branch.get(type);
191 XYSeries series = new XYSeries(name, false, true);
192 for (int j = 0; j < dataLength; j++) {
193 series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
195 data[axis].addSeries(series);
198 if (axisLabel[axis] == null)
199 axisLabel[axis] = type.getName();
201 axisLabel[axis] += "; " + type.getName();
205 // Create the chart using the factory to get all default settings
206 JFreeChart chart = ChartFactory.createXYLineChart(
207 //// Simulated flight
208 trans.get("PlotDialog.Chart.Simulatedflight"),
212 PlotOrientation.VERTICAL,
218 chart.addSubtitle(new TextTitle(config.getName()));
220 // Add the data and formatting to the plot
221 XYPlot plot = chart.getXYPlot();
223 for (int i = 0; i < 2; i++) {
224 // Check whether axis has any data
225 if (data[i].getSeriesCount() > 0) {
226 // Create and set axis
227 double min = axes.get(i).getMinValue();
228 double max = axes.get(i).getMaxValue();
229 NumberAxis axis = new PresetNumberAxis(min, max);
230 axis.setLabel(axisLabel[i]);
231 // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
232 plot.setRangeAxis(axisno, axis);
234 // Add data and map to the axis
235 plot.setDataset(axisno, data[i]);
236 ModifiedXYItemRenderer r = new ModifiedXYItemRenderer();
237 r.setBaseShapesVisible(initialShowPoints);
238 r.setBaseShapesFilled(true);
239 for (int j = 0; j < data[i].getSeriesCount(); j++) {
240 r.setSeriesStroke(j, new BasicStroke(PLOT_STROKE_WIDTH));
243 plot.setRenderer(axisno, r);
244 plot.mapDatasetToRangeAxis(axisno, axisno);
249 plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit));
250 plot.addDomainMarker(new ValueMarker(0));
251 plot.addRangeMarker(new ValueMarker(0));
255 // Create list of events to show (combine event too close to each other)
256 ArrayList<Double> timeList = new ArrayList<Double>();
257 ArrayList<String> eventList = new ArrayList<String>();
258 ArrayList<Color> colorList = new ArrayList<Color>();
259 ArrayList<Image> imageList = new ArrayList<Image>();
261 HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
263 double prevTime = -100;
268 List<FlightEvent> events = branch.getEvents();
269 for (int i = 0; i < events.size(); i++) {
270 FlightEvent event = events.get(i);
271 double t = event.getTime();
272 FlightEvent.Type type = event.getType();
274 if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) {
275 if (Math.abs(t - prevTime) <= 0.01) {
277 if (!typeSet.contains(type)) {
278 text = text + ", " + type.toString();
279 color = getEventColor(type);
280 image = EVENT_IMAGES.get(type);
287 timeList.add(prevTime);
289 colorList.add(color);
290 imageList.add(image);
293 text = type.toString();
294 color = getEventColor(type);
295 image = EVENT_IMAGES.get(type);
303 timeList.add(prevTime);
305 colorList.add(color);
306 imageList.add(image);
310 // Create the event markers
312 if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) {
314 // Domain time is plotted as vertical markers
315 for (int i = 0; i < eventList.size(); i++) {
316 double t = timeList.get(i);
317 String event = eventList.get(i);
318 color = colorList.get(i);
320 ValueMarker m = new ValueMarker(t);
323 m.setLabelPaint(color);
325 plot.addDomainMarker(m);
330 // Other domains are plotted as image annotations
331 List<Double> time = branch.get(FlightDataType.TYPE_TIME);
332 List<Double> domain = branch.get(config.getDomainAxisType());
334 for (int i = 0; i < eventList.size(); i++) {
335 final double t = timeList.get(i);
336 String event = eventList.get(i);
337 image = imageList.get(i);
342 // Calculate index and interpolation position a
344 int tindex = Collections.binarySearch(time, t);
346 tindex = -tindex - 1;
348 if (tindex >= time.size()) {
349 // index greater than largest value in time list
350 tindex = time.size() - 1;
352 } else if (tindex <= 0) {
353 // index smaller than smallest value in time list
358 double t1 = time.get(tindex);
359 double t2 = time.get(tindex + 1);
361 if ((t1 > t) || (t2 < t)) {
362 throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t);
365 if (MathUtil.equals(t1, t2)) {
368 a = 1 - (t - t1) / (t2 - t1);
374 xcoord = domain.get(tindex);
376 xcoord = a * domain.get(tindex) + (1 - a) * domain.get(tindex + 1);
379 for (int index = 0; index < config.getTypeCount(); index++) {
380 FlightDataType type = config.getType(index);
381 List<Double> range = branch.get(type);
383 // Image annotations are not supported on the right-side axis
384 // TODO: LOW: Can this be achieved by JFreeChart?
385 if (filled.getAxis(index) != SimulationPlotPanel.LEFT) {
391 ycoord = range.get(tindex);
393 ycoord = a * range.get(tindex) + (1 - a) * range.get(tindex + 1);
397 xcoord = config.getDomainAxisUnit().toUnit(xcoord);
398 ycoord = config.getUnit(index).toUnit(ycoord);
400 XYImageAnnotation annotation =
401 new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
402 annotation.setToolTipText(event);
403 plot.addAnnotation(annotation);
411 JPanel panel = new JPanel(new MigLayout("fill"));
414 ChartPanel chartPanel = new ChartPanel(chart,
420 chartPanel.setMouseWheelEnabled(true);
421 chartPanel.setEnforceFileExtensions(true);
422 chartPanel.setInitialDelay(500);
424 chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
426 panel.add(chartPanel, "grow, wrap 20lp");
428 //// Show data points
429 final JCheckBox check = new JCheckBox(trans.get("PlotDialog.CheckBox.Showdatapoints"));
430 check.setSelected(initialShowPoints);
431 check.addActionListener(new ActionListener() {
433 public void actionPerformed(ActionEvent e) {
434 boolean show = check.isSelected();
435 Application.getPreferences().putBoolean(Preferences.PLOT_SHOW_POINTS, show);
436 for (ModifiedXYItemRenderer r : renderers) {
437 r.setBaseShapesVisible(show);
441 panel.add(check, "split, left");
444 JLabel label = new StyledLabel(trans.get("PlotDialog.lbl.Chart"), -2);
445 panel.add(label, "gapleft para");
448 panel.add(new JPanel(), "growx");
451 JButton button = new JButton(trans.get("dlg.but.close"));
452 button.addActionListener(new ActionListener() {
454 public void actionPerformed(ActionEvent e) {
455 SimulationPlotDialog.this.dispose();
458 panel.add(button, "right");
460 this.setLocationByPlatform(true);
463 GUIUtil.setDisposableDialogOptions(this, button);
464 GUIUtil.rememberWindowSize(this);
467 private String getLabel(FlightDataType type, Unit unit) {
468 String name = type.getName();
469 if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
470 !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
471 name += " (" + unit.getUnit() + ")";
477 private class PresetNumberAxis extends NumberAxis {
478 private final double min;
479 private final double max;
481 public PresetNumberAxis(double min, double max) {
488 protected void autoAdjustRange() {
489 this.setRange(min, max);
495 * Static method that shows a plot with the specified parameters.
497 * @param parent the parent window, which will be blocked.
498 * @param simulation the simulation to plot.
499 * @param config the configuration of the plot.
501 public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
502 new SimulationPlotDialog(parent, simulation, config).setVisible(true);
507 private static Color getEventColor(FlightEvent.Type type) {
508 Color c = EVENT_COLORS.get(type);
511 return DEFAULT_EVENT_COLOR;
519 * A modification to the standard renderer that renders the domain marker
520 * labels vertically instead of horizontally.
522 private static class ModifiedXYItemRenderer extends StandardXYItemRenderer {
525 public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis,
526 Marker marker, Rectangle2D dataArea) {
528 if (!(marker instanceof ValueMarker)) {
529 // Use parent for all others
530 super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea);
535 * Draw the normal marker, but with rotated text.
536 * Copied from the overridden method.
538 ValueMarker vm = (ValueMarker) marker;
539 double value = vm.getValue();
540 Range range = domainAxis.getRange();
541 if (!range.contains(value)) {
545 double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge());
547 PlotOrientation orientation = plot.getOrientation();
549 if (orientation == PlotOrientation.HORIZONTAL) {
550 line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v);
552 line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY());
555 final Composite originalComposite = g2.getComposite();
556 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker
558 g2.setPaint(marker.getPaint());
559 g2.setStroke(marker.getStroke());
562 String label = marker.getLabel();
563 RectangleAnchor anchor = marker.getLabelAnchor();
565 Font labelFont = marker.getLabelFont();
566 g2.setFont(labelFont);
567 g2.setPaint(marker.getLabelPaint());
568 Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2,
569 orientation, dataArea, line.getBounds2D(), marker
570 .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor);
573 TextAnchor textAnchor = TextAnchor.TOP_RIGHT;
574 TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2,
575 (float) coordinates.getY(), textAnchor,
576 -Math.PI / 2, textAnchor);
578 g2.setComposite(originalComposite);