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.BugException;
13 import net.sf.openrocket.util.MathUtil;
14 import net.sf.openrocket.util.Pair;
17 public class PlotConfiguration implements Cloneable {
19 public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
21 ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
22 PlotConfiguration config;
24 config = new PlotConfiguration("Vertical motion vs. time");
25 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
26 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
27 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
28 config.setEvent(FlightEvent.Type.IGNITION, true);
29 config.setEvent(FlightEvent.Type.BURNOUT, true);
30 config.setEvent(FlightEvent.Type.APOGEE, true);
31 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
32 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
33 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
36 config = new PlotConfiguration("Total motion vs. time");
37 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
38 config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_TOTAL);
39 config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
40 config.setEvent(FlightEvent.Type.IGNITION, true);
41 config.setEvent(FlightEvent.Type.BURNOUT, true);
42 config.setEvent(FlightEvent.Type.APOGEE, true);
43 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
44 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
45 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
48 config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
49 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
50 config.setEvent(FlightEvent.Type.IGNITION, true);
51 config.setEvent(FlightEvent.Type.BURNOUT, true);
52 config.setEvent(FlightEvent.Type.APOGEE, true);
53 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
54 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
55 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
58 config = new PlotConfiguration("Stability vs. time");
59 config.addPlotDataType(FlightDataBranch.TYPE_STABILITY, 0);
60 config.addPlotDataType(FlightDataBranch.TYPE_CP_LOCATION, 1);
61 config.addPlotDataType(FlightDataBranch.TYPE_CG_LOCATION, 1);
62 config.setEvent(FlightEvent.Type.IGNITION, true);
63 config.setEvent(FlightEvent.Type.BURNOUT, true);
64 config.setEvent(FlightEvent.Type.APOGEE, true);
65 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
66 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
67 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
70 config = new PlotConfiguration("Drag coefficients vs. Mach number",
71 FlightDataBranch.TYPE_MACH_NUMBER);
72 config.addPlotDataType(FlightDataBranch.TYPE_DRAG_COEFF, 0);
73 config.addPlotDataType(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, 0);
74 config.addPlotDataType(FlightDataBranch.TYPE_BASE_DRAG_COEFF, 0);
75 config.addPlotDataType(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, 0);
78 config = new PlotConfiguration("Roll characteristics");
79 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_RATE, 0);
80 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, 1);
81 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, 1);
82 config.addPlotDataType(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, 1);
83 config.setEvent(FlightEvent.Type.IGNITION, true);
84 config.setEvent(FlightEvent.Type.LAUNCHROD, true);
85 config.setEvent(FlightEvent.Type.BURNOUT, true);
86 config.setEvent(FlightEvent.Type.APOGEE, true);
87 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
88 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
89 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
92 config = new PlotConfiguration("Angle of attack and orientation vs. time");
93 config.addPlotDataType(FlightDataBranch.TYPE_AOA, 0);
94 config.addPlotDataType(FlightDataBranch.TYPE_ORIENTATION_PHI);
95 config.addPlotDataType(FlightDataBranch.TYPE_ORIENTATION_THETA);
96 config.setEvent(FlightEvent.Type.IGNITION, true);
97 config.setEvent(FlightEvent.Type.BURNOUT, true);
98 config.setEvent(FlightEvent.Type.APOGEE, true);
99 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
100 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
101 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
104 config = new PlotConfiguration("Simulation time step and computation time");
105 config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
106 config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
107 config.setEvent(FlightEvent.Type.IGNITION, true);
108 config.setEvent(FlightEvent.Type.BURNOUT, true);
109 config.setEvent(FlightEvent.Type.APOGEE, true);
110 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
111 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
112 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
115 DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
120 /** Bonus given for the first type being on the first axis */
121 private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
124 * Bonus given if the first axis includes zero (to prefer first axis having zero over
127 private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
129 /** Bonus given for a common zero point on left and right axes. */
130 private static final double BONUS_COMMON_ZERO = 40.0;
132 /** Bonus given for only using a single axis. */
133 private static final double BONUS_ONLY_ONE_AXIS = 50.0;
136 private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range
140 /** The data types to be plotted. */
141 private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
143 private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
145 /** The corresponding Axis on which they will be plotted, or null to auto-select. */
146 private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
148 private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class);
150 /** The domain (x) axis. */
151 private FlightDataBranch.Type domainAxisType = null;
152 private Unit domainAxisUnit = null;
155 /** All available axes. */
156 private final int axesCount;
157 private ArrayList<Axis> allAxes = new ArrayList<Axis>();
161 private String name = null;
165 public PlotConfiguration() {
166 this(null, FlightDataBranch.TYPE_TIME);
169 public PlotConfiguration(String name) {
170 this(name, FlightDataBranch.TYPE_TIME);
173 public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
176 allAxes.add(new Axis());
177 allAxes.add(new Axis());
180 setDomainAxisType(domainType);
187 public FlightDataBranch.Type getDomainAxisType() {
188 return domainAxisType;
191 public void setDomainAxisType(FlightDataBranch.Type type) {
194 if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
199 domainAxisType = type;
201 domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
204 public Unit getDomainAxisUnit() {
205 return domainAxisUnit;
208 public void setDomainAxisUnit(Unit u) {
209 if (!domainAxisType.getUnitGroup().contains(u)) {
210 throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
217 public void addPlotDataType(FlightDataBranch.Type type) {
218 plotDataTypes.add(type);
219 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
220 plotDataAxes.add(-1);
223 public void addPlotDataType(FlightDataBranch.Type type, int axis) {
224 if (axis >= axesCount) {
225 throw new IllegalArgumentException("Axis index too large");
227 plotDataTypes.add(type);
228 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
229 plotDataAxes.add(axis);
235 public void setPlotDataType(int index, FlightDataBranch.Type type) {
236 FlightDataBranch.Type origType = plotDataTypes.get(index);
237 plotDataTypes.set(index, type);
239 if (origType.getUnitGroup() != type.getUnitGroup()) {
240 plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
244 public void setPlotDataUnit(int index, Unit unit) {
245 if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
246 throw new IllegalArgumentException("Attempting to set unit "+unit+" to group "
247 + plotDataTypes.get(index).getUnitGroup());
249 plotDataUnits.set(index, unit);
252 public void setPlotDataAxis(int index, int axis) {
253 if (axis >= axesCount) {
254 throw new IllegalArgumentException("Axis index too large");
256 plotDataAxes.set(index, axis);
260 public void setPlotDataType(int index, FlightDataBranch.Type type, Unit unit, int axis) {
261 if (axis >= axesCount) {
262 throw new IllegalArgumentException("Axis index too large");
264 plotDataTypes.set(index, type);
265 plotDataUnits.set(index, unit);
266 plotDataAxes.set(index, axis);
269 public void removePlotDataType(int index) {
270 plotDataTypes.remove(index);
271 plotDataUnits.remove(index);
272 plotDataAxes.remove(index);
277 public FlightDataBranch.Type getType (int index) {
278 return plotDataTypes.get(index);
280 public Unit getUnit(int index) {
281 return plotDataUnits.get(index);
283 public int getAxis(int index) {
284 return plotDataAxes.get(index);
287 public int getTypeCount() {
288 return plotDataTypes.size();
294 public Set<FlightEvent.Type> getActiveEvents() {
295 return (Set<FlightEvent.Type>) events.clone();
298 public void setEvent(FlightEvent.Type type, boolean active) {
306 public boolean isEventActive(FlightEvent.Type type) {
307 return events.contains(type);
314 public List<Axis> getAllAxes() {
315 List<Axis> list = new ArrayList<Axis>();
316 list.addAll(allAxes);
321 public String getName() {
325 public void setName(String name) {
330 * Returns the name of this PlotConfiguration.
333 public String toString() {
340 * Find the best combination of the auto-selectable axes.
342 * @return a new PlotConfiguration with the best fitting auto-selected axes and
343 * axes ranges selected.
345 public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
346 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
347 System.out.println("BEST FOUND, fitting");
348 config.fitAxes(data);
356 * Recursively search for the best combination of the auto-selectable axes.
357 * This is a brute-force search method.
359 * @return a new PlotConfiguration with the best fitting auto-selected axes and
360 * axes ranges selected, and the goodness value
362 private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
364 // Create copy to fill in
365 PlotConfiguration copy = this.clone();
368 for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
369 if (plotDataAxes.get(autoindex) < 0)
374 if (autoindex >= plotDataAxes.size()) {
375 // All axes have been assigned, just return since we are already the best
376 return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
380 // Set the auto-selected index one at a time and choose the best one
381 PlotConfiguration best = null;
382 double bestValue = Double.NEGATIVE_INFINITY;
383 for (int i=0; i < axesCount; i++) {
384 copy.plotDataAxes.set(autoindex, i);
385 Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
386 if (result.getV() > bestValue) {
387 best = result.getU();
388 bestValue = result.getV();
392 return new Pair<PlotConfiguration, Double>(best, bestValue);
400 * Fit the axes to hold the provided data. All of the plotDataAxis elements must
403 * NOTE: This method assumes that only two axes are used.
405 protected void fitAxes(FlightDataBranch data) {
408 for (Axis a: allAxes) {
412 // Add full range to the axes
413 int length = plotDataTypes.size();
414 for (int i=0; i<length; i++) {
415 FlightDataBranch.Type type = plotDataTypes.get(i);
416 Unit unit = plotDataUnits.get(i);
417 int index = plotDataAxes.get(i);
419 throw new IllegalStateException("fitAxes called with auto-selected axis");
421 Axis axis = allAxes.get(index);
423 double min = unit.toUnit(data.getMinimum(type));
424 double max = unit.toUnit(data.getMaximum(type));
430 // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
431 for (Axis a: allAxes) {
432 if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
433 a.addBound(a.getMinValue()-1);
434 a.addBound(a.getMaxValue()+1);
437 double addition = a.getRangeLength() * 0.03;
438 a.addBound(a.getMinValue() - addition);
439 a.addBound(a.getMaxValue() + addition);
442 dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
443 if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
449 // Check whether to use a common zero
450 Axis left = allAxes.get(0);
451 Axis right = allAxes.get(1);
453 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
454 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
455 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
460 //// Compute common zero
461 // TODO: MEDIUM: This algorithm may require tweaking
463 double min1 = left.getMinValue();
464 double max1 = left.getMaxValue();
465 double min2 = right.getMinValue();
466 double max2 = right.getMaxValue();
468 // Calculate and round scaling factor
469 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
470 Math.min(left.getRangeLength(), right.getRangeLength());
472 System.out.println("Scale: "+scale);
474 scale = roundScale(scale);
475 if (right.getRangeLength() > left.getRangeLength()) {
478 System.out.println("Rounded scale: " + scale);
480 // Scale right axis, enlarge axes if necessary and scale back
483 min1 = Math.min(min1, min2);
485 max1 = Math.max(max1, max2);
492 // Scale to unit length
493 // double scale1 = left.getRangeLength();
494 // double scale2 = right.getRangeLength();
496 // double min1 = left.getMinValue() / scale1;
497 // double max1 = left.getMaxValue() / scale1;
498 // double min2 = right.getMinValue() / scale2;
499 // double max2 = right.getMaxValue() / scale2;
501 // // Combine unit ranges
502 // min1 = MathUtil.min(min1, min2);
504 // max1 = MathUtil.max(max1, max2);
513 // // Compute common scale
514 // double range1 = max1-min1;
515 // double range2 = max2-min2;
517 // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
518 // double roundScale = roundScale(scale);
520 // if (range2 < range1) {
521 // if (roundScale < scale) {
522 // min2 = min1 / roundScale;
523 // max2 = max1 / roundScale;
525 // min1 = min2 * roundScale;
526 // max1 = max2 * roundScale;
529 // if (roundScale > scale) {
530 // min2 = min1 * roundScale;
531 // max2 = max1 * roundScale;
533 // min1 = min2 / roundScale;
534 // max1 = max2 / roundScale;
541 right.addBound(min2);
542 right.addBound(max2);
548 private double roundScale(double scale) {
550 while (scale >= 10) {
563 } else if (scale > 4.5) {
565 } else if (scale > 3) {
567 } else if (scale > 1.5) {
577 private double roundScaleUp(double scale) {
579 while (scale >= 10) {
590 } else if (scale > 4) {
592 } else if (scale > 2) {
594 } else if (scale > 1) {
603 private double roundScaleDown(double scale) {
605 while (scale >= 10) {
616 } else if (scale > 4) {
618 } else if (scale > 2) {
629 * Fits the axis ranges to the data and returns the "goodness value" of this
630 * selection of axes. All plotDataAxis elements must be non-null.
632 * NOTE: This method assumes that all data can fit into the axes ranges and
633 * that only two axes are used.
635 * @return a "goodness value", the larger the better.
637 protected double getGoodnessValue(FlightDataBranch data) {
639 int length = plotDataTypes.size();
641 // Fit the axes ranges to the data
645 * Calculate goodness of ranges. 100 points is given if the values fill the
646 * entire range, 0 if they fill none of it.
648 for (int i = 0; i < length; i++) {
649 FlightDataBranch.Type type = plotDataTypes.get(i);
650 Unit unit = plotDataUnits.get(i);
651 int index = plotDataAxes.get(i);
653 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
655 Axis axis = allAxes.get(index);
657 double min = unit.toUnit(data.getMinimum(type));
658 double max = unit.toUnit(data.getMaximum(type));
659 if (Double.isNaN(min) || Double.isNaN(max))
661 if (MathUtil.equals(min, max))
664 double d = (max-min) / axis.getRangeLength();
665 d = Math.sqrt(d); // Prioritize small ranges
666 goodness += d * 100.0;
671 * Add extra points for specific things.
674 // A little for the first type being on the first axis
675 if (plotDataAxes.get(0) == 0)
676 goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
678 // A little bonus if the first axis contains zero
679 Axis left = allAxes.get(0);
680 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
681 goodness += BONUS_FIRST_AXIS_HAS_ZERO;
683 // A boost if a common zero was used in the ranging
684 Axis right = allAxes.get(1);
685 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 &&
686 right.getMinValue() <= 0 && right.getMaxValue() >= 0)
687 goodness += BONUS_COMMON_ZERO;
689 // A boost if only one axis is used
690 if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
691 goodness += BONUS_ONLY_ONE_AXIS;
699 * Reset the units of this configuration to the default units. Returns this
702 * @return this PlotConfiguration.
704 public PlotConfiguration resetUnits() {
705 for (int i=0; i < plotDataTypes.size(); i++) {
706 plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
714 @SuppressWarnings("unchecked")
716 public PlotConfiguration clone() {
719 PlotConfiguration copy = (PlotConfiguration) super.clone();
721 // Shallow-clone all immutable lists
722 copy.plotDataTypes = (ArrayList<Type>) this.plotDataTypes.clone();
723 copy.plotDataAxes = (ArrayList<Integer>) this.plotDataAxes.clone();
724 copy.plotDataUnits = (ArrayList<Unit>) this.plotDataUnits.clone();
725 copy.events = this.events.clone();
727 // Deep-clone all Axis since they are mutable
728 copy.allAxes = new ArrayList<Axis>();
729 for (Axis a: this.allAxes) {
730 copy.allAxes.add(a.clone());
736 } catch (CloneNotSupportedException e) {
737 throw new BugException("BUG! Could not clone().");