import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.ExpressionParser;
+import net.sf.openrocket.util.InvalidExpressionException;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.Invalidator;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.MemoryManagement;
import net.sf.openrocket.util.Reflection;
import net.sf.openrocket.util.StateChangeListener;
-import net.sf.openrocket.util.exp4j.Calculable;
-import net.sf.openrocket.util.exp4j.ExpressionBuilder;
-import net.sf.openrocket.util.exp4j.UnknownFunctionException;
-import net.sf.openrocket.util.exp4j.UnparsableExpressionException;
/**
public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
private static final LogHelper log = Application.getLogger();
-
-
+
+
public static final DoubleModel ZERO = new DoubleModel(0);
-
+
//////////// JSpinner Model ////////////
-
+
/**
* Model suitable for JSpinner.
* Note: Previously used using JSpinner.NumberEditor and extended SpinnerNumberModel
* to be entered so that fractional units and expressions can be used.
*/
public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
-
+
+ private ExpressionParser parser = new ExpressionParser();
+
@Override
public Object getValue() {
return currentUnit.toString(DoubleModel.this.getValue());
}
-
+
@Override
public void setValue(Object value) {
if (firing > 0) {
Number num = Double.NaN;
// Set num if possible
- if ( value instanceof Number ) {
- num = (Number)value;
+ if (value instanceof Number) {
+ num = (Number) value;
}
- else if ( value instanceof String ) {
+ else if (value instanceof String) {
try {
- String newValString = (String)value;
- ExpressionBuilder builder=new ExpressionBuilder(newValString);
- Calculable calc=builder.build();
- num = calc.calculate();
+ String newValString = (String) value;
+ num = parser.parse(newValString);
+ } catch (InvalidExpressionException e) {
+ // Ignore
}
- catch ( java.lang.NumberFormatException e ) {
- } catch (UnknownFunctionException e) {
- } catch (UnparsableExpressionException e) {
- } catch (java.util.EmptyStackException e) {
- }
}
-
+
// Update the doublemodel with the new number or return to the last number if not possible
- if ( ((Double)num).isNaN() ) {
- DoubleModel.this.setValue( lastValue );
+ if (((Double) num).isNaN()) {
+ DoubleModel.this.setValue(lastValue);
log.user("SpinnerModel could not set value for " + DoubleModel.this.toString() + ". Could not convert " + value.toString());
- }
- else {
+ }
+ else {
double newValue = num.doubleValue();
double converted = currentUnit.fromUnit(newValue);
-
+
log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" converted=" + converted);
DoubleModel.this.setValue(converted);
}
// Force a refresh if text doesn't match up exactly with the stored value
- if ( ! ((Double)lastValue).toString().equals( this.getValue().toString() ) ) {
+ if (!((Double) lastValue).toString().equals(this.getValue().toString())) {
DoubleModel.this.fireStateChanged();
- log.debug("SpinnerModel "+DoubleModel.this.toString()+" refresh forced because string did not match actual value.");
+ log.debug("SpinnerModel " + DoubleModel.this.toString() + " refresh forced because string did not match actual value.");
}
}
-
+
@Override
public Object getNextValue() {
double d = currentUnit.toUnit(DoubleModel.this.getValue());
d = max;
return d;
}
-
+
@Override
public Object getPreviousValue() {
double d = currentUnit.toUnit(DoubleModel.this.getValue());
d = min;
return d;
}
-
+
@Override
public void addChangeListener(ChangeListener l) {
DoubleModel.this.addChangeListener(l);
}
-
+
@Override
public void removeChangeListener(ChangeListener l) {
DoubleModel.this.removeChangeListener(l);
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
}
-
+
/**
* Returns a new SpinnerModel with the same base as the DoubleModel.
* The values given to the JSpinner are in the currently selected units.
public SpinnerModel getSpinnerModel() {
return new ValueSpinnerModel();
}
-
+
//////////// JSlider model ////////////
-
+
private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
private static final int MAX = 1000;
-
+
/*
* Use linear scale value = linear1 * x + linear0 when x < linearPosition
* Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
*/
private final boolean islinear;
-
+
// Linear in range x <= linearPosition
private final double linearPosition;
-
+
// May be changing DoubleModels when using linear model
private final DoubleModel min, mid, max;
-
+
// Linear multiplier and constant
//private final double linear1;
//private final double linear0;
-
+
// Non-linear multiplier, exponent and constant
private double quad2, quad1, quad0;
-
+
public ValueSliderModel(DoubleModel min, DoubleModel max) {
this.islinear = true;
linearPosition = 1.0;
-
+
this.min = min;
this.mid = max; // Never use exponential scale
this.max = max;
-
+
min.addChangeListener(this);
max.addChangeListener(this);
-
+
quad2 = quad1 = quad0 = 0; // Not used
}
-
-
-
+
+
+
/**
* Generate a linear model from min to max.
*/
public ValueSliderModel(double min, double max) {
this.islinear = true;
linearPosition = 1.0;
-
+
this.min = new DoubleModel(min);
this.mid = new DoubleModel(max); // Never use exponential scale
this.max = new DoubleModel(max);
-
+
quad2 = quad1 = quad0 = 0; // Not used
}
-
+
public ValueSliderModel(double min, double mid, double max) {
this(min, 0.5, mid, max);
}
-
+
public ValueSliderModel(double min, double mid, DoubleModel max) {
this(min, 0.5, mid, max);
}
-
+
/*
* v(x) = mul * x^exp + add
*
* v(1) = mul + add = max
* v'(pos) = mul*exp * pos^(exp-1) = linearMul
*/
- public ValueSliderModel(double min, double pos, double mid, double max ) {
+ public ValueSliderModel(double min, double pos, double mid, double max) {
this(min, pos, mid, new DoubleModel(max));
}
+
public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
this.min = new DoubleModel(min);
this.mid = new DoubleModel(mid);
this.max = max;
-
+
this.islinear = false;
-
+
max.addChangeListener(this);
-
+
linearPosition = pos;
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
"min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
}
-
+
updateExponentialParameters();
-
+
}
-
+
private void updateExponentialParameters() {
double pos = this.linearPosition;
double minValue = this.min.getValue();
quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
}
-
+
private double pow2(double x) {
return x * x;
}
-
+
@Override
public int getValue() {
double value = DoubleModel.this.getValue();
return 0;
if (value >= max.getValue())
return MAX;
-
+
double x;
if (value <= mid.getValue()) {
// Use linear scale
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
} else {
// Use quadratic scale
}
return (int) (x * MAX);
}
-
-
+
+
@Override
public void setValue(int newValue) {
if (firing > 0) {
" value=" + newValue + ", currently firing events");
return;
}
-
+
double x = (double) newValue / MAX;
double scaledValue;
-
+
if (x <= linearPosition) {
// Use linear scale
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
} else {
// Use quadratic scale
scaledValue = quad2 * x * x + quad1 * x + quad0;
}
-
+
double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" scaledValue=" + scaledValue + " converted=" + converted);
DoubleModel.this.setValue(converted);
}
-
-
+
+
// Static get-methods
private boolean isAdjusting;
-
+
@Override
public int getExtent() {
return 0;
}
-
+
@Override
public int getMaximum() {
return MAX;
}
-
+
@Override
public int getMinimum() {
return 0;
}
-
+
@Override
public boolean getValueIsAdjusting() {
return isAdjusting;
}
-
+
// Ignore set-values
@Override
public void setExtent(int newExtent) {
}
-
+
@Override
public void setMaximum(int newMaximum) {
}
-
+
@Override
public void setMinimum(int newMinimum) {
}
-
+
@Override
public void setValueIsAdjusting(boolean b) {
isAdjusting = b;
}
-
+
@Override
public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
setValueIsAdjusting(adjusting);
setValue(value);
}
-
+
// Pass change listeners to the underlying model
@Override
public void addChangeListener(ChangeListener l) {
DoubleModel.this.addChangeListener(l);
}
-
+
@Override
public void removeChangeListener(ChangeListener l) {
DoubleModel.this.removeChangeListener(l);
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
-
+
@Override
public void stateChanged(EventObject e) {
// Min or max range has changed.
- if ( !islinear ) {
- double midValue = (max.getValue() - min.getValue()) /3.0;
+ if (!islinear) {
+ double midValue = (max.getValue() - min.getValue()) / 3.0;
mid.setValue(midValue);
updateExponentialParameters();
}
fireStateChanged();
}
}
-
-
+
+
public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
return new ValueSliderModel(min, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double max) {
return new ValueSliderModel(min, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double mid, double max) {
return new ValueSliderModel(min, mid, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
return new ValueSliderModel(min, mid, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
return new ValueSliderModel(min, pos, mid, max);
}
-
-
-
-
-
+
+
+
+
+
//////////// Action model ////////////
-
+
private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
private boolean oldValue = false;
-
+
public AutomaticActionModel() {
oldValue = isAutomatic();
addChangeListener(this);
}
-
-
+
+
@Override
public boolean isEnabled() {
return isAutomaticAvailable();
}
-
+
@Override
public Object getValue(String key) {
if (key.equals(Action.SELECTED_KEY)) {
}
return super.getValue(key);
}
-
+
@Override
public void putValue(String key, Object value) {
if (firing > 0) {
super.putValue(key, value);
}
}
-
+
// Implement a wrapper to the ChangeListeners
ArrayList<PropertyChangeListener> propertyChangeListeners =
new ArrayList<PropertyChangeListener>();
-
+
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeListeners.add(listener);
DoubleModel.this.addChangeListener(this);
}
-
+
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
propertyChangeListeners.remove(listener);
if (propertyChangeListeners.isEmpty())
DoubleModel.this.removeChangeListener(this);
}
-
+
// If the value has changed, generate an event to the listeners
@Override
public void stateChanged(EventObject e) {
((PropertyChangeListener) l[i]).propertyChange(event);
}
}
-
+
@Override
public void actionPerformed(ActionEvent e) {
// Setting performed in putValue
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
}
-
+
/**
* Returns a new Action corresponding to the changes of the automatic setting
* property of the value model. This may be used directly with e.g. check buttons.
public Action getAutomaticAction() {
return new AutomaticActionModel();
}
-
-
-
-
-
+
+
+
+
+
//////////// Main model /////////////
-
+
/*
* The main model handles all values in SI units, i.e. no conversion is made within the model.
*/
-
+
private final ChangeSource source;
private final String valueName;
private final double multiplier;
-
+
private final Method getMethod;
private final Method setMethod;
-
+
private final Method getAutoMethod;
private final Method setAutoMethod;
-
+
private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
-
+
private final UnitGroup units;
private Unit currentUnit;
-
+
private final double minValue;
private double maxValue;
-
+
private String toString = null;
-
-
+
+
private int firing = 0; // >0 when model itself is sending events
-
-
+
+
// Used to differentiate changes in valueName and other changes in the component:
private double lastValue = 0;
private boolean lastAutomatic = false;
-
+
private Invalidator invalidator = new Invalidator(this);
-
-
+
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value) {
this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value, UnitGroup unit) {
this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value, UnitGroup unit, double min) {
this(value, unit, min, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
this.lastValue = value;
this.minValue = min;
this.maxValue = max;
-
+
source = null;
valueName = "Constant value";
multiplier = 1;
-
+
getMethod = setMethod = null;
getAutoMethod = setAutoMethod = null;
units = unit;
currentUnit = units.getDefaultUnit();
}
-
-
+
+
/**
* Generates a new DoubleModel that changes the values of the specified component.
* The double value is read and written using the methods "get"/"set" + valueName.
this.source = source;
this.valueName = valueName;
this.multiplier = multiplier;
-
+
this.units = unit;
currentUnit = units.getDefaultUnit();
-
+
this.minValue = min;
this.maxValue = max;
-
+
try {
getMethod = source.getClass().getMethod("get" + valueName);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("get method for value '" + valueName +
"' not present in class " + source.getClass().getCanonicalName());
}
-
+
Method s = null;
try {
s = source.getClass().getMethod("set" + valueName, double.class);
} catch (NoSuchMethodException e1) {
} // Ignore
setMethod = s;
-
+
// Automatic selection methods
-
+
Method set = null, get = null;
-
+
try {
get = source.getClass().getMethod("is" + valueName + "Automatic");
set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
} catch (NoSuchMethodException e) {
} // ignore
-
+
if (set != null && get != null) {
getAutoMethod = get;
setAutoMethod = set;
getAutoMethod = null;
setAutoMethod = null;
}
-
+
}
-
+
public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
double min) {
this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
double min, double max) {
this(source, valueName, 1.0, unit, min, max);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double min) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double min, double max) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
}
-
-
-
+
+
+
/**
* Returns the value of the variable (in SI units).
*/
public double getValue() {
if (getMethod == null) // Constant value
return lastValue;
-
+
try {
return (Double) getMethod.invoke(source) * multiplier;
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Sets the value of the variable.
* @param v New value for parameter in SI units.
*/
public void setValue(double v) {
checkState(true);
-
+
log.debug("Setting value " + v + " for " + this);
if (setMethod == null) {
if (getMethod != null) {
fireStateChanged();
return;
}
-
+
try {
setMethod.invoke(source, v / multiplier);
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Returns whether setting the value automatically is available.
*/
public boolean isAutomaticAvailable() {
return (getAutoMethod != null) && (setAutoMethod != null);
}
-
+
/**
* Returns whether the value is currently being set automatically.
* Returns false if automatic setting is not available at all.
public boolean isAutomatic() {
if (getAutoMethod == null)
return false;
-
+
try {
return (Boolean) getAutoMethod.invoke(source);
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Sets whether the value should be set automatically. Simply fires a
* state change event if automatic setting is not available.
*/
public void setAutomatic(boolean auto) {
checkState(true);
-
+
if (setAutoMethod == null) {
log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
fireStateChanged(); // in case something is out-of-sync
return;
}
-
+
log.debug("Setting automatic to " + auto + " for " + this);
lastAutomatic = auto;
try {
throw Reflection.handleWrappedException(e);
}
}
-
-
+
+
/**
* Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
* @return The most recently set unit.
public Unit getCurrentUnit() {
return currentUnit;
}
-
+
/**
* Sets the current Unit. The unit must be one of those included in the UnitGroup.
* @param u The unit to set active.
currentUnit = u;
fireStateChanged();
}
-
-
+
+
/**
* Returns the UnitGroup associated with the parameter value.
*
public UnitGroup getUnitGroup() {
return units;
}
-
-
-
+
+
+
/**
* Add a listener to the model. Adds the model as a listener to the value source if this
* is the first listener.
@Override
public void addChangeListener(EventListener l) {
checkState(true);
-
+
if (listeners.isEmpty()) {
if (source != null) {
source.addChangeListener(this);
lastAutomatic = isAutomatic();
}
}
-
+
listeners.add(l);
log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
}
-
+
/**
* Remove a listener from the model. Removes the model from being a listener to the Component
* if this was the last listener of the model.
@Override
public void removeChangeListener(EventListener l) {
checkState(false);
-
+
listeners.remove(l);
if (listeners.isEmpty() && source != null) {
source.removeChangeListener(this);
}
log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
}
-
-
+
+
/**
* Invalidates this model by removing all listeners and removing this from
* listening to the source. After invalidation no listeners can be added to this
public void invalidate() {
log.verbose("Invalidating " + this);
invalidator.invalidate();
-
+
if (!listeners.isEmpty()) {
log.warn("Invalidating " + this + " while still having listeners " + listeners);
}
}
MemoryManagement.collectable(this);
}
-
-
+
+
private void checkState(boolean error) {
invalidator.check(error);
}
-
-
+
+
@Override
protected void finalize() throws Throwable {
super.finalize();
log.warn(this + " being garbage-collected while having listeners " + listeners);
}
};
-
-
+
+
/**
* Fire a ChangeEvent to all listeners.
*/
protected void fireStateChanged() {
checkState(true);
-
+
EventObject event = new EventObject(this);
ChangeEvent cevent = new ChangeEvent(this);
firing++;
// Copy the list before iterating to prevent concurrent modification exceptions.
EventListener[] ls = listeners.toArray(new EventListener[0]);
for (EventListener l : ls) {
- if ( l instanceof StateChangeListener ) {
- ((StateChangeListener)l).stateChanged(event);
- } else if ( l instanceof ChangeListener ) {
- ((ChangeListener)l).stateChanged(cevent);
+ if (l instanceof StateChangeListener) {
+ ((StateChangeListener) l).stateChanged(event);
+ } else if (l instanceof ChangeListener) {
+ ((ChangeListener) l).stateChanged(cevent);
}
}
firing--;
}
-
+
/**
* Called when the component changes. Checks whether the modeled value has changed, and if
* it has, updates lastValue and generates ChangeEvents for all listeners of the model.
@Override
public void stateChanged(EventObject e) {
checkState(true);
-
+
double v = getValue();
boolean b = isAutomatic();
if (lastValue == v && lastAutomatic == b)
lastAutomatic = b;
fireStateChanged();
}
-
-
+
+
/**
* Explain the DoubleModel as a String.
*/