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.unit.Unit;
19 import net.sf.openrocket.unit.UnitGroup;
20 import net.sf.openrocket.util.ChangeSource;
21 import net.sf.openrocket.util.MathUtil;
25 * A model connector that can read and modify any value of any ChangeSource that
26 * has the appropriate get/set methods defined.
28 * The variable is defined in the constructor by providing the variable name as a string
29 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
30 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
32 * Sub-models suitable for JSpinners and other components are available from the appropriate
35 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
38 public class DoubleModel implements ChangeListener, ChangeSource {
39 private static final boolean DEBUG_LISTENERS = false;
41 public static final DoubleModel ZERO = new DoubleModel(0);
43 //////////// JSpinner Model ////////////
46 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
47 * to be compatible with the NumberEditor, but only has the necessary methods defined.
49 private class ValueSpinnerModel extends SpinnerNumberModel {
52 public Object getValue() {
53 return currentUnit.toUnit(DoubleModel.this.getValue());
54 // return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
58 public void setValue(Object value) {
60 System.out.println("setValue("+value+") called, valueName="+valueName+
63 if (firing > 0) // Ignore, if called when model is sending events
65 Number num = (Number)value;
66 double newValue = num.doubleValue();
67 DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
71 // double newValue = Double.parseDouble((String)value);
72 // DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
73 // } catch (NumberFormatException e) {
74 // DoubleModel.this.fireStateChanged();
79 public Object getNextValue() {
80 double d = currentUnit.toUnit(DoubleModel.this.getValue());
81 double max = currentUnit.toUnit(maxValue);
82 if (MathUtil.equals(d,max))
84 d = currentUnit.getNextValue(d);
88 // return makeString(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);
101 // return makeString(d);
106 public Comparable<Double> getMinimum() {
107 return currentUnit.toUnit(minValue);
111 public Comparable<Double> getMaximum() {
112 return currentUnit.toUnit(maxValue);
117 public void addChangeListener(ChangeListener l) {
118 DoubleModel.this.addChangeListener(l);
122 public void removeChangeListener(ChangeListener l) {
123 DoubleModel.this.removeChangeListener(l);
128 * Returns a new SpinnerModel with the same base as the DoubleModel.
129 * The values given to the JSpinner are in the currently selected units.
131 * @return A compatibility layer for a SpinnerModel.
133 public SpinnerModel getSpinnerModel() {
134 return new ValueSpinnerModel();
141 //////////// JSlider model ////////////
143 private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
144 private static final int MAX = 1000;
147 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
148 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
151 // Linear in range x <= linearPosition
152 private final double linearPosition;
154 // May be changing DoubleModels when using linear model
155 private final DoubleModel min, mid, max;
157 // Linear multiplier and constant
158 //private final double linear1;
159 //private final double linear0;
161 // Non-linear multiplier, exponent and constant
162 private final double quad2,quad1,quad0;
166 public ValueSliderModel(DoubleModel min, DoubleModel max) {
167 linearPosition = 1.0;
170 this.mid = max; // Never use exponential scale
173 min.addChangeListener(this);
174 max.addChangeListener(this);
176 quad2 = quad1 = quad0 = 0; // Not used
182 * Generate a linear model from min to max.
184 public ValueSliderModel(double min, double max) {
185 linearPosition = 1.0;
187 this.min = new DoubleModel(min);
188 this.mid = new DoubleModel(max); // Never use exponential scale
189 this.max = new DoubleModel(max);
191 quad2 = quad1 = quad0 = 0; // Not used
194 public ValueSliderModel(double min, double mid, double max) {
195 this(min,0.5,mid,max);
199 * v(x) = mul * x^exp + add
201 * v(pos) = mul * pos^exp + add = mid
202 * v(1) = mul + add = max
203 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
205 public ValueSliderModel(double min, double pos, double mid, double max) {
206 this.min = new DoubleModel(min);
207 this.mid = new DoubleModel(mid);
208 this.max = new DoubleModel(max);
211 linearPosition = pos;
213 //linear1 = (mid-min)/pos;
215 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
216 throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
217 "min="+min+" mid="+mid+" max="+max+" pos="+pos);
221 * quad2..0 are calculated such that
222 * f(pos) = mid - continuity
223 * f(1) = max - end point
224 * f'(pos) = linear1 - continuity of derivative
227 double delta = (mid-min)/pos;
228 quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
229 quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
230 quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
234 private double pow2(double x) {
238 public int getValue() {
239 double value = DoubleModel.this.getValue();
240 if (value <= min.getValue())
242 if (value >= max.getValue())
246 if (value <= mid.getValue()) {
249 //linear1 = (mid-min)/pos;
251 x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
253 // Use quadratic scale
254 // Further solution of the quadratic equation
255 // a*x^2 + b*x + c-value == 0
256 x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
262 public void setValue(int newValue) {
263 if (firing > 0) // Ignore loops
266 double x = (double)newValue/MAX;
269 if (x <= linearPosition) {
272 //linear1 = (mid-min)/pos;
274 value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
276 // Use quadratic scale
277 value = quad2*x*x + quad1*x + quad0;
280 DoubleModel.this.setValue(currentUnit.fromUnit(
281 currentUnit.round(currentUnit.toUnit(value))));
285 // Static get-methods
286 private boolean isAdjusting;
287 public int getExtent() { return 0; }
288 public int getMaximum() { return MAX; }
289 public int getMinimum() { return 0; }
290 public boolean getValueIsAdjusting() { return isAdjusting; }
293 public void setExtent(int newExtent) { }
294 public void setMaximum(int newMaximum) { }
295 public void setMinimum(int newMinimum) { }
296 public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
298 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
299 setValueIsAdjusting(adjusting);
303 // Pass change listeners to the underlying model
304 public void addChangeListener(ChangeListener l) {
305 DoubleModel.this.addChangeListener(l);
308 public void removeChangeListener(ChangeListener l) {
309 DoubleModel.this.removeChangeListener(l);
314 public void stateChanged(ChangeEvent e) {
315 // Min or max range has changed.
316 // Fire if not already firing
323 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
324 return new ValueSliderModel(min,max);
327 public BoundedRangeModel getSliderModel(double min, double max) {
328 return new ValueSliderModel(min,max);
331 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
332 return new ValueSliderModel(min,mid,max);
335 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
336 return new ValueSliderModel(min,pos,mid,max);
343 //////////// Action model ////////////
345 private class AutomaticActionModel extends AbstractAction implements ChangeListener {
346 private boolean oldValue = false;
348 public AutomaticActionModel() {
349 oldValue = isAutomatic();
350 addChangeListener(this);
355 public boolean isEnabled() {
356 // TODO: LOW: does not reflect if component is currently able to support automatic setting
357 return isAutomaticAvailable();
361 public Object getValue(String key) {
362 if (key.equals(Action.SELECTED_KEY)) {
363 oldValue = isAutomatic();
366 return super.getValue(key);
370 public void putValue(String key, Object value) {
373 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
374 oldValue = (Boolean)value;
375 setAutomatic((Boolean)value);
377 super.putValue(key, value);
381 // Implement a wrapper to the ChangeListeners
382 ArrayList<PropertyChangeListener> propertyChangeListeners =
383 new ArrayList<PropertyChangeListener>();
385 public void addPropertyChangeListener(PropertyChangeListener listener) {
386 propertyChangeListeners.add(listener);
387 DoubleModel.this.addChangeListener(this);
390 public void removePropertyChangeListener(PropertyChangeListener listener) {
391 propertyChangeListeners.remove(listener);
392 if (propertyChangeListeners.isEmpty())
393 DoubleModel.this.removeChangeListener(this);
395 // If the value has changed, generate an event to the listeners
396 public void stateChanged(ChangeEvent e) {
397 boolean newValue = isAutomatic();
398 if (oldValue == newValue)
400 PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
403 Object[] l = propertyChangeListeners.toArray();
404 for (int i=0; i<l.length; i++) {
405 ((PropertyChangeListener)l[i]).propertyChange(event);
409 public void actionPerformed(ActionEvent e) {
410 // Setting performed in putValue
416 * Returns a new Action corresponding to the changes of the automatic setting
417 * property of the value model. This may be used directly with e.g. check buttons.
419 * @return A compatibility layer for an Action.
421 public Action getAutomaticAction() {
422 return new AutomaticActionModel();
430 //////////// Main model /////////////
433 * The main model handles all values in SI units, i.e. no conversion is made within the model.
436 private final ChangeSource source;
437 private final String valueName;
438 private final double multiplier;
440 private final Method getMethod;
441 private final Method setMethod;
443 private final Method getAutoMethod;
444 private final Method setAutoMethod;
446 private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
448 private final UnitGroup units;
449 private Unit currentUnit;
451 private final double minValue;
452 private final double maxValue;
455 private int firing = 0; // >0 when model itself is sending events
458 // Used to differentiate changes in valueName and other changes in the component:
459 private double lastValue = 0;
460 private boolean lastAutomatic = false;
463 public DoubleModel(double value) {
464 this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
467 public DoubleModel(double value, UnitGroup unit) {
468 this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
471 public DoubleModel(double value, UnitGroup unit, double min) {
472 this(value,unit,min,Double.POSITIVE_INFINITY);
475 public DoubleModel(double value, UnitGroup unit, double min, double max) {
476 this.lastValue = value;
481 valueName = "Constant value";
484 getMethod = setMethod = null;
485 getAutoMethod = setAutoMethod = null;
487 currentUnit = units.getDefaultUnit();
492 * Generates a new DoubleModel that changes the values of the specified component.
493 * The double value is read and written using the methods "get"/"set" + valueName.
495 * @param source Component whose parameter to use.
496 * @param valueName Name of metods used to get/set the parameter.
497 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
498 * @param min Minimum value allowed (in SI units)
499 * @param max Maximum value allowed (in SI units)
501 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
502 double min, double max) {
503 this.source = source;
504 this.valueName = valueName;
505 this.multiplier = multiplier;
508 currentUnit = units.getDefaultUnit();
514 getMethod = source.getClass().getMethod("get" + valueName);
515 } catch (NoSuchMethodException e) {
516 throw new IllegalArgumentException("get method for value '"+valueName+
517 "' not present in class "+source.getClass().getCanonicalName());
522 s = source.getClass().getMethod("set" + valueName,double.class);
523 } catch (NoSuchMethodException e1) { } // Ignore
526 // Automatic selection methods
528 Method set=null,get=null;
531 get = source.getClass().getMethod("is" + valueName + "Automatic");
532 set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
533 } catch (NoSuchMethodException e) { } // ignore
535 if (set!=null && get!=null) {
539 getAutoMethod = null;
540 setAutoMethod = null;
545 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
547 this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
550 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
551 this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
554 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
555 double min, double max) {
556 this(source,valueName,1.0,unit,min,max);
559 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
560 this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
563 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
564 this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
567 public DoubleModel(ChangeSource source, String valueName) {
568 this(source,valueName,1.0,UnitGroup.UNITS_NONE,
569 Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
572 public DoubleModel(ChangeSource source, String valueName, double min) {
573 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
576 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
577 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
583 * Returns the value of the variable (in SI units).
585 public double getValue() {
586 if (getMethod==null) // Constant value
590 return (Double)getMethod.invoke(source)*multiplier;
591 } catch (IllegalArgumentException e) {
592 throw new RuntimeException("BUG: Unable to invoke getMethod of "+this, e);
593 } catch (IllegalAccessException e) {
594 throw new RuntimeException("BUG: Unable to invoke getMethod of "+this, e);
595 } catch (InvocationTargetException e) {
596 throw new RuntimeException("BUG: Unable to invoke getMethod of "+this, e);
601 * Sets the value of the variable.
602 * @param v New value for parameter in SI units.
604 public void setValue(double v) {
605 if (setMethod==null) {
606 if (getMethod != null) {
607 throw new RuntimeException("setMethod not available for variable '"+valueName+
608 "' in class "+source.getClass().getCanonicalName());
616 setMethod.invoke(source, v/multiplier);
618 } catch (IllegalArgumentException e) {
619 throw new RuntimeException("BUG: Unable to invoke setMethod of "+this, e);
620 } catch (IllegalAccessException e) {
621 throw new RuntimeException("BUG: Unable to invoke setMethod of "+this, e);
622 } catch (InvocationTargetException e) {
623 throw new RuntimeException("Setter method of "+this+" threw exception", e);
629 * Returns whether setting the value automatically is available.
631 public boolean isAutomaticAvailable() {
632 return (getAutoMethod != null) && (setAutoMethod != null);
636 * Returns whether the value is currently being set automatically.
637 * Returns false if automatic setting is not available at all.
639 public boolean isAutomatic() {
640 if (getAutoMethod == null)
644 return (Boolean)getAutoMethod.invoke(source);
645 } catch (IllegalArgumentException e) {
647 } catch (IllegalAccessException e) {
649 } catch (InvocationTargetException e) {
652 return false; // Should not occur
656 * Sets whether the value should be set automatically. Simply fires a
657 * state change event if automatic setting is not available.
659 public void setAutomatic(boolean auto) {
660 if (setAutoMethod == null) {
661 fireStateChanged(); // in case something is out-of-sync
666 lastAutomatic = auto;
667 setAutoMethod.invoke(source, auto);
669 } catch (IllegalArgumentException e) {
671 } catch (IllegalAccessException e) {
673 } catch (InvocationTargetException e) {
676 fireStateChanged(); // Should not occur
681 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
682 * @return The most recently set unit.
684 public Unit getCurrentUnit() {
689 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
690 * @param u The unit to set active.
692 public void setCurrentUnit(Unit u) {
693 if (currentUnit == u)
701 * Returns the UnitGroup associated with the parameter value.
703 * @return The UnitGroup given to the constructor.
705 public UnitGroup getUnitGroup() {
712 * Add a listener to the model. Adds the model as a listener to the value source if this
713 * is the first listener.
714 * @param l Listener to add.
716 public void addChangeListener(ChangeListener l) {
717 if (listeners.isEmpty()) {
718 if (source != null) {
719 source.addChangeListener(this);
720 lastValue = getValue();
721 lastAutomatic = isAutomatic();
727 System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
731 * Remove a listener from the model. Removes the model from being a listener to the Component
732 * if this was the last listener of the model.
733 * @param l Listener to remove.
735 public void removeChangeListener(ChangeListener l) {
737 if (listeners.isEmpty() && source != null) {
738 source.removeChangeListener(this);
741 System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
745 * Fire a ChangeEvent to all listeners.
747 protected void fireStateChanged() {
748 Object[] l = listeners.toArray();
749 ChangeEvent event = new ChangeEvent(this);
751 for (int i=0; i<l.length; i++)
752 ((ChangeListener)l[i]).stateChanged(event);
757 * Called when the component changes. Checks whether the modeled value has changed, and if
758 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
760 public void stateChanged(ChangeEvent e) {
761 double v = getValue();
762 boolean b = isAutomatic();
763 if (lastValue == v && lastAutomatic == b)
771 * Explain the DoubleModel as a String.
774 public String toString() {
776 return "DoubleModel[constant="+lastValue+"]";
777 return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";