Merge commit '42b2e5ca519766e37ce6941ba4faecc9691cc403' into upstream
[debian/openrocket] / core / src / net / sf / openrocket / gui / plot / SimulationPlotDialog.java
1 package net.sf.openrocket.gui.plot;
2
3 import java.awt.AlphaComposite;
4 import java.awt.BasicStroke;
5 import java.awt.Color;
6 import java.awt.Composite;
7 import java.awt.Font;
8 import java.awt.Graphics2D;
9 import java.awt.Image;
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;
23 import java.util.Map;
24
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;
32
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;
47
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;
67
68 /**
69  * Dialog that shows a plot of a simulation results based on user options.
70  * 
71  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
72  */
73 public class SimulationPlotDialog extends JDialog {
74         
75         private static final float PLOT_STROKE_WIDTH = 1.5f;
76         private static final Translator trans = Application.getTranslator();
77         
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>();
81         static {
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));
93         }
94         
95         private static final Map<FlightEvent.Type, Image> EVENT_IMAGES =
96                         new HashMap<FlightEvent.Type, Image>();
97         static {
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");
111         }
112         
113         private static void loadImage(FlightEvent.Type type, String file) {
114                 InputStream is;
115                 
116                 is = ClassLoader.getSystemResourceAsStream(file);
117                 if (is == null) {
118                         //System.out.println("ERROR: File " + file + " not found!");
119                         return;
120                 }
121                 
122                 try {
123                         Image image = ImageIO.read(is);
124                         EVENT_IMAGES.put(type, image);
125                 } catch (IOException ignore) {
126                         ignore.printStackTrace();
127                 }
128         }
129         
130         
131
132
133         private final List<ModifiedXYItemRenderer> renderers =
134                         new ArrayList<ModifiedXYItemRenderer>();
135         
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);
140                 
141                 final boolean initialShowPoints = Application.getPreferences().getBoolean(Preferences.PLOT_SHOW_POINTS, false);
142                 
143
144                 // Fill the auto-selections
145                 FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
146                 PlotConfiguration filled = config.fillAutoAxes(branch);
147                 List<Axis> axes = filled.getAllAxes();
148                 
149
150                 // Create the data series for both axes
151                 XYSeriesCollection[] data = new XYSeriesCollection[2];
152                 data[0] = new XYSeriesCollection();
153                 data[1] = new XYSeriesCollection();
154                 
155
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.");
161                 }
162                 List<Double> x = branch.get(domainType);
163                 
164
165                 // Get plot length (ignore trailing NaN's)
166                 int typeCount = filled.getTypeCount();
167                 int dataLength = 0;
168                 for (int i = 0; i < typeCount; i++) {
169                         FlightDataType type = filled.getType(i);
170                         List<Double> y = branch.get(type);
171                         
172                         for (int j = dataLength; j < y.size(); j++) {
173                                 if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j)))
174                                         dataLength = j;
175                         }
176                 }
177                 dataLength = Math.min(dataLength, x.size());
178                 
179
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++) {
183                         // Get info
184                         FlightDataType type = filled.getType(i);
185                         Unit unit = filled.getUnit(i);
186                         int axis = filled.getAxis(i);
187                         String name = getLabel(type, unit);
188                         
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)));
194                         }
195                         data[axis].addSeries(series);
196                         
197                         // Update axis label
198                         if (axisLabel[axis] == null)
199                                 axisLabel[axis] = type.getName();
200                         else
201                                 axisLabel[axis] += "; " + type.getName();
202                 }
203                 
204
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"),
209                                 null,
210                                 null,
211                                 null,
212                                 PlotOrientation.VERTICAL,
213                                 true,
214                                 true,
215                                 false
216                                 );
217                 
218                 chart.addSubtitle(new TextTitle(config.getName()));
219                 
220                 // Add the data and formatting to the plot
221                 XYPlot plot = chart.getXYPlot();
222                 int axisno = 0;
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);
233                                 
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));
241                                 }
242                                 renderers.add(r);
243                                 plot.setRenderer(axisno, r);
244                                 plot.mapDatasetToRangeAxis(axisno, axisno);
245                                 axisno++;
246                         }
247                 }
248                 
249                 plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit));
250                 plot.addDomainMarker(new ValueMarker(0));
251                 plot.addRangeMarker(new ValueMarker(0));
252                 
253
254
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>();
260                 
261                 HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
262                 
263                 double prevTime = -100;
264                 String text = null;
265                 Color color = null;
266                 Image image = null;
267                 
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();
273                         
274                         if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) {
275                                 if (Math.abs(t - prevTime) <= 0.01) {
276                                         
277                                         if (!typeSet.contains(type)) {
278                                                 text = text + ", " + type.toString();
279                                                 color = getEventColor(type);
280                                                 image = EVENT_IMAGES.get(type);
281                                                 typeSet.add(type);
282                                         }
283                                         
284                                 } else {
285                                         
286                                         if (text != null) {
287                                                 timeList.add(prevTime);
288                                                 eventList.add(text);
289                                                 colorList.add(color);
290                                                 imageList.add(image);
291                                         }
292                                         prevTime = t;
293                                         text = type.toString();
294                                         color = getEventColor(type);
295                                         image = EVENT_IMAGES.get(type);
296                                         typeSet.clear();
297                                         typeSet.add(type);
298                                         
299                                 }
300                         }
301                 }
302                 if (text != null) {
303                         timeList.add(prevTime);
304                         eventList.add(text);
305                         colorList.add(color);
306                         imageList.add(image);
307                 }
308                 
309
310                 // Create the event markers
311                 
312                 if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) {
313                         
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);
319                                 
320                                 ValueMarker m = new ValueMarker(t);
321                                 m.setLabel(event);
322                                 m.setPaint(color);
323                                 m.setLabelPaint(color);
324                                 m.setAlpha(0.7f);
325                                 plot.addDomainMarker(m);
326                         }
327                         
328                 } else {
329                         
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());
333                         
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);
338                                 
339                                 if (image == null)
340                                         continue;
341                                 
342                                 // Calculate index and interpolation position a
343                                 final double a;
344                                 int tindex = Collections.binarySearch(time, t);
345                                 if (tindex < 0) {
346                                         tindex = -tindex - 1;
347                                 }
348                                 if (tindex >= time.size()) {
349                                         // index greater than largest value in time list
350                                         tindex = time.size() - 1;
351                                         a = 0;
352                                 } else if (tindex <= 0) {
353                                         // index smaller than smallest value in time list
354                                         tindex = 0;
355                                         a = 0;
356                                 } else {
357                                         tindex--;
358                                         double t1 = time.get(tindex);
359                                         double t2 = time.get(tindex + 1);
360                                         
361                                         if ((t1 > t) || (t2 < t)) {
362                                                 throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t);
363                                         }
364                                         
365                                         if (MathUtil.equals(t1, t2)) {
366                                                 a = 0;
367                                         } else {
368                                                 a = 1 - (t - t1) / (t2 - t1);
369                                         }
370                                 }
371                                 
372                                 double xcoord;
373                                 if (a == 0) {
374                                         xcoord = domain.get(tindex);
375                                 } else {
376                                         xcoord = a * domain.get(tindex) + (1 - a) * domain.get(tindex + 1);
377                                 }
378                                 
379                                 for (int index = 0; index < config.getTypeCount(); index++) {
380                                         FlightDataType type = config.getType(index);
381                                         List<Double> range = branch.get(type);
382                                         
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) {
386                                                 continue;
387                                         }
388                                         
389                                         double ycoord;
390                                         if (a == 0) {
391                                                 ycoord = range.get(tindex);
392                                         } else {
393                                                 ycoord = a * range.get(tindex) + (1 - a) * range.get(tindex + 1);
394                                         }
395                                         
396                                         // Convert units
397                                         xcoord = config.getDomainAxisUnit().toUnit(xcoord);
398                                         ycoord = config.getUnit(index).toUnit(ycoord);
399                                         
400                                         XYImageAnnotation annotation =
401                                                                 new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
402                                         annotation.setToolTipText(event);
403                                         plot.addAnnotation(annotation);
404                                 }
405                         }
406                 }
407                 
408
409                 // Create the dialog
410                 
411                 JPanel panel = new JPanel(new MigLayout("fill"));
412                 this.add(panel);
413                 
414                 ChartPanel chartPanel = new ChartPanel(chart,
415                                 false, // properties
416                                 true, // save
417                                 false, // print
418                                 true, // zoom
419                                 true); // tooltips
420                 chartPanel.setMouseWheelEnabled(true);
421                 chartPanel.setEnforceFileExtensions(true);
422                 chartPanel.setInitialDelay(500);
423                 
424                 chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
425                 
426                 panel.add(chartPanel, "grow, wrap 20lp");
427                 
428                 //// Show data points
429                 final JCheckBox check = new JCheckBox(trans.get("PlotDialog.CheckBox.Showdatapoints"));
430                 check.setSelected(initialShowPoints);
431                 check.addActionListener(new ActionListener() {
432                         @Override
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);
438                                 }
439                         }
440                 });
441                 panel.add(check, "split, left");
442                 
443
444                 JLabel label = new StyledLabel(trans.get("PlotDialog.lbl.Chart"), -2);
445                 panel.add(label, "gapleft para");
446                 
447
448                 panel.add(new JPanel(), "growx");
449                 
450                 //// Close button
451                 JButton button = new JButton(trans.get("dlg.but.close"));
452                 button.addActionListener(new ActionListener() {
453                         @Override
454                         public void actionPerformed(ActionEvent e) {
455                                 SimulationPlotDialog.this.dispose();
456                         }
457                 });
458                 panel.add(button, "right");
459                 
460                 this.setLocationByPlatform(true);
461                 this.pack();
462                 
463                 GUIUtil.setDisposableDialogOptions(this, button);
464                 GUIUtil.rememberWindowSize(this);
465         }
466         
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() + ")";
472                 return name;
473         }
474         
475         
476
477         private class PresetNumberAxis extends NumberAxis {
478                 private final double min;
479                 private final double max;
480                 
481                 public PresetNumberAxis(double min, double max) {
482                         this.min = min;
483                         this.max = max;
484                         autoAdjustRange();
485                 }
486                 
487                 @Override
488                 protected void autoAdjustRange() {
489                         this.setRange(min, max);
490                 }
491         }
492         
493         
494         /**
495          * Static method that shows a plot with the specified parameters.
496          * 
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.
500          */
501         public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
502                 new SimulationPlotDialog(parent, simulation, config).setVisible(true);
503         }
504         
505         
506
507         private static Color getEventColor(FlightEvent.Type type) {
508                 Color c = EVENT_COLORS.get(type);
509                 if (c != null)
510                         return c;
511                 return DEFAULT_EVENT_COLOR;
512         }
513         
514         
515
516
517
518         /**
519          * A modification to the standard renderer that renders the domain marker
520          * labels vertically instead of horizontally.
521          */
522         private static class ModifiedXYItemRenderer extends StandardXYItemRenderer {
523                 
524                 @Override
525                 public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis,
526                                 Marker marker, Rectangle2D dataArea) {
527                         
528                         if (!(marker instanceof ValueMarker)) {
529                                 // Use parent for all others
530                                 super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea);
531                                 return;
532                         }
533                         
534                         /*
535                          * Draw the normal marker, but with rotated text.
536                          * Copied from the overridden method.
537                          */
538                         ValueMarker vm = (ValueMarker) marker;
539                         double value = vm.getValue();
540                         Range range = domainAxis.getRange();
541                         if (!range.contains(value)) {
542                                 return;
543                         }
544                         
545                         double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge());
546                         
547                         PlotOrientation orientation = plot.getOrientation();
548                         Line2D line = null;
549                         if (orientation == PlotOrientation.HORIZONTAL) {
550                                 line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v);
551                         } else {
552                                 line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY());
553                         }
554                         
555                         final Composite originalComposite = g2.getComposite();
556                         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker
557                                         .getAlpha()));
558                         g2.setPaint(marker.getPaint());
559                         g2.setStroke(marker.getStroke());
560                         g2.draw(line);
561                         
562                         String label = marker.getLabel();
563                         RectangleAnchor anchor = marker.getLabelAnchor();
564                         if (label != null) {
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);
571                                 
572                                 // Changed:
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);
577                         }
578                         g2.setComposite(originalComposite);
579                 }
580                 
581         }
582         
583 }