]> git.gag.com Git - debian/openrocket/blob - core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java
30da01f9154bb855162562c74e8b1d825ea6e689
[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
122                 @Override
123                 public void addChangeListener(ChangeListener l) {
124                         DoubleModel.this.addChangeListener(l);
125                 }
126
127                 @Override
128                 public void removeChangeListener(ChangeListener l) {
129                         DoubleModel.this.removeChangeListener(l);
130                 }
131
132                 @Override
133                 public void invalidate() {
134                         DoubleModel.this.invalidate();
135                 }
136         }
137
138         /**
139          * Returns a new SpinnerModel with the same base as the DoubleModel.
140          * The values given to the JSpinner are in the currently selected units.
141          * 
142          * @return  A compatibility layer for a SpinnerModel.
143          */
144         public SpinnerModel getSpinnerModel() {
145                 return new ValueSpinnerModel();
146         }
147
148         ////////////  JSlider model  ////////////
149
150         private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
151                 private static final int MAX = 1000;
152
153                 /*
154                  * Use linear scale  value = linear1 * x + linear0  when x < linearPosition
155                  * Use quadratic scale  value = quad2 * x^2 + quad1 * x + quad0  otherwise
156                  */
157                 private final boolean islinear;
158
159                 // Linear in range x <= linearPosition
160                 private final double linearPosition;
161
162                 // May be changing DoubleModels when using linear model
163                 private final DoubleModel min, mid, max;
164
165                 // Linear multiplier and constant
166                 //private final double linear1;
167                 //private final double linear0;
168
169                 // Non-linear multiplier, exponent and constant
170                 private double quad2, quad1, quad0;
171
172                 public ValueSliderModel(DoubleModel min, DoubleModel max) {
173                         this.islinear = true;
174                         linearPosition = 1.0;
175
176                         this.min = min;
177                         this.mid = max; // Never use exponential scale
178                         this.max = max;
179
180                         min.addChangeListener(this);
181                         max.addChangeListener(this);
182
183                         quad2 = quad1 = quad0 = 0; // Not used
184                 }
185
186
187
188                 /**
189                  * Generate a linear model from min to max.
190                  */
191                 public ValueSliderModel(double min, double max) {
192                         this.islinear = true;
193                         linearPosition = 1.0;
194
195                         this.min = new DoubleModel(min);
196                         this.mid = new DoubleModel(max); // Never use exponential scale
197                         this.max = new DoubleModel(max);
198
199                         quad2 = quad1 = quad0 = 0; // Not used
200                 }
201
202                 public ValueSliderModel(double min, double mid, double max) {
203                         this(min, 0.5, mid, max);
204                 }
205
206                 public ValueSliderModel(double min, double mid, DoubleModel max) {
207                         this(min, 0.5, mid, max);
208                 }
209
210                 /*
211                  * v(x)  = mul * x^exp + add
212                  * 
213                  * v(pos)  = mul * pos^exp + add = mid
214                  * v(1)    = mul + add = max
215                  * v'(pos) = mul*exp * pos^(exp-1) = linearMul
216                  */
217                 public ValueSliderModel(double min, double pos, double mid, double max ) {
218                         this(min, pos, mid, new DoubleModel(max));
219                 }
220                 public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
221                         this.min = new DoubleModel(min);
222                         this.mid = new DoubleModel(mid);
223                         this.max = max;
224
225                         this.islinear = false;
226
227                         max.addChangeListener(this);
228
229                         linearPosition = pos;
230                         //linear0 = min;
231                         //linear1 = (mid-min)/pos;
232
233                         if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
234                                 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
235                                                 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
236                         }
237
238                         updateExponentialParameters();
239
240                 }
241
242                 private void updateExponentialParameters() {
243                         double pos = this.linearPosition;
244                         double minValue = this.min.getValue();
245                         double midValue = this.mid.getValue();
246                         double maxValue = this.max.getValue();
247                         /*
248                          * quad2..0 are calculated such that
249                          *   f(pos)  = mid      - continuity
250                          *   f(1)    = max      - end point
251                          *   f'(pos) = linear1  - continuity of derivative
252                          */
253                         double delta = (midValue - minValue) / pos;
254                         quad2 = (maxValue - midValue - delta + delta * pos) / pow2(pos - 1);
255                         quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
256                         quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
257                 }
258
259                 private double pow2(double x) {
260                         return x * x;
261                 }
262
263                 @Override
264                 public int getValue() {
265                         double value = DoubleModel.this.getValue();
266                         if (value <= min.getValue())
267                                 return 0;
268                         if (value >= max.getValue())
269                                 return MAX;
270
271                         double x;
272                         if (value <= mid.getValue()) {
273                                 // Use linear scale
274                                 //linear0 = min;
275                                 //linear1 = (mid-min)/pos;
276
277                                 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
278                         } else {
279                                 // Use quadratic scale
280                                 // Further solution of the quadratic equation
281                                 //   a*x^2 + b*x + c-value == 0
282                                 x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
283                         }
284                         return (int) (x * MAX);
285                 }
286
287
288                 @Override
289                 public void setValue(int newValue) {
290                         if (firing > 0) {
291                                 // Ignore loops
292                                 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
293                                                 " value=" + newValue + ", currently firing events");
294                                 return;
295                         }
296
297                         double x = (double) newValue / MAX;
298                         double scaledValue;
299
300                         if (x <= linearPosition) {
301                                 // Use linear scale
302                                 //linear0 = min;
303                                 //linear1 = (mid-min)/pos;
304
305                                 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
306                         } else {
307                                 // Use quadratic scale
308                                 scaledValue = quad2 * x * x + quad1 * x + quad0;
309                         }
310
311                         double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
312                         log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
313                                         " scaledValue=" + scaledValue + " converted=" + converted);
314                         DoubleModel.this.setValue(converted);
315                 }
316
317
318                 // Static get-methods
319                 private boolean isAdjusting;
320
321                 @Override
322                 public int getExtent() {
323                         return 0;
324                 }
325
326                 @Override
327                 public int getMaximum() {
328                         return MAX;
329                 }
330
331                 @Override
332                 public int getMinimum() {
333                         return 0;
334                 }
335
336                 @Override
337                 public boolean getValueIsAdjusting() {
338                         return isAdjusting;
339                 }
340
341                 // Ignore set-values
342                 @Override
343                 public void setExtent(int newExtent) {
344                 }
345
346                 @Override
347                 public void setMaximum(int newMaximum) {
348                 }
349
350                 @Override
351                 public void setMinimum(int newMinimum) {
352                 }
353
354                 @Override
355                 public void setValueIsAdjusting(boolean b) {
356                         isAdjusting = b;
357                 }
358
359                 @Override
360                 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
361                         setValueIsAdjusting(adjusting);
362                         setValue(value);
363                 }
364
365                 // Pass change listeners to the underlying model
366                 @Override
367                 public void addChangeListener(ChangeListener l) {
368                         DoubleModel.this.addChangeListener(l);
369                 }
370
371                 @Override
372                 public void removeChangeListener(ChangeListener l) {
373                         DoubleModel.this.removeChangeListener(l);
374                 }
375
376                 @Override
377                 public void invalidate() {
378                         DoubleModel.this.invalidate();
379                 }
380
381                 @Override
382                 public void stateChanged(EventObject e) {
383                         // Min or max range has changed.
384                         if ( !islinear ) {
385                                 double midValue = (max.getValue() - min.getValue()) /3.0;
386                                 mid.setValue(midValue);
387                                 updateExponentialParameters();
388                         }
389                         // Fire if not already firing
390                         if (firing == 0)
391                                 fireStateChanged();
392                 }
393         }
394
395
396         public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
397                 return new ValueSliderModel(min, max);
398         }
399
400         public BoundedRangeModel getSliderModel(double min, double max) {
401                 return new ValueSliderModel(min, max);
402         }
403
404         public BoundedRangeModel getSliderModel(double min, double mid, double max) {
405                 return new ValueSliderModel(min, mid, max);
406         }
407
408         public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
409                 return new ValueSliderModel(min, mid, max);
410         }
411
412         public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
413                 return new ValueSliderModel(min, pos, mid, max);
414         }
415
416
417
418
419
420         ////////////  Action model  ////////////
421
422         private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
423                 private boolean oldValue = false;
424
425                 public AutomaticActionModel() {
426                         oldValue = isAutomatic();
427                         addChangeListener(this);
428                 }
429
430
431                 @Override
432                 public boolean isEnabled() {
433                         return isAutomaticAvailable();
434                 }
435
436                 @Override
437                 public Object getValue(String key) {
438                         if (key.equals(Action.SELECTED_KEY)) {
439                                 oldValue = isAutomatic();
440                                 return oldValue;
441                         }
442                         return super.getValue(key);
443                 }
444
445                 @Override
446                 public void putValue(String key, Object value) {
447                         if (firing > 0) {
448                                 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
449                                                 " key=" + key + " value=" + value + ", currently firing events");
450                                 return;
451                         }
452                         if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
453                                 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
454                                                 " key=" + key + " value=" + value);
455                                 oldValue = (Boolean) value;
456                                 setAutomatic((Boolean) value);
457                         } else {
458                                 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
459                                                 " key=" + key + " value=" + value);
460                                 super.putValue(key, value);
461                         }
462                 }
463
464                 // Implement a wrapper to the ChangeListeners
465                 ArrayList<PropertyChangeListener> propertyChangeListeners =
466                                 new ArrayList<PropertyChangeListener>();
467
468                 @Override
469                 public void addPropertyChangeListener(PropertyChangeListener listener) {
470                         propertyChangeListeners.add(listener);
471                         DoubleModel.this.addChangeListener(this);
472                 }
473
474                 @Override
475                 public void removePropertyChangeListener(PropertyChangeListener listener) {
476                         propertyChangeListeners.remove(listener);
477                         if (propertyChangeListeners.isEmpty())
478                                 DoubleModel.this.removeChangeListener(this);
479                 }
480
481                 // If the value has changed, generate an event to the listeners
482                 @Override
483                 public void stateChanged(EventObject e) {
484                         boolean newValue = isAutomatic();
485                         if (oldValue == newValue)
486                                 return;
487                         PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
488                                         oldValue, newValue);
489                         oldValue = newValue;
490                         Object[] l = propertyChangeListeners.toArray();
491                         for (int i = 0; i < l.length; i++) {
492                                 ((PropertyChangeListener) l[i]).propertyChange(event);
493                         }
494                 }
495
496                 @Override
497                 public void actionPerformed(ActionEvent e) {
498                         // Setting performed in putValue
499                 }
500
501                 @Override
502                 public void invalidate() {
503                         DoubleModel.this.invalidate();
504                 }
505         }
506
507         /**
508          * Returns a new Action corresponding to the changes of the automatic setting
509          * property of the value model.  This may be used directly with e.g. check buttons.
510          * 
511          * @return  A compatibility layer for an Action.
512          */
513         public Action getAutomaticAction() {
514                 return new AutomaticActionModel();
515         }
516
517
518
519
520
521         ////////////  Main model  /////////////
522
523         /*
524          * The main model handles all values in SI units, i.e. no conversion is made within the model.
525          */
526
527         private final ChangeSource source;
528         private final String valueName;
529         private final double multiplier;
530
531         private final Method getMethod;
532         private final Method setMethod;
533
534         private final Method getAutoMethod;
535         private final Method setAutoMethod;
536
537         private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
538
539         private final UnitGroup units;
540         private Unit currentUnit;
541
542         private final double minValue;
543         private double maxValue;
544
545         private String toString = null;
546
547
548         private int firing = 0; //  >0 when model itself is sending events
549
550
551         // Used to differentiate changes in valueName and other changes in the component:
552         private double lastValue = 0;
553         private boolean lastAutomatic = false;
554
555         private Invalidator invalidator = new Invalidator(this);
556
557
558         /**
559          * Generate a DoubleModel that contains an internal double value.
560          * 
561          * @param value         the initial value.
562          */
563         public DoubleModel(double value) {
564                 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
565         }
566
567         /**
568          * Generate a DoubleModel that contains an internal double value.
569          * 
570          * @param value         the initial value.
571          * @param unit          the unit for the value.
572          */
573         public DoubleModel(double value, UnitGroup unit) {
574                 this(value, unit, 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          * @param min           minimum value.
583          */
584         public DoubleModel(double value, UnitGroup unit, double min) {
585                 this(value, unit, min, Double.POSITIVE_INFINITY);
586         }
587
588         /**
589          * Generate a DoubleModel that contains an internal double value.
590          * 
591          * @param value         the initial value.
592          * @param unit          the unit for the value.
593          * @param min           minimum value.
594          * @param max           maximum value.
595          */
596         public DoubleModel(double value, UnitGroup unit, double min, double max) {
597                 this.lastValue = value;
598                 this.minValue = min;
599                 this.maxValue = max;
600
601                 source = null;
602                 valueName = "Constant value";
603                 multiplier = 1;
604
605                 getMethod = setMethod = null;
606                 getAutoMethod = setAutoMethod = null;
607                 units = unit;
608                 currentUnit = units.getDefaultUnit();
609         }
610
611
612         /**
613          * Generates a new DoubleModel that changes the values of the specified component.
614          * The double value is read and written using the methods "get"/"set" + valueName.
615          *  
616          * @param source Component whose parameter to use.
617          * @param valueName Name of methods used to get/set the parameter.
618          * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
619          * @param min Minimum value allowed (in SI units)
620          * @param max Maximum value allowed (in SI units)
621          */
622         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
623                         double min, double max) {
624                 this.source = source;
625                 this.valueName = valueName;
626                 this.multiplier = multiplier;
627
628                 this.units = unit;
629                 currentUnit = units.getDefaultUnit();
630
631                 this.minValue = min;
632                 this.maxValue = max;
633
634                 try {
635                         getMethod = source.getClass().getMethod("get" + valueName);
636                 } catch (NoSuchMethodException e) {
637                         throw new IllegalArgumentException("get method for value '" + valueName +
638                                         "' not present in class " + source.getClass().getCanonicalName());
639                 }
640
641                 Method s = null;
642                 try {
643                         s = source.getClass().getMethod("set" + valueName, double.class);
644                 } catch (NoSuchMethodException e1) {
645                 } // Ignore
646                 setMethod = s;
647
648                 // Automatic selection methods
649
650                 Method set = null, get = null;
651
652                 try {
653                         get = source.getClass().getMethod("is" + valueName + "Automatic");
654                         set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
655                 } catch (NoSuchMethodException e) {
656                 } // ignore
657
658                 if (set != null && get != null) {
659                         getAutoMethod = get;
660                         setAutoMethod = set;
661                 } else {
662                         getAutoMethod = null;
663                         setAutoMethod = null;
664                 }
665
666         }
667
668         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
669                         double min) {
670                 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
671         }
672
673         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
674                 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
675         }
676
677         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
678                         double min, double max) {
679                 this(source, valueName, 1.0, unit, min, max);
680         }
681
682         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
683                 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
684         }
685
686         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
687                 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
688         }
689
690         public DoubleModel(ChangeSource source, String valueName) {
691                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
692                                 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
693         }
694
695         public DoubleModel(ChangeSource source, String valueName, double min) {
696                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
697         }
698
699         public DoubleModel(ChangeSource source, String valueName, double min, double max) {
700                 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
701         }
702
703
704
705         /**
706          * Returns the value of the variable (in SI units).
707          */
708         public double getValue() {
709                 if (getMethod == null) // Constant value
710                         return lastValue;
711
712                 try {
713                         return (Double) getMethod.invoke(source) * multiplier;
714                 } catch (IllegalArgumentException e) {
715                         throw new BugException("Unable to invoke getMethod of " + this, e);
716                 } catch (IllegalAccessException e) {
717                         throw new BugException("Unable to invoke getMethod of " + this, e);
718                 } catch (InvocationTargetException e) {
719                         throw Reflection.handleWrappedException(e);
720                 }
721         }
722
723         /**
724          * Sets the value of the variable.
725          * @param v New value for parameter in SI units.
726          */
727         public void setValue(double v) {
728                 checkState(true);
729
730                 log.debug("Setting value " + v + " for " + this);
731                 if (setMethod == null) {
732                         if (getMethod != null) {
733                                 throw new BugException("setMethod not available for variable '" + valueName +
734                                                 "' in class " + source.getClass().getCanonicalName());
735                         }
736                         lastValue = v;
737                         fireStateChanged();
738                         return;
739                 }
740
741                 try {
742                         setMethod.invoke(source, v / multiplier);
743                 } catch (IllegalArgumentException e) {
744                         throw new BugException("Unable to invoke setMethod of " + this, e);
745                 } catch (IllegalAccessException e) {
746                         throw new BugException("Unable to invoke setMethod of " + this, e);
747                 } catch (InvocationTargetException e) {
748                         throw Reflection.handleWrappedException(e);
749                 }
750         }
751
752         /**
753          * Returns whether setting the value automatically is available.
754          */
755         public boolean isAutomaticAvailable() {
756                 return (getAutoMethod != null) && (setAutoMethod != null);
757         }
758
759         /**
760          * Returns whether the value is currently being set automatically.
761          * Returns false if automatic setting is not available at all.
762          */
763         public boolean isAutomatic() {
764                 if (getAutoMethod == null)
765                         return false;
766
767                 try {
768                         return (Boolean) getAutoMethod.invoke(source);
769                 } catch (IllegalArgumentException e) {
770                         throw new BugException("Method call failed", e);
771                 } catch (IllegalAccessException e) {
772                         throw new BugException("Method call failed", e);
773                 } catch (InvocationTargetException e) {
774                         throw Reflection.handleWrappedException(e);
775                 }
776         }
777
778         /**
779          * Sets whether the value should be set automatically.  Simply fires a
780          * state change event if automatic setting is not available.
781          */
782         public void setAutomatic(boolean auto) {
783                 checkState(true);
784
785                 if (setAutoMethod == null) {
786                         log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
787                         fireStateChanged(); // in case something is out-of-sync
788                         return;
789                 }
790
791                 log.debug("Setting automatic to " + auto + " for " + this);
792                 lastAutomatic = auto;
793                 try {
794                         setAutoMethod.invoke(source, auto);
795                 } catch (IllegalArgumentException e) {
796                         throw new BugException(e);
797                 } catch (IllegalAccessException e) {
798                         throw new BugException(e);
799                 } catch (InvocationTargetException e) {
800                         throw Reflection.handleWrappedException(e);
801                 }
802         }
803
804
805         /**
806          * Returns the current Unit.  At the beginning it is the default unit of the UnitGroup.
807          * @return The most recently set unit.
808          */
809         public Unit getCurrentUnit() {
810                 return currentUnit;
811         }
812
813         /**
814          * Sets the current Unit.  The unit must be one of those included in the UnitGroup.
815          * @param u  The unit to set active.
816          */
817         public void setCurrentUnit(Unit u) {
818                 checkState(true);
819                 if (currentUnit == u)
820                         return;
821                 log.debug("Setting unit for " + this + " to '" + u + "'");
822                 currentUnit = u;
823                 fireStateChanged();
824         }
825
826
827         /**
828          * Returns the UnitGroup associated with the parameter value.
829          *
830          * @return The UnitGroup given to the constructor.
831          */
832         public UnitGroup getUnitGroup() {
833                 return units;
834         }
835
836
837
838         /**
839          * Add a listener to the model.  Adds the model as a listener to the value source if this
840          * is the first listener.
841          * @param l Listener to add.
842          */
843         @Override
844         public void addChangeListener(EventListener l) {
845                 checkState(true);
846
847                 if (listeners.isEmpty()) {
848                         if (source != null) {
849                                 source.addChangeListener(this);
850                                 lastValue = getValue();
851                                 lastAutomatic = isAutomatic();
852                         }
853                 }
854
855                 listeners.add(l);
856                 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
857         }
858
859         /**
860          * Remove a listener from the model.  Removes the model from being a listener to the Component
861          * if this was the last listener of the model.
862          * @param l Listener to remove.
863          */
864         @Override
865         public void removeChangeListener(EventListener l) {
866                 checkState(false);
867
868                 listeners.remove(l);
869                 if (listeners.isEmpty() && source != null) {
870                         source.removeChangeListener(this);
871                 }
872                 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
873         }
874
875
876         /**
877          * Invalidates this model by removing all listeners and removing this from
878          * listening to the source.  After invalidation no listeners can be added to this
879          * model and the value cannot be set.
880          */
881         @Override
882         public void invalidate() {
883                 log.verbose("Invalidating " + this);
884                 invalidator.invalidate();
885
886                 if (!listeners.isEmpty()) {
887                         log.warn("Invalidating " + this + " while still having listeners " + listeners);
888                 }
889                 listeners.clear();
890                 if (source != null) {
891                         source.removeChangeListener(this);
892                 }
893                 MemoryManagement.collectable(this);
894         }
895
896
897         private void checkState(boolean error) {
898                 invalidator.check(error);
899         }
900
901
902         @Override
903         protected void finalize() throws Throwable {
904                 super.finalize();
905                 if (!listeners.isEmpty()) {
906                         log.warn(this + " being garbage-collected while having listeners " + listeners);
907                 }
908         };
909
910
911         /**
912          * Fire a ChangeEvent to all listeners.
913          */
914         protected void fireStateChanged() {
915                 checkState(true);
916
917                 EventObject event = new EventObject(this);
918                 ChangeEvent cevent = new ChangeEvent(this);
919                 firing++;
920                 // Copy the list before iterating to prevent concurrent modification exceptions.
921                 EventListener[] ls = listeners.toArray(new EventListener[0]);
922                 for (EventListener l : ls) {
923                         if ( l instanceof StateChangeListener ) {
924                                 ((StateChangeListener)l).stateChanged(event);
925                         } else if ( l instanceof ChangeListener ) {
926                                 ((ChangeListener)l).stateChanged(cevent);
927                         }
928                 }
929                 firing--;
930         }
931
932         /**
933          * Called when the component changes.  Checks whether the modeled value has changed, and if
934          * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
935          */
936         @Override
937         public void stateChanged(EventObject e) {
938                 checkState(true);
939
940                 double v = getValue();
941                 boolean b = isAutomatic();
942                 if (lastValue == v && lastAutomatic == b)
943                         return;
944                 lastValue = v;
945                 lastAutomatic = b;
946                 fireStateChanged();
947         }
948
949
950         /**
951          * Explain the DoubleModel as a String.
952          */
953         @Override
954         public String toString() {
955                 if (toString == null) {
956                         if (source == null) {
957                                 toString = "DoubleModel[constant=" + lastValue + "]";
958                         } else {
959                                 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
960                         }
961                 }
962                 return toString;
963         }
964 }