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.BugException;
21 import net.sf.openrocket.util.ChangeSource;
22 import net.sf.openrocket.util.MathUtil;
23 import net.sf.openrocket.util.Reflection;
27 * A model connector that can read and modify any value of any ChangeSource that
28 * has the appropriate get/set methods defined.
30 * The variable is defined in the constructor by providing the variable name as a string
31 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
32 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
34 * Sub-models suitable for JSpinners and other components are available from the appropriate
37 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
40 public class DoubleModel implements ChangeListener, ChangeSource {
41 private static final boolean DEBUG_LISTENERS = false;
43 public static final DoubleModel ZERO = new DoubleModel(0);
45 //////////// JSpinner Model ////////////
48 * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
49 * to be compatible with the NumberEditor, but only has the necessary methods defined.
51 private class ValueSpinnerModel extends SpinnerNumberModel {
54 public Object getValue() {
55 return currentUnit.toUnit(DoubleModel.this.getValue());
56 // return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
60 public void setValue(Object value) {
62 System.out.println("setValue("+value+") called, valueName="+valueName+
65 if (firing > 0) // Ignore, if called when model is sending events
67 Number num = (Number)value;
68 double newValue = num.doubleValue();
69 DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
73 // double newValue = Double.parseDouble((String)value);
74 // DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
75 // } catch (NumberFormatException e) {
76 // DoubleModel.this.fireStateChanged();
81 public Object getNextValue() {
82 double d = currentUnit.toUnit(DoubleModel.this.getValue());
83 double max = currentUnit.toUnit(maxValue);
84 if (MathUtil.equals(d,max))
86 d = currentUnit.getNextValue(d);
90 // return makeString(d);
94 public Object getPreviousValue() {
95 double d = currentUnit.toUnit(DoubleModel.this.getValue());
96 double min = currentUnit.toUnit(minValue);
97 if (MathUtil.equals(d,min))
99 d = currentUnit.getPreviousValue(d);
103 // return makeString(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);
130 * Returns a new SpinnerModel with the same base as the DoubleModel.
131 * The values given to the JSpinner are in the currently selected units.
133 * @return A compatibility layer for a SpinnerModel.
135 public SpinnerModel getSpinnerModel() {
136 return new ValueSpinnerModel();
143 //////////// JSlider model ////////////
145 private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
146 private static final int MAX = 1000;
149 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
150 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
153 // Linear in range x <= linearPosition
154 private final double linearPosition;
156 // May be changing DoubleModels when using linear model
157 private final DoubleModel min, mid, max;
159 // Linear multiplier and constant
160 //private final double linear1;
161 //private final double linear0;
163 // Non-linear multiplier, exponent and constant
164 private final double quad2,quad1,quad0;
168 public ValueSliderModel(DoubleModel min, DoubleModel max) {
169 linearPosition = 1.0;
172 this.mid = max; // Never use exponential scale
175 min.addChangeListener(this);
176 max.addChangeListener(this);
178 quad2 = quad1 = quad0 = 0; // Not used
184 * Generate a linear model from min to max.
186 public ValueSliderModel(double min, double max) {
187 linearPosition = 1.0;
189 this.min = new DoubleModel(min);
190 this.mid = new DoubleModel(max); // Never use exponential scale
191 this.max = new DoubleModel(max);
193 quad2 = quad1 = quad0 = 0; // Not used
196 public ValueSliderModel(double min, double mid, double max) {
197 this(min,0.5,mid,max);
201 * v(x) = mul * x^exp + add
203 * v(pos) = mul * pos^exp + add = mid
204 * v(1) = mul + add = max
205 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
207 public ValueSliderModel(double min, double pos, double mid, double max) {
208 this.min = new DoubleModel(min);
209 this.mid = new DoubleModel(mid);
210 this.max = new DoubleModel(max);
213 linearPosition = pos;
215 //linear1 = (mid-min)/pos;
217 if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
218 throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
219 "min="+min+" mid="+mid+" max="+max+" pos="+pos);
223 * quad2..0 are calculated such that
224 * f(pos) = mid - continuity
225 * f(1) = max - end point
226 * f'(pos) = linear1 - continuity of derivative
229 double delta = (mid-min)/pos;
230 quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
231 quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
232 quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
236 private double pow2(double x) {
240 public int getValue() {
241 double value = DoubleModel.this.getValue();
242 if (value <= min.getValue())
244 if (value >= max.getValue())
248 if (value <= mid.getValue()) {
251 //linear1 = (mid-min)/pos;
253 x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
255 // Use quadratic scale
256 // Further solution of the quadratic equation
257 // a*x^2 + b*x + c-value == 0
258 x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
264 public void setValue(int newValue) {
265 if (firing > 0) // Ignore loops
268 double x = (double)newValue/MAX;
271 if (x <= linearPosition) {
274 //linear1 = (mid-min)/pos;
276 value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
278 // Use quadratic scale
279 value = quad2*x*x + quad1*x + quad0;
282 DoubleModel.this.setValue(currentUnit.fromUnit(
283 currentUnit.round(currentUnit.toUnit(value))));
287 // Static get-methods
288 private boolean isAdjusting;
289 public int getExtent() { return 0; }
290 public int getMaximum() { return MAX; }
291 public int getMinimum() { return 0; }
292 public boolean getValueIsAdjusting() { return isAdjusting; }
295 public void setExtent(int newExtent) { }
296 public void setMaximum(int newMaximum) { }
297 public void setMinimum(int newMinimum) { }
298 public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
300 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
301 setValueIsAdjusting(adjusting);
305 // Pass change listeners to the underlying model
306 public void addChangeListener(ChangeListener l) {
307 DoubleModel.this.addChangeListener(l);
310 public void removeChangeListener(ChangeListener l) {
311 DoubleModel.this.removeChangeListener(l);
316 public void stateChanged(ChangeEvent e) {
317 // Min or max range has changed.
318 // Fire if not already firing
325 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
326 return new ValueSliderModel(min,max);
329 public BoundedRangeModel getSliderModel(double min, double max) {
330 return new ValueSliderModel(min,max);
333 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
334 return new ValueSliderModel(min,mid,max);
337 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
338 return new ValueSliderModel(min,pos,mid,max);
345 //////////// Action model ////////////
347 private class AutomaticActionModel extends AbstractAction implements ChangeListener {
348 private boolean oldValue = false;
350 public AutomaticActionModel() {
351 oldValue = isAutomatic();
352 addChangeListener(this);
357 public boolean isEnabled() {
358 // TODO: LOW: does not reflect if component is currently able to support automatic setting
359 return isAutomaticAvailable();
363 public Object getValue(String key) {
364 if (key.equals(Action.SELECTED_KEY)) {
365 oldValue = isAutomatic();
368 return super.getValue(key);
372 public void putValue(String key, Object value) {
375 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
376 oldValue = (Boolean)value;
377 setAutomatic((Boolean)value);
379 super.putValue(key, value);
383 // Implement a wrapper to the ChangeListeners
384 ArrayList<PropertyChangeListener> propertyChangeListeners =
385 new ArrayList<PropertyChangeListener>();
387 public void addPropertyChangeListener(PropertyChangeListener listener) {
388 propertyChangeListeners.add(listener);
389 DoubleModel.this.addChangeListener(this);
392 public void removePropertyChangeListener(PropertyChangeListener listener) {
393 propertyChangeListeners.remove(listener);
394 if (propertyChangeListeners.isEmpty())
395 DoubleModel.this.removeChangeListener(this);
397 // If the value has changed, generate an event to the listeners
398 public void stateChanged(ChangeEvent e) {
399 boolean newValue = isAutomatic();
400 if (oldValue == newValue)
402 PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
405 Object[] l = propertyChangeListeners.toArray();
406 for (int i=0; i<l.length; i++) {
407 ((PropertyChangeListener)l[i]).propertyChange(event);
411 public void actionPerformed(ActionEvent e) {
412 // Setting performed in putValue
418 * Returns a new Action corresponding to the changes of the automatic setting
419 * property of the value model. This may be used directly with e.g. check buttons.
421 * @return A compatibility layer for an Action.
423 public Action getAutomaticAction() {
424 return new AutomaticActionModel();
432 //////////// Main model /////////////
435 * The main model handles all values in SI units, i.e. no conversion is made within the model.
438 private final ChangeSource source;
439 private final String valueName;
440 private final double multiplier;
442 private final Method getMethod;
443 private final Method setMethod;
445 private final Method getAutoMethod;
446 private final Method setAutoMethod;
448 private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
450 private final UnitGroup units;
451 private Unit currentUnit;
453 private final double minValue;
454 private final double maxValue;
457 private int firing = 0; // >0 when model itself is sending events
460 // Used to differentiate changes in valueName and other changes in the component:
461 private double lastValue = 0;
462 private boolean lastAutomatic = false;
465 public DoubleModel(double value) {
466 this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
469 public DoubleModel(double value, UnitGroup unit) {
470 this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
473 public DoubleModel(double value, UnitGroup unit, double min) {
474 this(value,unit,min,Double.POSITIVE_INFINITY);
477 public DoubleModel(double value, UnitGroup unit, double min, double max) {
478 this.lastValue = value;
483 valueName = "Constant value";
486 getMethod = setMethod = null;
487 getAutoMethod = setAutoMethod = null;
489 currentUnit = units.getDefaultUnit();
494 * Generates a new DoubleModel that changes the values of the specified component.
495 * The double value is read and written using the methods "get"/"set" + valueName.
497 * @param source Component whose parameter to use.
498 * @param valueName Name of metods used to get/set the parameter.
499 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
500 * @param min Minimum value allowed (in SI units)
501 * @param max Maximum value allowed (in SI units)
503 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
504 double min, double max) {
505 this.source = source;
506 this.valueName = valueName;
507 this.multiplier = multiplier;
510 currentUnit = units.getDefaultUnit();
516 getMethod = source.getClass().getMethod("get" + valueName);
517 } catch (NoSuchMethodException e) {
518 throw new IllegalArgumentException("get method for value '"+valueName+
519 "' not present in class "+source.getClass().getCanonicalName());
524 s = source.getClass().getMethod("set" + valueName,double.class);
525 } catch (NoSuchMethodException e1) { } // Ignore
528 // Automatic selection methods
530 Method set=null,get=null;
533 get = source.getClass().getMethod("is" + valueName + "Automatic");
534 set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
535 } catch (NoSuchMethodException e) { } // ignore
537 if (set!=null && get!=null) {
541 getAutoMethod = null;
542 setAutoMethod = null;
547 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
549 this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
552 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
553 this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
556 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
557 double min, double max) {
558 this(source,valueName,1.0,unit,min,max);
561 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
562 this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
565 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
566 this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
569 public DoubleModel(ChangeSource source, String valueName) {
570 this(source,valueName,1.0,UnitGroup.UNITS_NONE,
571 Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
574 public DoubleModel(ChangeSource source, String valueName, double min) {
575 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
578 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
579 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
585 * Returns the value of the variable (in SI units).
587 public double getValue() {
588 if (getMethod==null) // Constant value
592 return (Double)getMethod.invoke(source)*multiplier;
593 } catch (IllegalArgumentException e) {
594 throw new BugException("Unable to invoke getMethod of "+this, e);
595 } catch (IllegalAccessException e) {
596 throw new BugException("Unable to invoke getMethod of "+this, e);
597 } catch (InvocationTargetException e) {
598 throw Reflection.handleWrappedException(e);
603 * Sets the value of the variable.
604 * @param v New value for parameter in SI units.
606 public void setValue(double v) {
607 if (setMethod==null) {
608 if (getMethod != null) {
609 throw new RuntimeException("setMethod not available for variable '"+valueName+
610 "' in class "+source.getClass().getCanonicalName());
618 setMethod.invoke(source, v/multiplier);
619 } catch (IllegalArgumentException e) {
620 throw new BugException("Unable to invoke setMethod of "+this, e);
621 } catch (IllegalAccessException e) {
622 throw new BugException("Unable to invoke setMethod of "+this, e);
623 } catch (InvocationTargetException e) {
624 throw Reflection.handleWrappedException(e);
630 * Returns whether setting the value automatically is available.
632 public boolean isAutomaticAvailable() {
633 return (getAutoMethod != null) && (setAutoMethod != null);
637 * Returns whether the value is currently being set automatically.
638 * Returns false if automatic setting is not available at all.
640 public boolean isAutomatic() {
641 if (getAutoMethod == null)
645 return (Boolean)getAutoMethod.invoke(source);
646 } catch (IllegalArgumentException e) {
647 throw new BugException("Method call failed", e);
648 } catch (IllegalAccessException e) {
649 throw new BugException("Method call failed", e);
650 } catch (InvocationTargetException e) {
651 throw Reflection.handleWrappedException(e);
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
665 lastAutomatic = auto;
667 setAutoMethod.invoke(source, auto);
668 } catch (IllegalArgumentException e) {
669 throw new BugException(e);
670 } catch (IllegalAccessException e) {
671 throw new BugException(e);
672 } catch (InvocationTargetException e) {
673 throw Reflection.handleWrappedException(e);
679 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
680 * @return The most recently set unit.
682 public Unit getCurrentUnit() {
687 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
688 * @param u The unit to set active.
690 public void setCurrentUnit(Unit u) {
691 if (currentUnit == u)
699 * Returns the UnitGroup associated with the parameter value.
701 * @return The UnitGroup given to the constructor.
703 public UnitGroup getUnitGroup() {
710 * Add a listener to the model. Adds the model as a listener to the value source if this
711 * is the first listener.
712 * @param l Listener to add.
714 public void addChangeListener(ChangeListener l) {
715 if (listeners.isEmpty()) {
716 if (source != null) {
717 source.addChangeListener(this);
718 lastValue = getValue();
719 lastAutomatic = isAutomatic();
725 System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
729 * Remove a listener from the model. Removes the model from being a listener to the Component
730 * if this was the last listener of the model.
731 * @param l Listener to remove.
733 public void removeChangeListener(ChangeListener l) {
735 if (listeners.isEmpty() && source != null) {
736 source.removeChangeListener(this);
739 System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
743 * Fire a ChangeEvent to all listeners.
745 protected void fireStateChanged() {
746 Object[] l = listeners.toArray();
747 ChangeEvent event = new ChangeEvent(this);
749 for (int i=0; i<l.length; i++)
750 ((ChangeListener)l[i]).stateChanged(event);
755 * Called when the component changes. Checks whether the modeled value has changed, and if
756 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
758 public void stateChanged(ChangeEvent e) {
759 double v = getValue();
760 boolean b = isAutomatic();
761 if (lastValue == v && lastAutomatic == b)
769 * Explain the DoubleModel as a String.
772 public String toString() {
774 return "DoubleModel[constant="+lastValue+"]";
775 return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";