added svn:ignores
[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.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.simulation.FlightDataBranch;
37 import net.sf.openrocket.simulation.FlightDataType;
38 import net.sf.openrocket.simulation.FlightEvent;
39 import net.sf.openrocket.unit.Unit;
40 import net.sf.openrocket.unit.UnitGroup;
41 import net.sf.openrocket.util.BugException;
42 import net.sf.openrocket.util.GUIUtil;
43 import net.sf.openrocket.util.MathUtil;
44 import net.sf.openrocket.util.Prefs;
45
46 import org.jfree.chart.ChartFactory;
47 import org.jfree.chart.ChartPanel;
48 import org.jfree.chart.JFreeChart;
49 import org.jfree.chart.annotations.XYImageAnnotation;
50 import org.jfree.chart.axis.NumberAxis;
51 import org.jfree.chart.axis.ValueAxis;
52 import org.jfree.chart.plot.Marker;
53 import org.jfree.chart.plot.PlotOrientation;
54 import org.jfree.chart.plot.ValueMarker;
55 import org.jfree.chart.plot.XYPlot;
56 import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
57 import org.jfree.chart.title.TextTitle;
58 import org.jfree.data.Range;
59 import org.jfree.data.xy.XYSeries;
60 import org.jfree.data.xy.XYSeriesCollection;
61 import org.jfree.text.TextUtilities;
62 import org.jfree.ui.LengthAdjustmentType;
63 import org.jfree.ui.RectangleAnchor;
64 import org.jfree.ui.TextAnchor;
65
66 public class PlotDialog extends JDialog {
67         
68         private static final float PLOT_STROKE_WIDTH = 1.5f;
69         
70         private static final Color DEFAULT_EVENT_COLOR = new Color(0, 0, 0);
71         private static final Map<FlightEvent.Type, Color> EVENT_COLORS =
72                         new HashMap<FlightEvent.Type, Color>();
73         static {
74                 EVENT_COLORS.put(FlightEvent.Type.LAUNCH, new Color(255, 0, 0));
75                 EVENT_COLORS.put(FlightEvent.Type.LIFTOFF, new Color(0, 80, 196));
76                 EVENT_COLORS.put(FlightEvent.Type.LAUNCHROD, new Color(0, 100, 80));
77                 EVENT_COLORS.put(FlightEvent.Type.IGNITION, new Color(230, 130, 15));
78                 EVENT_COLORS.put(FlightEvent.Type.BURNOUT, new Color(80, 55, 40));
79                 EVENT_COLORS.put(FlightEvent.Type.EJECTION_CHARGE, new Color(80, 55, 40));
80                 EVENT_COLORS.put(FlightEvent.Type.STAGE_SEPARATION, new Color(80, 55, 40));
81                 EVENT_COLORS.put(FlightEvent.Type.APOGEE, new Color(15, 120, 15));
82                 EVENT_COLORS.put(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, new Color(0, 0, 128));
83                 EVENT_COLORS.put(FlightEvent.Type.GROUND_HIT, new Color(0, 0, 0));
84                 EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128, 0, 0));
85         }
86         
87         private static final Map<FlightEvent.Type, Image> EVENT_IMAGES =
88                         new HashMap<FlightEvent.Type, Image>();
89         static {
90                 loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
91                 loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
92                 loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
93                 loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
94                 loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
95                 loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png");
96                 loadImage(FlightEvent.Type.STAGE_SEPARATION,
97                                 "pix/eventicons/event-stage-separation.png");
98                 loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
99                 loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
100                                 "pix/eventicons/event-recovery-device-deployment.png");
101                 loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
102                 loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
103         }
104         
105         private static void loadImage(FlightEvent.Type type, String file) {
106                 InputStream is;
107                 
108                 is = ClassLoader.getSystemResourceAsStream(file);
109                 if (is == null) {
110                         System.out.println("ERROR: File " + file + " not found!");
111                         return;
112                 }
113                 
114                 try {
115                         Image image = ImageIO.read(is);
116                         EVENT_IMAGES.put(type, image);
117                 } catch (IOException ignore) {
118                         ignore.printStackTrace();
119                 }
120         }
121         
122         
123
124
125         private final List<ModifiedXYItemRenderer> renderers =
126                         new ArrayList<ModifiedXYItemRenderer>();
127         
128         private PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
129                 super(parent, "Flight data plot");
130                 this.setModalityType(ModalityType.DOCUMENT_MODAL);
131                 
132                 final boolean initialShowPoints = Prefs.getBoolean(Prefs.PLOT_SHOW_POINTS, false);
133                 
134
135                 // Fill the auto-selections
136                 FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
137                 PlotConfiguration filled = config.fillAutoAxes(branch);
138                 List<Axis> axes = filled.getAllAxes();
139                 
140
141                 // Create the data series for both axes
142                 XYSeriesCollection[] data = new XYSeriesCollection[2];
143                 data[0] = new XYSeriesCollection();
144                 data[1] = new XYSeriesCollection();
145                 
146
147                 // Get the domain axis type
148                 final FlightDataType domainType = filled.getDomainAxisType();
149                 final Unit domainUnit = filled.getDomainAxisUnit();
150                 if (domainType == null) {
151                         throw new IllegalArgumentException("Domain axis type not specified.");
152                 }
153                 List<Double> x = branch.get(domainType);
154                 
155
156                 // Get plot length (ignore trailing NaN's)
157                 int typeCount = filled.getTypeCount();
158                 int dataLength = 0;
159                 for (int i = 0; i < typeCount; i++) {
160                         FlightDataType type = filled.getType(i);
161                         List<Double> y = branch.get(type);
162                         
163                         for (int j = dataLength; j < y.size(); j++) {
164                                 if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j)))
165                                         dataLength = j;
166                         }
167                 }
168                 dataLength = Math.min(dataLength, x.size());
169                 
170
171                 // Create the XYSeries objects from the flight data and store into the collections
172                 String[] axisLabel = new String[2];
173                 for (int i = 0; i < typeCount; i++) {
174                         // Get info
175                         FlightDataType type = filled.getType(i);
176                         Unit unit = filled.getUnit(i);
177                         int axis = filled.getAxis(i);
178                         String name = getLabel(type, unit);
179                         
180                         // Store data in provided units
181                         List<Double> y = branch.get(type);
182                         XYSeries series = new XYSeries(name, false, true);
183                         for (int j = 0; j < dataLength; j++) {
184                                 series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
185                         }
186                         data[axis].addSeries(series);
187                         
188                         // Update axis label
189                         if (axisLabel[axis] == null)
190                                 axisLabel[axis] = type.getName();
191                         else
192                                 axisLabel[axis] += "; " + type.getName();
193                 }
194                 
195
196                 // Create the chart using the factory to get all default settings
197                 JFreeChart chart = ChartFactory.createXYLineChart(
198                                 "Simulated flight",
199                                 null,
200                                 null,
201                                 null,
202                                 PlotOrientation.VERTICAL,
203                                 true,
204                                 true,
205                                 false
206                                 );
207                 
208                 chart.addSubtitle(new TextTitle(config.getName()));
209                 
210                 // Add the data and formatting to the plot
211                 XYPlot plot = chart.getXYPlot();
212                 int axisno = 0;
213                 for (int i = 0; i < 2; i++) {
214                         // Check whether axis has any data
215                         if (data[i].getSeriesCount() > 0) {
216                                 // Create and set axis
217                                 double min = axes.get(i).getMinValue();
218                                 double max = axes.get(i).getMaxValue();
219                                 NumberAxis axis = new PresetNumberAxis(min, max);
220                                 axis.setLabel(axisLabel[i]);
221                                 //                              axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
222                                 plot.setRangeAxis(axisno, axis);
223                                 
224                                 // Add data and map to the axis
225                                 plot.setDataset(axisno, data[i]);
226                                 ModifiedXYItemRenderer r = new ModifiedXYItemRenderer();
227                                 r.setBaseShapesVisible(initialShowPoints);
228                                 r.setBaseShapesFilled(true);
229                                 for (int j = 0; j < data[i].getSeriesCount(); j++) {
230                                         r.setSeriesStroke(j, new BasicStroke(PLOT_STROKE_WIDTH));
231                                 }
232                                 renderers.add(r);
233                                 plot.setRenderer(axisno, r);
234                                 plot.mapDatasetToRangeAxis(axisno, axisno);
235                                 axisno++;
236                         }
237                 }
238                 
239                 plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit));
240                 plot.addDomainMarker(new ValueMarker(0));
241                 plot.addRangeMarker(new ValueMarker(0));
242                 
243
244
245                 // Create list of events to show (combine event too close to each other)
246                 ArrayList<Double> timeList = new ArrayList<Double>();
247                 ArrayList<String> eventList = new ArrayList<String>();
248                 ArrayList<Color> colorList = new ArrayList<Color>();
249                 ArrayList<Image> imageList = new ArrayList<Image>();
250                 
251                 HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
252                 
253                 double prevTime = -100;
254                 String text = null;
255                 Color color = null;
256                 Image image = null;
257                 
258                 List<FlightEvent> events = branch.getEvents();
259                 for (int i = 0; i < events.size(); i++) {
260                         FlightEvent event = events.get(i);
261                         double t = event.getTime();
262                         FlightEvent.Type type = event.getType();
263                         
264                         if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) {
265                                 if (Math.abs(t - prevTime) <= 0.01) {
266                                         
267                                         if (!typeSet.contains(type)) {
268                                                 text = text + ", " + type.toString();
269                                                 color = getEventColor(type);
270                                                 image = EVENT_IMAGES.get(type);
271                                                 typeSet.add(type);
272                                         }
273                                         
274                                 } else {
275                                         
276                                         if (text != null) {
277                                                 timeList.add(prevTime);
278                                                 eventList.add(text);
279                                                 colorList.add(color);
280                                                 imageList.add(image);
281                                         }
282                                         prevTime = t;
283                                         text = type.toString();
284                                         color = getEventColor(type);
285                                         image = EVENT_IMAGES.get(type);
286                                         typeSet.clear();
287                                         typeSet.add(type);
288                                         
289                                 }
290                         }
291                 }
292                 if (text != null) {
293                         timeList.add(prevTime);
294                         eventList.add(text);
295                         colorList.add(color);
296                         imageList.add(image);
297                 }
298                 
299
300                 // Create the event markers
301                 
302                 if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) {
303                         
304                         // Domain time is plotted as vertical markers
305                         for (int i = 0; i < eventList.size(); i++) {
306                                 double t = timeList.get(i);
307                                 String event = eventList.get(i);
308                                 color = colorList.get(i);
309                                 
310                                 ValueMarker m = new ValueMarker(t);
311                                 m.setLabel(event);
312                                 m.setPaint(color);
313                                 m.setLabelPaint(color);
314                                 m.setAlpha(0.7f);
315                                 plot.addDomainMarker(m);
316                         }
317                         
318                 } else {
319                         
320                         // Other domains are plotted as image annotations
321                         List<Double> time = branch.get(FlightDataType.TYPE_TIME);
322                         List<Double> domain = branch.get(config.getDomainAxisType());
323                         
324                         for (int i = 0; i < eventList.size(); i++) {
325                                 final double t = timeList.get(i);
326                                 String event = eventList.get(i);
327                                 image = imageList.get(i);
328                                 
329                                 if (image == null)
330                                         continue;
331                                 
332                                 // Calculate index and interpolation position a
333                                 final double a;
334                                 int tindex = Collections.binarySearch(time, t);
335                                 if (tindex < 0) {
336                                         tindex = -tindex - 1;
337                                 }
338                                 if (tindex >= time.size()) {
339                                         // index greater than largest value in time list
340                                         tindex = time.size() - 1;
341                                         a = 0;
342                                 } else if (tindex <= 0) {
343                                         // index smaller than smallest value in time list
344                                         tindex = 0;
345                                         a = 0;
346                                 } else {
347                                         tindex--;
348                                         double t1 = time.get(tindex);
349                                         double t2 = time.get(tindex + 1);
350                                         
351                                         if ((t1 > t) || (t2 < t)) {
352                                                 throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t);
353                                         }
354                                         
355                                         if (MathUtil.equals(t1, t2)) {
356                                                 a = 0;
357                                         } else {
358                                                 a = 1 - (t - t1) / (t2 - t1);
359                                         }
360                                 }
361                                 
362                                 final double xcoord;
363                                 if (a == 0) {
364                                         xcoord = domain.get(tindex);
365                                 } else {
366                                         xcoord = a * domain.get(tindex) + (1 - a) * domain.get(tindex + 1);
367                                 }
368                                 
369                                 for (int index = 0; index < config.getTypeCount(); index++) {
370                                         FlightDataType type = config.getType(index);
371                                         List<Double> range = branch.get(type);
372                                         
373                                         final double ycoord;
374                                         if (a == 0) {
375                                                 ycoord = range.get(tindex);
376                                         } else {
377                                                 ycoord = a * range.get(tindex) + (1 - a) * range.get(tindex + 1);
378                                         }
379                                         
380                                         XYImageAnnotation annotation =
381                                                         new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
382                                         annotation.setToolTipText(event);
383                                         plot.addAnnotation(annotation);
384                                 }
385                         }
386                 }
387                 
388
389                 // Create the dialog
390                 
391                 JPanel panel = new JPanel(new MigLayout("fill"));
392                 this.add(panel);
393                 
394                 ChartPanel chartPanel = new ChartPanel(chart,
395                                 false, // properties
396                                 true, // save
397                                 false, // print
398                                 true, // zoom
399                                 true); // tooltips
400                 chartPanel.setMouseWheelEnabled(true);
401                 chartPanel.setEnforceFileExtensions(true);
402                 chartPanel.setInitialDelay(500);
403                 
404                 chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
405                 
406                 panel.add(chartPanel, "grow, wrap 20lp");
407                 
408                 final JCheckBox check = new JCheckBox("Show data points");
409                 check.setSelected(initialShowPoints);
410                 check.addActionListener(new ActionListener() {
411                         @Override
412                         public void actionPerformed(ActionEvent e) {
413                                 boolean show = check.isSelected();
414                                 Prefs.putBoolean(Prefs.PLOT_SHOW_POINTS, show);
415                                 for (ModifiedXYItemRenderer r : renderers) {
416                                         r.setBaseShapesVisible(show);
417                                 }
418                         }
419                 });
420                 panel.add(check, "split, left");
421                 
422
423                 JLabel label = new StyledLabel("Click+drag down+right to zoom in, up+left to zoom out", -2);
424                 panel.add(label, "gapleft para");
425                 
426
427                 panel.add(new JPanel(), "growx");
428                 
429                 JButton button = new JButton("Close");
430                 button.addActionListener(new ActionListener() {
431                         @Override
432                         public void actionPerformed(ActionEvent e) {
433                                 PlotDialog.this.dispose();
434                         }
435                 });
436                 panel.add(button, "right");
437                 
438                 this.setLocationByPlatform(true);
439                 this.pack();
440                 
441                 GUIUtil.setDisposableDialogOptions(this, button);
442         }
443         
444         
445         private String getLabel(FlightDataType type, Unit unit) {
446                 String name = type.getName();
447                 if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
448                                 !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
449                         name += " (" + unit.getUnit() + ")";
450                 return name;
451         }
452         
453         
454
455         private class PresetNumberAxis extends NumberAxis {
456                 private final double min;
457                 private final double max;
458                 
459                 public PresetNumberAxis(double min, double max) {
460                         this.min = min;
461                         this.max = max;
462                         autoAdjustRange();
463                 }
464                 
465                 @Override
466                 protected void autoAdjustRange() {
467                         this.setRange(min, max);
468                 }
469         }
470         
471         
472         /**
473          * Static method that shows a plot with the specified parameters.
474          * 
475          * @param parent                the parent window, which will be blocked.
476          * @param simulation    the simulation to plot.
477          * @param config                the configuration of the plot.
478          */
479         public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
480                 new PlotDialog(parent, simulation, config).setVisible(true);
481         }
482         
483         
484
485         private static Color getEventColor(FlightEvent.Type type) {
486                 Color c = EVENT_COLORS.get(type);
487                 if (c != null)
488                         return c;
489                 return DEFAULT_EVENT_COLOR;
490         }
491         
492         
493
494
495
496         /**
497          * A modification to the standard renderer that renders the domain marker
498          * labels vertically instead of horizontally.
499          */
500         private static class ModifiedXYItemRenderer extends StandardXYItemRenderer {
501                 
502                 @Override
503                 public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis,
504                                 Marker marker, Rectangle2D dataArea) {
505                         
506                         if (!(marker instanceof ValueMarker)) {
507                                 // Use parent for all others
508                                 super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea);
509                                 return;
510                         }
511                         
512                         /*
513                          * Draw the normal marker, but with rotated text.
514                          * Copied from the overridden method.
515                          */
516                         ValueMarker vm = (ValueMarker) marker;
517                         double value = vm.getValue();
518                         Range range = domainAxis.getRange();
519                         if (!range.contains(value)) {
520                                 return;
521                         }
522                         
523                         double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge());
524                         
525                         PlotOrientation orientation = plot.getOrientation();
526                         Line2D line = null;
527                         if (orientation == PlotOrientation.HORIZONTAL) {
528                                 line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v);
529                         } else {
530                                 line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY());
531                         }
532                         
533                         final Composite originalComposite = g2.getComposite();
534                         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker
535                                         .getAlpha()));
536                         g2.setPaint(marker.getPaint());
537                         g2.setStroke(marker.getStroke());
538                         g2.draw(line);
539                         
540                         String label = marker.getLabel();
541                         RectangleAnchor anchor = marker.getLabelAnchor();
542                         if (label != null) {
543                                 Font labelFont = marker.getLabelFont();
544                                 g2.setFont(labelFont);
545                                 g2.setPaint(marker.getLabelPaint());
546                                 Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2,
547                                                 orientation, dataArea, line.getBounds2D(), marker
548                                                                 .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor);
549                                 
550                                 // Changed:
551                                 TextAnchor textAnchor = TextAnchor.TOP_RIGHT;
552                                 TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2,
553                                                 (float) coordinates.getY(), textAnchor,
554                                                 -Math.PI / 2, textAnchor);
555                         }
556                         g2.setComposite(originalComposite);
557                 }
558                 
559         }
560         
561 }