Updates for 0.9.5
[debian/openrocket] / src / net / sf / openrocket / gui / plot / PlotDialog.java
1 package net.sf.openrocket.gui.plot;
2
3 import java.awt.AlphaComposite;
4 import java.awt.Color;
5 import java.awt.Composite;
6 import java.awt.Font;
7 import java.awt.Graphics2D;
8 import java.awt.Image;
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;
22 import java.util.Map;
23
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;
30
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;
42
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;
62
63 public class PlotDialog extends JDialog {
64         
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>();
68         static {
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));
80         }
81
82         private static final Map<FlightEvent.Type, Image> EVENT_IMAGES =
83                 new HashMap<FlightEvent.Type, Image>();
84         static {
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");
98         }
99
100     private static void loadImage(FlightEvent.Type type, String file) {
101         InputStream is;
102  
103         is = ClassLoader.getSystemResourceAsStream(file);
104         if (is == null) {
105                 System.out.println("ERROR: File " + file + " not found!");
106                 return;
107         }
108         
109         try {
110                 Image image = ImageIO.read(is);
111                 EVENT_IMAGES.put(type, image);
112         } catch (IOException ignore) {
113                 ignore.printStackTrace();
114         }
115     }
116     
117     
118     
119     
120     private final List<ModifiedXYItemRenderer> renderers =
121         new ArrayList<ModifiedXYItemRenderer>();
122         
123         private PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
124                 super(parent, "Flight data plot");
125                 this.setModalityType(ModalityType.DOCUMENT_MODAL);
126                 
127                 final boolean initialShowPoints = Prefs.getBoolean(Prefs.PLOT_SHOW_POINTS, false);
128                 
129                 
130                 // Fill the auto-selections
131                 FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
132                 PlotConfiguration filled = config.fillAutoAxes(branch);
133                 List<Axis> axes = filled.getAllAxes();
134
135
136                 // Create the data series for both axes
137                 XYSeriesCollection[] data = new XYSeriesCollection[2];
138                 data[0] = new XYSeriesCollection();
139                 data[1] = new XYSeriesCollection();
140                 
141                 
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.");
147                 }
148                 List<Double> x = branch.get(domainType);
149                 
150                 
151                 // Get plot length (ignore trailing NaN's)
152                 int typeCount = filled.getTypeCount();
153                 int dataLength = 0;
154                 for (int i=0; i<typeCount; i++) {
155                         FlightDataBranch.Type type = filled.getType(i);
156                         List<Double> y = branch.get(type);
157                         
158                         for (int j = dataLength; j < y.size(); j++) {
159                                 if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j)))
160                                         dataLength = j;
161                         }
162                 }
163                 dataLength = Math.min(dataLength, x.size());
164                 
165                 
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++) {
169                         // Get info
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);
174                         
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)));
180                         }
181                         data[axis].addSeries(series);
182
183                         // Update axis label
184                         if (axisLabel[axis] == null)
185                                 axisLabel[axis] = type.getName();
186                         else
187                                 axisLabel[axis] += "; " + type.getName();
188                 }
189                 
190                 
191                 // Create the chart using the factory to get all default settings
192         JFreeChart chart = ChartFactory.createXYLineChart(
193             "Simulated flight",
194             null, 
195             null, 
196             null,
197             PlotOrientation.VERTICAL,
198             true,
199             true,
200             false
201         );
202                 
203         chart.addSubtitle(new TextTitle(config.getName()));
204         
205                 // Add the data and formatting to the plot
206                 XYPlot plot = chart.getXYPlot();
207                 int axisno = 0;
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);
218                                 
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);
224                                 renderers.add(r);
225                                 plot.setRenderer(axisno, r);
226                                 plot.mapDatasetToRangeAxis(axisno, axisno);
227                                 axisno++;
228                         }
229                 }
230                 
231                 plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
232                 plot.addDomainMarker(new ValueMarker(0));
233                 plot.addRangeMarker(new ValueMarker(0));
234                 
235                 
236                 
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>();
242                 
243                 HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
244                 
245                 double prevTime = -100;
246                 String text = null;
247                 Color color = null;
248                 Image image = null;
249                 
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();
255                         
256                         if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) {
257                                 if (Math.abs(t - prevTime) <= 0.01) {
258                                         
259                                         if (!typeSet.contains(type)) {
260                                                 text = text + ", " + event.getV().getType().toString();
261                                                 color = getEventColor(type);
262                                                 image = EVENT_IMAGES.get(type);
263                                                 typeSet.add(type);
264                                         }
265                                         
266                                 } else {
267                                         
268                                         if (text != null) {
269                                                 timeList.add(prevTime);
270                                                 eventList.add(text);
271                                                 colorList.add(color);
272                                                 imageList.add(image);
273                                         }
274                                         prevTime = t;
275                                         text = type.toString();
276                                         color = getEventColor(type);
277                                         image = EVENT_IMAGES.get(type);
278                                         typeSet.clear();
279                                         typeSet.add(type);
280                                         
281                                 }
282                         }
283                 }
284                 if (text != null) {
285                         timeList.add(prevTime);
286                         eventList.add(text);
287                         colorList.add(color);
288                         imageList.add(image);
289                 }
290                 
291                 
292                 // Create the event markers
293                 
294                 if (config.getDomainAxisType() == FlightDataBranch.TYPE_TIME) {
295                         
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);
301                                 
302                                 ValueMarker m = new ValueMarker(t);
303                                 m.setLabel(event);
304                                 m.setPaint(color);
305                                 m.setLabelPaint(color);
306                                 m.setAlpha(0.7f);
307                                 plot.addDomainMarker(m);
308                         }
309                         
310                 } else {
311                         
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());
315                         
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);
320                                 
321                                 if (image == null)
322                                         continue;
323                                 
324                                 // Calculate index and interpolation position a
325                                 final double a;
326                                 int tindex = Collections.binarySearch(time, t);
327                                 if (tindex < 0) {
328                                         tindex = -tindex -1;
329                                 }
330                                 if (tindex >= time.size()) {
331                                         // index greater than largest value in time list
332                                         tindex = time.size()-1;
333                                         a = 0;
334                                 } else if (tindex <= 0) {
335                                         // index smaller than smallest value in time list
336                                         tindex = 0;
337                                         a = 0;
338                                 } else {
339                                         assert(tindex > 0);
340                                         tindex--;
341                                         double t1 = time.get(tindex);
342                                         double t2 = time.get(tindex+1);
343                                         
344                                         if ((t1 > t) || (t2 < t)) {
345                                                 throw new BugException("BUG: t1="+t1+" t2="+t2+" t="+t);
346                                         }
347                                         
348                                         if (MathUtil.equals(t1, t2)) {
349                                                 a = 0;
350                                         } else {
351                                                 a = 1 - (t-t1) / (t2-t1);
352                                         }
353                                 }
354                                 
355                                 final double xcoord;
356                                 if (a == 0) {
357                                         xcoord = domain.get(tindex);
358                                 } else {
359                                         xcoord = a * domain.get(tindex) + (1-a) * domain.get(tindex+1);
360                                 }
361                                 
362                                 for (int index = 0; index < config.getTypeCount(); index++) {
363                                         FlightDataBranch.Type type = config.getType(index);
364                                         List<Double> range = branch.get(type);
365                                         
366                                         final double ycoord;
367                                         if (a == 0) {
368                                                 ycoord = range.get(tindex);
369                                         } else {
370                                                 ycoord = a * range.get(tindex) + (1-a) * range.get(tindex+1);
371                                         }
372                                         
373                                         XYImageAnnotation annotation = 
374                                                 new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
375                                         annotation.setToolTipText(event);
376                                         plot.addAnnotation(annotation);
377                                 }
378                         }
379                 }
380                 
381                 
382                 // Create the dialog
383                 
384                 JPanel panel = new JPanel(new MigLayout("fill"));
385                 this.add(panel);
386                 
387                 ChartPanel chartPanel = new ChartPanel(chart,
388                                 false, // properties
389                                 true,  // save
390                                 false, // print
391                                 true,  // zoom
392                                 true); // tooltips
393                 chartPanel.setMouseWheelEnabled(true);
394                 chartPanel.setEnforceFileExtensions(true);
395                 chartPanel.setInitialDelay(500);
396                 
397                 chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
398                 
399                 panel.add(chartPanel, "grow, wrap 20lp");
400                 
401                 final JCheckBox check = new JCheckBox("Show data points");
402                 check.setSelected(initialShowPoints);
403                 check.addActionListener(new ActionListener() {
404                         @Override
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);
410                                 }
411                         }
412                 });
413                 panel.add(check, "split, left");
414                 
415                 panel.add(new JPanel(), "growx");
416
417                 JButton button = new JButton("Close");
418                 button.addActionListener(new ActionListener() {
419                         @Override
420                         public void actionPerformed(ActionEvent e) {
421                                 PlotDialog.this.dispose();
422                         }
423                 });
424                 panel.add(button, "right");
425
426                 this.setLocationByPlatform(true);
427                 this.pack();
428                 
429                 GUIUtil.setDisposableDialogOptions(this, button);
430         }
431         
432         
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() + ")";
438                 return name;
439         }
440         
441
442         
443         private class PresetNumberAxis extends NumberAxis {
444                 private final double min;
445                 private final double max;
446                 
447                 public PresetNumberAxis(double min, double max) {
448                         this.min = min;
449                         this.max = max;
450                         autoAdjustRange();
451                 }
452                 
453                 @Override
454                 protected void autoAdjustRange() {
455                         this.setRange(min, max);
456                 }
457         }
458         
459         
460         /**
461          * Static method that shows a plot with the specified parameters.
462          * 
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.
466          */
467         public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
468                 new PlotDialog(parent, simulation, config).setVisible(true);
469         }
470         
471         
472         
473         private static Color getEventColor(FlightEvent.Type type) {
474                 Color c = EVENT_COLORS.get(type);
475                 if (c != null)
476                         return c;
477                 return DEFAULT_EVENT_COLOR;
478         }
479         
480         
481
482         
483         
484         /**
485          * A modification to the standard renderer that renders the domain marker
486          * labels vertically instead of horizontally.
487          */
488         private static class ModifiedXYItemRenderer extends StandardXYItemRenderer {
489
490                 @Override
491                 public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis,
492                                 Marker marker, Rectangle2D dataArea) {
493
494                         if (!(marker instanceof ValueMarker)) {
495                                 // Use parent for all others
496                                 super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea);
497                                 return;
498                         }
499
500                         /*
501                          * Draw the normal marker, but with rotated text.
502                          * Copied from the overridden method.
503                          */
504                         ValueMarker vm = (ValueMarker) marker;
505                         double value = vm.getValue();
506                         Range range = domainAxis.getRange();
507                         if (!range.contains(value)) {
508                                 return;
509                         }
510
511                         double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge());
512
513                         PlotOrientation orientation = plot.getOrientation();
514                         Line2D line = null;
515                         if (orientation == PlotOrientation.HORIZONTAL) {
516                                 line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v);
517                         } else {
518                                 line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY());
519                         }
520
521                         final Composite originalComposite = g2.getComposite();
522                         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker
523                                         .getAlpha()));
524                         g2.setPaint(marker.getPaint());
525                         g2.setStroke(marker.getStroke());
526                         g2.draw(line);
527
528                         String label = marker.getLabel();
529                         RectangleAnchor anchor = marker.getLabelAnchor();
530                         if (label != null) {
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);
537                                 
538                                 // Changed:
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);
543                         }
544                         g2.setComposite(originalComposite);
545                 }
546
547         }
548
549 }