1 package net.sf.openrocket.gui.adaptors;
3 import java.awt.event.ActionEvent;
4 import java.beans.PropertyChangeEvent;
5 import java.beans.PropertyChangeListener;
6 import java.lang.reflect.InvocationTargetException;
7 import java.lang.reflect.Method;
8 import java.util.ArrayList;
10 import javax.swing.AbstractAction;
11 import javax.swing.Action;
12 import javax.swing.BoundedRangeModel;
13 import javax.swing.SpinnerModel;
14 import javax.swing.SpinnerNumberModel;
15 import javax.swing.event.ChangeEvent;
16 import javax.swing.event.ChangeListener;
18 import net.sf.openrocket.logging.LogHelper;
19 import net.sf.openrocket.startup.Application;
20 import net.sf.openrocket.unit.Unit;
21 import net.sf.openrocket.unit.UnitGroup;
22 import net.sf.openrocket.util.BugException;
23 import net.sf.openrocket.util.ChangeSource;
24 import net.sf.openrocket.util.MathUtil;
25 import net.sf.openrocket.util.Reflection;
29 * A model connector that can read and modify any value of any ChangeSource that
30 * has the appropriate get/set methods defined.
32 * The variable is defined in the constructor by providing the variable name as a string
33 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
34 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
36 * Sub-models suitable for JSpinners and other components are available from the appropriate
39 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
42 public class DoubleModel implements ChangeListener, ChangeSource {
43 private static final LogHelper log = Application.getLogger();
46 public static final DoubleModel ZERO = new DoubleModel(0);
48 //////////// JSpinner Model ////////////
51 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
52 * to be compatible with the NumberEditor, but only has the necessary methods defined.
54 private class ValueSpinnerModel extends SpinnerNumberModel {
57 public Object getValue() {
58 return currentUnit.toUnit(DoubleModel.this.getValue());
62 public void setValue(Object value) {
64 // Ignore, if called when model is sending events
65 log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
66 " value=" + value + ", currently firing events");
69 Number num = (Number) value;
70 double newValue = num.doubleValue();
71 double converted = currentUnit.fromUnit(newValue);
73 log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
74 " converted=" + converted);
75 DoubleModel.this.setValue(converted);
80 public Object getNextValue() {
81 double d = currentUnit.toUnit(DoubleModel.this.getValue());
82 double max = currentUnit.toUnit(maxValue);
83 if (MathUtil.equals(d, max))
85 d = currentUnit.getNextValue(d);
92 public Object getPreviousValue() {
93 double d = currentUnit.toUnit(DoubleModel.this.getValue());
94 double min = currentUnit.toUnit(minValue);
95 if (MathUtil.equals(d, min))
97 d = currentUnit.getPreviousValue(d);
105 public Comparable<Double> getMinimum() {
106 return currentUnit.toUnit(minValue);
110 public Comparable<Double> getMaximum() {
111 return currentUnit.toUnit(maxValue);
116 public void addChangeListener(ChangeListener l) {
117 DoubleModel.this.addChangeListener(l);
121 public void removeChangeListener(ChangeListener l) {
122 DoubleModel.this.removeChangeListener(l);
127 * Returns a new SpinnerModel with the same base as the DoubleModel.
128 * The values given to the JSpinner are in the currently selected units.
130 * @return A compatibility layer for a SpinnerModel.
132 public SpinnerModel getSpinnerModel() {
133 return new ValueSpinnerModel();
140 //////////// JSlider model ////////////
142 private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
143 private static final int MAX = 1000;
146 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
147 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
150 // Linear in range x <= linearPosition
151 private final double linearPosition;
153 // May be changing DoubleModels when using linear model
154 private final DoubleModel min, mid, max;
156 // Linear multiplier and constant
157 //private final double linear1;
158 //private final double linear0;
160 // Non-linear multiplier, exponent and constant
161 private final double quad2, quad1, quad0;
165 public ValueSliderModel(DoubleModel min, DoubleModel max) {
166 linearPosition = 1.0;
169 this.mid = max; // Never use exponential scale
172 min.addChangeListener(this);
173 max.addChangeListener(this);
175 quad2 = quad1 = quad0 = 0; // Not used
181 * Generate a linear model from min to max.
183 public ValueSliderModel(double min, double max) {
184 linearPosition = 1.0;
186 this.min = new DoubleModel(min);
187 this.mid = new DoubleModel(max); // Never use exponential scale
188 this.max = new DoubleModel(max);
190 quad2 = quad1 = quad0 = 0; // Not used
193 public ValueSliderModel(double min, double mid, double max) {
194 this(min, 0.5, mid, max);
198 * v(x) = mul * x^exp + add
200 * v(pos) = mul * pos^exp + add = mid
201 * v(1) = mul + add = max
202 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
204 public ValueSliderModel(double min, double pos, double mid, double max) {
205 this.min = new DoubleModel(min);
206 this.mid = new DoubleModel(mid);
207 this.max = new DoubleModel(max);
210 linearPosition = pos;
212 //linear1 = (mid-min)/pos;
214 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
215 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
216 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
220 * quad2..0 are calculated such that
221 * f(pos) = mid - continuity
222 * f(1) = max - end point
223 * f'(pos) = linear1 - continuity of derivative
226 double delta = (mid - min) / pos;
227 quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1);
228 quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1);
229 quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1);
233 private double pow2(double x) {
237 public int getValue() {
238 double value = DoubleModel.this.getValue();
239 if (value <= min.getValue())
241 if (value >= max.getValue())
245 if (value <= mid.getValue()) {
248 //linear1 = (mid-min)/pos;
250 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
252 // Use quadratic scale
253 // Further solution of the quadratic equation
254 // a*x^2 + b*x + c-value == 0
255 x = (Math.sqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
257 return (int) (x * MAX);
261 public void setValue(int newValue) {
264 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
265 " value=" + newValue + ", currently firing events");
269 double x = (double) newValue / MAX;
272 if (x <= linearPosition) {
275 //linear1 = (mid-min)/pos;
277 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
279 // Use quadratic scale
280 scaledValue = quad2 * x * x + quad1 * x + quad0;
283 double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
284 log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
285 " scaledValue=" + scaledValue + " converted=" + converted);
286 DoubleModel.this.setValue(converted);
290 // Static get-methods
291 private boolean isAdjusting;
293 public int getExtent() {
297 public int getMaximum() {
301 public int getMinimum() {
305 public boolean getValueIsAdjusting() {
310 public void setExtent(int newExtent) {
313 public void setMaximum(int newMaximum) {
316 public void setMinimum(int newMinimum) {
319 public void setValueIsAdjusting(boolean b) {
323 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
324 setValueIsAdjusting(adjusting);
328 // Pass change listeners to the underlying model
329 public void addChangeListener(ChangeListener l) {
330 DoubleModel.this.addChangeListener(l);
333 public void removeChangeListener(ChangeListener l) {
334 DoubleModel.this.removeChangeListener(l);
339 public void stateChanged(ChangeEvent e) {
340 // Min or max range has changed.
341 // Fire if not already firing
348 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
349 return new ValueSliderModel(min, max);
352 public BoundedRangeModel getSliderModel(double min, double max) {
353 return new ValueSliderModel(min, max);
356 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
357 return new ValueSliderModel(min, mid, max);
360 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
361 return new ValueSliderModel(min, pos, mid, max);
368 //////////// Action model ////////////
370 private class AutomaticActionModel extends AbstractAction implements ChangeListener {
371 private boolean oldValue = false;
373 public AutomaticActionModel() {
374 oldValue = isAutomatic();
375 addChangeListener(this);
380 public boolean isEnabled() {
381 return isAutomaticAvailable();
385 public Object getValue(String key) {
386 if (key.equals(Action.SELECTED_KEY)) {
387 oldValue = isAutomatic();
390 return super.getValue(key);
394 public void putValue(String key, Object value) {
396 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
397 " key=" + key + " value=" + value + ", currently firing events");
400 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
401 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
402 " key=" + key + " value=" + value);
403 oldValue = (Boolean) value;
404 setAutomatic((Boolean) value);
406 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
407 " key=" + key + " value=" + value);
408 super.putValue(key, value);
412 // Implement a wrapper to the ChangeListeners
413 ArrayList<PropertyChangeListener> propertyChangeListeners =
414 new ArrayList<PropertyChangeListener>();
417 public void addPropertyChangeListener(PropertyChangeListener listener) {
418 propertyChangeListeners.add(listener);
419 DoubleModel.this.addChangeListener(this);
423 public void removePropertyChangeListener(PropertyChangeListener listener) {
424 propertyChangeListeners.remove(listener);
425 if (propertyChangeListeners.isEmpty())
426 DoubleModel.this.removeChangeListener(this);
429 // If the value has changed, generate an event to the listeners
430 public void stateChanged(ChangeEvent e) {
431 boolean newValue = isAutomatic();
432 if (oldValue == newValue)
434 PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
437 Object[] l = propertyChangeListeners.toArray();
438 for (int i = 0; i < l.length; i++) {
439 ((PropertyChangeListener) l[i]).propertyChange(event);
443 public void actionPerformed(ActionEvent e) {
444 // Setting performed in putValue
450 * Returns a new Action corresponding to the changes of the automatic setting
451 * property of the value model. This may be used directly with e.g. check buttons.
453 * @return A compatibility layer for an Action.
455 public Action getAutomaticAction() {
456 return new AutomaticActionModel();
463 //////////// Main model /////////////
466 * The main model handles all values in SI units, i.e. no conversion is made within the model.
469 private final ChangeSource source;
470 private final String valueName;
471 private final double multiplier;
473 private final Method getMethod;
474 private final Method setMethod;
476 private final Method getAutoMethod;
477 private final Method setAutoMethod;
479 private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
481 private final UnitGroup units;
482 private Unit currentUnit;
484 private final double minValue;
485 private final double maxValue;
487 private String toString = null;
490 private int firing = 0; // >0 when model itself is sending events
493 // Used to differentiate changes in valueName and other changes in the component:
494 private double lastValue = 0;
495 private boolean lastAutomatic = false;
498 public DoubleModel(double value) {
499 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
502 public DoubleModel(double value, UnitGroup unit) {
503 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
506 public DoubleModel(double value, UnitGroup unit, double min) {
507 this(value, unit, min, Double.POSITIVE_INFINITY);
510 public DoubleModel(double value, UnitGroup unit, double min, double max) {
511 this.lastValue = value;
516 valueName = "Constant value";
519 getMethod = setMethod = null;
520 getAutoMethod = setAutoMethod = null;
522 currentUnit = units.getDefaultUnit();
527 * Generates a new DoubleModel that changes the values of the specified component.
528 * The double value is read and written using the methods "get"/"set" + valueName.
530 * @param source Component whose parameter to use.
531 * @param valueName Name of metods used to get/set the parameter.
532 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
533 * @param min Minimum value allowed (in SI units)
534 * @param max Maximum value allowed (in SI units)
536 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
537 double min, double max) {
538 this.source = source;
539 this.valueName = valueName;
540 this.multiplier = multiplier;
543 currentUnit = units.getDefaultUnit();
549 getMethod = source.getClass().getMethod("get" + valueName);
550 } catch (NoSuchMethodException e) {
551 throw new IllegalArgumentException("get method for value '" + valueName +
552 "' not present in class " + source.getClass().getCanonicalName());
557 s = source.getClass().getMethod("set" + valueName, double.class);
558 } catch (NoSuchMethodException e1) {
562 // Automatic selection methods
564 Method set = null, get = null;
567 get = source.getClass().getMethod("is" + valueName + "Automatic");
568 set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
569 } catch (NoSuchMethodException e) {
572 if (set != null && get != null) {
576 getAutoMethod = null;
577 setAutoMethod = null;
582 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
584 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
587 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
588 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
591 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
592 double min, double max) {
593 this(source, valueName, 1.0, unit, min, max);
596 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
597 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
600 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
601 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
604 public DoubleModel(ChangeSource source, String valueName) {
605 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
606 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
609 public DoubleModel(ChangeSource source, String valueName, double min) {
610 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
613 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
614 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
620 * Returns the value of the variable (in SI units).
622 public double getValue() {
623 if (getMethod == null) // Constant value
627 return (Double) getMethod.invoke(source) * multiplier;
628 } catch (IllegalArgumentException e) {
629 throw new BugException("Unable to invoke getMethod of " + this, e);
630 } catch (IllegalAccessException e) {
631 throw new BugException("Unable to invoke getMethod of " + this, e);
632 } catch (InvocationTargetException e) {
633 throw Reflection.handleWrappedException(e);
638 * Sets the value of the variable.
639 * @param v New value for parameter in SI units.
641 public void setValue(double v) {
642 log.debug("Setting value " + v + " for " + this);
643 if (setMethod == null) {
644 if (getMethod != null) {
645 throw new BugException("setMethod not available for variable '" + valueName +
646 "' in class " + source.getClass().getCanonicalName());
654 setMethod.invoke(source, v / multiplier);
655 } catch (IllegalArgumentException e) {
656 throw new BugException("Unable to invoke setMethod of " + this, e);
657 } catch (IllegalAccessException e) {
658 throw new BugException("Unable to invoke setMethod of " + this, e);
659 } catch (InvocationTargetException e) {
660 throw Reflection.handleWrappedException(e);
666 * Returns whether setting the value automatically is available.
668 public boolean isAutomaticAvailable() {
669 return (getAutoMethod != null) && (setAutoMethod != null);
673 * Returns whether the value is currently being set automatically.
674 * Returns false if automatic setting is not available at all.
676 public boolean isAutomatic() {
677 if (getAutoMethod == null)
681 return (Boolean) getAutoMethod.invoke(source);
682 } catch (IllegalArgumentException e) {
683 throw new BugException("Method call failed", e);
684 } catch (IllegalAccessException e) {
685 throw new BugException("Method call failed", e);
686 } catch (InvocationTargetException e) {
687 throw Reflection.handleWrappedException(e);
692 * Sets whether the value should be set automatically. Simply fires a
693 * state change event if automatic setting is not available.
695 public void setAutomatic(boolean auto) {
696 if (setAutoMethod == null) {
697 log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
698 fireStateChanged(); // in case something is out-of-sync
702 log.debug("Setting automatic to " + auto + " for " + this);
703 lastAutomatic = auto;
705 setAutoMethod.invoke(source, auto);
706 } catch (IllegalArgumentException e) {
707 throw new BugException(e);
708 } catch (IllegalAccessException e) {
709 throw new BugException(e);
710 } catch (InvocationTargetException e) {
711 throw Reflection.handleWrappedException(e);
717 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
718 * @return The most recently set unit.
720 public Unit getCurrentUnit() {
725 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
726 * @param u The unit to set active.
728 public void setCurrentUnit(Unit u) {
729 if (currentUnit == u)
731 log.debug("Setting unit for " + this + " to '" + u + "'");
738 * Returns the UnitGroup associated with the parameter value.
740 * @return The UnitGroup given to the constructor.
742 public UnitGroup getUnitGroup() {
749 * Add a listener to the model. Adds the model as a listener to the value source if this
750 * is the first listener.
751 * @param l Listener to add.
753 public void addChangeListener(ChangeListener l) {
754 if (listeners.isEmpty()) {
755 if (source != null) {
756 source.addChangeListener(this);
757 lastValue = getValue();
758 lastAutomatic = isAutomatic();
763 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
767 * Remove a listener from the model. Removes the model from being a listener to the Component
768 * if this was the last listener of the model.
769 * @param l Listener to remove.
771 public void removeChangeListener(ChangeListener l) {
773 if (listeners.isEmpty() && source != null) {
774 source.removeChangeListener(this);
776 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
781 protected void finalize() throws Throwable {
783 if (!listeners.isEmpty()) {
784 log.warn(this + " being garbage-collected while having listeners " + listeners);
790 * Fire a ChangeEvent to all listeners.
792 protected void fireStateChanged() {
793 Object[] l = listeners.toArray();
794 ChangeEvent event = new ChangeEvent(this);
796 for (int i = 0; i < l.length; i++)
797 ((ChangeListener) l[i]).stateChanged(event);
802 * Called when the component changes. Checks whether the modeled value has changed, and if
803 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
805 public void stateChanged(ChangeEvent e) {
806 double v = getValue();
807 boolean b = isAutomatic();
808 if (lastValue == v && lastAutomatic == b)
817 * Explain the DoubleModel as a String.
820 public String toString() {
821 if (toString == null) {
822 if (source == null) {
823 toString = "DoubleModel[constant=" + lastValue + "]";
825 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";