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 //////////// JSpinner Model ////////////
44 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
45 * to be compatible with the NumberEditor, but only has the necessary methods defined.
47 private class ValueSpinnerModel extends SpinnerNumberModel {
50 public Object getValue() {
51 return currentUnit.toUnit(DoubleModel.this.getValue());
52 // return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
56 public void setValue(Object value) {
58 System.out.println("setValue("+value+") called, valueName="+valueName+
61 if (firing > 0) // Ignore, if called when model is sending events
63 Number num = (Number)value;
64 double newValue = num.doubleValue();
65 DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
69 // double newValue = Double.parseDouble((String)value);
70 // DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
71 // } catch (NumberFormatException e) {
72 // DoubleModel.this.fireStateChanged();
77 public Object getNextValue() {
78 double d = currentUnit.toUnit(DoubleModel.this.getValue());
79 double max = currentUnit.toUnit(maxValue);
80 if (MathUtil.equals(d,max))
82 d = currentUnit.getNextValue(d);
86 // return makeString(d);
90 public Object getPreviousValue() {
91 double d = currentUnit.toUnit(DoubleModel.this.getValue());
92 double min = currentUnit.toUnit(minValue);
93 if (MathUtil.equals(d,min))
95 d = currentUnit.getPreviousValue(d);
99 // return makeString(d);
104 public Comparable<Double> getMinimum() {
105 return currentUnit.toUnit(minValue);
109 public Comparable<Double> getMaximum() {
110 return currentUnit.toUnit(maxValue);
115 public void addChangeListener(ChangeListener l) {
116 DoubleModel.this.addChangeListener(l);
120 public void removeChangeListener(ChangeListener l) {
121 DoubleModel.this.removeChangeListener(l);
126 * Returns a new SpinnerModel with the same base as the DoubleModel.
127 * The values given to the JSpinner are in the currently selected units.
129 * @return A compatibility layer for a SpinnerModel.
131 public SpinnerModel getSpinnerModel() {
132 return new ValueSpinnerModel();
139 //////////// JSlider model ////////////
141 private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
142 private static final int MAX = 1000;
145 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
146 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
149 // Linear in range x <= linearPosition
150 private final double linearPosition;
152 // May be changing DoubleModels when using linear model
153 private final DoubleModel min, mid, max;
155 // Linear multiplier and constant
156 //private final double linear1;
157 //private final double linear0;
159 // Non-linear multiplier, exponent and constant
160 private final double quad2,quad1,quad0;
164 public ValueSliderModel(DoubleModel min, DoubleModel max) {
165 linearPosition = 1.0;
168 this.mid = max; // Never use exponential scale
171 min.addChangeListener(this);
172 max.addChangeListener(this);
174 quad2 = quad1 = quad0 = 0; // Not used
180 * Generate a linear model from min to max.
182 public ValueSliderModel(double min, double max) {
183 linearPosition = 1.0;
185 this.min = new DoubleModel(min);
186 this.mid = new DoubleModel(max); // Never use exponential scale
187 this.max = new DoubleModel(max);
189 quad2 = quad1 = quad0 = 0; // Not used
192 public ValueSliderModel(double min, double mid, double max) {
193 this(min,0.5,mid,max);
197 * v(x) = mul * x^exp + add
199 * v(pos) = mul * pos^exp + add = mid
200 * v(1) = mul + add = max
201 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
203 public ValueSliderModel(double min, double pos, double mid, double max) {
204 this.min = new DoubleModel(min);
205 this.mid = new DoubleModel(mid);
206 this.max = new DoubleModel(max);
209 linearPosition = pos;
211 //linear1 = (mid-min)/pos;
213 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
214 throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
215 "min="+min+" mid="+mid+" max="+max+" pos="+pos);
219 * quad2..0 are calculated such that
220 * f(pos) = mid - continuity
221 * f(1) = max - end point
222 * f'(pos) = linear1 - continuity of derivative
225 double delta = (mid-min)/pos;
226 quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
227 quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
228 quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
232 private double pow2(double x) {
236 public int getValue() {
237 double value = DoubleModel.this.getValue();
238 if (value <= min.getValue())
240 if (value >= max.getValue())
244 if (value <= mid.getValue()) {
247 //linear1 = (mid-min)/pos;
249 x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
251 // Use quadratic scale
252 // Further solution of the quadratic equation
253 // a*x^2 + b*x + c-value == 0
254 x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
260 public void setValue(int newValue) {
261 if (firing > 0) // Ignore loops
264 double x = (double)newValue/MAX;
267 if (x <= linearPosition) {
270 //linear1 = (mid-min)/pos;
272 value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
274 // Use quadratic scale
275 value = quad2*x*x + quad1*x + quad0;
278 DoubleModel.this.setValue(currentUnit.fromUnit(
279 currentUnit.round(currentUnit.toUnit(value))));
283 // Static get-methods
284 private boolean isAdjusting;
285 public int getExtent() { return 0; }
286 public int getMaximum() { return MAX; }
287 public int getMinimum() { return 0; }
288 public boolean getValueIsAdjusting() { return isAdjusting; }
291 public void setExtent(int newExtent) { }
292 public void setMaximum(int newMaximum) { }
293 public void setMinimum(int newMinimum) { }
294 public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
296 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
297 setValueIsAdjusting(adjusting);
301 // Pass change listeners to the underlying model
302 public void addChangeListener(ChangeListener l) {
303 DoubleModel.this.addChangeListener(l);
306 public void removeChangeListener(ChangeListener l) {
307 DoubleModel.this.removeChangeListener(l);
312 public void stateChanged(ChangeEvent e) {
313 // Min or max range has changed.
314 // Fire if not already firing
321 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
322 return new ValueSliderModel(min,max);
325 public BoundedRangeModel getSliderModel(double min, double max) {
326 return new ValueSliderModel(min,max);
329 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
330 return new ValueSliderModel(min,mid,max);
333 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
334 return new ValueSliderModel(min,pos,mid,max);
341 //////////// Action model ////////////
343 private class AutomaticActionModel extends AbstractAction implements ChangeListener {
344 private boolean oldValue = false;
346 public AutomaticActionModel() {
347 oldValue = isAutomatic();
348 addChangeListener(this);
353 public boolean isEnabled() {
354 // TODO: LOW: does not reflect if component is currently able to support automatic setting
355 return isAutomaticAvailable();
359 public Object getValue(String key) {
360 if (key.equals(Action.SELECTED_KEY)) {
361 oldValue = isAutomatic();
364 return super.getValue(key);
368 public void putValue(String key, Object value) {
371 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
372 oldValue = (Boolean)value;
373 setAutomatic((Boolean)value);
375 super.putValue(key, value);
379 // Implement a wrapper to the ChangeListeners
380 ArrayList<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
382 public void addPropertyChangeListener(PropertyChangeListener listener) {
383 listeners.add(listener);
384 DoubleModel.this.addChangeListener(this);
387 public void removePropertyChangeListener(PropertyChangeListener listener) {
388 listeners.remove(listener);
389 if (listeners.isEmpty())
390 DoubleModel.this.removeChangeListener(this);
392 // If the value has changed, generate an event to the listeners
393 public void stateChanged(ChangeEvent e) {
394 boolean newValue = isAutomatic();
395 if (oldValue == newValue)
397 PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
400 Object[] l = listeners.toArray();
401 for (int i=0; i<l.length; i++) {
402 ((PropertyChangeListener)l[i]).propertyChange(event);
406 public void actionPerformed(ActionEvent e) {
407 // Setting performed in putValue
413 * Returns a new Action corresponding to the changes of the automatic setting
414 * property of the value model. This may be used directly with e.g. check buttons.
416 * @return A compatibility layer for an Action.
418 public Action getAutomaticAction() {
419 return new AutomaticActionModel();
427 //////////// Main model /////////////
430 * The main model handles all values in SI units, i.e. no conversion is made within the model.
433 private final ChangeSource source;
434 private final String valueName;
435 private final double multiplier;
437 private final Method getMethod;
438 private final Method setMethod;
440 private final Method getAutoMethod;
441 private final Method setAutoMethod;
443 private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
445 private final UnitGroup units;
446 private Unit currentUnit;
448 private final double minValue;
449 private final double maxValue;
452 private int firing = 0; // >0 when model itself is sending events
455 // Used to differentiate changes in valueName and other changes in the component:
456 private double lastValue = 0;
457 private boolean lastAutomatic = false;
460 public DoubleModel(double value) {
461 this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
464 public DoubleModel(double value, UnitGroup unit) {
465 this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
468 public DoubleModel(double value, UnitGroup unit, double min) {
469 this(value,unit,min,Double.POSITIVE_INFINITY);
472 public DoubleModel(double value, UnitGroup unit, double min, double max) {
473 this.lastValue = value;
478 valueName = "Constant value";
481 getMethod = setMethod = null;
482 getAutoMethod = setAutoMethod = null;
484 currentUnit = units.getDefaultUnit();
489 * Generates a new DoubleModel that changes the values of the specified component.
490 * The double value is read and written using the methods "get"/"set" + valueName.
492 * @param source Component whose parameter to use.
493 * @param valueName Name of metods used to get/set the parameter.
494 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
495 * @param min Minimum value allowed (in SI units)
496 * @param max Maximum value allowed (in SI units)
498 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
499 double min, double max) {
500 this.source = source;
501 this.valueName = valueName;
502 this.multiplier = multiplier;
505 currentUnit = units.getDefaultUnit();
511 getMethod = source.getClass().getMethod("get" + valueName);
512 } catch (NoSuchMethodException e) {
513 throw new IllegalArgumentException("get method for value '"+valueName+
514 "' not present in class "+source.getClass().getCanonicalName());
519 s = source.getClass().getMethod("set" + valueName,double.class);
520 } catch (NoSuchMethodException e1) { } // Ignore
523 // Automatic selection methods
525 Method set=null,get=null;
528 get = source.getClass().getMethod("is" + valueName + "Automatic");
529 set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
530 } catch (NoSuchMethodException e) { } // ignore
532 if (set!=null && get!=null) {
536 getAutoMethod = null;
537 setAutoMethod = null;
542 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
544 this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
547 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
548 this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
551 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
552 double min, double max) {
553 this(source,valueName,1.0,unit,min,max);
556 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
557 this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
560 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
561 this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
564 public DoubleModel(ChangeSource source, String valueName) {
565 this(source,valueName,1.0,UnitGroup.UNITS_NONE,
566 Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
569 public DoubleModel(ChangeSource source, String valueName, double min) {
570 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
573 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
574 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
580 * Returns the value of the variable (in SI units).
582 public double getValue() {
583 if (getMethod==null) // Constant value
587 return (Double)getMethod.invoke(source)*multiplier;
588 } catch (IllegalArgumentException e) {
590 } catch (IllegalAccessException e) {
592 } catch (InvocationTargetException e) {
595 return lastValue; // Should not occur
599 * Sets the value of the variable.
600 * @param v New value for parameter in SI units.
602 public void setValue(double v) {
603 if (setMethod==null) {
604 if (getMethod != null) {
605 throw new RuntimeException("setMethod not available for variable '"+valueName+
606 "' in class "+source.getClass().getCanonicalName());
614 setMethod.invoke(source, v/multiplier);
616 } catch (IllegalArgumentException e) {
618 } catch (IllegalAccessException e) {
620 } catch (InvocationTargetException e) {
623 fireStateChanged(); // Should not occur
628 * Returns whether setting the value automatically is available.
630 public boolean isAutomaticAvailable() {
631 return (getAutoMethod != null) && (setAutoMethod != null);
635 * Returns whether the value is currently being set automatically.
636 * Returns false if automatic setting is not available at all.
638 public boolean isAutomatic() {
639 if (getAutoMethod == null)
643 return (Boolean)getAutoMethod.invoke(source);
644 } catch (IllegalArgumentException e) {
646 } catch (IllegalAccessException e) {
648 } catch (InvocationTargetException e) {
651 return false; // Should not occur
655 * Sets whether the value should be set automatically. Simply fires a
656 * state change event if automatic setting is not available.
658 public void setAutomatic(boolean auto) {
659 if (setAutoMethod == null) {
660 fireStateChanged(); // in case something is out-of-sync
665 lastAutomatic = auto;
666 setAutoMethod.invoke(source, auto);
668 } catch (IllegalArgumentException e) {
670 } catch (IllegalAccessException e) {
672 } catch (InvocationTargetException e) {
675 fireStateChanged(); // Should not occur
680 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
681 * @return The most recently set unit.
683 public Unit getCurrentUnit() {
688 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
689 * @param u The unit to set active.
691 public void setCurrentUnit(Unit u) {
692 if (currentUnit == u)
700 * Returns the UnitGroup associated with the parameter value.
702 * @return The UnitGroup given to the constructor.
704 public UnitGroup getUnitGroup() {
711 * Add a listener to the model. Adds the model as a listener to the Component if this
712 * is the first listener.
713 * @param l Listener to add.
715 public void addChangeListener(ChangeListener l) {
716 if (listeners.isEmpty()) {
717 if (source != null) {
718 source.addChangeListener(this);
719 lastValue = getValue();
720 lastAutomatic = isAutomatic();
726 System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
730 * Remove a listener from the model. Removes the model from being a listener to the Component
731 * if this was the last listener of the model.
732 * @param l Listener to remove.
734 public void removeChangeListener(ChangeListener l) {
736 if (listeners.isEmpty() && source != null) {
737 source.removeChangeListener(this);
740 System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
744 * Fire a ChangeEvent to all listeners.
746 protected void fireStateChanged() {
747 Object[] l = listeners.toArray();
748 ChangeEvent event = new ChangeEvent(this);
750 for (int i=0; i<l.length; i++)
751 ((ChangeListener)l[i]).stateChanged(event);
756 * Called when the component changes. Checks whether the modeled value has changed, and if
757 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
759 public void stateChanged(ChangeEvent e) {
760 double v = getValue();
761 boolean b = isAutomatic();
762 if (lastValue == v && lastAutomatic == b)
770 * Explain the DoubleModel as a String.
773 public String toString() {
775 return "DoubleModel[constant="+lastValue+"]";
776 return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";