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