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;
9 import java.util.EventListener;
10 import java.util.EventObject;
12 import javax.swing.AbstractAction;
13 import javax.swing.AbstractSpinnerModel;
14 import javax.swing.Action;
15 import javax.swing.BoundedRangeModel;
16 import javax.swing.SpinnerModel;
17 import javax.swing.event.ChangeEvent;
18 import javax.swing.event.ChangeListener;
20 import net.sf.openrocket.logging.LogHelper;
21 import net.sf.openrocket.startup.Application;
22 import net.sf.openrocket.unit.Unit;
23 import net.sf.openrocket.unit.UnitGroup;
24 import net.sf.openrocket.util.BugException;
25 import net.sf.openrocket.util.ChangeSource;
26 import net.sf.openrocket.util.FractionUtil;
27 import net.sf.openrocket.util.Invalidatable;
28 import net.sf.openrocket.util.Invalidator;
29 import net.sf.openrocket.util.MathUtil;
30 import net.sf.openrocket.util.MemoryManagement;
31 import net.sf.openrocket.util.Reflection;
32 import net.sf.openrocket.util.StateChangeListener;
36 * A model connector that can read and modify any value of any ChangeSource that
37 * has the appropriate get/set methods defined.
39 * The variable is defined in the constructor by providing the variable name as a string
40 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
41 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
43 * Sub-models suitable for JSpinners and other components are available from the appropriate
46 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
49 public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
50 private static final LogHelper log = Application.getLogger();
53 public static final DoubleModel ZERO = new DoubleModel(0);
55 //////////// JSpinner Model ////////////
58 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
59 * to be compatible with the NumberEditor, but only has the necessary methods defined.
61 public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
64 public Object getValue() {
65 return currentUnit.toString(DoubleModel.this.getValue());
69 public void setValue(Object value) {
71 // Ignore, if called when model is sending events
72 log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
73 " value=" + value + ", currently firing events");
77 if ( value instanceof Number ) {
79 } else if ( value instanceof String ) {
81 String newValString = (String)value;
82 num = FractionUtil.parseFraction(newValString);
84 catch ( java.lang.NumberFormatException nfex ) {
89 double newValue = num.doubleValue();
90 double converted = currentUnit.fromUnit(newValue);
92 log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
93 " converted=" + converted);
94 DoubleModel.this.setValue(converted);
99 public Object getNextValue() {
100 double d = currentUnit.toUnit(DoubleModel.this.getValue());
101 double max = currentUnit.toUnit(maxValue);
102 if (MathUtil.equals(d, max))
104 d = currentUnit.getNextValue(d);
111 public Object getPreviousValue() {
112 double d = currentUnit.toUnit(DoubleModel.this.getValue());
113 double min = currentUnit.toUnit(minValue);
114 if (MathUtil.equals(d, min))
116 d = currentUnit.getPreviousValue(d);
121 /* FIXME - put min & max back
123 public Comparable<Double> getMinimum() {
124 return currentUnit.toUnit(minValue);
128 public Comparable<Double> getMaximum() {
129 return currentUnit.toUnit(maxValue);
133 public void addChangeListener(ChangeListener l) {
134 DoubleModel.this.addChangeListener(l);
138 public void removeChangeListener(ChangeListener l) {
139 DoubleModel.this.removeChangeListener(l);
143 public void invalidate() {
144 DoubleModel.this.invalidate();
149 * Returns a new SpinnerModel with the same base as the DoubleModel.
150 * The values given to the JSpinner are in the currently selected units.
152 * @return A compatibility layer for a SpinnerModel.
154 public SpinnerModel getSpinnerModel() {
155 return new ValueSpinnerModel();
158 //////////// JSlider model ////////////
160 private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
161 private static final int MAX = 1000;
164 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
165 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
167 private final boolean islinear;
169 // Linear in range x <= linearPosition
170 private final double linearPosition;
172 // May be changing DoubleModels when using linear model
173 private final DoubleModel min, mid, max;
175 // Linear multiplier and constant
176 //private final double linear1;
177 //private final double linear0;
179 // Non-linear multiplier, exponent and constant
180 private double quad2, quad1, quad0;
182 public ValueSliderModel(DoubleModel min, DoubleModel max) {
183 this.islinear = true;
184 linearPosition = 1.0;
187 this.mid = max; // Never use exponential scale
190 min.addChangeListener(this);
191 max.addChangeListener(this);
193 quad2 = quad1 = quad0 = 0; // Not used
199 * Generate a linear model from min to max.
201 public ValueSliderModel(double min, double max) {
202 this.islinear = true;
203 linearPosition = 1.0;
205 this.min = new DoubleModel(min);
206 this.mid = new DoubleModel(max); // Never use exponential scale
207 this.max = new DoubleModel(max);
209 quad2 = quad1 = quad0 = 0; // Not used
212 public ValueSliderModel(double min, double mid, double max) {
213 this(min, 0.5, mid, max);
216 public ValueSliderModel(double min, double mid, DoubleModel max) {
217 this(min, 0.5, mid, max);
221 * v(x) = mul * x^exp + add
223 * v(pos) = mul * pos^exp + add = mid
224 * v(1) = mul + add = max
225 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
227 public ValueSliderModel(double min, double pos, double mid, double max ) {
228 this(min, pos, mid, new DoubleModel(max));
230 public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
231 this.min = new DoubleModel(min);
232 this.mid = new DoubleModel(mid);
235 this.islinear = false;
237 max.addChangeListener(this);
239 linearPosition = pos;
241 //linear1 = (mid-min)/pos;
243 if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
244 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
245 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
248 updateExponentialParameters();
252 private void updateExponentialParameters() {
253 double pos = this.linearPosition;
254 double minValue = this.min.getValue();
255 double midValue = this.mid.getValue();
256 double maxValue = this.max.getValue();
258 * quad2..0 are calculated such that
259 * f(pos) = mid - continuity
260 * f(1) = max - end point
261 * f'(pos) = linear1 - continuity of derivative
263 double delta = (midValue - minValue) / pos;
264 quad2 = (maxValue - midValue - delta + delta * pos) / pow2(pos - 1);
265 quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
266 quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
269 private double pow2(double x) {
274 public int getValue() {
275 double value = DoubleModel.this.getValue();
276 if (value <= min.getValue())
278 if (value >= max.getValue())
282 if (value <= mid.getValue()) {
285 //linear1 = (mid-min)/pos;
287 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
289 // Use quadratic scale
290 // Further solution of the quadratic equation
291 // a*x^2 + b*x + c-value == 0
292 x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
294 return (int) (x * MAX);
299 public void setValue(int newValue) {
302 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
303 " value=" + newValue + ", currently firing events");
307 double x = (double) newValue / MAX;
310 if (x <= linearPosition) {
313 //linear1 = (mid-min)/pos;
315 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
317 // Use quadratic scale
318 scaledValue = quad2 * x * x + quad1 * x + quad0;
321 double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
322 log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
323 " scaledValue=" + scaledValue + " converted=" + converted);
324 DoubleModel.this.setValue(converted);
328 // Static get-methods
329 private boolean isAdjusting;
332 public int getExtent() {
337 public int getMaximum() {
342 public int getMinimum() {
347 public boolean getValueIsAdjusting() {
353 public void setExtent(int newExtent) {
357 public void setMaximum(int newMaximum) {
361 public void setMinimum(int newMinimum) {
365 public void setValueIsAdjusting(boolean b) {
370 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
371 setValueIsAdjusting(adjusting);
375 // Pass change listeners to the underlying model
377 public void addChangeListener(ChangeListener l) {
378 DoubleModel.this.addChangeListener(l);
382 public void removeChangeListener(ChangeListener l) {
383 DoubleModel.this.removeChangeListener(l);
387 public void invalidate() {
388 DoubleModel.this.invalidate();
392 public void stateChanged(EventObject e) {
393 // Min or max range has changed.
395 double midValue = (max.getValue() - min.getValue()) /3.0;
396 mid.setValue(midValue);
397 updateExponentialParameters();
399 // Fire if not already firing
406 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
407 return new ValueSliderModel(min, max);
410 public BoundedRangeModel getSliderModel(double min, double max) {
411 return new ValueSliderModel(min, max);
414 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
415 return new ValueSliderModel(min, mid, max);
418 public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
419 return new ValueSliderModel(min, mid, max);
422 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
423 return new ValueSliderModel(min, pos, mid, max);
430 //////////// Action model ////////////
432 private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
433 private boolean oldValue = false;
435 public AutomaticActionModel() {
436 oldValue = isAutomatic();
437 addChangeListener(this);
442 public boolean isEnabled() {
443 return isAutomaticAvailable();
447 public Object getValue(String key) {
448 if (key.equals(Action.SELECTED_KEY)) {
449 oldValue = isAutomatic();
452 return super.getValue(key);
456 public void putValue(String key, Object value) {
458 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
459 " key=" + key + " value=" + value + ", currently firing events");
462 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
463 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
464 " key=" + key + " value=" + value);
465 oldValue = (Boolean) value;
466 setAutomatic((Boolean) value);
468 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
469 " key=" + key + " value=" + value);
470 super.putValue(key, value);
474 // Implement a wrapper to the ChangeListeners
475 ArrayList<PropertyChangeListener> propertyChangeListeners =
476 new ArrayList<PropertyChangeListener>();
479 public void addPropertyChangeListener(PropertyChangeListener listener) {
480 propertyChangeListeners.add(listener);
481 DoubleModel.this.addChangeListener(this);
485 public void removePropertyChangeListener(PropertyChangeListener listener) {
486 propertyChangeListeners.remove(listener);
487 if (propertyChangeListeners.isEmpty())
488 DoubleModel.this.removeChangeListener(this);
491 // If the value has changed, generate an event to the listeners
493 public void stateChanged(EventObject e) {
494 boolean newValue = isAutomatic();
495 if (oldValue == newValue)
497 PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
500 Object[] l = propertyChangeListeners.toArray();
501 for (int i = 0; i < l.length; i++) {
502 ((PropertyChangeListener) l[i]).propertyChange(event);
507 public void actionPerformed(ActionEvent e) {
508 // Setting performed in putValue
512 public void invalidate() {
513 DoubleModel.this.invalidate();
518 * Returns a new Action corresponding to the changes of the automatic setting
519 * property of the value model. This may be used directly with e.g. check buttons.
521 * @return A compatibility layer for an Action.
523 public Action getAutomaticAction() {
524 return new AutomaticActionModel();
531 //////////// Main model /////////////
534 * The main model handles all values in SI units, i.e. no conversion is made within the model.
537 private final ChangeSource source;
538 private final String valueName;
539 private final double multiplier;
541 private final Method getMethod;
542 private final Method setMethod;
544 private final Method getAutoMethod;
545 private final Method setAutoMethod;
547 private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
549 private final UnitGroup units;
550 private Unit currentUnit;
552 private final double minValue;
553 private double maxValue;
555 private String toString = null;
558 private int firing = 0; // >0 when model itself is sending events
561 // Used to differentiate changes in valueName and other changes in the component:
562 private double lastValue = 0;
563 private boolean lastAutomatic = false;
565 private Invalidator invalidator = new Invalidator(this);
569 * Generate a DoubleModel that contains an internal double value.
571 * @param value the initial value.
573 public DoubleModel(double value) {
574 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
578 * Generate a DoubleModel that contains an internal double value.
580 * @param value the initial value.
581 * @param unit the unit for the value.
583 public DoubleModel(double value, UnitGroup unit) {
584 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
588 * Generate a DoubleModel that contains an internal double value.
590 * @param value the initial value.
591 * @param unit the unit for the value.
592 * @param min minimum value.
594 public DoubleModel(double value, UnitGroup unit, double min) {
595 this(value, unit, min, Double.POSITIVE_INFINITY);
599 * Generate a DoubleModel that contains an internal double value.
601 * @param value the initial value.
602 * @param unit the unit for the value.
603 * @param min minimum value.
604 * @param max maximum value.
606 public DoubleModel(double value, UnitGroup unit, double min, double max) {
607 this.lastValue = value;
612 valueName = "Constant value";
615 getMethod = setMethod = null;
616 getAutoMethod = setAutoMethod = null;
618 currentUnit = units.getDefaultUnit();
623 * Generates a new DoubleModel that changes the values of the specified component.
624 * The double value is read and written using the methods "get"/"set" + valueName.
626 * @param source Component whose parameter to use.
627 * @param valueName Name of methods used to get/set the parameter.
628 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
629 * @param min Minimum value allowed (in SI units)
630 * @param max Maximum value allowed (in SI units)
632 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
633 double min, double max) {
634 this.source = source;
635 this.valueName = valueName;
636 this.multiplier = multiplier;
639 currentUnit = units.getDefaultUnit();
645 getMethod = source.getClass().getMethod("get" + valueName);
646 } catch (NoSuchMethodException e) {
647 throw new IllegalArgumentException("get method for value '" + valueName +
648 "' not present in class " + source.getClass().getCanonicalName());
653 s = source.getClass().getMethod("set" + valueName, double.class);
654 } catch (NoSuchMethodException e1) {
658 // Automatic selection methods
660 Method set = null, get = null;
663 get = source.getClass().getMethod("is" + valueName + "Automatic");
664 set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
665 } catch (NoSuchMethodException e) {
668 if (set != null && get != null) {
672 getAutoMethod = null;
673 setAutoMethod = null;
678 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
680 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
683 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
684 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
687 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
688 double min, double max) {
689 this(source, valueName, 1.0, unit, min, max);
692 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
693 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
696 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
697 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
700 public DoubleModel(ChangeSource source, String valueName) {
701 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
702 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
705 public DoubleModel(ChangeSource source, String valueName, double min) {
706 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
709 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
710 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
716 * Returns the value of the variable (in SI units).
718 public double getValue() {
719 if (getMethod == null) // Constant value
723 return (Double) getMethod.invoke(source) * multiplier;
724 } catch (IllegalArgumentException e) {
725 throw new BugException("Unable to invoke getMethod of " + this, e);
726 } catch (IllegalAccessException e) {
727 throw new BugException("Unable to invoke getMethod of " + this, e);
728 } catch (InvocationTargetException e) {
729 throw Reflection.handleWrappedException(e);
734 * Sets the value of the variable.
735 * @param v New value for parameter in SI units.
737 public void setValue(double v) {
740 log.debug("Setting value " + v + " for " + this);
741 if (setMethod == null) {
742 if (getMethod != null) {
743 throw new BugException("setMethod not available for variable '" + valueName +
744 "' in class " + source.getClass().getCanonicalName());
752 setMethod.invoke(source, v / multiplier);
753 } catch (IllegalArgumentException e) {
754 throw new BugException("Unable to invoke setMethod of " + this, e);
755 } catch (IllegalAccessException e) {
756 throw new BugException("Unable to invoke setMethod of " + this, e);
757 } catch (InvocationTargetException e) {
758 throw Reflection.handleWrappedException(e);
763 * Returns whether setting the value automatically is available.
765 public boolean isAutomaticAvailable() {
766 return (getAutoMethod != null) && (setAutoMethod != null);
770 * Returns whether the value is currently being set automatically.
771 * Returns false if automatic setting is not available at all.
773 public boolean isAutomatic() {
774 if (getAutoMethod == null)
778 return (Boolean) getAutoMethod.invoke(source);
779 } catch (IllegalArgumentException e) {
780 throw new BugException("Method call failed", e);
781 } catch (IllegalAccessException e) {
782 throw new BugException("Method call failed", e);
783 } catch (InvocationTargetException e) {
784 throw Reflection.handleWrappedException(e);
789 * Sets whether the value should be set automatically. Simply fires a
790 * state change event if automatic setting is not available.
792 public void setAutomatic(boolean auto) {
795 if (setAutoMethod == null) {
796 log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
797 fireStateChanged(); // in case something is out-of-sync
801 log.debug("Setting automatic to " + auto + " for " + this);
802 lastAutomatic = auto;
804 setAutoMethod.invoke(source, auto);
805 } catch (IllegalArgumentException e) {
806 throw new BugException(e);
807 } catch (IllegalAccessException e) {
808 throw new BugException(e);
809 } catch (InvocationTargetException e) {
810 throw Reflection.handleWrappedException(e);
816 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
817 * @return The most recently set unit.
819 public Unit getCurrentUnit() {
824 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
825 * @param u The unit to set active.
827 public void setCurrentUnit(Unit u) {
829 if (currentUnit == u)
831 log.debug("Setting unit for " + this + " to '" + u + "'");
838 * Returns the UnitGroup associated with the parameter value.
840 * @return The UnitGroup given to the constructor.
842 public UnitGroup getUnitGroup() {
849 * Add a listener to the model. Adds the model as a listener to the value source if this
850 * is the first listener.
851 * @param l Listener to add.
854 public void addChangeListener(EventListener l) {
857 if (listeners.isEmpty()) {
858 if (source != null) {
859 source.addChangeListener(this);
860 lastValue = getValue();
861 lastAutomatic = isAutomatic();
866 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
870 * Remove a listener from the model. Removes the model from being a listener to the Component
871 * if this was the last listener of the model.
872 * @param l Listener to remove.
875 public void removeChangeListener(EventListener l) {
879 if (listeners.isEmpty() && source != null) {
880 source.removeChangeListener(this);
882 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
887 * Invalidates this model by removing all listeners and removing this from
888 * listening to the source. After invalidation no listeners can be added to this
889 * model and the value cannot be set.
892 public void invalidate() {
893 log.verbose("Invalidating " + this);
894 invalidator.invalidate();
896 if (!listeners.isEmpty()) {
897 log.warn("Invalidating " + this + " while still having listeners " + listeners);
900 if (source != null) {
901 source.removeChangeListener(this);
903 MemoryManagement.collectable(this);
907 private void checkState(boolean error) {
908 invalidator.check(error);
913 protected void finalize() throws Throwable {
915 if (!listeners.isEmpty()) {
916 log.warn(this + " being garbage-collected while having listeners " + listeners);
922 * Fire a ChangeEvent to all listeners.
924 protected void fireStateChanged() {
927 EventObject event = new EventObject(this);
928 ChangeEvent cevent = new ChangeEvent(this);
930 // Copy the list before iterating to prevent concurrent modification exceptions.
931 EventListener[] ls = listeners.toArray(new EventListener[0]);
932 for (EventListener l : ls) {
933 if ( l instanceof StateChangeListener ) {
934 ((StateChangeListener)l).stateChanged(event);
935 } else if ( l instanceof ChangeListener ) {
936 ((ChangeListener)l).stateChanged(cevent);
943 * Called when the component changes. Checks whether the modeled value has changed, and if
944 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
947 public void stateChanged(EventObject e) {
950 double v = getValue();
951 boolean b = isAutomatic();
952 if (lastValue == v && lastAutomatic == b)
961 * Explain the DoubleModel as a String.
964 public String toString() {
965 if (toString == null) {
966 if (source == null) {
967 toString = "DoubleModel[constant=" + lastValue + "]";
969 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";