diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java new file mode 100644 index 000000000..bb5ee2fe1 --- /dev/null +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java @@ -0,0 +1,536 @@ +package info.openrocket.swing.gui.plot; + +import info.openrocket.core.simulation.DataBranch; +import info.openrocket.core.simulation.DataType; +import info.openrocket.core.unit.Unit; +import info.openrocket.core.util.ArrayList; +import info.openrocket.core.util.BugException; +import info.openrocket.core.util.MathUtil; +import info.openrocket.core.util.Pair; + +import java.util.Collections; +import java.util.List; + +public class PlotConfiguration> implements Cloneable { + /** 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 and units to be plotted. */ + protected ArrayList plotDataTypes = new ArrayList<>(); + protected ArrayList plotDataUnits = new ArrayList<>(); + + /** The corresponding Axis on which they will be plotted, or null to auto-select. */ + protected ArrayList plotDataAxes = new ArrayList<>(); + + /** The domain (x) axis. */ + protected T domainAxisType = null; + protected Unit domainAxisUnit = null; + + + /** All available axes. */ + protected final int axesCount; + protected List allAxes = new ArrayList<>(); + + private String name; + + public PlotConfiguration(String name, T domainType) { + this.name = name; + + // Two axes + allAxes.add(new Axis()); + allAxes.add(new Axis()); + axesCount = 2; + + setDomainAxisType(domainType); + } + + //// PlotDataTypes + + public void addPlotDataType(T type) { + plotDataTypes.add(type); + plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); + plotDataAxes.add(-1); + } + + public void addPlotDataType(T 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, T type) { + T 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, T 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 T 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(); + } + + + //// Axis + + public T getDomainAxisType() { + return domainAxisType; + } + + public void setDomainAxisType(T type) { + boolean setUnit; + + setUnit = domainAxisType == null || domainAxisType.getUnitGroup() != type.getUnitGroup(); + + 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; + } + + public List getAllAxes() { + List list = new ArrayList<>(); + list.addAll(allAxes); + return list; + } + + /** + * Find the best combination of the auto-selectable axes. + * + * @return a new PlotConfiguration with the best fitting auto-selected axes and + * axes ranges selected. + */ + @SuppressWarnings("unchecked") + public > C fillAutoAxes(B data) { + C config = (C) recursiveFillAutoAxes(data).getU(); + 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, Double> recursiveFillAutoAxes(B data) { + // Create copy to fill in + PlotConfiguration copy = this.cloneConfiguration(); + + 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<>(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, Double> result = copy.recursiveFillAutoAxes(data); + if (result.getV() > bestValue) { + best = result.getU(); + bestValue = result.getV(); + } + } + + return new Pair<>(best, bestValue); + } + + + protected void fitAxes(B data) { + this.fitAxes(Collections.singletonList(data)); + } + + /** + * Fit the axes to hold the provided data. All of the plotDataAxis elements must + * be non-negative. + *

+ * NOTE: This method assumes that only two axes are used. + */ + public void fitAxes(List 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++) { + T 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.get(0).getMinimum(type)); + double max = unit.toUnit(data.get(0).getMaximum(type)); + + for (int j = 1; j < data.size(); j++) { + // Ignore empty data + if (data.get(j).getLength() == 0) { + continue; + } + min = Math.min(min, unit.toUnit(data.get(j).getMinimum(type))); + max = Math.max(max, unit.toUnit(data.get(j).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 + double min1 = left.getMinValue(); + double max1 = left.getMaxValue(); + double min2 = right.getMinValue(); + double max2 = right.getMaxValue(); + + // Get the zero locations, scaled down to a value from 0 to 1 (0 = bottom of y axis, 1 = top) + double zeroLoc1 = -min1 / left.getRangeLength(); + double zeroLoc2 = -min2 / right.getRangeLength(); + + // Scale the right axis + if (zeroLoc1 > zeroLoc2) { + min2 = -max2 * (zeroLoc1 / (1 - zeroLoc1)); + } else { + max2 = min2 * (-1 / zeroLoc1 + 1); + } + + // Apply scale + left.addBound(min1); + left.addBound(max1); + right.addBound(min2); + right.addBound(max2); + } + + //// Helper methods + /** + * Fits the axis ranges to the data and returns the "goodness value" of this + * selection of axes. All plotDataAxis elements must be non-null. + *

+ * 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(B 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++) { + T 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. + */ + @SuppressWarnings("unchecked") + public > C resetUnits() { + for (int i = 0; i < plotDataTypes.size(); i++) { + plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit()); + } + return (C) this; + } + + 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; + } + + + //// Other + 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; + } + + @Override + public PlotConfiguration clone() { + return cloneConfiguration(); + } + + @SuppressWarnings("unchecked") + protected > C cloneConfiguration() { + try { + C copy = (C) super.clone(); + + // Shallow-clone all immutable lists + copy.plotDataTypes = this.plotDataTypes.clone(); + copy.plotDataAxes = this.plotDataAxes.clone(); + copy.plotDataUnits = this.plotDataUnits.clone(); + + // Deep-clone all Axis since they are mutable + copy.allAxes = new ArrayList<>(); + for (Axis a : this.allAxes) { + copy.allAxes.add(a.clone()); + } + + return copy; + + } catch (CloneNotSupportedException e) { + throw new BugException("BUG! Could not clone()."); + } + } +} diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java index f128ba0cc..88f14d643 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java @@ -1,8 +1,6 @@ package info.openrocket.swing.gui.plot; -import java.util.Collections; import java.util.EnumSet; -import java.util.List; import java.util.Set; import info.openrocket.core.l10n.Translator; @@ -10,22 +8,18 @@ import info.openrocket.core.simulation.FlightDataBranch; import info.openrocket.core.simulation.FlightDataType; import info.openrocket.core.simulation.FlightEvent; import info.openrocket.core.startup.Application; -import info.openrocket.core.unit.Unit; import info.openrocket.core.util.ArrayList; -import info.openrocket.core.util.BugException; -import info.openrocket.core.util.MathUtil; -import info.openrocket.core.util.Pair; -public class SimulationPlotConfiguration implements Cloneable { +public class SimulationPlotConfiguration extends PlotConfiguration { private static final Translator trans = Application.getTranslator(); - + public static final SimulationPlotConfiguration[] DEFAULT_CONFIGURATIONS; static { ArrayList configs = new ArrayList<>(); SimulationPlotConfiguration config; - + //// Vertical motion vs. time config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Verticalmotion")); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); @@ -41,7 +35,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Total motion vs. time config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Totalmotion")); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); @@ -57,7 +51,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Flight side profile config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X); config.addPlotDataType(FlightDataType.TYPE_ALTITUDE); @@ -71,8 +65,8 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - - + + //// Ground track config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Groundtrack"), FlightDataType.TYPE_POSITION_X); config.addPlotDataType(FlightDataType.TYPE_POSITION_Y, 0); @@ -85,7 +79,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Stability vs. time config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Stability")); config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0); @@ -100,7 +94,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Drag coefficients vs. Mach number config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Dragcoef"), FlightDataType.TYPE_MACH_NUMBER); @@ -111,7 +105,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Roll characteristics config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Rollcharacteristics")); config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0); @@ -129,7 +123,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Angle of attack and orientation vs. time config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Angleofattack")); config.addPlotDataType(FlightDataType.TYPE_AOA, 0); @@ -145,7 +139,7 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + //// Simulation time step and computation time config = new SimulationPlotConfiguration(trans.get("PlotConfiguration.Simulationtime")); config.addPlotDataType(FlightDataType.TYPE_TIME_STEP); @@ -160,53 +154,13 @@ public class SimulationPlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true); configs.add(config); - + DEFAULT_CONFIGURATIONS = configs.toArray(new SimulationPlotConfiguration[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 plotDataTypes = new ArrayList<>(); - - private ArrayList plotDataUnits = new ArrayList<>(); - - /** The corresponding Axis on which they will be plotted, or null to auto-select. */ - private ArrayList plotDataAxes = new ArrayList<>(); + private EnumSet 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 allAxes = new ArrayList<>(); - - private String name = null; - public SimulationPlotConfiguration() { this(null, FlightDataType.TYPE_TIME); @@ -217,123 +171,11 @@ public class SimulationPlotConfiguration implements Cloneable { } public SimulationPlotConfiguration(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(); + super(name, domainType); } /// Events - public Set getActiveEvents() { return events.clone(); } @@ -349,385 +191,14 @@ public class SimulationPlotConfiguration implements Cloneable { public boolean isEventActive(FlightEvent.Type type) { return events.contains(type); } - - - - - - public List getAllAxes() { - List list = new ArrayList<>(); - list.addAll(allAxes); - return list; - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Returns the name of this SimulationPlotConfiguration. - */ - @Override - public String toString() { - return name; - } - - - - /** - * Find the best combination of the auto-selectable axes. - * - * @return a new SimulationPlotConfiguration with the best fitting auto-selected axes and - * axes ranges selected. - */ - public SimulationPlotConfiguration fillAutoAxes(FlightDataBranch data) { - SimulationPlotConfiguration 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 SimulationPlotConfiguration with the best fitting auto-selected axes and - * axes ranges selected, and the goodness value - */ - private Pair recursiveFillAutoAxes(FlightDataBranch data) { - - // Create copy to fill in - SimulationPlotConfiguration 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<>(copy, copy.getGoodnessValue(data)); - } - - - // Set the auto-selected index one at a time and choose the best one - SimulationPlotConfiguration best = null; - double bestValue = Double.NEGATIVE_INFINITY; - for (int i = 0; i < axesCount; i++) { - copy.plotDataAxes.set(autoindex, i); - Pair result = copy.recursiveFillAutoAxes(data); - if (result.getV() > bestValue) { - best = result.getU(); - bestValue = result.getV(); - } - } - - return new Pair<>(best, bestValue); - } - protected void fitAxes(FlightDataBranch data) { - this.fitAxes(Collections.singletonList(data)); - } - - /** - * Fit the axes to hold the provided data. All of the plotDataAxis elements must - * be non-negative. - *

- * NOTE: This method assumes that only two axes are used. - */ - public void fitAxes(List 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.get(0).getMinimum(type)); - double max = unit.toUnit(data.get(0).getMaximum(type)); - - for (int j = 1; j < data.size(); j++) { - // Ignore empty data - if (data.get(j).getLength() == 0) { - continue; - } - min = Math.min(min, unit.toUnit(data.get(j).getMinimum(type))); - max = Math.max(max, unit.toUnit(data.get(j).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 - - double min1 = left.getMinValue(); - double max1 = left.getMaxValue(); - double min2 = right.getMinValue(); - double max2 = right.getMaxValue(); - - // Get the zero locations, scaled down to a value from 0 to 1 (0 = bottom of y axis, 1 = top) - double zeroLoc1 = -min1 / left.getRangeLength(); - double zeroLoc2 = -min2 / right.getRangeLength(); - - // Scale the right axis - if (zeroLoc1 > zeroLoc2) { - min2 = -max2 * (zeroLoc1 / (1 - zeroLoc1)); - } else { - max2 = min2 * (-1 / zeroLoc1 + 1); - } - - // 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. - *

- * 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 - * SimulationPlotConfiguration. - * - * @return this SimulationPlotConfiguration. - */ - public SimulationPlotConfiguration resetUnits() { - for (int i = 0; i < plotDataTypes.size(); i++) { - plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit()); - } - return this; - } - - - - @Override public SimulationPlotConfiguration clone() { - try { - - SimulationPlotConfiguration copy = (SimulationPlotConfiguration) 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<>(); - for (Axis a : this.allAxes) { - copy.allAxes.add(a.clone()); - } - - return copy; - - - } catch (CloneNotSupportedException e) { - throw new BugException("BUG! Could not clone()."); - } + SimulationPlotConfiguration copy = (SimulationPlotConfiguration) super.clone(); + copy.events = this.events.clone(); + + return copy; } }