1 package net.sf.openrocket.gui.plot;
3 import java.util.EnumSet;
7 import net.sf.openrocket.l10n.Translator;
8 import net.sf.openrocket.simulation.FlightDataBranch;
9 import net.sf.openrocket.simulation.FlightDataType;
10 import net.sf.openrocket.simulation.FlightEvent;
11 import net.sf.openrocket.startup.Application;
12 import net.sf.openrocket.unit.Unit;
13 import net.sf.openrocket.util.ArrayList;
14 import net.sf.openrocket.util.BugException;
15 import net.sf.openrocket.util.MathUtil;
16 import net.sf.openrocket.util.Pair;
19 public class PlotConfiguration implements Cloneable {
21 private static final Translator trans = Application.getTranslator();
23 public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
25 ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
26 PlotConfiguration config;
28 //// Vertical motion vs. time
29 config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion"));
30 config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
31 config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z);
32 config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z);
33 config.setEvent(FlightEvent.Type.IGNITION, true);
34 config.setEvent(FlightEvent.Type.BURNOUT, true);
35 config.setEvent(FlightEvent.Type.APOGEE, true);
36 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
37 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
38 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
41 //// Total motion vs. time
42 config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion"));
43 config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
44 config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL);
45 config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL);
46 config.setEvent(FlightEvent.Type.IGNITION, true);
47 config.setEvent(FlightEvent.Type.BURNOUT, true);
48 config.setEvent(FlightEvent.Type.APOGEE, true);
49 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
50 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
51 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
54 //// Flight side profile
55 config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X);
56 config.addPlotDataType(FlightDataType.TYPE_ALTITUDE);
57 config.setEvent(FlightEvent.Type.IGNITION, true);
58 config.setEvent(FlightEvent.Type.BURNOUT, true);
59 config.setEvent(FlightEvent.Type.APOGEE, true);
60 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
61 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
62 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
65 //// Stability vs. time
66 config = new PlotConfiguration("Stability vs. time");
67 config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0);
68 config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1);
69 config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1);
70 config.setEvent(FlightEvent.Type.IGNITION, true);
71 config.setEvent(FlightEvent.Type.BURNOUT, true);
72 config.setEvent(FlightEvent.Type.APOGEE, true);
73 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
74 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
75 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
78 //// Drag coefficients vs. Mach number
79 config = new PlotConfiguration("Drag coefficients vs. Mach number",
80 FlightDataType.TYPE_MACH_NUMBER);
81 config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0);
82 config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0);
83 config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0);
84 config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0);
87 //// Roll characteristics
88 config = new PlotConfiguration("Roll characteristics");
89 config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0);
90 config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1);
91 config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1);
92 config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1);
93 config.setEvent(FlightEvent.Type.IGNITION, true);
94 config.setEvent(FlightEvent.Type.LAUNCHROD, true);
95 config.setEvent(FlightEvent.Type.BURNOUT, true);
96 config.setEvent(FlightEvent.Type.APOGEE, true);
97 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
98 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
99 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
102 //// Angle of attack and orientation vs. time
103 config = new PlotConfiguration("Angle of attack and orientation vs. time");
104 config.addPlotDataType(FlightDataType.TYPE_AOA, 0);
105 config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI);
106 config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA);
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 //// Simulation time step and computation time
116 config = new PlotConfiguration("Simulation time step and computation time");
117 config.addPlotDataType(FlightDataType.TYPE_TIME_STEP);
118 config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME);
119 config.setEvent(FlightEvent.Type.IGNITION, true);
120 config.setEvent(FlightEvent.Type.BURNOUT, true);
121 config.setEvent(FlightEvent.Type.APOGEE, true);
122 config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
123 config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
124 config.setEvent(FlightEvent.Type.GROUND_HIT, true);
127 DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
132 /** Bonus given for the first type being on the first axis */
133 private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
136 * Bonus given if the first axis includes zero (to prefer first axis having zero over
139 private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
141 /** Bonus given for a common zero point on left and right axes. */
142 private static final double BONUS_COMMON_ZERO = 40.0;
144 /** Bonus given for only using a single axis. */
145 private static final double BONUS_ONLY_ONE_AXIS = 50.0;
148 private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range
152 /** The data types to be plotted. */
153 private ArrayList<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>();
155 private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
157 /** The corresponding Axis on which they will be plotted, or null to auto-select. */
158 private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
160 private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class);
162 /** The domain (x) axis. */
163 private FlightDataType domainAxisType = null;
164 private Unit domainAxisUnit = null;
167 /** All available axes. */
168 private final int axesCount;
169 private ArrayList<Axis> allAxes = new ArrayList<Axis>();
173 private String name = null;
177 public PlotConfiguration() {
178 this(null, FlightDataType.TYPE_TIME);
181 public PlotConfiguration(String name) {
182 this(name, FlightDataType.TYPE_TIME);
185 public PlotConfiguration(String name, FlightDataType domainType) {
188 allAxes.add(new Axis());
189 allAxes.add(new Axis());
192 setDomainAxisType(domainType);
199 public FlightDataType getDomainAxisType() {
200 return domainAxisType;
203 public void setDomainAxisType(FlightDataType type) {
206 if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
211 domainAxisType = type;
213 domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
216 public Unit getDomainAxisUnit() {
217 return domainAxisUnit;
220 public void setDomainAxisUnit(Unit u) {
221 if (!domainAxisType.getUnitGroup().contains(u)) {
222 throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType);
229 public void addPlotDataType(FlightDataType type) {
230 plotDataTypes.add(type);
231 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
232 plotDataAxes.add(-1);
235 public void addPlotDataType(FlightDataType type, int axis) {
236 if (axis >= axesCount) {
237 throw new IllegalArgumentException("Axis index too large");
239 plotDataTypes.add(type);
240 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
241 plotDataAxes.add(axis);
247 public void setPlotDataType(int index, FlightDataType type) {
248 FlightDataType origType = plotDataTypes.get(index);
249 plotDataTypes.set(index, type);
251 if (origType.getUnitGroup() != type.getUnitGroup()) {
252 plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
256 public void setPlotDataUnit(int index, Unit unit) {
257 if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
258 throw new IllegalArgumentException("Attempting to set unit " + unit + " to group "
259 + plotDataTypes.get(index).getUnitGroup());
261 plotDataUnits.set(index, unit);
264 public void setPlotDataAxis(int index, int axis) {
265 if (axis >= axesCount) {
266 throw new IllegalArgumentException("Axis index too large");
268 plotDataAxes.set(index, axis);
272 public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) {
273 if (axis >= axesCount) {
274 throw new IllegalArgumentException("Axis index too large");
276 plotDataTypes.set(index, type);
277 plotDataUnits.set(index, unit);
278 plotDataAxes.set(index, axis);
281 public void removePlotDataType(int index) {
282 plotDataTypes.remove(index);
283 plotDataUnits.remove(index);
284 plotDataAxes.remove(index);
289 public FlightDataType getType(int index) {
290 return plotDataTypes.get(index);
293 public Unit getUnit(int index) {
294 return plotDataUnits.get(index);
297 public int getAxis(int index) {
298 return plotDataAxes.get(index);
301 public int getTypeCount() {
302 return plotDataTypes.size();
308 public Set<FlightEvent.Type> getActiveEvents() {
309 return events.clone();
312 public void setEvent(FlightEvent.Type type, boolean active) {
320 public boolean isEventActive(FlightEvent.Type type) {
321 return events.contains(type);
328 public List<Axis> getAllAxes() {
329 List<Axis> list = new ArrayList<Axis>();
330 list.addAll(allAxes);
335 public String getName() {
339 public void setName(String name) {
344 * Returns the name of this PlotConfiguration.
347 public String toString() {
354 * Find the best combination of the auto-selectable axes.
356 * @return a new PlotConfiguration with the best fitting auto-selected axes and
357 * axes ranges selected.
359 public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
360 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
361 System.out.println("BEST FOUND, fitting");
362 config.fitAxes(data);
370 * Recursively search for the best combination of the auto-selectable axes.
371 * This is a brute-force search method.
373 * @return a new PlotConfiguration with the best fitting auto-selected axes and
374 * axes ranges selected, and the goodness value
376 private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
378 // Create copy to fill in
379 PlotConfiguration copy = this.clone();
382 for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) {
383 if (plotDataAxes.get(autoindex) < 0)
388 if (autoindex >= plotDataAxes.size()) {
389 // All axes have been assigned, just return since we are already the best
390 return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
394 // Set the auto-selected index one at a time and choose the best one
395 PlotConfiguration best = null;
396 double bestValue = Double.NEGATIVE_INFINITY;
397 for (int i = 0; i < axesCount; i++) {
398 copy.plotDataAxes.set(autoindex, i);
399 Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
400 if (result.getV() > bestValue) {
401 best = result.getU();
402 bestValue = result.getV();
406 return new Pair<PlotConfiguration, Double>(best, bestValue);
414 * Fit the axes to hold the provided data. All of the plotDataAxis elements must
417 * NOTE: This method assumes that only two axes are used.
419 protected void fitAxes(FlightDataBranch data) {
422 for (Axis a : allAxes) {
426 // Add full range to the axes
427 int length = plotDataTypes.size();
428 for (int i = 0; i < length; i++) {
429 FlightDataType type = plotDataTypes.get(i);
430 Unit unit = plotDataUnits.get(i);
431 int index = plotDataAxes.get(i);
433 throw new IllegalStateException("fitAxes called with auto-selected axis");
435 Axis axis = allAxes.get(index);
437 double min = unit.toUnit(data.getMinimum(type));
438 double max = unit.toUnit(data.getMaximum(type));
444 // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
445 for (Axis a : allAxes) {
446 if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
447 a.addBound(a.getMinValue() - 1);
448 a.addBound(a.getMaxValue() + 1);
451 double addition = a.getRangeLength() * 0.03;
452 a.addBound(a.getMinValue() - addition);
453 a.addBound(a.getMaxValue() + addition);
456 dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
457 if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
463 // Check whether to use a common zero
464 Axis left = allAxes.get(0);
465 Axis right = allAxes.get(1);
467 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
468 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
469 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
474 //// Compute common zero
475 // TODO: MEDIUM: This algorithm may require tweaking
477 double min1 = left.getMinValue();
478 double max1 = left.getMaxValue();
479 double min2 = right.getMinValue();
480 double max2 = right.getMaxValue();
482 // Calculate and round scaling factor
483 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
484 Math.min(left.getRangeLength(), right.getRangeLength());
486 System.out.println("Scale: " + scale);
488 scale = roundScale(scale);
489 if (right.getRangeLength() > left.getRangeLength()) {
492 System.out.println("Rounded scale: " + scale);
494 // Scale right axis, enlarge axes if necessary and scale back
497 min1 = Math.min(min1, min2);
499 max1 = Math.max(max1, max2);
506 // Scale to unit length
507 // double scale1 = left.getRangeLength();
508 // double scale2 = right.getRangeLength();
510 // double min1 = left.getMinValue() / scale1;
511 // double max1 = left.getMaxValue() / scale1;
512 // double min2 = right.getMinValue() / scale2;
513 // double max2 = right.getMaxValue() / scale2;
515 // // Combine unit ranges
516 // min1 = MathUtil.min(min1, min2);
518 // max1 = MathUtil.max(max1, max2);
527 // // Compute common scale
528 // double range1 = max1-min1;
529 // double range2 = max2-min2;
531 // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
532 // double roundScale = roundScale(scale);
534 // if (range2 < range1) {
535 // if (roundScale < scale) {
536 // min2 = min1 / roundScale;
537 // max2 = max1 / roundScale;
539 // min1 = min2 * roundScale;
540 // max1 = max2 * roundScale;
543 // if (roundScale > scale) {
544 // min2 = min1 * roundScale;
545 // max2 = max1 * roundScale;
547 // min1 = min2 / roundScale;
548 // max1 = max2 / roundScale;
555 right.addBound(min2);
556 right.addBound(max2);
562 private double roundScale(double scale) {
564 while (scale >= 10) {
577 } else if (scale > 4.5) {
579 } else if (scale > 3) {
581 } else if (scale > 1.5) {
591 private double roundScaleUp(double scale) {
593 while (scale >= 10) {
604 } else if (scale > 4) {
606 } else if (scale > 2) {
608 } else if (scale > 1) {
617 private double roundScaleDown(double scale) {
619 while (scale >= 10) {
630 } else if (scale > 4) {
632 } else if (scale > 2) {
643 * Fits the axis ranges to the data and returns the "goodness value" of this
644 * selection of axes. All plotDataAxis elements must be non-null.
646 * NOTE: This method assumes that all data can fit into the axes ranges and
647 * that only two axes are used.
649 * @return a "goodness value", the larger the better.
651 protected double getGoodnessValue(FlightDataBranch data) {
653 int length = plotDataTypes.size();
655 // Fit the axes ranges to the data
659 * Calculate goodness of ranges. 100 points is given if the values fill the
660 * entire range, 0 if they fill none of it.
662 for (int i = 0; i < length; i++) {
663 FlightDataType type = plotDataTypes.get(i);
664 Unit unit = plotDataUnits.get(i);
665 int index = plotDataAxes.get(i);
667 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
669 Axis axis = allAxes.get(index);
671 double min = unit.toUnit(data.getMinimum(type));
672 double max = unit.toUnit(data.getMaximum(type));
673 if (Double.isNaN(min) || Double.isNaN(max))
675 if (MathUtil.equals(min, max))
678 double d = (max - min) / axis.getRangeLength();
679 d = Math.sqrt(d); // Prioritize small ranges
680 goodness += d * 100.0;
685 * Add extra points for specific things.
688 // A little for the first type being on the first axis
689 if (plotDataAxes.get(0) == 0)
690 goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
692 // A little bonus if the first axis contains zero
693 Axis left = allAxes.get(0);
694 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
695 goodness += BONUS_FIRST_AXIS_HAS_ZERO;
697 // A boost if a common zero was used in the ranging
698 Axis right = allAxes.get(1);
699 if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 &&
700 right.getMinValue() <= 0 && right.getMaxValue() >= 0)
701 goodness += BONUS_COMMON_ZERO;
703 // A boost if only one axis is used
704 if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
705 goodness += BONUS_ONLY_ONE_AXIS;
713 * Reset the units of this configuration to the default units. Returns this
716 * @return this PlotConfiguration.
718 public PlotConfiguration resetUnits() {
719 for (int i = 0; i < plotDataTypes.size(); i++) {
720 plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
729 public PlotConfiguration clone() {
732 PlotConfiguration copy = (PlotConfiguration) super.clone();
734 // Shallow-clone all immutable lists
735 copy.plotDataTypes = this.plotDataTypes.clone();
736 copy.plotDataAxes = this.plotDataAxes.clone();
737 copy.plotDataUnits = this.plotDataUnits.clone();
738 copy.events = this.events.clone();
740 // Deep-clone all Axis since they are mutable
741 copy.allAxes = new ArrayList<Axis>();
742 for (Axis a : this.allAxes) {
743 copy.allAxes.add(a.clone());
749 } catch (CloneNotSupportedException e) {
750 throw new BugException("BUG! Could not clone().");