1 package net.sf.openrocket.gui.plot;
3 import java.util.EnumSet;
7 import net.sf.openrocket.simulation.FlightDataBranch;
8 import net.sf.openrocket.simulation.FlightDataType;
9 import net.sf.openrocket.simulation.FlightEvent;
10 import net.sf.openrocket.unit.Unit;
11 import net.sf.openrocket.util.ArrayList;
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(FlightDataType.TYPE_ALTITUDE, 0);
26 config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z);
27 config.addPlotDataType(FlightDataType.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(FlightDataType.TYPE_ALTITUDE, 0);
38 config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL);
39 config.addPlotDataType(FlightDataType.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", FlightDataType.TYPE_POSITION_X);
49 config.addPlotDataType(FlightDataType.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(FlightDataType.TYPE_STABILITY, 0);
60 config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1);
61 config.addPlotDataType(FlightDataType.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 FlightDataType.TYPE_MACH_NUMBER);
72 config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0);
73 config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0);
74 config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0);
75 config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0);
78 config = new PlotConfiguration("Roll characteristics");
79 config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0);
80 config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1);
81 config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1);
82 config.addPlotDataType(FlightDataType.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(FlightDataType.TYPE_AOA, 0);
94 config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI);
95 config.addPlotDataType(FlightDataType.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(FlightDataType.TYPE_TIME_STEP);
106 config.addPlotDataType(FlightDataType.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<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>();
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 FlightDataType 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, FlightDataType.TYPE_TIME);
169 public PlotConfiguration(String name) {
170 this(name, FlightDataType.TYPE_TIME);
173 public PlotConfiguration(String name, FlightDataType domainType) {
176 allAxes.add(new Axis());
177 allAxes.add(new Axis());
180 setDomainAxisType(domainType);
187 public FlightDataType getDomainAxisType() {
188 return domainAxisType;
191 public void setDomainAxisType(FlightDataType 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(FlightDataType type) {
218 plotDataTypes.add(type);
219 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
220 plotDataAxes.add(-1);
223 public void addPlotDataType(FlightDataType 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, FlightDataType type) {
236 FlightDataType 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, FlightDataType 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 FlightDataType getType(int index) {
278 return plotDataTypes.get(index);
281 public Unit getUnit(int index) {
282 return plotDataUnits.get(index);
285 public int getAxis(int index) {
286 return plotDataAxes.get(index);
289 public int getTypeCount() {
290 return plotDataTypes.size();
296 public Set<FlightEvent.Type> getActiveEvents() {
297 return events.clone();
300 public void setEvent(FlightEvent.Type type, boolean active) {
308 public boolean isEventActive(FlightEvent.Type type) {
309 return events.contains(type);
316 public List<Axis> getAllAxes() {
317 List<Axis> list = new ArrayList<Axis>();
318 list.addAll(allAxes);
323 public String getName() {
327 public void setName(String name) {
332 * Returns the name of this PlotConfiguration.
335 public String toString() {
342 * Find the best combination of the auto-selectable axes.
344 * @return a new PlotConfiguration with the best fitting auto-selected axes and
345 * axes ranges selected.
347 public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
348 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
349 System.out.println("BEST FOUND, fitting");
350 config.fitAxes(data);
358 * Recursively search for the best combination of the auto-selectable axes.
359 * This is a brute-force search method.
361 * @return a new PlotConfiguration with the best fitting auto-selected axes and
362 * axes ranges selected, and the goodness value
364 private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
366 // Create copy to fill in
367 PlotConfiguration copy = this.clone();
370 for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) {
371 if (plotDataAxes.get(autoindex) < 0)
376 if (autoindex >= plotDataAxes.size()) {
377 // All axes have been assigned, just return since we are already the best
378 return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
382 // Set the auto-selected index one at a time and choose the best one
383 PlotConfiguration best = null;
384 double bestValue = Double.NEGATIVE_INFINITY;
385 for (int i = 0; i < axesCount; i++) {
386 copy.plotDataAxes.set(autoindex, i);
387 Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
388 if (result.getV() > bestValue) {
389 best = result.getU();
390 bestValue = result.getV();
394 return new Pair<PlotConfiguration, Double>(best, bestValue);
402 * Fit the axes to hold the provided data. All of the plotDataAxis elements must
405 * NOTE: This method assumes that only two axes are used.
407 protected void fitAxes(FlightDataBranch data) {
410 for (Axis a : allAxes) {
414 // Add full range to the axes
415 int length = plotDataTypes.size();
416 for (int i = 0; i < length; i++) {
417 FlightDataType type = plotDataTypes.get(i);
418 Unit unit = plotDataUnits.get(i);
419 int index = plotDataAxes.get(i);
421 throw new IllegalStateException("fitAxes called with auto-selected axis");
423 Axis axis = allAxes.get(index);
425 double min = unit.toUnit(data.getMinimum(type));
426 double max = unit.toUnit(data.getMaximum(type));
432 // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
433 for (Axis a : allAxes) {
434 if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
435 a.addBound(a.getMinValue() - 1);
436 a.addBound(a.getMaxValue() + 1);
439 double addition = a.getRangeLength() * 0.03;
440 a.addBound(a.getMinValue() - addition);
441 a.addBound(a.getMaxValue() + addition);
444 dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
445 if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
451 // Check whether to use a common zero
452 Axis left = allAxes.get(0);
453 Axis right = allAxes.get(1);
455 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
456 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
457 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
462 //// Compute common zero
463 // TODO: MEDIUM: This algorithm may require tweaking
465 double min1 = left.getMinValue();
466 double max1 = left.getMaxValue();
467 double min2 = right.getMinValue();
468 double max2 = right.getMaxValue();
470 // Calculate and round scaling factor
471 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
472 Math.min(left.getRangeLength(), right.getRangeLength());
474 System.out.println("Scale: " + scale);
476 scale = roundScale(scale);
477 if (right.getRangeLength() > left.getRangeLength()) {
480 System.out.println("Rounded scale: " + scale);
482 // Scale right axis, enlarge axes if necessary and scale back
485 min1 = Math.min(min1, min2);
487 max1 = Math.max(max1, max2);
494 // Scale to unit length
495 // double scale1 = left.getRangeLength();
496 // double scale2 = right.getRangeLength();
498 // double min1 = left.getMinValue() / scale1;
499 // double max1 = left.getMaxValue() / scale1;
500 // double min2 = right.getMinValue() / scale2;
501 // double max2 = right.getMaxValue() / scale2;
503 // // Combine unit ranges
504 // min1 = MathUtil.min(min1, min2);
506 // max1 = MathUtil.max(max1, max2);
515 // // Compute common scale
516 // double range1 = max1-min1;
517 // double range2 = max2-min2;
519 // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
520 // double roundScale = roundScale(scale);
522 // if (range2 < range1) {
523 // if (roundScale < scale) {
524 // min2 = min1 / roundScale;
525 // max2 = max1 / roundScale;
527 // min1 = min2 * roundScale;
528 // max1 = max2 * roundScale;
531 // if (roundScale > scale) {
532 // min2 = min1 * roundScale;
533 // max2 = max1 * roundScale;
535 // min1 = min2 / roundScale;
536 // max1 = max2 / roundScale;
543 right.addBound(min2);
544 right.addBound(max2);
550 private double roundScale(double scale) {
552 while (scale >= 10) {
565 } else if (scale > 4.5) {
567 } else if (scale > 3) {
569 } else if (scale > 1.5) {
579 private double roundScaleUp(double scale) {
581 while (scale >= 10) {
592 } else if (scale > 4) {
594 } else if (scale > 2) {
596 } else if (scale > 1) {
605 private double roundScaleDown(double scale) {
607 while (scale >= 10) {
618 } else if (scale > 4) {
620 } else if (scale > 2) {
631 * Fits the axis ranges to the data and returns the "goodness value" of this
632 * selection of axes. All plotDataAxis elements must be non-null.
634 * NOTE: This method assumes that all data can fit into the axes ranges and
635 * that only two axes are used.
637 * @return a "goodness value", the larger the better.
639 protected double getGoodnessValue(FlightDataBranch data) {
641 int length = plotDataTypes.size();
643 // Fit the axes ranges to the data
647 * Calculate goodness of ranges. 100 points is given if the values fill the
648 * entire range, 0 if they fill none of it.
650 for (int i = 0; i < length; i++) {
651 FlightDataType type = plotDataTypes.get(i);
652 Unit unit = plotDataUnits.get(i);
653 int index = plotDataAxes.get(i);
655 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
657 Axis axis = allAxes.get(index);
659 double min = unit.toUnit(data.getMinimum(type));
660 double max = unit.toUnit(data.getMaximum(type));
661 if (Double.isNaN(min) || Double.isNaN(max))
663 if (MathUtil.equals(min, max))
666 double d = (max - min) / axis.getRangeLength();
667 d = Math.sqrt(d); // Prioritize small ranges
668 goodness += d * 100.0;
673 * Add extra points for specific things.
676 // A little for the first type being on the first axis
677 if (plotDataAxes.get(0) == 0)
678 goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
680 // A little bonus if the first axis contains zero
681 Axis left = allAxes.get(0);
682 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
683 goodness += BONUS_FIRST_AXIS_HAS_ZERO;
685 // A boost if a common zero was used in the ranging
686 Axis right = allAxes.get(1);
687 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 &&
688 right.getMinValue() <= 0 && right.getMaxValue() >= 0)
689 goodness += BONUS_COMMON_ZERO;
691 // A boost if only one axis is used
692 if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
693 goodness += BONUS_ONLY_ONE_AXIS;
701 * Reset the units of this configuration to the default units. Returns this
704 * @return this PlotConfiguration.
706 public PlotConfiguration resetUnits() {
707 for (int i = 0; i < plotDataTypes.size(); i++) {
708 plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
717 public PlotConfiguration clone() {
720 PlotConfiguration copy = (PlotConfiguration) super.clone();
722 // Shallow-clone all immutable lists
723 copy.plotDataTypes = this.plotDataTypes.clone();
724 copy.plotDataAxes = this.plotDataAxes.clone();
725 copy.plotDataUnits = this.plotDataUnits.clone();
726 copy.events = this.events.clone();
728 // Deep-clone all Axis since they are mutable
729 copy.allAxes = new ArrayList<Axis>();
730 for (Axis a : this.allAxes) {
731 copy.allAxes.add(a.clone());
737 } catch (CloneNotSupportedException e) {
738 throw new BugException("BUG! Could not clone().");