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