Refactor SimulationPlotConfiguration to use generic PlotConfiguration
This commit is contained in:
parent
6e152bc83f
commit
1465ed4f98
@ -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<T extends DataType, B extends DataBranch<T>> 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<T> plotDataTypes = new ArrayList<>();
|
||||
protected ArrayList<Unit> plotDataUnits = new ArrayList<>();
|
||||
|
||||
/** The corresponding Axis on which they will be plotted, or null to auto-select. */
|
||||
protected ArrayList<Integer> plotDataAxes = new ArrayList<>();
|
||||
|
||||
/** The domain (x) axis. */
|
||||
protected T domainAxisType = null;
|
||||
protected Unit domainAxisUnit = null;
|
||||
|
||||
|
||||
/** All available axes. */
|
||||
protected final int axesCount;
|
||||
protected List<Axis> 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<Axis> getAllAxes() {
|
||||
List<Axis> 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 extends PlotConfiguration<T, B>> 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<PlotConfiguration<T, B>, Double> recursiveFillAutoAxes(B data) {
|
||||
// Create copy to fill in
|
||||
PlotConfiguration<T, B> 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<T, B> best = null;
|
||||
double bestValue = Double.NEGATIVE_INFINITY;
|
||||
for (int i = 0; i < axesCount; i++) {
|
||||
copy.plotDataAxes.set(autoindex, i);
|
||||
Pair<PlotConfiguration<T, B>, 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.
|
||||
* <p>
|
||||
* NOTE: This method assumes that only two axes are used.
|
||||
*/
|
||||
public void fitAxes(List<B> 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.
|
||||
* <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(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 extends PlotConfiguration<T, B>> 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<T, B> clone() {
|
||||
return cloneConfiguration();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <C extends PlotConfiguration<T, B>> 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().");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<FlightDataType, FlightDataBranch> {
|
||||
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
|
||||
public static final SimulationPlotConfiguration[] DEFAULT_CONFIGURATIONS;
|
||||
static {
|
||||
ArrayList<SimulationPlotConfiguration> 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<FlightDataType> plotDataTypes = new ArrayList<>();
|
||||
|
||||
private ArrayList<Unit> plotDataUnits = new ArrayList<>();
|
||||
|
||||
/** The corresponding Axis on which they will be plotted, or null to auto-select. */
|
||||
private ArrayList<Integer> plotDataAxes = new ArrayList<>();
|
||||
|
||||
|
||||
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<>();
|
||||
|
||||
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<FlightEvent.Type> 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<Axis> getAllAxes() {
|
||||
List<Axis> 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<SimulationPlotConfiguration, Double> 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<SimulationPlotConfiguration, Double> 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.
|
||||
* <p>
|
||||
* NOTE: This method assumes that only two axes are used.
|
||||
*/
|
||||
public void fitAxes(List<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.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.
|
||||
* <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
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user