1 package net.sf.openrocket.gui.plot;
3 import java.util.ArrayList;
4 import java.util.EnumSet;
8 import net.sf.openrocket.simulation.FlightDataBranch;
9 import net.sf.openrocket.simulation.FlightEvent;
10 import net.sf.openrocket.simulation.FlightDataBranch.Type;
11 import net.sf.openrocket.unit.Unit;
12 import net.sf.openrocket.util.MathUtil;
13 import net.sf.openrocket.util.Pair;
16 public class PlotConfiguration implements Cloneable {
18 public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
20 ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
21 PlotConfiguration config;
23 config = new PlotConfiguration("Vertical motion vs. time");
24 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
25 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
26 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
27 config.setEvent(FlightEvent.Type.IGNITION, true);
28 config.setEvent(FlightEvent.Type.BURNOUT, true);
29 config.setEvent(FlightEvent.Type.APOGEE, true);
30 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
31 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
32 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
35 config = new PlotConfiguration("Total motion vs. time");
36 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
37 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_TOTAL);
38 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
39 config.setEvent(FlightEvent.Type.IGNITION, true);
40 config.setEvent(FlightEvent.Type.BURNOUT, true);
41 config.setEvent(FlightEvent.Type.APOGEE, true);
42 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
43 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
44 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
47 config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
48 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
49 config.setEvent(FlightEvent.Type.IGNITION, true);
50 config.setEvent(FlightEvent.Type.BURNOUT, true);
51 config.setEvent(FlightEvent.Type.APOGEE, true);
52 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
53 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
54 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
57 config = new PlotConfiguration("Stability vs. time");
58 config.addPlotDataType(FlightDataBranch.TYPE_STABILITY, 0);
59 config.addPlotDataType(FlightDataBranch.TYPE_CP_LOCATION, 1);
60 config.addPlotDataType(FlightDataBranch.TYPE_CG_LOCATION, 1);
61 config.setEvent(FlightEvent.Type.IGNITION, true);
62 config.setEvent(FlightEvent.Type.BURNOUT, true);
63 config.setEvent(FlightEvent.Type.APOGEE, true);
64 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
65 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
66 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
69 config = new PlotConfiguration("Drag coefficients vs. Mach number",
70 FlightDataBranch.TYPE_MACH_NUMBER);
71 config.addPlotDataType(FlightDataBranch.TYPE_DRAG_COEFF, 0);
72 config.addPlotDataType(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, 0);
73 config.addPlotDataType(FlightDataBranch.TYPE_BASE_DRAG_COEFF, 0);
74 config.addPlotDataType(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, 0);
77 config = new PlotConfiguration("Roll characteristics");
78 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_RATE, 0);
79 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, 1);
80 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, 1);
81 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, 1);
82 config.setEvent(FlightEvent.Type.IGNITION, true);
83 config.setEvent(FlightEvent.Type.LAUNCHROD, true);
84 config.setEvent(FlightEvent.Type.BURNOUT, true);
85 config.setEvent(FlightEvent.Type.APOGEE, true);
86 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
87 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
88 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
91 config = new PlotConfiguration("Angle of attack and orientation vs. time");
92 config.addPlotDataType(FlightDataBranch.TYPE_AOA, 0);
93 config.addPlotDataType(FlightDataBranch.TYPE_ORIENTATION_PHI);
94 config.addPlotDataType(FlightDataBranch.TYPE_ORIENTATION_THETA);
95 config.setEvent(FlightEvent.Type.IGNITION, true);
96 config.setEvent(FlightEvent.Type.BURNOUT, true);
97 config.setEvent(FlightEvent.Type.APOGEE, true);
98 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
99 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
100 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
103 config = new PlotConfiguration("Simulation time step and computation time");
104 config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
105 config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
106 config.setEvent(FlightEvent.Type.IGNITION, true);
107 config.setEvent(FlightEvent.Type.BURNOUT, true);
108 config.setEvent(FlightEvent.Type.APOGEE, true);
109 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
110 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
111 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
114 DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
119 /** Bonus given for the first type being on the first axis */
120 private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
123 * Bonus given if the first axis includes zero (to prefer first axis having zero over
126 private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
128 /** Bonus given for a common zero point on left and right axes. */
129 private static final double BONUS_COMMON_ZERO = 40.0;
131 /** Bonus given for only using a single axis. */
132 private static final double BONUS_ONLY_ONE_AXIS = 50.0;
135 private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range
139 /** The data types to be plotted. */
140 private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
142 private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
144 /** The corresponding Axis on which they will be plotted, or null to auto-select. */
145 private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
147 private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class);
149 /** The domain (x) axis. */
150 private FlightDataBranch.Type domainAxisType = null;
151 private Unit domainAxisUnit = null;
154 /** All available axes. */
155 private final int axesCount;
156 private ArrayList<Axis> allAxes = new ArrayList<Axis>();
160 private String name = null;
164 public PlotConfiguration() {
165 this(null, FlightDataBranch.TYPE_TIME);
168 public PlotConfiguration(String name) {
169 this(name, FlightDataBranch.TYPE_TIME);
172 public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
175 allAxes.add(new Axis());
176 allAxes.add(new Axis());
179 setDomainAxisType(domainType);
186 public FlightDataBranch.Type getDomainAxisType() {
187 return domainAxisType;
190 public void setDomainAxisType(FlightDataBranch.Type type) {
193 if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
198 domainAxisType = type;
200 domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
203 public Unit getDomainAxisUnit() {
204 return domainAxisUnit;
207 public void setDomainAxisUnit(Unit u) {
208 if (!domainAxisType.getUnitGroup().contains(u)) {
209 throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
216 public void addPlotDataType(FlightDataBranch.Type type) {
217 plotDataTypes.add(type);
218 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
219 plotDataAxes.add(-1);
222 public void addPlotDataType(FlightDataBranch.Type type, int axis) {
223 if (axis >= axesCount) {
224 throw new IllegalArgumentException("Axis index too large");
226 plotDataTypes.add(type);
227 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
228 plotDataAxes.add(axis);
234 public void setPlotDataType(int index, FlightDataBranch.Type type) {
235 FlightDataBranch.Type origType = plotDataTypes.get(index);
236 plotDataTypes.set(index, type);
238 if (origType.getUnitGroup() != type.getUnitGroup()) {
239 plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
243 public void setPlotDataUnit(int index, Unit unit) {
244 if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
245 throw new IllegalArgumentException("Attempting to set unit "+unit+" to group "
246 + plotDataTypes.get(index).getUnitGroup());
248 plotDataUnits.set(index, unit);
251 public void setPlotDataAxis(int index, int axis) {
252 if (axis >= axesCount) {
253 throw new IllegalArgumentException("Axis index too large");
255 plotDataAxes.set(index, axis);
259 public void setPlotDataType(int index, FlightDataBranch.Type type, Unit unit, int axis) {
260 if (axis >= axesCount) {
261 throw new IllegalArgumentException("Axis index too large");
263 plotDataTypes.set(index, type);
264 plotDataUnits.set(index, unit);
265 plotDataAxes.set(index, axis);
268 public void removePlotDataType(int index) {
269 plotDataTypes.remove(index);
270 plotDataUnits.remove(index);
271 plotDataAxes.remove(index);
276 public FlightDataBranch.Type getType (int index) {
277 return plotDataTypes.get(index);
279 public Unit getUnit(int index) {
280 return plotDataUnits.get(index);
282 public int getAxis(int index) {
283 return plotDataAxes.get(index);
286 public int getTypeCount() {
287 return plotDataTypes.size();
293 public Set<FlightEvent.Type> getActiveEvents() {
294 return (Set<FlightEvent.Type>) events.clone();
297 public void setEvent(FlightEvent.Type type, boolean active) {
305 public boolean isEventActive(FlightEvent.Type type) {
306 return events.contains(type);
313 public List<Axis> getAllAxes() {
314 List<Axis> list = new ArrayList<Axis>();
315 list.addAll(allAxes);
320 public String getName() {
324 public void setName(String name) {
329 * Returns the name of this PlotConfiguration.
332 public String toString() {
339 * Find the best combination of the auto-selectable axes.
341 * @return a new PlotConfiguration with the best fitting auto-selected axes and
342 * axes ranges selected.
344 public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
345 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
346 System.out.println("BEST FOUND, fitting");
347 config.fitAxes(data);
355 * Recursively search for the best combination of the auto-selectable axes.
356 * This is a brute-force search method.
358 * @return a new PlotConfiguration with the best fitting auto-selected axes and
359 * axes ranges selected, and the goodness value
361 private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
363 // Create copy to fill in
364 PlotConfiguration copy = this.clone();
367 for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
368 if (plotDataAxes.get(autoindex) < 0)
373 if (autoindex >= plotDataAxes.size()) {
374 // All axes have been assigned, just return since we are already the best
375 return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
379 // Set the auto-selected index one at a time and choose the best one
380 PlotConfiguration best = null;
381 double bestValue = Double.NEGATIVE_INFINITY;
382 for (int i=0; i < axesCount; i++) {
383 copy.plotDataAxes.set(autoindex, i);
384 Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
385 if (result.getV() > bestValue) {
386 best = result.getU();
387 bestValue = result.getV();
391 return new Pair<PlotConfiguration, Double>(best, bestValue);
399 * Fit the axes to hold the provided data. All of the plotDataAxis elements must
402 * NOTE: This method assumes that only two axes are used.
404 protected void fitAxes(FlightDataBranch data) {
407 for (Axis a: allAxes) {
411 // Add full range to the axes
412 int length = plotDataTypes.size();
413 for (int i=0; i<length; i++) {
414 FlightDataBranch.Type type = plotDataTypes.get(i);
415 Unit unit = plotDataUnits.get(i);
416 int index = plotDataAxes.get(i);
418 throw new IllegalStateException("fitAxes called with auto-selected axis");
420 Axis axis = allAxes.get(index);
422 double min = unit.toUnit(data.getMinimum(type));
423 double max = unit.toUnit(data.getMaximum(type));
429 // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
430 for (Axis a: allAxes) {
431 if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
432 a.addBound(a.getMinValue()-1);
433 a.addBound(a.getMaxValue()+1);
436 double addition = a.getRangeLength() * 0.03;
437 a.addBound(a.getMinValue() - addition);
438 a.addBound(a.getMaxValue() + addition);
441 dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
442 if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
448 // Check whether to use a common zero
449 Axis left = allAxes.get(0);
450 Axis right = allAxes.get(1);
452 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
453 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
454 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
459 //// Compute common zero
460 // TODO: MEDIUM: This algorithm may require tweaking
462 double min1 = left.getMinValue();
463 double max1 = left.getMaxValue();
464 double min2 = right.getMinValue();
465 double max2 = right.getMaxValue();
467 // Calculate and round scaling factor
468 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
469 Math.min(left.getRangeLength(), right.getRangeLength());
471 System.out.println("Scale: "+scale);
473 scale = roundScale(scale);
474 if (right.getRangeLength() > left.getRangeLength()) {
477 System.out.println("Rounded scale: " + scale);
479 // Scale right axis, enlarge axes if necessary and scale back
482 min1 = Math.min(min1, min2);
484 max1 = Math.max(max1, max2);
491 // Scale to unit length
492 // double scale1 = left.getRangeLength();
493 // double scale2 = right.getRangeLength();
495 // double min1 = left.getMinValue() / scale1;
496 // double max1 = left.getMaxValue() / scale1;
497 // double min2 = right.getMinValue() / scale2;
498 // double max2 = right.getMaxValue() / scale2;
500 // // Combine unit ranges
501 // min1 = MathUtil.min(min1, min2);
503 // max1 = MathUtil.max(max1, max2);
512 // // Compute common scale
513 // double range1 = max1-min1;
514 // double range2 = max2-min2;
516 // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
517 // double roundScale = roundScale(scale);
519 // if (range2 < range1) {
520 // if (roundScale < scale) {
521 // min2 = min1 / roundScale;
522 // max2 = max1 / roundScale;
524 // min1 = min2 * roundScale;
525 // max1 = max2 * roundScale;
528 // if (roundScale > scale) {
529 // min2 = min1 * roundScale;
530 // max2 = max1 * roundScale;
532 // min1 = min2 / roundScale;
533 // max1 = max2 / roundScale;
540 right.addBound(min2);
541 right.addBound(max2);
547 private double roundScale(double scale) {
549 while (scale >= 10) {
562 } else if (scale > 4.5) {
564 } else if (scale > 3) {
566 } else if (scale > 1.5) {
576 private double roundScaleUp(double scale) {
578 while (scale >= 10) {
589 } else if (scale > 4) {
591 } else if (scale > 2) {
593 } else if (scale > 1) {
602 private double roundScaleDown(double scale) {
604 while (scale >= 10) {
615 } else if (scale > 4) {
617 } else if (scale > 2) {
628 * Fits the axis ranges to the data and returns the "goodness value" of this
629 * selection of axes. All plotDataAxis elements must be non-null.
631 * NOTE: This method assumes that all data can fit into the axes ranges and
632 * that only two axes are used.
634 * @return a "goodness value", the larger the better.
636 protected double getGoodnessValue(FlightDataBranch data) {
638 int length = plotDataTypes.size();
640 // Fit the axes ranges to the data
644 * Calculate goodness of ranges. 100 points is given if the values fill the
645 * entire range, 0 if they fill none of it.
647 for (int i = 0; i < length; i++) {
648 FlightDataBranch.Type type = plotDataTypes.get(i);
649 Unit unit = plotDataUnits.get(i);
650 int index = plotDataAxes.get(i);
652 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
654 Axis axis = allAxes.get(index);
656 double min = unit.toUnit(data.getMinimum(type));
657 double max = unit.toUnit(data.getMaximum(type));
658 if (Double.isNaN(min) || Double.isNaN(max))
660 if (MathUtil.equals(min, max))
663 double d = (max-min) / axis.getRangeLength();
664 d = Math.sqrt(d); // Prioritize small ranges
665 goodness += d * 100.0;
670 * Add extra points for specific things.
673 // A little for the first type being on the first axis
674 if (plotDataAxes.get(0) == 0)
675 goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
677 // A little bonus if the first axis contains zero
678 Axis left = allAxes.get(0);
679 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
680 goodness += BONUS_FIRST_AXIS_HAS_ZERO;
682 // A boost if a common zero was used in the ranging
683 Axis right = allAxes.get(1);
684 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 &&
685 right.getMinValue() <= 0 && right.getMaxValue() >= 0)
686 goodness += BONUS_COMMON_ZERO;
688 // A boost if only one axis is used
689 if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
690 goodness += BONUS_ONLY_ONE_AXIS;
698 * Reset the units of this configuration to the default units. Returns this
701 * @return this PlotConfiguration.
703 public PlotConfiguration resetUnits() {
704 for (int i=0; i < plotDataTypes.size(); i++) {
705 plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
713 @SuppressWarnings("unchecked")
715 public PlotConfiguration clone() {
718 PlotConfiguration copy = (PlotConfiguration) super.clone();
720 // Shallow-clone all immutable lists
721 copy.plotDataTypes = (ArrayList<Type>) this.plotDataTypes.clone();
722 copy.plotDataAxes = (ArrayList<Integer>) this.plotDataAxes.clone();
723 copy.plotDataUnits = (ArrayList<Unit>) this.plotDataUnits.clone();
724 copy.events = this.events.clone();
726 // Deep-clone all Axis since they are mutable
727 copy.allAxes = new ArrayList<Axis>();
728 for (Axis a: this.allAxes) {
729 copy.allAxes.add(a.clone());
735 } catch (CloneNotSupportedException e) {
736 throw new RuntimeException("BUG! Could not clone().");