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