1 package net.sf.openrocket.gui.plot;
3 import java.util.ArrayList;
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;
13 public class PlotConfiguration implements Cloneable {
15 public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
17 ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
18 PlotConfiguration config;
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);
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);
32 config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
33 config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
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);
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);
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);
57 config = new PlotConfiguration("Simulation time step and computation time");
58 config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
59 config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
62 DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
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;
71 * Bonus given if the first axis includes zero (to prefer first axis having zero over
74 private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
76 /** Bonus given for a common zero point on left and right axes. */
77 private static final double BONUS_COMMON_ZERO = 40.0;
79 /** Bonus given for only using a single axis. */
80 private static final double BONUS_ONLY_ONE_AXIS = 50.0;
83 private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range
87 /** The data types to be plotted. */
88 private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
90 private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
92 /** The corresponding Axis on which they will be plotted, or null to auto-select. */
93 private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
96 /** The domain (x) axis. */
97 private FlightDataBranch.Type domainAxisType = null;
98 private Unit domainAxisUnit = null;
101 /** All available axes. */
102 private final int axesCount;
103 private ArrayList<Axis> allAxes = new ArrayList<Axis>();
107 private String name = null;
111 public PlotConfiguration() {
112 this(null, FlightDataBranch.TYPE_TIME);
115 public PlotConfiguration(String name) {
116 this(name, FlightDataBranch.TYPE_TIME);
119 public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
122 allAxes.add(new Axis());
123 allAxes.add(new Axis());
126 setDomainAxisType(domainType);
133 public FlightDataBranch.Type getDomainAxisType() {
134 return domainAxisType;
137 public void setDomainAxisType(FlightDataBranch.Type type) {
140 if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
145 domainAxisType = type;
147 domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
150 public Unit getDomainAxisUnit() {
151 return domainAxisUnit;
154 public void setDomainAxisUnit(Unit u) {
155 if (!domainAxisType.getUnitGroup().contains(u)) {
156 throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
163 public void addPlotDataType(FlightDataBranch.Type type) {
164 plotDataTypes.add(type);
165 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
166 plotDataAxes.add(-1);
169 public void addPlotDataType(FlightDataBranch.Type type, int axis) {
170 if (axis >= axesCount) {
171 throw new IllegalArgumentException("Axis index too large");
173 plotDataTypes.add(type);
174 plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
175 plotDataAxes.add(axis);
181 public void setPlotDataType(int index, FlightDataBranch.Type type) {
182 FlightDataBranch.Type origType = plotDataTypes.get(index);
183 plotDataTypes.set(index, type);
185 if (origType.getUnitGroup() != type.getUnitGroup()) {
186 plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
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());
195 plotDataUnits.set(index, unit);
198 public void setPlotDataAxis(int index, int axis) {
199 if (axis >= axesCount) {
200 throw new IllegalArgumentException("Axis index too large");
202 plotDataAxes.set(index, axis);
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");
210 plotDataTypes.set(index, type);
211 plotDataUnits.set(index, unit);
212 plotDataAxes.set(index, axis);
215 public void removePlotDataType(int index) {
216 plotDataTypes.remove(index);
217 plotDataUnits.remove(index);
218 plotDataAxes.remove(index);
223 public FlightDataBranch.Type getType (int index) {
224 return plotDataTypes.get(index);
226 public Unit getUnit(int index) {
227 return plotDataUnits.get(index);
229 public int getAxis(int index) {
230 return plotDataAxes.get(index);
233 public int getTypeCount() {
234 return plotDataTypes.size();
239 public List<Axis> getAllAxes() {
240 List<Axis> list = new ArrayList<Axis>();
241 list.addAll(allAxes);
246 public String getName() {
250 public void setName(String name) {
255 * Returns the name of this PlotConfiguration.
258 public String toString() {
265 * Find the best combination of the auto-selectable axes.
267 * @return a new PlotConfiguration with the best fitting auto-selected axes and
268 * axes ranges selected.
270 public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
271 PlotConfiguration config = recursiveFillAutoAxes(data).getU();
272 System.out.println("BEST FOUND, fitting");
273 config.fitAxes(data);
281 * Recursively search for the best combination of the auto-selectable axes.
282 * This is a brute-force search method.
284 * @return a new PlotConfiguration with the best fitting auto-selected axes and
285 * axes ranges selected, and the goodness value
287 private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
289 // Create copy to fill in
290 PlotConfiguration copy = this.clone();
293 for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
294 if (plotDataAxes.get(autoindex) < 0)
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));
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();
317 return new Pair<PlotConfiguration, Double>(best, bestValue);
325 * Fit the axes to hold the provided data. All of the plotDataAxis elements must
328 * NOTE: This method assumes that only two axes are used.
330 protected void fitAxes(FlightDataBranch data) {
333 for (Axis a: allAxes) {
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);
344 throw new IllegalStateException("fitAxes called with auto-selected axis");
346 Axis axis = allAxes.get(index);
348 double min = unit.toUnit(data.getMinimum(type));
349 double max = unit.toUnit(data.getMaximum(type));
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);
362 double addition = a.getRangeLength() * 0.03;
363 a.addBound(a.getMinValue() - addition);
364 a.addBound(a.getMaxValue() + addition);
367 dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
368 if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
374 // Check whether to use a common zero
375 Axis left = allAxes.get(0);
376 Axis right = allAxes.get(1);
378 if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
379 right.getMinValue() > 0 || right.getMaxValue() < 0 ||
380 Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
385 //// Compute common zero
386 // TODO: MEDIUM: This algorithm may require tweaking
388 double min1 = left.getMinValue();
389 double max1 = left.getMaxValue();
390 double min2 = right.getMinValue();
391 double max2 = right.getMaxValue();
393 // Calculate and round scaling factor
394 double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
395 Math.min(left.getRangeLength(), right.getRangeLength());
397 System.out.println("Scale: "+scale);
399 scale = roundScale(scale);
400 if (right.getRangeLength() > left.getRangeLength()) {
403 System.out.println("Rounded scale: " + scale);
405 // Scale right axis, enlarge axes if necessary and scale back
408 min1 = Math.min(min1, min2);
410 max1 = Math.max(max1, max2);
417 // Scale to unit length
418 // double scale1 = left.getRangeLength();
419 // double scale2 = right.getRangeLength();
421 // double min1 = left.getMinValue() / scale1;
422 // double max1 = left.getMaxValue() / scale1;
423 // double min2 = right.getMinValue() / scale2;
424 // double max2 = right.getMaxValue() / scale2;
426 // // Combine unit ranges
427 // min1 = MathUtil.min(min1, min2);
429 // max1 = MathUtil.max(max1, max2);
438 // // Compute common scale
439 // double range1 = max1-min1;
440 // double range2 = max2-min2;
442 // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
443 // double roundScale = roundScale(scale);
445 // if (range2 < range1) {
446 // if (roundScale < scale) {
447 // min2 = min1 / roundScale;
448 // max2 = max1 / roundScale;
450 // min1 = min2 * roundScale;
451 // max1 = max2 * roundScale;
454 // if (roundScale > scale) {
455 // min2 = min1 * roundScale;
456 // max2 = max1 * roundScale;
458 // min1 = min2 / roundScale;
459 // max1 = max2 / roundScale;
466 right.addBound(min2);
467 right.addBound(max2);
473 private double roundScale(double scale) {
475 while (scale >= 10) {
488 } else if (scale > 4.5) {
490 } else if (scale > 3) {
492 } else if (scale > 1.5) {
502 private double roundScaleUp(double scale) {
504 while (scale >= 10) {
515 } else if (scale > 4) {
517 } else if (scale > 2) {
519 } else if (scale > 1) {
528 private double roundScaleDown(double scale) {
530 while (scale >= 10) {
541 } else if (scale > 4) {
543 } else if (scale > 2) {
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.
557 * NOTE: This method assumes that all data can fit into the axes ranges and
558 * that only two axes are used.
560 * @return a "goodness value", the larger the better.
562 protected double getGoodnessValue(FlightDataBranch data) {
564 int length = plotDataTypes.size();
566 // Fit the axes ranges to the data
570 * Calculate goodness of ranges. 100 points is given if the values fill the
571 * entire range, 0 if they fill none of it.
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);
578 throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
580 Axis axis = allAxes.get(index);
582 double min = unit.toUnit(data.getMinimum(type));
583 double max = unit.toUnit(data.getMaximum(type));
584 if (Double.isNaN(min) || Double.isNaN(max))
586 if (MathUtil.equals(min, max))
589 double d = (max-min) / axis.getRangeLength();
590 d = Math.sqrt(d); // Prioritize small ranges
591 goodness += d * 100.0;
596 * Add extra points for specific things.
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;
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;
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;
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;
624 * Reset the units of this configuration to the default units. Returns this
627 * @return this PlotConfiguration.
629 public PlotConfiguration resetUnits() {
630 for (int i=0; i < plotDataTypes.size(); i++) {
631 plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
639 @SuppressWarnings("unchecked")
641 public PlotConfiguration clone() {
644 PlotConfiguration copy = (PlotConfiguration) super.clone();
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();
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());
660 } catch (CloneNotSupportedException e) {
661 throw new RuntimeException("BUG! Could not clone().");