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.Invalidatable;
25 import net.sf.openrocket.util.Invalidator;
26 import net.sf.openrocket.util.MathUtil;
27 import net.sf.openrocket.util.MemoryManagement;
28 import net.sf.openrocket.util.Reflection;
32 * A model connector that can read and modify any value of any ChangeSource that
33 * has the appropriate get/set methods defined.
35 * The variable is defined in the constructor by providing the variable name as a string
36 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
37 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
39 * Sub-models suitable for JSpinners and other components are available from the appropriate
42 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
45 public class DoubleModel implements ChangeListener, ChangeSource, Invalidatable {
46 private static final LogHelper log = Application.getLogger();
49 public static final DoubleModel ZERO = new DoubleModel(0);
51 //////////// JSpinner Model ////////////
54 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
55 * to be compatible with the NumberEditor, but only has the necessary methods defined.
57 private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable {
60 public Object getValue() {
61 return currentUnit.toUnit(DoubleModel.this.getValue());
65 public void setValue(Object value) {
67 // Ignore, if called when model is sending events
68 log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
69 " value=" + value + ", currently firing events");
72 Number num = (Number) value;
73 double newValue = num.doubleValue();
74 double converted = currentUnit.fromUnit(newValue);
76 log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
77 " converted=" + converted);
78 DoubleModel.this.setValue(converted);
83 public Object getNextValue() {
84 double d = currentUnit.toUnit(DoubleModel.this.getValue());
85 double max = currentUnit.toUnit(maxValue);
86 if (MathUtil.equals(d, max))
88 d = currentUnit.getNextValue(d);
95 public Object getPreviousValue() {
96 double d = currentUnit.toUnit(DoubleModel.this.getValue());
97 double min = currentUnit.toUnit(minValue);
98 if (MathUtil.equals(d, min))
100 d = currentUnit.getPreviousValue(d);
108 public Comparable<Double> getMinimum() {
109 return currentUnit.toUnit(minValue);
113 public Comparable<Double> getMaximum() {
114 return currentUnit.toUnit(maxValue);
119 public void addChangeListener(ChangeListener l) {
120 DoubleModel.this.addChangeListener(l);
124 public void removeChangeListener(ChangeListener l) {
125 DoubleModel.this.removeChangeListener(l);
129 public void invalidate() {
130 DoubleModel.this.invalidate();
135 * Returns a new SpinnerModel with the same base as the DoubleModel.
136 * The values given to the JSpinner are in the currently selected units.
138 * @return A compatibility layer for a SpinnerModel.
140 public SpinnerModel getSpinnerModel() {
141 return new ValueSpinnerModel();
148 //////////// JSlider model ////////////
150 private class ValueSliderModel implements BoundedRangeModel, ChangeListener, Invalidatable {
151 private static final int MAX = 1000;
154 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
155 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
158 // Linear in range x <= linearPosition
159 private final double linearPosition;
161 // May be changing DoubleModels when using linear model
162 private final DoubleModel min, mid, max;
164 // Linear multiplier and constant
165 //private final double linear1;
166 //private final double linear0;
168 // Non-linear multiplier, exponent and constant
169 private final double quad2, quad1, quad0;
173 public ValueSliderModel(DoubleModel min, DoubleModel max) {
174 linearPosition = 1.0;
177 this.mid = max; // Never use exponential scale
180 min.addChangeListener(this);
181 max.addChangeListener(this);
183 quad2 = quad1 = quad0 = 0; // Not used
189 * Generate a linear model from min to max.
191 public ValueSliderModel(double min, double max) {
192 linearPosition = 1.0;
194 this.min = new DoubleModel(min);
195 this.mid = new DoubleModel(max); // Never use exponential scale
196 this.max = new DoubleModel(max);
198 quad2 = quad1 = quad0 = 0; // Not used
201 public ValueSliderModel(double min, double mid, double max) {
202 this(min, 0.5, mid, max);
206 * v(x) = mul * x^exp + add
208 * v(pos) = mul * pos^exp + add = mid
209 * v(1) = mul + add = max
210 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
212 public ValueSliderModel(double min, double pos, double mid, double max) {
213 this.min = new DoubleModel(min);
214 this.mid = new DoubleModel(mid);
215 this.max = new DoubleModel(max);
218 linearPosition = pos;
220 //linear1 = (mid-min)/pos;
222 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
223 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
224 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
228 * quad2..0 are calculated such that
229 * f(pos) = mid - continuity
230 * f(1) = max - end point
231 * f'(pos) = linear1 - continuity of derivative
234 double delta = (mid - min) / pos;
235 quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1);
236 quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1);
237 quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1);
241 private double pow2(double x) {
246 public int getValue() {
247 double value = DoubleModel.this.getValue();
248 if (value <= min.getValue())
250 if (value >= max.getValue())
254 if (value <= mid.getValue()) {
257 //linear1 = (mid-min)/pos;
259 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
261 // Use quadratic scale
262 // Further solution of the quadratic equation
263 // a*x^2 + b*x + c-value == 0
264 x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
266 return (int) (x * MAX);
271 public void setValue(int newValue) {
274 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
275 " value=" + newValue + ", currently firing events");
279 double x = (double) newValue / MAX;
282 if (x <= linearPosition) {
285 //linear1 = (mid-min)/pos;
287 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
289 // Use quadratic scale
290 scaledValue = quad2 * x * x + quad1 * x + quad0;
293 double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
294 log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
295 " scaledValue=" + scaledValue + " converted=" + converted);
296 DoubleModel.this.setValue(converted);
300 // Static get-methods
301 private boolean isAdjusting;
304 public int getExtent() {
309 public int getMaximum() {
314 public int getMinimum() {
319 public boolean getValueIsAdjusting() {
325 public void setExtent(int newExtent) {
329 public void setMaximum(int newMaximum) {
333 public void setMinimum(int newMinimum) {
337 public void setValueIsAdjusting(boolean b) {
342 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
343 setValueIsAdjusting(adjusting);
347 // Pass change listeners to the underlying model
349 public void addChangeListener(ChangeListener l) {
350 DoubleModel.this.addChangeListener(l);
354 public void removeChangeListener(ChangeListener l) {
355 DoubleModel.this.removeChangeListener(l);
359 public void invalidate() {
360 DoubleModel.this.invalidate();
364 public void stateChanged(ChangeEvent e) {
365 // Min or max range has changed.
366 // Fire if not already firing
373 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
374 return new ValueSliderModel(min, max);
377 public BoundedRangeModel getSliderModel(double min, double max) {
378 return new ValueSliderModel(min, max);
381 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
382 return new ValueSliderModel(min, mid, max);
385 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
386 return new ValueSliderModel(min, pos, mid, max);
393 //////////// Action model ////////////
395 private class AutomaticActionModel extends AbstractAction implements ChangeListener, Invalidatable {
396 private boolean oldValue = false;
398 public AutomaticActionModel() {
399 oldValue = isAutomatic();
400 addChangeListener(this);
405 public boolean isEnabled() {
406 return isAutomaticAvailable();
410 public Object getValue(String key) {
411 if (key.equals(Action.SELECTED_KEY)) {
412 oldValue = isAutomatic();
415 return super.getValue(key);
419 public void putValue(String key, Object value) {
421 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
422 " key=" + key + " value=" + value + ", currently firing events");
425 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
426 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
427 " key=" + key + " value=" + value);
428 oldValue = (Boolean) value;
429 setAutomatic((Boolean) value);
431 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
432 " key=" + key + " value=" + value);
433 super.putValue(key, value);
437 // Implement a wrapper to the ChangeListeners
438 ArrayList<PropertyChangeListener> propertyChangeListeners =
439 new ArrayList<PropertyChangeListener>();
442 public void addPropertyChangeListener(PropertyChangeListener listener) {
443 propertyChangeListeners.add(listener);
444 DoubleModel.this.addChangeListener(this);
448 public void removePropertyChangeListener(PropertyChangeListener listener) {
449 propertyChangeListeners.remove(listener);
450 if (propertyChangeListeners.isEmpty())
451 DoubleModel.this.removeChangeListener(this);
454 // If the value has changed, generate an event to the listeners
456 public void stateChanged(ChangeEvent e) {
457 boolean newValue = isAutomatic();
458 if (oldValue == newValue)
460 PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
463 Object[] l = propertyChangeListeners.toArray();
464 for (int i = 0; i < l.length; i++) {
465 ((PropertyChangeListener) l[i]).propertyChange(event);
470 public void actionPerformed(ActionEvent e) {
471 // Setting performed in putValue
475 public void invalidate() {
476 DoubleModel.this.invalidate();
481 * Returns a new Action corresponding to the changes of the automatic setting
482 * property of the value model. This may be used directly with e.g. check buttons.
484 * @return A compatibility layer for an Action.
486 public Action getAutomaticAction() {
487 return new AutomaticActionModel();
494 //////////// Main model /////////////
497 * The main model handles all values in SI units, i.e. no conversion is made within the model.
500 private final ChangeSource source;
501 private final String valueName;
502 private final double multiplier;
504 private final Method getMethod;
505 private final Method setMethod;
507 private final Method getAutoMethod;
508 private final Method setAutoMethod;
510 private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
512 private final UnitGroup units;
513 private Unit currentUnit;
515 private final double minValue;
516 private final double maxValue;
518 private String toString = null;
521 private int firing = 0; // >0 when model itself is sending events
524 // Used to differentiate changes in valueName and other changes in the component:
525 private double lastValue = 0;
526 private boolean lastAutomatic = false;
528 private Invalidator invalidator = new Invalidator(this);
532 * Generate a DoubleModel that contains an internal double value.
534 * @param value the initial value.
536 public DoubleModel(double value) {
537 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
541 * Generate a DoubleModel that contains an internal double value.
543 * @param value the initial value.
544 * @param unit the unit for the value.
546 public DoubleModel(double value, UnitGroup unit) {
547 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
551 * Generate a DoubleModel that contains an internal double value.
553 * @param value the initial value.
554 * @param unit the unit for the value.
555 * @param min minimum value.
557 public DoubleModel(double value, UnitGroup unit, double min) {
558 this(value, unit, min, Double.POSITIVE_INFINITY);
562 * Generate a DoubleModel that contains an internal double value.
564 * @param value the initial value.
565 * @param unit the unit for the value.
566 * @param min minimum value.
567 * @param max maximum value.
569 public DoubleModel(double value, UnitGroup unit, double min, double max) {
570 this.lastValue = value;
575 valueName = "Constant value";
578 getMethod = setMethod = null;
579 getAutoMethod = setAutoMethod = null;
581 currentUnit = units.getDefaultUnit();
586 * Generates a new DoubleModel that changes the values of the specified component.
587 * The double value is read and written using the methods "get"/"set" + valueName.
589 * @param source Component whose parameter to use.
590 * @param valueName Name of methods used to get/set the parameter.
591 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
592 * @param min Minimum value allowed (in SI units)
593 * @param max Maximum value allowed (in SI units)
595 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
596 double min, double max) {
597 this.source = source;
598 this.valueName = valueName;
599 this.multiplier = multiplier;
602 currentUnit = units.getDefaultUnit();
608 getMethod = source.getClass().getMethod("get" + valueName);
609 } catch (NoSuchMethodException e) {
610 throw new IllegalArgumentException("get method for value '" + valueName +
611 "' not present in class " + source.getClass().getCanonicalName());
616 s = source.getClass().getMethod("set" + valueName, double.class);
617 } catch (NoSuchMethodException e1) {
621 // Automatic selection methods
623 Method set = null, get = null;
626 get = source.getClass().getMethod("is" + valueName + "Automatic");
627 set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
628 } catch (NoSuchMethodException e) {
631 if (set != null && get != null) {
635 getAutoMethod = null;
636 setAutoMethod = null;
641 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
643 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
646 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
647 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
650 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
651 double min, double max) {
652 this(source, valueName, 1.0, unit, min, max);
655 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
656 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
659 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
660 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
663 public DoubleModel(ChangeSource source, String valueName) {
664 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
665 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
668 public DoubleModel(ChangeSource source, String valueName, double min) {
669 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
672 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
673 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
679 * Returns the value of the variable (in SI units).
681 public double getValue() {
682 if (getMethod == null) // Constant value
686 return (Double) getMethod.invoke(source) * multiplier;
687 } catch (IllegalArgumentException e) {
688 throw new BugException("Unable to invoke getMethod of " + this, e);
689 } catch (IllegalAccessException e) {
690 throw new BugException("Unable to invoke getMethod of " + this, e);
691 } catch (InvocationTargetException e) {
692 throw Reflection.handleWrappedException(e);
697 * Sets the value of the variable.
698 * @param v New value for parameter in SI units.
700 public void setValue(double v) {
703 log.debug("Setting value " + v + " for " + this);
704 if (setMethod == null) {
705 if (getMethod != null) {
706 throw new BugException("setMethod not available for variable '" + valueName +
707 "' in class " + source.getClass().getCanonicalName());
715 setMethod.invoke(source, v / multiplier);
716 } catch (IllegalArgumentException e) {
717 throw new BugException("Unable to invoke setMethod of " + this, e);
718 } catch (IllegalAccessException e) {
719 throw new BugException("Unable to invoke setMethod of " + this, e);
720 } catch (InvocationTargetException e) {
721 throw Reflection.handleWrappedException(e);
727 * Returns whether setting the value automatically is available.
729 public boolean isAutomaticAvailable() {
730 return (getAutoMethod != null) && (setAutoMethod != null);
734 * Returns whether the value is currently being set automatically.
735 * Returns false if automatic setting is not available at all.
737 public boolean isAutomatic() {
738 if (getAutoMethod == null)
742 return (Boolean) getAutoMethod.invoke(source);
743 } catch (IllegalArgumentException e) {
744 throw new BugException("Method call failed", e);
745 } catch (IllegalAccessException e) {
746 throw new BugException("Method call failed", e);
747 } catch (InvocationTargetException e) {
748 throw Reflection.handleWrappedException(e);
753 * Sets whether the value should be set automatically. Simply fires a
754 * state change event if automatic setting is not available.
756 public void setAutomatic(boolean auto) {
759 if (setAutoMethod == null) {
760 log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
761 fireStateChanged(); // in case something is out-of-sync
765 log.debug("Setting automatic to " + auto + " for " + this);
766 lastAutomatic = auto;
768 setAutoMethod.invoke(source, auto);
769 } catch (IllegalArgumentException e) {
770 throw new BugException(e);
771 } catch (IllegalAccessException e) {
772 throw new BugException(e);
773 } catch (InvocationTargetException e) {
774 throw Reflection.handleWrappedException(e);
780 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
781 * @return The most recently set unit.
783 public Unit getCurrentUnit() {
788 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
789 * @param u The unit to set active.
791 public void setCurrentUnit(Unit u) {
793 if (currentUnit == u)
795 log.debug("Setting unit for " + this + " to '" + u + "'");
802 * Returns the UnitGroup associated with the parameter value.
804 * @return The UnitGroup given to the constructor.
806 public UnitGroup getUnitGroup() {
813 * Add a listener to the model. Adds the model as a listener to the value source if this
814 * is the first listener.
815 * @param l Listener to add.
818 public void addChangeListener(ChangeListener l) {
821 if (listeners.isEmpty()) {
822 if (source != null) {
823 source.addChangeListener(this);
824 lastValue = getValue();
825 lastAutomatic = isAutomatic();
830 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
834 * Remove a listener from the model. Removes the model from being a listener to the Component
835 * if this was the last listener of the model.
836 * @param l Listener to remove.
839 public void removeChangeListener(ChangeListener l) {
843 if (listeners.isEmpty() && source != null) {
844 source.removeChangeListener(this);
846 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
851 * Invalidates this model by removing all listeners and removing this from
852 * listening to the source. After invalidation no listeners can be added to this
853 * model and the value cannot be set.
856 public void invalidate() {
857 log.verbose("Invalidating " + this);
858 invalidator.invalidate();
860 if (!listeners.isEmpty()) {
861 log.warn("Invalidating " + this + " while still having listeners " + listeners);
864 if (source != null) {
865 source.removeChangeListener(this);
867 MemoryManagement.collectable(this);
871 private void checkState(boolean error) {
872 invalidator.check(error);
877 protected void finalize() throws Throwable {
879 if (!listeners.isEmpty()) {
880 log.warn(this + " being garbage-collected while having listeners " + listeners);
886 * Fire a ChangeEvent to all listeners.
888 protected void fireStateChanged() {
891 Object[] l = listeners.toArray();
892 ChangeEvent event = new ChangeEvent(this);
894 for (int i = 0; i < l.length; i++)
895 ((ChangeListener) l[i]).stateChanged(event);
900 * Called when the component changes. Checks whether the modeled value has changed, and if
901 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
904 public void stateChanged(ChangeEvent e) {
907 double v = getValue();
908 boolean b = isAutomatic();
909 if (lastValue == v && lastAutomatic == b)
918 * Explain the DoubleModel as a String.
921 public String toString() {
922 if (toString == null) {
923 if (source == null) {
924 toString = "DoubleModel[constant=" + lastValue + "]";
926 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";