Initial commit
[debian/openrocket] / src / net / sf / openrocket / gui / plot / PlotConfiguration.java
1 package net.sf.openrocket.gui.plot;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import net.sf.openrocket.simulation.FlightDataBranch;
7 import net.sf.openrocket.simulation.FlightDataBranch.Type;
8 import net.sf.openrocket.unit.Unit;
9 import net.sf.openrocket.util.MathUtil;
10 import net.sf.openrocket.util.Pair;
11
12
13 public class PlotConfiguration implements Cloneable {
14         
15         public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
16         static {
17                 ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
18                 PlotConfiguration config;
19                 
20                 config = new PlotConfiguration("Vertical motion vs. time");
21                 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
22                 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
23                 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
24                 configs.add(config);
25                 
26                 config = new PlotConfiguration("Total motion vs. time");
27                 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
28                 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_TOTAL);
29                 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
30                 configs.add(config);
31                 
32                 config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
33                 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
34                 configs.add(config);
35
36                 config = new PlotConfiguration("Stability vs. time");
37                 config.addPlotDataType(FlightDataBranch.TYPE_STABILITY, 0);
38                 config.addPlotDataType(FlightDataBranch.TYPE_CP_LOCATION, 1);
39                 config.addPlotDataType(FlightDataBranch.TYPE_CG_LOCATION, 1);
40                 configs.add(config);
41                 
42                 config = new PlotConfiguration("Drag coefficients vs. Mach number", 
43                                 FlightDataBranch.TYPE_MACH_NUMBER);
44                 config.addPlotDataType(FlightDataBranch.TYPE_DRAG_COEFF, 0);
45                 config.addPlotDataType(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, 0);
46                 config.addPlotDataType(FlightDataBranch.TYPE_BASE_DRAG_COEFF, 0);
47                 config.addPlotDataType(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, 0);
48                 configs.add(config);
49
50                 config = new PlotConfiguration("Roll characteristics");
51                 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_RATE, 0);
52                 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, 1);
53                 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, 1);
54                 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, 1);
55                 configs.add(config);
56
57                 config = new PlotConfiguration("Simulation time step and computation time");
58                 config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
59                 config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
60                 configs.add(config);
61
62                 DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
63         }
64         
65         
66         
67         /** Bonus given for the first type being on the first axis */
68         private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
69
70         /**
71          * Bonus given if the first axis includes zero (to prefer first axis having zero over 
72          * the others) 
73          */
74         private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
75         
76         /** Bonus given for a common zero point on left and right axes. */
77         private static final double BONUS_COMMON_ZERO = 40.0;
78         
79         /** Bonus given for only using a single axis. */
80         private static final double BONUS_ONLY_ONE_AXIS = 50.0;
81         
82         
83         private static final double INCLUDE_ZERO_DISTANCE = 0.3;  // 30% of total range
84         
85         
86
87         /** The data types to be plotted. */
88         private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
89         
90         private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
91         
92         /** The corresponding Axis on which they will be plotted, or null to auto-select. */
93         private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
94
95         
96         /** The domain (x) axis. */
97         private FlightDataBranch.Type domainAxisType = null;
98         private Unit domainAxisUnit = null;
99         
100         
101         /** All available axes. */
102         private final int axesCount;
103         private ArrayList<Axis> allAxes = new ArrayList<Axis>();
104         
105         
106         
107         private String name = null;
108         
109         
110         
111         public PlotConfiguration() {
112                 this(null, FlightDataBranch.TYPE_TIME);
113         }
114         
115         public PlotConfiguration(String name) {
116                 this(name, FlightDataBranch.TYPE_TIME);
117         }
118         
119         public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
120                 this.name = name;
121                 // Two axes
122                 allAxes.add(new Axis());
123                 allAxes.add(new Axis());
124                 axesCount = 2;
125                 
126                 setDomainAxisType(domainType);
127         }
128         
129         
130         
131         
132         
133         public FlightDataBranch.Type getDomainAxisType() {
134                 return domainAxisType;
135         }
136         
137         public void setDomainAxisType(FlightDataBranch.Type type) {
138                 boolean setUnit;
139                 
140                 if (domainAxisType != null  &&  domainAxisType.getUnitGroup() == type.getUnitGroup())
141                         setUnit = false;
142                 else
143                         setUnit = true;
144                 
145                 domainAxisType = type;
146                 if (setUnit)
147                         domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
148         }
149         
150         public Unit getDomainAxisUnit() {
151                 return domainAxisUnit;
152         }
153         
154         public void setDomainAxisUnit(Unit u) {
155                 if (!domainAxisType.getUnitGroup().contains(u)) {
156                         throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
157                 }
158                 domainAxisUnit = u;
159         }
160         
161         
162         
163         public void addPlotDataType(FlightDataBranch.Type type) {
164                 plotDataTypes.add(type);
165                 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
166                 plotDataAxes.add(-1);
167         }
168
169         public void addPlotDataType(FlightDataBranch.Type type, int axis) {
170                 if (axis >= axesCount) {
171                         throw new IllegalArgumentException("Axis index too large");
172                 }
173                 plotDataTypes.add(type);
174                 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
175                 plotDataAxes.add(axis);
176         }
177
178
179         
180         
181         public void setPlotDataType(int index, FlightDataBranch.Type type) {
182                 FlightDataBranch.Type origType = plotDataTypes.get(index);
183                 plotDataTypes.set(index, type);
184                 
185                 if (origType.getUnitGroup() != type.getUnitGroup()) {
186                         plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
187                 }
188         }
189         
190         public void setPlotDataUnit(int index, Unit unit) {
191                 if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
192                         throw new IllegalArgumentException("Attempting to set unit "+unit+" to group "
193                                         + plotDataTypes.get(index).getUnitGroup());
194                 }
195                 plotDataUnits.set(index, unit);
196         }
197         
198         public void setPlotDataAxis(int index, int axis) {
199                 if (axis >= axesCount) {
200                         throw new IllegalArgumentException("Axis index too large");
201                 }
202                 plotDataAxes.set(index, axis);
203         }
204         
205         
206         public void setPlotDataType(int index, FlightDataBranch.Type type, Unit unit, int axis) {
207                 if (axis >= axesCount) {
208                         throw new IllegalArgumentException("Axis index too large");
209                 }
210                 plotDataTypes.set(index, type);
211                 plotDataUnits.set(index, unit);
212                 plotDataAxes.set(index, axis);
213         }
214         
215         public void removePlotDataType(int index) {
216                 plotDataTypes.remove(index);
217                 plotDataUnits.remove(index);
218                 plotDataAxes.remove(index);
219         }
220         
221         
222         
223         public FlightDataBranch.Type getType (int index) {
224                 return plotDataTypes.get(index);
225         }
226         public Unit getUnit(int index) {
227                 return plotDataUnits.get(index);
228         }
229         public int getAxis(int index) {
230                 return plotDataAxes.get(index);
231         }
232         
233         public int getTypeCount() {
234                 return plotDataTypes.size();
235         }
236         
237         
238         
239         public List<Axis> getAllAxes() {
240                 List<Axis> list = new ArrayList<Axis>();
241                 list.addAll(allAxes);
242                 return list;
243         }
244         
245         
246         public String getName() {
247                 return name;
248         }
249         
250         public void setName(String name) {
251                 this.name = name;
252         }
253         
254         /**
255          * Returns the name of this PlotConfiguration.
256          */
257         @Override
258         public String toString() {
259                 return name;
260         }
261         
262         
263         
264         /**
265          * Find the best combination of the auto-selectable axes.
266          * 
267          * @return      a new PlotConfiguration with the best fitting auto-selected axes and
268          *                      axes ranges selected.
269          */
270         public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
271                 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
272                 System.out.println("BEST FOUND, fitting");
273                 config.fitAxes(data);
274                 return config;
275         }
276         
277         
278         
279         
280         /**
281          * Recursively search for the best combination of the auto-selectable axes.
282          * This is a brute-force search method.
283          * 
284          * @return      a new PlotConfiguration with the best fitting auto-selected axes and
285          *                      axes ranges selected, and the goodness value
286          */
287         private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
288                 
289                 // Create copy to fill in
290                 PlotConfiguration copy = this.clone();
291                 
292                 int autoindex; 
293                 for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
294                         if (plotDataAxes.get(autoindex) < 0)
295                                 break;
296                 }
297                 
298                 
299                 if (autoindex >= plotDataAxes.size()) {
300                         // All axes have been assigned, just return since we are already the best
301                         return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
302                 }
303                 
304                 
305                 // Set the auto-selected index one at a time and choose the best one
306                 PlotConfiguration best = null;
307                 double bestValue = Double.NEGATIVE_INFINITY;
308                 for (int i=0; i < axesCount; i++) {
309                         copy.plotDataAxes.set(autoindex, i);
310                         Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
311                         if (result.getV() > bestValue) {
312                                 best = result.getU();
313                                 bestValue = result.getV();
314                         }
315                 }
316                 
317                 return new Pair<PlotConfiguration, Double>(best, bestValue);
318         }
319         
320         
321         
322         
323         
324         /**
325          * Fit the axes to hold the provided data.  All of the plotDataAxis elements must
326          * be non-negative.
327          * <p>
328          * NOTE: This method assumes that only two axes are used.
329          */
330         protected void fitAxes(FlightDataBranch data) {
331                 
332                 // Reset axes
333                 for (Axis a: allAxes) {
334                         a.reset();
335                 }
336                 
337                 // Add full range to the axes
338                 int length = plotDataTypes.size();
339                 for (int i=0; i<length; i++) {
340                         FlightDataBranch.Type type = plotDataTypes.get(i);
341                         Unit unit = plotDataUnits.get(i);
342                         int index = plotDataAxes.get(i);
343                         if (index < 0) {
344                                 throw new IllegalStateException("fitAxes called with auto-selected axis");
345                         }
346                         Axis axis = allAxes.get(index);
347                         
348                         double min = unit.toUnit(data.getMinimum(type));
349                         double max = unit.toUnit(data.getMaximum(type));
350
351                         axis.addBound(min);
352                         axis.addBound(max);
353                 }
354                 
355                 // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
356                 for (Axis a: allAxes) {
357                         if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
358                                 a.addBound(a.getMinValue()-1);
359                                 a.addBound(a.getMaxValue()+1);
360                         }
361                         
362                         double addition = a.getRangeLength() * 0.03;
363                         a.addBound(a.getMinValue() - addition);
364                         a.addBound(a.getMaxValue() + addition);
365                         
366                         double dist;
367                         dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
368                         if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
369                                 a.addBound(0);
370                         }
371                 }
372
373                 
374                 // Check whether to use a common zero
375                 Axis left = allAxes.get(0);
376                 Axis right = allAxes.get(1);
377                 
378                 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
379                                 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
380                                 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
381                         return;
382                 
383                 
384                 
385                 //// Compute common zero
386                 // TODO: MEDIUM: This algorithm may require tweaking
387
388                 double min1 = left.getMinValue();
389                 double max1 = left.getMaxValue();
390                 double min2 = right.getMinValue();
391                 double max2 = right.getMaxValue();
392                 
393                 // Calculate and round scaling factor
394                 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
395                                                 Math.min(left.getRangeLength(), right.getRangeLength());
396                 
397                 System.out.println("Scale: "+scale);
398                 
399                 scale = roundScale(scale);
400                 if (right.getRangeLength() > left.getRangeLength()) {
401                         scale = 1/scale;
402                 }
403                 System.out.println("Rounded scale: " + scale);
404
405                 // Scale right axis, enlarge axes if necessary and scale back
406                 min2 *= scale;
407                 max2 *= scale;
408                 min1 = Math.min(min1, min2);
409                 min2 = min1;
410                 max1 = Math.max(max1, max2);
411                 max2 = max1;
412                 min2 /= scale;
413                 max2 /= scale;
414                 
415                 
416                 
417                 // Scale to unit length
418 //              double scale1 = left.getRangeLength();
419 //              double scale2 = right.getRangeLength();
420 //              
421 //              double min1 = left.getMinValue() / scale1;
422 //              double max1 = left.getMaxValue() / scale1;
423 //              double min2 = right.getMinValue() / scale2;
424 //              double max2 = right.getMaxValue() / scale2;
425 //              
426 //              // Combine unit ranges
427 //              min1 = MathUtil.min(min1, min2);
428 //              min2 = min1;
429 //              max1 = MathUtil.max(max1, max2);
430 //              max2 = max1;
431 //              
432 //              // Scale up
433 //              min1 *= scale1;
434 //              max1 *= scale1;
435 //              min2 *= scale2;
436 //              max2 *= scale2;
437 //              
438 //              // Compute common scale
439 //              double range1 = max1-min1;
440 //              double range2 = max2-min2;
441 //              
442 //              double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
443 //              double roundScale = roundScale(scale);
444 //
445 //              if (range2 < range1) {
446 //                      if (roundScale < scale) {
447 //                              min2 = min1 / roundScale;
448 //                              max2 = max1 / roundScale;
449 //                      } else {
450 //                              min1 = min2 * roundScale;
451 //                              max1 = max2 * roundScale;
452 //                      }
453 //              } else {
454 //                      if (roundScale > scale) {
455 //                              min2 = min1 * roundScale;
456 //                              max2 = max1 * roundScale;
457 //                      } else {
458 //                              min1 = min2 / roundScale;
459 //                              max1 = max2 / roundScale;
460 //                      }
461 //              }
462                 
463                 // Apply scale
464                 left.addBound(min1);
465                 left.addBound(max1);
466                 right.addBound(min2);
467                 right.addBound(max2);
468                 
469         }
470         
471         
472
473         private double roundScale(double scale) {
474                 double mul = 1;
475                 while (scale >= 10) {
476                         scale /= 10;
477                         mul *= 10;
478                 }
479                 while (scale < 1) {
480                         scale *= 10;
481                         mul /= 10;
482                 }
483                 
484                 // 1 2 4 5 10
485                 
486                 if (scale > 7.5) {
487                         scale = 10;
488                 } else if (scale > 4.5) {
489                         scale = 5;
490                 } else if (scale > 3) {
491                         scale = 4;
492                 } else if (scale > 1.5) {
493                         scale = 2;
494                 } else {
495                         scale = 1;
496                 }
497                 return scale*mul;
498         }
499         
500         
501         
502         private double roundScaleUp(double scale) {
503                 double mul = 1;
504                 while (scale >= 10) {
505                         scale /= 10;
506                         mul *= 10;
507                 }
508                 while (scale < 1) {
509                         scale *= 10;
510                         mul /= 10;
511                 }
512                 
513                 if (scale > 5) {
514                         scale = 10;
515                 } else if (scale > 4) {
516                         scale = 5;
517                 } else if (scale > 2) {
518                         scale = 4;
519                 } else if (scale > 1) {
520                         scale = 2;
521                 } else {
522                         scale = 1;
523                 }
524                 return scale*mul;
525         }
526         
527
528         private double roundScaleDown(double scale) {
529                 double mul = 1;
530                 while (scale >= 10) {
531                         scale /= 10;
532                         mul *= 10;
533                 }
534                 while (scale < 1) {
535                         scale *= 10;
536                         mul /= 10;
537                 }
538                 
539                 if (scale > 5) {
540                         scale = 5;
541                 } else if (scale > 4) {
542                         scale = 4;
543                 } else if (scale > 2) {
544                         scale = 2;
545                 } else {
546                         scale = 1;
547                 }
548                 return scale*mul;
549         }
550         
551         
552         
553         /**
554          * Fits the axis ranges to the data and returns the "goodness value" of this 
555          * selection of axes.  All plotDataAxis elements must be non-null.
556          * <p>
557          * NOTE: This method assumes that all data can fit into the axes ranges and
558          * that only two axes are used.
559          *
560          * @return      a "goodness value", the larger the better.
561          */
562         protected double getGoodnessValue(FlightDataBranch data) {
563                 double goodness = 0;
564                 int length = plotDataTypes.size();
565                 
566                 // Fit the axes ranges to the data
567                 fitAxes(data);
568                 
569                 /*
570                  * Calculate goodness of ranges.  100 points is given if the values fill the
571                  * entire range, 0 if they fill none of it.
572                  */
573                 for (int i = 0; i < length; i++) {
574                         FlightDataBranch.Type type = plotDataTypes.get(i);
575                         Unit unit = plotDataUnits.get(i);
576                         int index = plotDataAxes.get(i);
577                         if (index < 0) {
578                                 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
579                         }
580                         Axis axis = allAxes.get(index);
581                         
582                         double min = unit.toUnit(data.getMinimum(type));
583                         double max = unit.toUnit(data.getMaximum(type));
584                         if (Double.isNaN(min) || Double.isNaN(max))
585                                 continue;
586                         if (MathUtil.equals(min, max))
587                                 continue;
588                         
589                         double d = (max-min) / axis.getRangeLength();
590                         d = Math.sqrt(d);  // Prioritize small ranges
591                         goodness += d * 100.0;
592                 }
593                 
594                 
595                 /*
596                  * Add extra points for specific things.
597                  */
598
599                 // A little for the first type being on the first axis
600                 if (plotDataAxes.get(0) == 0)
601                         goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
602                 
603                 // A little bonus if the first axis contains zero
604                 Axis left = allAxes.get(0);
605                 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
606                         goodness += BONUS_FIRST_AXIS_HAS_ZERO;
607                 
608                 // A boost if a common zero was used in the ranging
609                 Axis right = allAxes.get(1);
610                 if (left.getMinValue() <= 0 &&  left.getMaxValue() >= 0 &&
611                                 right.getMinValue() <= 0 && right.getMaxValue() >= 0)
612                         goodness += BONUS_COMMON_ZERO;
613                 
614                 // A boost if only one axis is used
615                 if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
616                         goodness += BONUS_ONLY_ONE_AXIS;
617                 
618                 return goodness;
619         }
620         
621         
622         
623         /**
624          * Reset the units of this configuration to the default units. Returns this
625          * PlotConfiguration.
626          * 
627          * @return   this PlotConfiguration.
628          */
629         public PlotConfiguration resetUnits() {
630                 for (int i=0; i < plotDataTypes.size(); i++) {
631                         plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
632                 }
633                 return this;
634         }
635         
636         
637         
638         
639         @SuppressWarnings("unchecked")
640         @Override
641         public PlotConfiguration clone() {
642                 try {
643                         
644                         PlotConfiguration copy = (PlotConfiguration) super.clone();
645                         
646                         // Shallow-clone all immutable lists
647                         copy.plotDataTypes = (ArrayList<Type>) this.plotDataTypes.clone();
648                         copy.plotDataAxes = (ArrayList<Integer>) this.plotDataAxes.clone();
649                         copy.plotDataUnits = (ArrayList<Unit>) this.plotDataUnits.clone();
650                         
651                         // Deep-clone all Axis since they are mutable
652                         copy.allAxes = new ArrayList<Axis>();
653                         for (Axis a: this.allAxes) {
654                                 copy.allAxes.add(a.clone());
655                         }
656                         
657                         return copy;
658                         
659                         
660                 } catch (CloneNotSupportedException e) {
661                         throw new RuntimeException("BUG! Could not clone().");
662                 }
663         }
664         
665 }