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.Action;
14 import javax.swing.BoundedRangeModel;
15 import javax.swing.SpinnerModel;
16 import javax.swing.SpinnerNumberModel;
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.Invalidatable;
27 import net.sf.openrocket.util.Invalidator;
28 import net.sf.openrocket.util.MathUtil;
29 import net.sf.openrocket.util.MemoryManagement;
30 import net.sf.openrocket.util.Reflection;
31 import net.sf.openrocket.util.StateChangeListener;
35 * A model connector that can read and modify any value of any ChangeSource that
36 * has the appropriate get/set methods defined.
38 * The variable is defined in the constructor by providing the variable name as a string
39 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
40 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
42 * Sub-models suitable for JSpinners and other components are available from the appropriate
45 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
48 public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
49 private static final LogHelper log = Application.getLogger();
52 public static final DoubleModel ZERO = new DoubleModel(0);
54 //////////// JSpinner Model ////////////
57 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
58 * to be compatible with the NumberEditor, but only has the necessary methods defined.
60 private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable {
63 public Object getValue() {
64 return currentUnit.toUnit(DoubleModel.this.getValue());
68 public void setValue(Object value) {
70 // Ignore, if called when model is sending events
71 log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
72 " value=" + value + ", currently firing events");
75 Number num = (Number) value;
76 double newValue = num.doubleValue();
77 double converted = currentUnit.fromUnit(newValue);
79 log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
80 " converted=" + converted);
81 DoubleModel.this.setValue(converted);
86 public Object getNextValue() {
87 double d = currentUnit.toUnit(DoubleModel.this.getValue());
88 double max = currentUnit.toUnit(maxValue);
89 if (MathUtil.equals(d, max))
91 d = currentUnit.getNextValue(d);
98 public Object getPreviousValue() {
99 double d = currentUnit.toUnit(DoubleModel.this.getValue());
100 double min = currentUnit.toUnit(minValue);
101 if (MathUtil.equals(d, min))
103 d = currentUnit.getPreviousValue(d);
111 public Comparable<Double> getMinimum() {
112 return currentUnit.toUnit(minValue);
116 public Comparable<Double> getMaximum() {
117 return currentUnit.toUnit(maxValue);
122 public void addChangeListener(ChangeListener l) {
123 DoubleModel.this.addChangeListener(l);
127 public void removeChangeListener(ChangeListener l) {
128 DoubleModel.this.removeChangeListener(l);
132 public void invalidate() {
133 DoubleModel.this.invalidate();
138 * Returns a new SpinnerModel with the same base as the DoubleModel.
139 * The values given to the JSpinner are in the currently selected units.
141 * @return A compatibility layer for a SpinnerModel.
143 public SpinnerModel getSpinnerModel() {
144 return new ValueSpinnerModel();
151 //////////// JSlider model ////////////
153 private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
154 private static final int MAX = 1000;
157 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
158 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
161 // Linear in range x <= linearPosition
162 private final double linearPosition;
164 // May be changing DoubleModels when using linear model
165 private final DoubleModel min, mid, max;
167 // Linear multiplier and constant
168 //private final double linear1;
169 //private final double linear0;
171 // Non-linear multiplier, exponent and constant
172 private final double quad2, quad1, quad0;
176 public ValueSliderModel(DoubleModel min, DoubleModel max) {
177 linearPosition = 1.0;
180 this.mid = max; // Never use exponential scale
183 min.addChangeListener(this);
184 max.addChangeListener(this);
186 quad2 = quad1 = quad0 = 0; // Not used
192 * Generate a linear model from min to max.
194 public ValueSliderModel(double min, double max) {
195 linearPosition = 1.0;
197 this.min = new DoubleModel(min);
198 this.mid = new DoubleModel(max); // Never use exponential scale
199 this.max = new DoubleModel(max);
201 quad2 = quad1 = quad0 = 0; // Not used
204 public ValueSliderModel(double min, double mid, double max) {
205 this(min, 0.5, mid, max);
209 * v(x) = mul * x^exp + add
211 * v(pos) = mul * pos^exp + add = mid
212 * v(1) = mul + add = max
213 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
215 public ValueSliderModel(double min, double pos, double mid, double max) {
216 this.min = new DoubleModel(min);
217 this.mid = new DoubleModel(mid);
218 this.max = new DoubleModel(max);
221 linearPosition = pos;
223 //linear1 = (mid-min)/pos;
225 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
226 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
227 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
231 * quad2..0 are calculated such that
232 * f(pos) = mid - continuity
233 * f(1) = max - end point
234 * f'(pos) = linear1 - continuity of derivative
237 double delta = (mid - min) / pos;
238 quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1);
239 quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1);
240 quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1);
244 private double pow2(double x) {
249 public int getValue() {
250 double value = DoubleModel.this.getValue();
251 if (value <= min.getValue())
253 if (value >= max.getValue())
257 if (value <= mid.getValue()) {
260 //linear1 = (mid-min)/pos;
262 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
264 // Use quadratic scale
265 // Further solution of the quadratic equation
266 // a*x^2 + b*x + c-value == 0
267 x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
269 return (int) (x * MAX);
274 public void setValue(int newValue) {
277 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
278 " value=" + newValue + ", currently firing events");
282 double x = (double) newValue / MAX;
285 if (x <= linearPosition) {
288 //linear1 = (mid-min)/pos;
290 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
292 // Use quadratic scale
293 scaledValue = quad2 * x * x + quad1 * x + quad0;
296 double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
297 log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
298 " scaledValue=" + scaledValue + " converted=" + converted);
299 DoubleModel.this.setValue(converted);
303 // Static get-methods
304 private boolean isAdjusting;
307 public int getExtent() {
312 public int getMaximum() {
317 public int getMinimum() {
322 public boolean getValueIsAdjusting() {
328 public void setExtent(int newExtent) {
332 public void setMaximum(int newMaximum) {
336 public void setMinimum(int newMinimum) {
340 public void setValueIsAdjusting(boolean b) {
345 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
346 setValueIsAdjusting(adjusting);
350 // Pass change listeners to the underlying model
352 public void addChangeListener(ChangeListener l) {
353 DoubleModel.this.addChangeListener(l);
357 public void removeChangeListener(ChangeListener l) {
358 DoubleModel.this.removeChangeListener(l);
362 public void invalidate() {
363 DoubleModel.this.invalidate();
367 public void stateChanged(EventObject e) {
368 // Min or max range has changed.
369 // Fire if not already firing
376 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
377 return new ValueSliderModel(min, max);
380 public BoundedRangeModel getSliderModel(double min, double max) {
381 return new ValueSliderModel(min, max);
384 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
385 return new ValueSliderModel(min, mid, max);
388 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
389 return new ValueSliderModel(min, pos, mid, max);
396 //////////// Action model ////////////
398 private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
399 private boolean oldValue = false;
401 public AutomaticActionModel() {
402 oldValue = isAutomatic();
403 addChangeListener(this);
408 public boolean isEnabled() {
409 return isAutomaticAvailable();
413 public Object getValue(String key) {
414 if (key.equals(Action.SELECTED_KEY)) {
415 oldValue = isAutomatic();
418 return super.getValue(key);
422 public void putValue(String key, Object value) {
424 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
425 " key=" + key + " value=" + value + ", currently firing events");
428 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
429 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
430 " key=" + key + " value=" + value);
431 oldValue = (Boolean) value;
432 setAutomatic((Boolean) value);
434 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
435 " key=" + key + " value=" + value);
436 super.putValue(key, value);
440 // Implement a wrapper to the ChangeListeners
441 ArrayList<PropertyChangeListener> propertyChangeListeners =
442 new ArrayList<PropertyChangeListener>();
445 public void addPropertyChangeListener(PropertyChangeListener listener) {
446 propertyChangeListeners.add(listener);
447 DoubleModel.this.addChangeListener(this);
451 public void removePropertyChangeListener(PropertyChangeListener listener) {
452 propertyChangeListeners.remove(listener);
453 if (propertyChangeListeners.isEmpty())
454 DoubleModel.this.removeChangeListener(this);
457 // If the value has changed, generate an event to the listeners
459 public void stateChanged(EventObject e) {
460 boolean newValue = isAutomatic();
461 if (oldValue == newValue)
463 PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
466 Object[] l = propertyChangeListeners.toArray();
467 for (int i = 0; i < l.length; i++) {
468 ((PropertyChangeListener) l[i]).propertyChange(event);
473 public void actionPerformed(ActionEvent e) {
474 // Setting performed in putValue
478 public void invalidate() {
479 DoubleModel.this.invalidate();
484 * Returns a new Action corresponding to the changes of the automatic setting
485 * property of the value model. This may be used directly with e.g. check buttons.
487 * @return A compatibility layer for an Action.
489 public Action getAutomaticAction() {
490 return new AutomaticActionModel();
497 //////////// Main model /////////////
500 * The main model handles all values in SI units, i.e. no conversion is made within the model.
503 private final ChangeSource source;
504 private final String valueName;
505 private final double multiplier;
507 private final Method getMethod;
508 private final Method setMethod;
510 private final Method getAutoMethod;
511 private final Method setAutoMethod;
513 private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
515 private final UnitGroup units;
516 private Unit currentUnit;
518 private final double minValue;
519 private final double maxValue;
521 private String toString = null;
524 private int firing = 0; // >0 when model itself is sending events
527 // Used to differentiate changes in valueName and other changes in the component:
528 private double lastValue = 0;
529 private boolean lastAutomatic = false;
531 private Invalidator invalidator = new Invalidator(this);
535 * Generate a DoubleModel that contains an internal double value.
537 * @param value the initial value.
539 public DoubleModel(double value) {
540 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
544 * Generate a DoubleModel that contains an internal double value.
546 * @param value the initial value.
547 * @param unit the unit for the value.
549 public DoubleModel(double value, UnitGroup unit) {
550 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
554 * Generate a DoubleModel that contains an internal double value.
556 * @param value the initial value.
557 * @param unit the unit for the value.
558 * @param min minimum value.
560 public DoubleModel(double value, UnitGroup unit, double min) {
561 this(value, unit, min, Double.POSITIVE_INFINITY);
565 * Generate a DoubleModel that contains an internal double value.
567 * @param value the initial value.
568 * @param unit the unit for the value.
569 * @param min minimum value.
570 * @param max maximum value.
572 public DoubleModel(double value, UnitGroup unit, double min, double max) {
573 this.lastValue = value;
578 valueName = "Constant value";
581 getMethod = setMethod = null;
582 getAutoMethod = setAutoMethod = null;
584 currentUnit = units.getDefaultUnit();
589 * Generates a new DoubleModel that changes the values of the specified component.
590 * The double value is read and written using the methods "get"/"set" + valueName.
592 * @param source Component whose parameter to use.
593 * @param valueName Name of methods used to get/set the parameter.
594 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
595 * @param min Minimum value allowed (in SI units)
596 * @param max Maximum value allowed (in SI units)
598 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
599 double min, double max) {
600 this.source = source;
601 this.valueName = valueName;
602 this.multiplier = multiplier;
605 currentUnit = units.getDefaultUnit();
611 getMethod = source.getClass().getMethod("get" + valueName);
612 } catch (NoSuchMethodException e) {
613 throw new IllegalArgumentException("get method for value '" + valueName +
614 "' not present in class " + source.getClass().getCanonicalName());
619 s = source.getClass().getMethod("set" + valueName, double.class);
620 } catch (NoSuchMethodException e1) {
624 // Automatic selection methods
626 Method set = null, get = null;
629 get = source.getClass().getMethod("is" + valueName + "Automatic");
630 set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
631 } catch (NoSuchMethodException e) {
634 if (set != null && get != null) {
638 getAutoMethod = null;
639 setAutoMethod = null;
644 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
646 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
649 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
650 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
653 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
654 double min, double max) {
655 this(source, valueName, 1.0, unit, min, max);
658 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
659 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
662 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
663 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
666 public DoubleModel(ChangeSource source, String valueName) {
667 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
668 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
671 public DoubleModel(ChangeSource source, String valueName, double min) {
672 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
675 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
676 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
682 * Returns the value of the variable (in SI units).
684 public double getValue() {
685 if (getMethod == null) // Constant value
689 return (Double) getMethod.invoke(source) * multiplier;
690 } catch (IllegalArgumentException e) {
691 throw new BugException("Unable to invoke getMethod of " + this, e);
692 } catch (IllegalAccessException e) {
693 throw new BugException("Unable to invoke getMethod of " + this, e);
694 } catch (InvocationTargetException e) {
695 throw Reflection.handleWrappedException(e);
700 * Sets the value of the variable.
701 * @param v New value for parameter in SI units.
703 public void setValue(double v) {
706 log.debug("Setting value " + v + " for " + this);
707 if (setMethod == null) {
708 if (getMethod != null) {
709 throw new BugException("setMethod not available for variable '" + valueName +
710 "' in class " + source.getClass().getCanonicalName());
718 setMethod.invoke(source, v / multiplier);
719 } catch (IllegalArgumentException e) {
720 throw new BugException("Unable to invoke setMethod of " + this, e);
721 } catch (IllegalAccessException e) {
722 throw new BugException("Unable to invoke setMethod of " + this, e);
723 } catch (InvocationTargetException e) {
724 throw Reflection.handleWrappedException(e);
730 * Returns whether setting the value automatically is available.
732 public boolean isAutomaticAvailable() {
733 return (getAutoMethod != null) && (setAutoMethod != null);
737 * Returns whether the value is currently being set automatically.
738 * Returns false if automatic setting is not available at all.
740 public boolean isAutomatic() {
741 if (getAutoMethod == null)
745 return (Boolean) getAutoMethod.invoke(source);
746 } catch (IllegalArgumentException e) {
747 throw new BugException("Method call failed", e);
748 } catch (IllegalAccessException e) {
749 throw new BugException("Method call failed", e);
750 } catch (InvocationTargetException e) {
751 throw Reflection.handleWrappedException(e);
756 * Sets whether the value should be set automatically. Simply fires a
757 * state change event if automatic setting is not available.
759 public void setAutomatic(boolean auto) {
762 if (setAutoMethod == null) {
763 log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
764 fireStateChanged(); // in case something is out-of-sync
768 log.debug("Setting automatic to " + auto + " for " + this);
769 lastAutomatic = auto;
771 setAutoMethod.invoke(source, auto);
772 } catch (IllegalArgumentException e) {
773 throw new BugException(e);
774 } catch (IllegalAccessException e) {
775 throw new BugException(e);
776 } catch (InvocationTargetException e) {
777 throw Reflection.handleWrappedException(e);
783 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
784 * @return The most recently set unit.
786 public Unit getCurrentUnit() {
791 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
792 * @param u The unit to set active.
794 public void setCurrentUnit(Unit u) {
796 if (currentUnit == u)
798 log.debug("Setting unit for " + this + " to '" + u + "'");
805 * Returns the UnitGroup associated with the parameter value.
807 * @return The UnitGroup given to the constructor.
809 public UnitGroup getUnitGroup() {
816 * Add a listener to the model. Adds the model as a listener to the value source if this
817 * is the first listener.
818 * @param l Listener to add.
821 public void addChangeListener(EventListener l) {
824 if (listeners.isEmpty()) {
825 if (source != null) {
826 source.addChangeListener(this);
827 lastValue = getValue();
828 lastAutomatic = isAutomatic();
833 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
837 * Remove a listener from the model. Removes the model from being a listener to the Component
838 * if this was the last listener of the model.
839 * @param l Listener to remove.
842 public void removeChangeListener(EventListener l) {
846 if (listeners.isEmpty() && source != null) {
847 source.removeChangeListener(this);
849 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
854 * Invalidates this model by removing all listeners and removing this from
855 * listening to the source. After invalidation no listeners can be added to this
856 * model and the value cannot be set.
859 public void invalidate() {
860 log.verbose("Invalidating " + this);
861 invalidator.invalidate();
863 if (!listeners.isEmpty()) {
864 log.warn("Invalidating " + this + " while still having listeners " + listeners);
867 if (source != null) {
868 source.removeChangeListener(this);
870 MemoryManagement.collectable(this);
874 private void checkState(boolean error) {
875 invalidator.check(error);
880 protected void finalize() throws Throwable {
882 if (!listeners.isEmpty()) {
883 log.warn(this + " being garbage-collected while having listeners " + listeners);
889 * Fire a ChangeEvent to all listeners.
891 protected void fireStateChanged() {
894 EventObject event = new EventObject(this);
895 ChangeEvent cevent = new ChangeEvent(this);
897 // Copy the list before iterating to prevent concurrent modification exceptions.
898 EventListener[] ls = listeners.toArray(new EventListener[0]);
899 for (EventListener l : ls) {
900 if ( l instanceof StateChangeListener ) {
901 ((StateChangeListener)l).stateChanged(event);
902 } else if ( l instanceof ChangeListener ) {
903 ((ChangeListener)l).stateChanged(cevent);
910 * Called when the component changes. Checks whether the modeled value has changed, and if
911 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
914 public void stateChanged(EventObject e) {
917 double v = getValue();
918 boolean b = isAutomatic();
919 if (lastValue == v && lastAutomatic == b)
928 * Explain the DoubleModel as a String.
931 public String toString() {
932 if (toString == null) {
933 if (source == null) {
934 toString = "DoubleModel[constant=" + lastValue + "]";
936 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";