Added support for Unit 'in/64' which is inches with fractional representation. Doubl...
[debian/openrocket] / core / src / net / sf / openrocket / gui / adaptors / DoubleModel.java
1 package net.sf.openrocket.gui.adaptors;
2
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;
11
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;
19
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;
33
34
35 /**
36  * A model connector that can read and modify any value of any ChangeSource that
37  * has the appropriate get/set methods defined.  
38  * 
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.
42  * 
43  * Sub-models suitable for JSpinners and other components are available from the appropriate
44  * methods.
45  * 
46  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
47  */
48
49 public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
50         private static final LogHelper log = Application.getLogger();
51
52
53         public static final DoubleModel ZERO = new DoubleModel(0);
54
55         //////////// JSpinner Model ////////////
56
57         /**
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.
60          */
61         public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
62
63                 @Override
64                 public Object getValue() {
65                         return currentUnit.toString(DoubleModel.this.getValue());
66                 }
67
68                 @Override
69                 public void setValue(Object value) {
70                         if (firing > 0) {
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");
74                                 return;
75                         }
76                         Number num = 0;
77                         if ( value instanceof Number ) {
78                                 num = (Number)value;
79                         } else if ( value instanceof String ) {
80                                 try {
81                                         String newValString = (String)value;
82                                         num = FractionUtil.parseFraction(newValString);
83                                 }
84                                 catch ( java.lang.NumberFormatException nfex ) {
85                                         num = 0.0d;
86                                 }
87                         }
88
89                         double newValue = num.doubleValue();
90                         double converted = currentUnit.fromUnit(newValue);
91
92                         log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
93                                         " converted=" + converted);
94                         DoubleModel.this.setValue(converted);
95
96                 }
97
98                 @Override
99                 public Object getNextValue() {
100                         double d = currentUnit.toUnit(DoubleModel.this.getValue());
101                         double max = currentUnit.toUnit(maxValue);
102                         if (MathUtil.equals(d, max))
103                                 return null;
104                         d = currentUnit.getNextValue(d);
105                         if (d > max)
106                                 d = max;
107                         return d;
108                 }
109
110                 @Override
111                 public Object getPreviousValue() {
112                         double d = currentUnit.toUnit(DoubleModel.this.getValue());
113                         double min = currentUnit.toUnit(minValue);
114                         if (MathUtil.equals(d, min))
115                                 return null;
116                         d = currentUnit.getPreviousValue(d);
117                         if (d < min)
118                                 d = min;
119                         return d;
120                 }
121 /* FIXME - put min & max back
122                 @Override
123                 public Comparable<Double> getMinimum() {
124                         return currentUnit.toUnit(minValue);
125                 }
126
127                 @Override
128                 public Comparable<Double> getMaximum() {
129                         return currentUnit.toUnit(maxValue);
130                 }
131 */
132                 @Override
133                 public void addChangeListener(ChangeListener l) {
134                         DoubleModel.this.addChangeListener(l);
135                 }
136
137                 @Override
138                 public void removeChangeListener(ChangeListener l) {
139                         DoubleModel.this.removeChangeListener(l);
140                 }
141
142                 @Override
143                 public void invalidate() {
144                         DoubleModel.this.invalidate();
145                 }
146         }
147
148         /**
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.
151          * 
152          * @return  A compatibility layer for a SpinnerModel.
153          */
154         public SpinnerModel getSpinnerModel() {
155                 return new ValueSpinnerModel();
156         }
157
158         ////////////  JSlider model  ////////////
159
160         private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
161                 private static final int MAX = 1000;
162
163                 /*
164                  * Use linear scale  value = linear1 * x + linear0  when x < linearPosition
165                  * Use quadratic scale  value = quad2 * x^2 + quad1 * x + quad0  otherwise
166                  */
167                 private final boolean islinear;
168
169                 // Linear in range x <= linearPosition
170                 private final double linearPosition;
171
172                 // May be changing DoubleModels when using linear model
173                 private final DoubleModel min, mid, max;
174
175                 // Linear multiplier and constant
176                 //private final double linear1;
177                 //private final double linear0;
178
179                 // Non-linear multiplier, exponent and constant
180                 private double quad2, quad1, quad0;
181
182                 public ValueSliderModel(DoubleModel min, DoubleModel max) {
183                         this.islinear = true;
184                         linearPosition = 1.0;
185
186                         this.min = min;
187                         this.mid = max; // Never use exponential scale
188                         this.max = max;
189
190                         min.addChangeListener(this);
191                         max.addChangeListener(this);
192
193                         quad2 = quad1 = quad0 = 0; // Not used
194                 }
195
196
197
198                 /**
199                  * Generate a linear model from min to max.
200                  */
201                 public ValueSliderModel(double min, double max) {
202                         this.islinear = true;
203                         linearPosition = 1.0;
204
205                         this.min = new DoubleModel(min);
206                         this.mid = new DoubleModel(max); // Never use exponential scale
207                         this.max = new DoubleModel(max);
208
209                         quad2 = quad1 = quad0 = 0; // Not used
210                 }
211
212                 public ValueSliderModel(double min, double mid, double max) {
213                         this(min, 0.5, mid, max);
214                 }
215
216                 public ValueSliderModel(double min, double mid, DoubleModel max) {
217                         this(min, 0.5, mid, max);
218                 }
219
220                 /*
221                  * v(x)  = mul * x^exp + add
222                  * 
223                  * v(pos)  = mul * pos^exp + add = mid
224                  * v(1)    = mul + add = max
225                  * v'(pos) = mul*exp * pos^(exp-1) = linearMul
226                  */
227                 public ValueSliderModel(double min, double pos, double mid, double max ) {
228                         this(min, pos, mid, new DoubleModel(max));
229                 }
230                 public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
231                         this.min = new DoubleModel(min);
232                         this.mid = new DoubleModel(mid);
233                         this.max = max;
234
235                         this.islinear = false;
236
237                         max.addChangeListener(this);
238
239                         linearPosition = pos;
240                         //linear0 = min;
241                         //linear1 = (mid-min)/pos;
242
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);
246                         }
247
248                         updateExponentialParameters();
249
250                 }
251
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();
257                         /*
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
262                          */
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);
267                 }
268
269                 private double pow2(double x) {
270                         return x * x;
271                 }
272
273                 @Override
274                 public int getValue() {
275                         double value = DoubleModel.this.getValue();
276                         if (value <= min.getValue())
277                                 return 0;
278                         if (value >= max.getValue())
279                                 return MAX;
280
281                         double x;
282                         if (value <= mid.getValue()) {
283                                 // Use linear scale
284                                 //linear0 = min;
285                                 //linear1 = (mid-min)/pos;
286
287                                 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
288                         } else {
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);
293                         }
294                         return (int) (x * MAX);
295                 }
296
297
298                 @Override
299                 public void setValue(int newValue) {
300                         if (firing > 0) {
301                                 // Ignore loops
302                                 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
303                                                 " value=" + newValue + ", currently firing events");
304                                 return;
305                         }
306
307                         double x = (double) newValue / MAX;
308                         double scaledValue;
309
310                         if (x <= linearPosition) {
311                                 // Use linear scale
312                                 //linear0 = min;
313                                 //linear1 = (mid-min)/pos;
314
315                                 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
316                         } else {
317                                 // Use quadratic scale
318                                 scaledValue = quad2 * x * x + quad1 * x + quad0;
319                         }
320
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);
325                 }
326
327
328                 // Static get-methods
329                 private boolean isAdjusting;
330
331                 @Override
332                 public int getExtent() {
333                         return 0;
334                 }
335
336                 @Override
337                 public int getMaximum() {
338                         return MAX;
339                 }
340
341                 @Override
342                 public int getMinimum() {
343                         return 0;
344                 }
345
346                 @Override
347                 public boolean getValueIsAdjusting() {
348                         return isAdjusting;
349                 }
350
351                 // Ignore set-values
352                 @Override
353                 public void setExtent(int newExtent) {
354                 }
355
356                 @Override
357                 public void setMaximum(int newMaximum) {
358                 }
359
360                 @Override
361                 public void setMinimum(int newMinimum) {
362                 }
363
364                 @Override
365                 public void setValueIsAdjusting(boolean b) {
366                         isAdjusting = b;
367                 }
368
369                 @Override
370                 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
371                         setValueIsAdjusting(adjusting);
372                         setValue(value);
373                 }
374
375                 // Pass change listeners to the underlying model
376                 @Override
377                 public void addChangeListener(ChangeListener l) {
378                         DoubleModel.this.addChangeListener(l);
379                 }
380
381                 @Override
382                 public void removeChangeListener(ChangeListener l) {
383                         DoubleModel.this.removeChangeListener(l);
384                 }
385
386                 @Override
387                 public void invalidate() {
388                         DoubleModel.this.invalidate();
389                 }
390
391                 @Override
392                 public void stateChanged(EventObject e) {
393                         // Min or max range has changed.
394                         if ( !islinear ) {
395                                 double midValue = (max.getValue() - min.getValue()) /3.0;
396                                 mid.setValue(midValue);
397                                 updateExponentialParameters();
398                         }
399                         // Fire if not already firing
400                         if (firing == 0)
401                                 fireStateChanged();
402                 }
403         }
404
405
406         public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
407                 return new ValueSliderModel(min, max);
408         }
409
410         public BoundedRangeModel getSliderModel(double min, double max) {
411                 return new ValueSliderModel(min, max);
412         }
413
414         public BoundedRangeModel getSliderModel(double min, double mid, double max) {
415                 return new ValueSliderModel(min, mid, max);
416         }
417
418         public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
419                 return new ValueSliderModel(min, mid, max);
420         }
421
422         public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
423                 return new ValueSliderModel(min, pos, mid, max);
424         }
425
426
427
428
429
430         ////////////  Action model  ////////////
431
432         private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
433                 private boolean oldValue = false;
434
435                 public AutomaticActionModel() {
436                         oldValue = isAutomatic();
437                         addChangeListener(this);
438                 }
439
440
441                 @Override
442                 public boolean isEnabled() {
443                         return isAutomaticAvailable();
444                 }
445
446                 @Override
447                 public Object getValue(String key) {
448                         if (key.equals(Action.SELECTED_KEY)) {
449                                 oldValue = isAutomatic();
450                                 return oldValue;
451                         }
452                         return super.getValue(key);
453                 }
454
455                 @Override
456                 public void putValue(String key, Object value) {
457                         if (firing > 0) {
458                                 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
459                                                 " key=" + key + " value=" + value + ", currently firing events");
460                                 return;
461                         }
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);
467                         } else {
468                                 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
469                                                 " key=" + key + " value=" + value);
470                                 super.putValue(key, value);
471                         }
472                 }
473
474                 // Implement a wrapper to the ChangeListeners
475                 ArrayList<PropertyChangeListener> propertyChangeListeners =
476                                 new ArrayList<PropertyChangeListener>();
477
478                 @Override
479                 public void addPropertyChangeListener(PropertyChangeListener listener) {
480                         propertyChangeListeners.add(listener);
481                         DoubleModel.this.addChangeListener(this);
482                 }
483
484                 @Override
485                 public void removePropertyChangeListener(PropertyChangeListener listener) {
486                         propertyChangeListeners.remove(listener);
487                         if (propertyChangeListeners.isEmpty())
488                                 DoubleModel.this.removeChangeListener(this);
489                 }
490
491                 // If the value has changed, generate an event to the listeners
492                 @Override
493                 public void stateChanged(EventObject e) {
494                         boolean newValue = isAutomatic();
495                         if (oldValue == newValue)
496                                 return;
497                         PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
498                                         oldValue, newValue);
499                         oldValue = newValue;
500                         Object[] l = propertyChangeListeners.toArray();
501                         for (int i = 0; i < l.length; i++) {
502                                 ((PropertyChangeListener) l[i]).propertyChange(event);
503                         }
504                 }
505
506                 @Override
507                 public void actionPerformed(ActionEvent e) {
508                         // Setting performed in putValue
509                 }
510
511                 @Override
512                 public void invalidate() {
513                         DoubleModel.this.invalidate();
514                 }
515         }
516
517         /**
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.
520          * 
521          * @return  A compatibility layer for an Action.
522          */
523         public Action getAutomaticAction() {
524                 return new AutomaticActionModel();
525         }
526
527
528
529
530
531         ////////////  Main model  /////////////
532
533         /*
534          * The main model handles all values in SI units, i.e. no conversion is made within the model.
535          */
536
537         private final ChangeSource source;
538         private final String valueName;
539         private final double multiplier;
540
541         private final Method getMethod;
542         private final Method setMethod;
543
544         private final Method getAutoMethod;
545         private final Method setAutoMethod;
546
547         private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
548
549         private final UnitGroup units;
550         private Unit currentUnit;
551
552         private final double minValue;
553         private double maxValue;
554
555         private String toString = null;
556
557
558         private int firing = 0; //  >0 when model itself is sending events
559
560
561         // Used to differentiate changes in valueName and other changes in the component:
562         private double lastValue = 0;
563         private boolean lastAutomatic = false;
564
565         private Invalidator invalidator = new Invalidator(this);
566
567
568         /**
569          * Generate a DoubleModel that contains an internal double value.
570          * 
571          * @param value         the initial value.
572          */
573         public DoubleModel(double value) {
574                 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
575         }
576
577         /**
578          * Generate a DoubleModel that contains an internal double value.
579          * 
580          * @param value         the initial value.
581          * @param unit          the unit for the value.
582          */
583         public DoubleModel(double value, UnitGroup unit) {
584                 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
585         }
586
587         /**
588          * Generate a DoubleModel that contains an internal double value.
589          * 
590          * @param value         the initial value.
591          * @param unit          the unit for the value.
592          * @param min           minimum value.
593          */
594         public DoubleModel(double value, UnitGroup unit, double min) {
595                 this(value, unit, min, Double.POSITIVE_INFINITY);
596         }
597
598         /**
599          * Generate a DoubleModel that contains an internal double value.
600          * 
601          * @param value         the initial value.
602          * @param unit          the unit for the value.
603          * @param min           minimum value.
604          * @param max           maximum value.
605          */
606         public DoubleModel(double value, UnitGroup unit, double min, double max) {
607                 this.lastValue = value;
608                 this.minValue = min;
609                 this.maxValue = max;
610
611                 source = null;
612                 valueName = "Constant value";
613                 multiplier = 1;
614
615                 getMethod = setMethod = null;
616                 getAutoMethod = setAutoMethod = null;
617                 units = unit;
618                 currentUnit = units.getDefaultUnit();
619         }
620
621
622         /**
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.
625          *  
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)
631          */
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;
637
638                 this.units = unit;
639                 currentUnit = units.getDefaultUnit();
640
641                 this.minValue = min;
642                 this.maxValue = max;
643
644                 try {
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());
649                 }
650
651                 Method s = null;
652                 try {
653                         s = source.getClass().getMethod("set" + valueName, double.class);
654                 } catch (NoSuchMethodException e1) {
655                 } // Ignore
656                 setMethod = s;
657
658                 // Automatic selection methods
659
660                 Method set = null, get = null;
661
662                 try {
663                         get = source.getClass().getMethod("is" + valueName + "Automatic");
664                         set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
665                 } catch (NoSuchMethodException e) {
666                 } // ignore
667
668                 if (set != null && get != null) {
669                         getAutoMethod = get;
670                         setAutoMethod = set;
671                 } else {
672                         getAutoMethod = null;
673                         setAutoMethod = null;
674                 }
675
676         }
677
678         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
679                         double min) {
680                 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
681         }
682
683         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
684                 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
685         }
686
687         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
688                         double min, double max) {
689                 this(source, valueName, 1.0, unit, min, max);
690         }
691
692         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
693                 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
694         }
695
696         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
697                 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
698         }
699
700         public DoubleModel(ChangeSource source, String valueName) {
701                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
702                                 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
703         }
704
705         public DoubleModel(ChangeSource source, String valueName, double min) {
706                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
707         }
708
709         public DoubleModel(ChangeSource source, String valueName, double min, double max) {
710                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
711         }
712
713
714
715         /**
716          * Returns the value of the variable (in SI units).
717          */
718         public double getValue() {
719                 if (getMethod == null) // Constant value
720                         return lastValue;
721
722                 try {
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);
730                 }
731         }
732
733         /**
734          * Sets the value of the variable.
735          * @param v New value for parameter in SI units.
736          */
737         public void setValue(double v) {
738                 checkState(true);
739
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());
745                         }
746                         lastValue = v;
747                         fireStateChanged();
748                         return;
749                 }
750
751                 try {
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);
759                 }
760         }
761
762         /**
763          * Returns whether setting the value automatically is available.
764          */
765         public boolean isAutomaticAvailable() {
766                 return (getAutoMethod != null) && (setAutoMethod != null);
767         }
768
769         /**
770          * Returns whether the value is currently being set automatically.
771          * Returns false if automatic setting is not available at all.
772          */
773         public boolean isAutomatic() {
774                 if (getAutoMethod == null)
775                         return false;
776
777                 try {
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);
785                 }
786         }
787
788         /**
789          * Sets whether the value should be set automatically.  Simply fires a
790          * state change event if automatic setting is not available.
791          */
792         public void setAutomatic(boolean auto) {
793                 checkState(true);
794
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
798                         return;
799                 }
800
801                 log.debug("Setting automatic to " + auto + " for " + this);
802                 lastAutomatic = auto;
803                 try {
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);
811                 }
812         }
813
814
815         /**
816          * Returns the current Unit.  At the beginning it is the default unit of the UnitGroup.
817          * @return The most recently set unit.
818          */
819         public Unit getCurrentUnit() {
820                 return currentUnit;
821         }
822
823         /**
824          * Sets the current Unit.  The unit must be one of those included in the UnitGroup.
825          * @param u  The unit to set active.
826          */
827         public void setCurrentUnit(Unit u) {
828                 checkState(true);
829                 if (currentUnit == u)
830                         return;
831                 log.debug("Setting unit for " + this + " to '" + u + "'");
832                 currentUnit = u;
833                 fireStateChanged();
834         }
835
836
837         /**
838          * Returns the UnitGroup associated with the parameter value.
839          *
840          * @return The UnitGroup given to the constructor.
841          */
842         public UnitGroup getUnitGroup() {
843                 return units;
844         }
845
846
847
848         /**
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.
852          */
853         @Override
854         public void addChangeListener(EventListener l) {
855                 checkState(true);
856
857                 if (listeners.isEmpty()) {
858                         if (source != null) {
859                                 source.addChangeListener(this);
860                                 lastValue = getValue();
861                                 lastAutomatic = isAutomatic();
862                         }
863                 }
864
865                 listeners.add(l);
866                 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
867         }
868
869         /**
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.
873          */
874         @Override
875         public void removeChangeListener(EventListener l) {
876                 checkState(false);
877
878                 listeners.remove(l);
879                 if (listeners.isEmpty() && source != null) {
880                         source.removeChangeListener(this);
881                 }
882                 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
883         }
884
885
886         /**
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.
890          */
891         @Override
892         public void invalidate() {
893                 log.verbose("Invalidating " + this);
894                 invalidator.invalidate();
895
896                 if (!listeners.isEmpty()) {
897                         log.warn("Invalidating " + this + " while still having listeners " + listeners);
898                 }
899                 listeners.clear();
900                 if (source != null) {
901                         source.removeChangeListener(this);
902                 }
903                 MemoryManagement.collectable(this);
904         }
905
906
907         private void checkState(boolean error) {
908                 invalidator.check(error);
909         }
910
911
912         @Override
913         protected void finalize() throws Throwable {
914                 super.finalize();
915                 if (!listeners.isEmpty()) {
916                         log.warn(this + " being garbage-collected while having listeners " + listeners);
917                 }
918         };
919
920
921         /**
922          * Fire a ChangeEvent to all listeners.
923          */
924         protected void fireStateChanged() {
925                 checkState(true);
926
927                 EventObject event = new EventObject(this);
928                 ChangeEvent cevent = new ChangeEvent(this);
929                 firing++;
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);
937                         }
938                 }
939                 firing--;
940         }
941
942         /**
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.
945          */
946         @Override
947         public void stateChanged(EventObject e) {
948                 checkState(true);
949
950                 double v = getValue();
951                 boolean b = isAutomatic();
952                 if (lastValue == v && lastAutomatic == b)
953                         return;
954                 lastValue = v;
955                 lastAutomatic = b;
956                 fireStateChanged();
957         }
958
959
960         /**
961          * Explain the DoubleModel as a String.
962          */
963         @Override
964         public String toString() {
965                 if (toString == null) {
966                         if (source == null) {
967                                 toString = "DoubleModel[constant=" + lastValue + "]";
968                         } else {
969                                 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
970                         }
971                 }
972                 return toString;
973         }
974 }