1 package net.sf.openrocket.gui.plot;
3 import java.awt.AlphaComposite;
5 import java.awt.Composite;
7 import java.awt.Graphics2D;
9 import java.awt.Window;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.geom.Line2D;
13 import java.awt.geom.Point2D;
14 import java.awt.geom.Rectangle2D;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
24 import javax.imageio.ImageIO;
25 import javax.swing.BorderFactory;
26 import javax.swing.JButton;
27 import javax.swing.JCheckBox;
28 import javax.swing.JDialog;
29 import javax.swing.JPanel;
31 import net.miginfocom.swing.MigLayout;
32 import net.sf.openrocket.document.Simulation;
33 import net.sf.openrocket.simulation.FlightDataBranch;
34 import net.sf.openrocket.simulation.FlightEvent;
35 import net.sf.openrocket.unit.Unit;
36 import net.sf.openrocket.unit.UnitGroup;
37 import net.sf.openrocket.util.BugException;
38 import net.sf.openrocket.util.GUIUtil;
39 import net.sf.openrocket.util.MathUtil;
40 import net.sf.openrocket.util.Pair;
41 import net.sf.openrocket.util.Prefs;
43 import org.jfree.chart.ChartFactory;
44 import org.jfree.chart.ChartPanel;
45 import org.jfree.chart.JFreeChart;
46 import org.jfree.chart.annotations.XYImageAnnotation;
47 import org.jfree.chart.axis.NumberAxis;
48 import org.jfree.chart.axis.ValueAxis;
49 import org.jfree.chart.plot.Marker;
50 import org.jfree.chart.plot.PlotOrientation;
51 import org.jfree.chart.plot.ValueMarker;
52 import org.jfree.chart.plot.XYPlot;
53 import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
54 import org.jfree.chart.title.TextTitle;
55 import org.jfree.data.Range;
56 import org.jfree.data.xy.XYSeries;
57 import org.jfree.data.xy.XYSeriesCollection;
58 import org.jfree.text.TextUtilities;
59 import org.jfree.ui.LengthAdjustmentType;
60 import org.jfree.ui.RectangleAnchor;
61 import org.jfree.ui.TextAnchor;
63 public class PlotDialog extends JDialog {
65 private static final Color DEFAULT_EVENT_COLOR = new Color(0,0,0);
66 private static final Map<FlightEvent.Type, Color> EVENT_COLORS =
67 new HashMap<FlightEvent.Type, Color>();
69 EVENT_COLORS.put(FlightEvent.Type.LAUNCH, new Color(255,0,0));
70 EVENT_COLORS.put(FlightEvent.Type.LIFTOFF, new Color(0,80,196));
71 EVENT_COLORS.put(FlightEvent.Type.LAUNCHROD, new Color(0,100,80));
72 EVENT_COLORS.put(FlightEvent.Type.IGNITION, new Color(230,130,15));
73 EVENT_COLORS.put(FlightEvent.Type.BURNOUT, new Color(80,55,40));
74 EVENT_COLORS.put(FlightEvent.Type.EJECTION_CHARGE, new Color(80,55,40));
75 EVENT_COLORS.put(FlightEvent.Type.STAGE_SEPARATION, new Color(80,55,40));
76 EVENT_COLORS.put(FlightEvent.Type.APOGEE, new Color(15,120,15));
77 EVENT_COLORS.put(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, new Color(0,0,128));
78 EVENT_COLORS.put(FlightEvent.Type.GROUND_HIT, new Color(0,0,0));
79 EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128,0,0));
82 private static final Map<FlightEvent.Type, Image> EVENT_IMAGES =
83 new HashMap<FlightEvent.Type, Image>();
85 loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
86 loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
87 loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
88 loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
89 loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
90 loadImage(FlightEvent.Type.EJECTION_CHARGE,"pix/eventicons/event-ejection-charge.png");
91 loadImage(FlightEvent.Type.STAGE_SEPARATION,
92 "pix/eventicons/event-stage-separation.png");
93 loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
94 loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
95 "pix/eventicons/event-recovery-device-deployment.png");
96 loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
97 loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
100 private static void loadImage(FlightEvent.Type type, String file) {
103 is = ClassLoader.getSystemResourceAsStream(file);
105 System.out.println("ERROR: File " + file + " not found!");
110 Image image = ImageIO.read(is);
111 EVENT_IMAGES.put(type, image);
112 } catch (IOException ignore) {
113 ignore.printStackTrace();
120 private final List<ModifiedXYItemRenderer> renderers =
121 new ArrayList<ModifiedXYItemRenderer>();
123 private PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
124 super(parent, "Flight data plot");
125 this.setModalityType(ModalityType.DOCUMENT_MODAL);
127 final boolean initialShowPoints = Prefs.getBoolean(Prefs.PLOT_SHOW_POINTS, false);
130 // Fill the auto-selections
131 FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
132 PlotConfiguration filled = config.fillAutoAxes(branch);
133 List<Axis> axes = filled.getAllAxes();
136 // Create the data series for both axes
137 XYSeriesCollection[] data = new XYSeriesCollection[2];
138 data[0] = new XYSeriesCollection();
139 data[1] = new XYSeriesCollection();
142 // Get the domain axis type
143 final FlightDataBranch.Type domainType = filled.getDomainAxisType();
144 final Unit domainUnit = filled.getDomainAxisUnit();
145 if (domainType == null) {
146 throw new IllegalArgumentException("Domain axis type not specified.");
148 List<Double> x = branch.get(domainType);
151 // Get plot length (ignore trailing NaN's)
152 int typeCount = filled.getTypeCount();
154 for (int i=0; i<typeCount; i++) {
155 FlightDataBranch.Type type = filled.getType(i);
156 List<Double> y = branch.get(type);
158 for (int j = dataLength; j < y.size(); j++) {
159 if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j)))
163 dataLength = Math.min(dataLength, x.size());
166 // Create the XYSeries objects from the flight data and store into the collections
167 String[] axisLabel = new String[2];
168 for (int i = 0; i < typeCount; i++) {
170 FlightDataBranch.Type type = filled.getType(i);
171 Unit unit = filled.getUnit(i);
172 int axis = filled.getAxis(i);
173 String name = getLabel(type, unit);
175 // Store data in provided units
176 List<Double> y = branch.get(type);
177 XYSeries series = new XYSeries(name, false, true);
178 for (int j=0; j < dataLength; j++) {
179 series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
181 data[axis].addSeries(series);
184 if (axisLabel[axis] == null)
185 axisLabel[axis] = type.getName();
187 axisLabel[axis] += "; " + type.getName();
191 // Create the chart using the factory to get all default settings
192 JFreeChart chart = ChartFactory.createXYLineChart(
197 PlotOrientation.VERTICAL,
203 chart.addSubtitle(new TextTitle(config.getName()));
205 // Add the data and formatting to the plot
206 XYPlot plot = chart.getXYPlot();
208 for (int i=0; i<2; i++) {
209 // Check whether axis has any data
210 if (data[i].getSeriesCount() > 0) {
211 // Create and set axis
212 double min = axes.get(i).getMinValue();
213 double max = axes.get(i).getMaxValue();
214 NumberAxis axis = new PresetNumberAxis(min, max);
215 axis.setLabel(axisLabel[i]);
216 // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
217 plot.setRangeAxis(axisno, axis);
219 // Add data and map to the axis
220 plot.setDataset(axisno, data[i]);
221 ModifiedXYItemRenderer r = new ModifiedXYItemRenderer();
222 r.setBaseShapesVisible(initialShowPoints);
223 r.setBaseShapesFilled(true);
225 plot.setRenderer(axisno, r);
226 plot.mapDatasetToRangeAxis(axisno, axisno);
231 plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
232 plot.addDomainMarker(new ValueMarker(0));
233 plot.addRangeMarker(new ValueMarker(0));
237 // Create list of events to show (combine event too close to each other)
238 ArrayList<Double> timeList = new ArrayList<Double>();
239 ArrayList<String> eventList = new ArrayList<String>();
240 ArrayList<Color> colorList = new ArrayList<Color>();
241 ArrayList<Image> imageList = new ArrayList<Image>();
243 HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
245 double prevTime = -100;
250 List<Pair<Double, FlightEvent>> events = branch.getEvents();
251 for (int i=0; i < events.size(); i++) {
252 Pair<Double, FlightEvent> event = events.get(i);
253 double t = event.getU();
254 FlightEvent.Type type = event.getV().getType();
256 if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) {
257 if (Math.abs(t - prevTime) <= 0.01) {
259 if (!typeSet.contains(type)) {
260 text = text + ", " + event.getV().getType().toString();
261 color = getEventColor(type);
262 image = EVENT_IMAGES.get(type);
269 timeList.add(prevTime);
271 colorList.add(color);
272 imageList.add(image);
275 text = type.toString();
276 color = getEventColor(type);
277 image = EVENT_IMAGES.get(type);
285 timeList.add(prevTime);
287 colorList.add(color);
288 imageList.add(image);
292 // Create the event markers
294 if (config.getDomainAxisType() == FlightDataBranch.TYPE_TIME) {
296 // Domain time is plotted as vertical markers
297 for (int i=0; i < eventList.size(); i++) {
298 double t = timeList.get(i);
299 String event = eventList.get(i);
300 color = colorList.get(i);
302 ValueMarker m = new ValueMarker(t);
305 m.setLabelPaint(color);
307 plot.addDomainMarker(m);
312 // Other domains are plotted as image annotations
313 List<Double> time = branch.get(FlightDataBranch.TYPE_TIME);
314 List<Double> domain = branch.get(config.getDomainAxisType());
316 for (int i=0; i < eventList.size(); i++) {
317 final double t = timeList.get(i);
318 String event = eventList.get(i);
319 image = imageList.get(i);
324 // Calculate index and interpolation position a
326 int tindex = Collections.binarySearch(time, t);
330 if (tindex >= time.size()) {
331 // index greater than largest value in time list
332 tindex = time.size()-1;
334 } else if (tindex <= 0) {
335 // index smaller than smallest value in time list
341 double t1 = time.get(tindex);
342 double t2 = time.get(tindex+1);
344 if ((t1 > t) || (t2 < t)) {
345 throw new BugException("BUG: t1="+t1+" t2="+t2+" t="+t);
348 if (MathUtil.equals(t1, t2)) {
351 a = 1 - (t-t1) / (t2-t1);
357 xcoord = domain.get(tindex);
359 xcoord = a * domain.get(tindex) + (1-a) * domain.get(tindex+1);
362 for (int index = 0; index < config.getTypeCount(); index++) {
363 FlightDataBranch.Type type = config.getType(index);
364 List<Double> range = branch.get(type);
368 ycoord = range.get(tindex);
370 ycoord = a * range.get(tindex) + (1-a) * range.get(tindex+1);
373 XYImageAnnotation annotation =
374 new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
375 annotation.setToolTipText(event);
376 plot.addAnnotation(annotation);
384 JPanel panel = new JPanel(new MigLayout("fill"));
387 ChartPanel chartPanel = new ChartPanel(chart,
393 chartPanel.setMouseWheelEnabled(true);
394 chartPanel.setEnforceFileExtensions(true);
395 chartPanel.setInitialDelay(500);
397 chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
399 panel.add(chartPanel, "grow, wrap 20lp");
401 final JCheckBox check = new JCheckBox("Show data points");
402 check.setSelected(initialShowPoints);
403 check.addActionListener(new ActionListener() {
405 public void actionPerformed(ActionEvent e) {
406 boolean show = check.isSelected();
407 Prefs.putBoolean(Prefs.PLOT_SHOW_POINTS, show);
408 for (ModifiedXYItemRenderer r: renderers) {
409 r.setBaseShapesVisible(show);
413 panel.add(check, "split, left");
415 panel.add(new JPanel(), "growx");
417 JButton button = new JButton("Close");
418 button.addActionListener(new ActionListener() {
420 public void actionPerformed(ActionEvent e) {
421 PlotDialog.this.dispose();
424 panel.add(button, "right");
426 this.setLocationByPlatform(true);
429 GUIUtil.setDisposableDialogOptions(this, button);
433 private String getLabel(FlightDataBranch.Type type, Unit unit) {
434 String name = type.getName();
435 if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
436 !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
437 name += " ("+unit.getUnit() + ")";
443 private class PresetNumberAxis extends NumberAxis {
444 private final double min;
445 private final double max;
447 public PresetNumberAxis(double min, double max) {
454 protected void autoAdjustRange() {
455 this.setRange(min, max);
461 * Static method that shows a plot with the specified parameters.
463 * @param parent the parent window, which will be blocked.
464 * @param simulation the simulation to plot.
465 * @param config the configuration of the plot.
467 public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
468 new PlotDialog(parent, simulation, config).setVisible(true);
473 private static Color getEventColor(FlightEvent.Type type) {
474 Color c = EVENT_COLORS.get(type);
477 return DEFAULT_EVENT_COLOR;
485 * A modification to the standard renderer that renders the domain marker
486 * labels vertically instead of horizontally.
488 private static class ModifiedXYItemRenderer extends StandardXYItemRenderer {
491 public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis,
492 Marker marker, Rectangle2D dataArea) {
494 if (!(marker instanceof ValueMarker)) {
495 // Use parent for all others
496 super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea);
501 * Draw the normal marker, but with rotated text.
502 * Copied from the overridden method.
504 ValueMarker vm = (ValueMarker) marker;
505 double value = vm.getValue();
506 Range range = domainAxis.getRange();
507 if (!range.contains(value)) {
511 double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge());
513 PlotOrientation orientation = plot.getOrientation();
515 if (orientation == PlotOrientation.HORIZONTAL) {
516 line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v);
518 line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY());
521 final Composite originalComposite = g2.getComposite();
522 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker
524 g2.setPaint(marker.getPaint());
525 g2.setStroke(marker.getStroke());
528 String label = marker.getLabel();
529 RectangleAnchor anchor = marker.getLabelAnchor();
531 Font labelFont = marker.getLabelFont();
532 g2.setFont(labelFont);
533 g2.setPaint(marker.getLabelPaint());
534 Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2,
535 orientation, dataArea, line.getBounds2D(), marker
536 .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor);
539 TextAnchor textAnchor = TextAnchor.TOP_RIGHT;
540 TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX()+2,
541 (float) coordinates.getY(), textAnchor,
542 -Math.PI/2, textAnchor);
544 g2.setComposite(originalComposite);