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