package net.sf.openrocket.gui.plot; import java.util.EnumSet; import java.util.List; import java.util.Set; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Pair; public class PlotConfiguration implements Cloneable { private static final Translator trans = Application.getTranslator(); public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS; static { ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>(); PlotConfiguration config; //// Vertical motion vs. time config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion")); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z); config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Total motion vs. time config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion")); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL); config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Flight side profile config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Ground track config = new PlotConfiguration(trans.get("PlotConfiguration.Groundtrack"), FlightDataType.TYPE_POSITION_X); config.addPlotDataType(FlightDataType.TYPE_POSITION_Y, 0); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 1); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); configs.add(config); //// Stability vs. time config = new PlotConfiguration(trans.get("PlotConfiguration.Stability")); config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0); config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1); config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Drag coefficients vs. Mach number config = new PlotConfiguration(trans.get("PlotConfiguration.Dragcoef"), FlightDataType.TYPE_MACH_NUMBER); config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0); config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0); config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0); config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Roll characteristics config = new PlotConfiguration(trans.get("PlotConfiguration.Rollcharacteristics")); config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0); config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1); config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1); config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.LAUNCHROD, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Angle of attack and orientation vs. time config = new PlotConfiguration(trans.get("PlotConfiguration.Angleofattack")); config.addPlotDataType(FlightDataType.TYPE_AOA, 0); config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI); config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); //// Simulation time step and computation time config = new PlotConfiguration(trans.get("PlotConfiguration.Simulationtime")); config.addPlotDataType(FlightDataType.TYPE_TIME_STEP); config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME); config.setEvent(FlightEvent.Type.IGNITION, true); config.setEvent(FlightEvent.Type.BURNOUT, true); config.setEvent(FlightEvent.Type.APOGEE, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.EXCEPTION, true); configs.add(config); DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]); } /** Bonus given for the first type being on the first axis */ private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0; /** * Bonus given if the first axis includes zero (to prefer first axis having zero over * the others) */ private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0; /** Bonus given for a common zero point on left and right axes. */ private static final double BONUS_COMMON_ZERO = 40.0; /** Bonus given for only using a single axis. */ private static final double BONUS_ONLY_ONE_AXIS = 50.0; private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range /** The data types to be plotted. */ private ArrayList<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>(); private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>(); /** The corresponding Axis on which they will be plotted, or null to auto-select. */ private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>(); private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class); /** The domain (x) axis. */ private FlightDataType domainAxisType = null; private Unit domainAxisUnit = null; /** All available axes. */ private final int axesCount; private ArrayList<Axis> allAxes = new ArrayList<Axis>(); private String name = null; public PlotConfiguration() { this(null, FlightDataType.TYPE_TIME); } public PlotConfiguration(String name) { this(name, FlightDataType.TYPE_TIME); } public PlotConfiguration(String name, FlightDataType domainType) { this.name = name; // Two axes allAxes.add(new Axis()); allAxes.add(new Axis()); axesCount = 2; setDomainAxisType(domainType); } //// Axis public FlightDataType getDomainAxisType() { return domainAxisType; } public void setDomainAxisType(FlightDataType type) { boolean setUnit; if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup()) setUnit = false; else setUnit = true; domainAxisType = type; if (setUnit) domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit(); } public Unit getDomainAxisUnit() { return domainAxisUnit; } public void setDomainAxisUnit(Unit u) { if (!domainAxisType.getUnitGroup().contains(u)) { throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType); } domainAxisUnit = u; } //// FlightDataTypes public void addPlotDataType(FlightDataType type) { plotDataTypes.add(type); plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); plotDataAxes.add(-1); } public void addPlotDataType(FlightDataType type, int axis) { if (axis >= axesCount) { throw new IllegalArgumentException("Axis index too large"); } plotDataTypes.add(type); plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); plotDataAxes.add(axis); } public void setPlotDataType(int index, FlightDataType type) { FlightDataType origType = plotDataTypes.get(index); plotDataTypes.set(index, type); if (origType.getUnitGroup() != type.getUnitGroup()) { plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit()); } } public void setPlotDataUnit(int index, Unit unit) { if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) { throw new IllegalArgumentException("Attempting to set unit " + unit + " to group " + plotDataTypes.get(index).getUnitGroup()); } plotDataUnits.set(index, unit); } public void setPlotDataAxis(int index, int axis) { if (axis >= axesCount) { throw new IllegalArgumentException("Axis index too large"); } plotDataAxes.set(index, axis); } public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) { if (axis >= axesCount) { throw new IllegalArgumentException("Axis index too large"); } plotDataTypes.set(index, type); plotDataUnits.set(index, unit); plotDataAxes.set(index, axis); } public void removePlotDataType(int index) { plotDataTypes.remove(index); plotDataUnits.remove(index); plotDataAxes.remove(index); } public FlightDataType getType(int index) { return plotDataTypes.get(index); } public Unit getUnit(int index) { return plotDataUnits.get(index); } public int getAxis(int index) { return plotDataAxes.get(index); } public int getTypeCount() { return plotDataTypes.size(); } /// Events public Set<FlightEvent.Type> getActiveEvents() { return events.clone(); } public void setEvent(FlightEvent.Type type, boolean active) { if (active) { events.add(type); } else { events.remove(type); } } public boolean isEventActive(FlightEvent.Type type) { return events.contains(type); } public List<Axis> getAllAxes() { List<Axis> list = new ArrayList<Axis>(); list.addAll(allAxes); return list; } public String getName() { return name; } public void setName(String name) { this.name = name; } /** * Returns the name of this PlotConfiguration. */ @Override public String toString() { return name; } /** * Find the best combination of the auto-selectable axes. * * @return a new PlotConfiguration with the best fitting auto-selected axes and * axes ranges selected. */ public PlotConfiguration fillAutoAxes(FlightDataBranch data) { PlotConfiguration config = recursiveFillAutoAxes(data).getU(); //System.out.println("BEST FOUND, fitting"); config.fitAxes(data); return config; } /** * Recursively search for the best combination of the auto-selectable axes. * This is a brute-force search method. * * @return a new PlotConfiguration with the best fitting auto-selected axes and * axes ranges selected, and the goodness value */ private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) { // Create copy to fill in PlotConfiguration copy = this.clone(); int autoindex; for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) { if (plotDataAxes.get(autoindex) < 0) break; } if (autoindex >= plotDataAxes.size()) { // All axes have been assigned, just return since we are already the best return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data)); } // Set the auto-selected index one at a time and choose the best one PlotConfiguration best = null; double bestValue = Double.NEGATIVE_INFINITY; for (int i = 0; i < axesCount; i++) { copy.plotDataAxes.set(autoindex, i); Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data); if (result.getV() > bestValue) { best = result.getU(); bestValue = result.getV(); } } return new Pair<PlotConfiguration, Double>(best, bestValue); } /** * Fit the axes to hold the provided data. All of the plotDataAxis elements must * be non-negative. * <p> * NOTE: This method assumes that only two axes are used. */ protected void fitAxes(FlightDataBranch data) { // Reset axes for (Axis a : allAxes) { a.reset(); } // Add full range to the axes int length = plotDataTypes.size(); for (int i = 0; i < length; i++) { FlightDataType type = plotDataTypes.get(i); Unit unit = plotDataUnits.get(i); int index = plotDataAxes.get(i); if (index < 0) { throw new IllegalStateException("fitAxes called with auto-selected axis"); } Axis axis = allAxes.get(index); double min = unit.toUnit(data.getMinimum(type)); double max = unit.toUnit(data.getMaximum(type)); axis.addBound(min); axis.addBound(max); } // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close for (Axis a : allAxes) { if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) { a.addBound(a.getMinValue() - 1); a.addBound(a.getMaxValue() + 1); } double addition = a.getRangeLength() * 0.03; a.addBound(a.getMinValue() - addition); a.addBound(a.getMaxValue() + addition); double dist; dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue())); if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) { a.addBound(0); } } // Check whether to use a common zero Axis left = allAxes.get(0); Axis right = allAxes.get(1); if (left.getMinValue() > 0 || left.getMaxValue() < 0 || right.getMinValue() > 0 || right.getMaxValue() < 0 || Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) return; //// Compute common zero // TODO: MEDIUM: This algorithm may require tweaking double min1 = left.getMinValue(); double max1 = left.getMaxValue(); double min2 = right.getMinValue(); double max2 = right.getMaxValue(); // Calculate and round scaling factor double scale = Math.max(left.getRangeLength(), right.getRangeLength()) / Math.min(left.getRangeLength(), right.getRangeLength()); //System.out.println("Scale: " + scale); scale = roundScale(scale); if (right.getRangeLength() > left.getRangeLength()) { scale = 1 / scale; } //System.out.println("Rounded scale: " + scale); // Scale right axis, enlarge axes if necessary and scale back min2 *= scale; max2 *= scale; min1 = Math.min(min1, min2); min2 = min1; max1 = Math.max(max1, max2); max2 = max1; min2 /= scale; max2 /= scale; // Apply scale left.addBound(min1); left.addBound(max1); right.addBound(min2); right.addBound(max2); } private double roundScale(double scale) { double mul = 1; while (scale >= 10) { scale /= 10; mul *= 10; } while (scale < 1) { scale *= 10; mul /= 10; } // 1 2 4 5 10 if (scale > 7.5) { scale = 10; } else if (scale > 4.5) { scale = 5; } else if (scale > 3) { scale = 4; } else if (scale > 1.5) { scale = 2; } else { scale = 1; } return scale * mul; } @SuppressWarnings("unused") private double roundScaleUp(double scale) { double mul = 1; while (scale >= 10) { scale /= 10; mul *= 10; } while (scale < 1) { scale *= 10; mul /= 10; } if (scale > 5) { scale = 10; } else if (scale > 4) { scale = 5; } else if (scale > 2) { scale = 4; } else if (scale > 1) { scale = 2; } else { scale = 1; } return scale * mul; } @SuppressWarnings("unused") private double roundScaleDown(double scale) { double mul = 1; while (scale >= 10) { scale /= 10; mul *= 10; } while (scale < 1) { scale *= 10; mul /= 10; } if (scale > 5) { scale = 5; } else if (scale > 4) { scale = 4; } else if (scale > 2) { scale = 2; } else { scale = 1; } return scale * mul; } /** * Fits the axis ranges to the data and returns the "goodness value" of this * selection of axes. All plotDataAxis elements must be non-null. * <p> * NOTE: This method assumes that all data can fit into the axes ranges and * that only two axes are used. * * @return a "goodness value", the larger the better. */ protected double getGoodnessValue(FlightDataBranch data) { double goodness = 0; int length = plotDataTypes.size(); // Fit the axes ranges to the data fitAxes(data); /* * Calculate goodness of ranges. 100 points is given if the values fill the * entire range, 0 if they fill none of it. */ for (int i = 0; i < length; i++) { FlightDataType type = plotDataTypes.get(i); Unit unit = plotDataUnits.get(i); int index = plotDataAxes.get(i); if (index < 0) { throw new IllegalStateException("getGoodnessValue called with auto-selected axis"); } Axis axis = allAxes.get(index); double min = unit.toUnit(data.getMinimum(type)); double max = unit.toUnit(data.getMaximum(type)); if (Double.isNaN(min) || Double.isNaN(max)) continue; if (MathUtil.equals(min, max)) continue; double d = (max - min) / axis.getRangeLength(); d = MathUtil.safeSqrt(d); // Prioritize small ranges goodness += d * 100.0; } /* * Add extra points for specific things. */ // A little for the first type being on the first axis if (plotDataAxes.get(0) == 0) goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS; // A little bonus if the first axis contains zero Axis left = allAxes.get(0); if (left.getMinValue() <= 0 && left.getMaxValue() >= 0) goodness += BONUS_FIRST_AXIS_HAS_ZERO; // A boost if a common zero was used in the ranging Axis right = allAxes.get(1); if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 && right.getMinValue() <= 0 && right.getMaxValue() >= 0) goodness += BONUS_COMMON_ZERO; // A boost if only one axis is used if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) goodness += BONUS_ONLY_ONE_AXIS; return goodness; } /** * Reset the units of this configuration to the default units. Returns this * PlotConfiguration. * * @return this PlotConfiguration. */ public PlotConfiguration resetUnits() { for (int i = 0; i < plotDataTypes.size(); i++) { plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit()); } return this; } @Override public PlotConfiguration clone() { try { PlotConfiguration copy = (PlotConfiguration) super.clone(); // Shallow-clone all immutable lists copy.plotDataTypes = this.plotDataTypes.clone(); copy.plotDataAxes = this.plotDataAxes.clone(); copy.plotDataUnits = this.plotDataUnits.clone(); copy.events = this.events.clone(); // Deep-clone all Axis since they are mutable copy.allAxes = new ArrayList<Axis>(); for (Axis a : this.allAxes) { copy.allAxes.add(a.clone()); } return copy; } catch (CloneNotSupportedException e) { throw new BugException("BUG! Could not clone()."); } } }