language selector, bug fixed
[debian/openrocket] / src / net / sf / openrocket / gui / dialogs / ScaleDialog.java
1 package net.sf.openrocket.gui.dialogs;
2
3 import java.awt.Window;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
11
12 import javax.swing.JButton;
13 import javax.swing.JCheckBox;
14 import javax.swing.JComboBox;
15 import javax.swing.JDialog;
16 import javax.swing.JLabel;
17 import javax.swing.JPanel;
18 import javax.swing.JSpinner;
19 import javax.swing.event.ChangeEvent;
20 import javax.swing.event.ChangeListener;
21
22 import net.miginfocom.swing.MigLayout;
23 import net.sf.openrocket.document.OpenRocketDocument;
24 import net.sf.openrocket.gui.SpinnerEditor;
25 import net.sf.openrocket.gui.adaptors.DoubleModel;
26 import net.sf.openrocket.gui.components.BasicSlider;
27 import net.sf.openrocket.gui.components.UnitSelector;
28 import net.sf.openrocket.gui.main.ExceptionHandler;
29 import net.sf.openrocket.l10n.Translator;
30 import net.sf.openrocket.logging.LogHelper;
31 import net.sf.openrocket.rocketcomponent.BodyComponent;
32 import net.sf.openrocket.rocketcomponent.BodyTube;
33 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
34 import net.sf.openrocket.rocketcomponent.FinSet;
35 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
36 import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
37 import net.sf.openrocket.rocketcomponent.InnerTube;
38 import net.sf.openrocket.rocketcomponent.LaunchLug;
39 import net.sf.openrocket.rocketcomponent.MassComponent;
40 import net.sf.openrocket.rocketcomponent.MassObject;
41 import net.sf.openrocket.rocketcomponent.Parachute;
42 import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
43 import net.sf.openrocket.rocketcomponent.RingComponent;
44 import net.sf.openrocket.rocketcomponent.RocketComponent;
45 import net.sf.openrocket.rocketcomponent.ShockCord;
46 import net.sf.openrocket.rocketcomponent.Streamer;
47 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
48 import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
49 import net.sf.openrocket.rocketcomponent.Transition;
50 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
51 import net.sf.openrocket.startup.Application;
52 import net.sf.openrocket.unit.Unit;
53 import net.sf.openrocket.unit.UnitGroup;
54 import net.sf.openrocket.util.BugException;
55 import net.sf.openrocket.util.Coordinate;
56 import net.sf.openrocket.util.GUIUtil;
57 import net.sf.openrocket.util.MathUtil;
58 import net.sf.openrocket.util.Reflection;
59 import net.sf.openrocket.util.Reflection.Method;
60
61 /**
62  * Dialog that allows scaling the rocket design.
63  * 
64  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
65  */
66 public class ScaleDialog extends JDialog {
67         
68         private static final LogHelper log = Application.getLogger();
69         private static final Translator trans = Application.getTranslator();
70         
71
72         /*
73          * Scaler implementations
74          * 
75          * Each scaled value (except override cg/mass) is defined using a Scaler instance.
76          */
77         private static final Map<Class<? extends RocketComponent>, List<Scaler>> SCALERS =
78                         new HashMap<Class<? extends RocketComponent>, List<Scaler>>();
79         static {
80                 List<Scaler> list;
81                 
82                 // RocketComponent
83                 addScaler(RocketComponent.class, "PositionValue");
84                 SCALERS.get(RocketComponent.class).add(new OverrideScaler());
85                 
86                 // BodyComponent
87                 addScaler(BodyComponent.class, "Length");
88                 
89                 // SymmetricComponent
90                 addScaler(SymmetricComponent.class, "Thickness", "isFilled");
91                 
92                 // Transition + Nose cone
93                 addScaler(Transition.class, "ForeRadius", "isForeRadiusAutomatic");
94                 addScaler(Transition.class, "AftRadius", "isAftRadiusAutomatic");
95                 addScaler(Transition.class, "ForeShoulderRadius");
96                 addScaler(Transition.class, "ForeShoulderThickness");
97                 addScaler(Transition.class, "ForeShoulderLength");
98                 addScaler(Transition.class, "AftShoulderRadius");
99                 addScaler(Transition.class, "AftShoulderThickness");
100                 addScaler(Transition.class, "AftShoulderLength");
101                 
102                 // Body tube
103                 addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic");
104                 addScaler(BodyTube.class, "MotorOverhang");
105                 
106                 // Launch lug
107                 addScaler(LaunchLug.class, "OuterRadius");
108                 addScaler(LaunchLug.class, "Thickness");
109                 addScaler(LaunchLug.class, "Length");
110                 
111                 // FinSet
112                 addScaler(FinSet.class, "Thickness");
113                 addScaler(FinSet.class, "TabHeight");
114                 addScaler(FinSet.class, "TabLength");
115                 addScaler(FinSet.class, "TabShift");
116                 
117                 // TrapezoidFinSet
118                 addScaler(TrapezoidFinSet.class, "Sweep");
119                 addScaler(TrapezoidFinSet.class, "RootChord");
120                 addScaler(TrapezoidFinSet.class, "TipChord");
121                 addScaler(TrapezoidFinSet.class, "Height");
122                 
123                 // EllipticalFinSet
124                 addScaler(EllipticalFinSet.class, "Length");
125                 addScaler(EllipticalFinSet.class, "Height");
126                 
127                 // FreeformFinSet
128                 list = new ArrayList<ScaleDialog.Scaler>(1);
129                 list.add(new FreeformFinSetScaler());
130                 SCALERS.put(FreeformFinSet.class, list);
131                 
132                 // MassObject
133                 addScaler(MassObject.class, "Length");
134                 addScaler(MassObject.class, "Radius");
135                 addScaler(MassObject.class, "RadialPosition");
136                 
137                 // MassComponent
138                 list = new ArrayList<ScaleDialog.Scaler>(1);
139                 list.add(new MassComponentScaler());
140                 SCALERS.put(MassComponent.class, list);
141                 
142                 // Parachute
143                 addScaler(Parachute.class, "Diameter");
144                 addScaler(Parachute.class, "LineLength");
145                 
146                 // Streamer
147                 addScaler(Streamer.class, "StripLength");
148                 addScaler(Streamer.class, "StripWidth");
149                 
150                 // ShockCord
151                 addScaler(ShockCord.class, "CordLength");
152                 
153                 // RingComponent
154                 addScaler(RingComponent.class, "Length");
155                 addScaler(RingComponent.class, "RadialPosition");
156                 
157                 // ThicknessRingComponent
158                 addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
159                 addScaler(ThicknessRingComponent.class, "Thickness");
160                 
161                 // InnerTube
162                 addScaler(InnerTube.class, "MotorOverhang");
163                 
164                 // RadiusRingComponent
165                 addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
166                 addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic");
167         }
168         
169         private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName) {
170                 addScaler(componentClass, methodName, null);
171         }
172         
173         private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
174                 List<Scaler> list = SCALERS.get(componentClass);
175                 if (list == null) {
176                         list = new ArrayList<ScaleDialog.Scaler>();
177                         SCALERS.put(componentClass, list);
178                 }
179                 list.add(new GeneralScaler(componentClass, methodName, autoMethodName));
180         }
181         
182         
183
184
185
186         private static final double DEFAULT_INITIAL_SIZE = 0.1; // meters
187         private static final double SCALE_MIN = 0.01;
188         private static final double SCALE_MAX = 100.0;
189         
190         private static final String SCALE_ROCKET = trans.get("lbl.scaleRocket");
191         private static final String SCALE_SUBSELECTION = trans.get("lbl.scaleSubselection");
192         private static final String SCALE_SELECTION = trans.get("lbl.scaleSelection");
193         
194
195
196
197         private final DoubleModel multiplier = new DoubleModel(1.0, UnitGroup.UNITS_RELATIVE, SCALE_MIN, SCALE_MAX);
198         private final DoubleModel fromField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0);
199         private final DoubleModel toField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0);
200         
201         private final OpenRocketDocument document;
202         private final RocketComponent selection;
203         
204         private final JComboBox selectionOption;
205         private final JCheckBox scaleMassValues;
206         
207         private boolean changing = false;
208         
209         /**
210          * Sole constructor.
211          * 
212          * @param document              the document to modify.
213          * @param selection             the currently selected component (or <code>null</code> if none selected).
214          * @param parent                the parent window.
215          */
216         public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) {
217                 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
218                 
219                 this.document = document;
220                 this.selection = selection;
221                 
222                 // Generate options for scaling
223                 List<String> options = new ArrayList<String>();
224                 options.add(SCALE_ROCKET);
225                 if (selection != null && selection.getChildCount() > 0) {
226                         options.add(SCALE_SUBSELECTION);
227                 }
228                 if (selection != null) {
229                         options.add(SCALE_SELECTION);
230                 }
231                 
232
233                 /*
234                  * Select initial size for "from" field.
235                  * 
236                  * If a component is selected, either its diameter (for SymmetricComponents) or length is selected.
237                  * Otherwise the maximum body diameter is selected.  As a fallback DEFAULT_INITIAL_SIZE is used.
238                  */
239                 // 
240                 double initialSize = 0;
241                 if (selection != null) {
242                         if (selection instanceof SymmetricComponent) {
243                                 SymmetricComponent s = (SymmetricComponent) selection;
244                                 initialSize = s.getForeRadius() * 2;
245                                 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
246                         } else {
247                                 initialSize = selection.getLength();
248                         }
249                 } else {
250                         for (RocketComponent c : document.getRocket()) {
251                                 if (c instanceof SymmetricComponent) {
252                                         SymmetricComponent s = (SymmetricComponent) c;
253                                         initialSize = s.getForeRadius() * 2;
254                                         initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
255                                 }
256                         }
257                 }
258                 if (initialSize < 0.001) {
259                         Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
260                         initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
261                 }
262                 
263                 fromField.setValue(initialSize);
264                 toField.setValue(initialSize);
265                 
266
267                 // Add actions to the values
268                 multiplier.addChangeListener(new ChangeListener() {
269                         @Override
270                         public void stateChanged(ChangeEvent e) {
271                                 if (!changing) {
272                                         changing = true;
273                                         updateToField();
274                                         changing = false;
275                                 }
276                         }
277                 });
278                 fromField.addChangeListener(new ChangeListener() {
279                         @Override
280                         public void stateChanged(ChangeEvent e) {
281                                 if (!changing) {
282                                         changing = true;
283                                         updateToField();
284                                         changing = false;
285                                 }
286                         }
287                 });
288                 toField.addChangeListener(new ChangeListener() {
289                         @Override
290                         public void stateChanged(ChangeEvent e) {
291                                 if (!changing) {
292                                         changing = true;
293                                         updateMultiplier();
294                                         changing = false;
295                                 }
296                         }
297                 });
298                 
299
300
301                 String tip;
302                 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
303                 this.add(panel);
304                 
305
306                 // Scaling selection
307                 tip = trans.get("lbl.scale.ttip");
308                 JLabel label = new JLabel(trans.get("lbl.scale"));
309                 label.setToolTipText(tip);
310                 panel.add(label, "span, split, gapright unrel");
311                 
312                 selectionOption = new JComboBox(options.toArray());
313                 selectionOption.setEditable(false);
314                 selectionOption.setToolTipText(tip);
315                 panel.add(selectionOption, "growx, wrap para*2");
316                 
317
318                 // Scale multiplier
319                 tip = trans.get("lbl.scaling.ttip");
320                 label = new JLabel(trans.get("lbl.scaling"));
321                 label.setToolTipText(tip);
322                 panel.add(label, "gapright unrel");
323                 
324
325                 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
326                 spin.setEditor(new SpinnerEditor(spin));
327                 spin.setToolTipText(tip);
328                 panel.add(spin, "w :30lp:65lp");
329                 
330                 UnitSelector unit = new UnitSelector(multiplier);
331                 unit.setToolTipText(tip);
332                 panel.add(unit, "w 30lp");
333                 BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0));
334                 slider.setToolTipText(tip);
335                 panel.add(slider, "w 100lp, growx, wrap para");
336                 
337
338                 // Scale from ... to ...
339                 tip = trans.get("lbl.scaleFromTo.ttip");
340                 label = new JLabel(trans.get("lbl.scaleFrom"));
341                 label.setToolTipText(tip);
342                 panel.add(label, "gapright unrel, right");
343                 
344                 spin = new JSpinner(fromField.getSpinnerModel());
345                 spin.setEditor(new SpinnerEditor(spin));
346                 spin.setToolTipText(tip);
347                 panel.add(spin, "span, split, w :30lp:65lp");
348                 
349                 unit = new UnitSelector(fromField);
350                 unit.setToolTipText(tip);
351                 panel.add(unit, "w 30lp");
352                 
353                 label = new JLabel(trans.get("lbl.scaleTo"));
354                 label.setToolTipText(tip);
355                 panel.add(label, "gap unrel");
356                 
357                 spin = new JSpinner(toField.getSpinnerModel());
358                 spin.setEditor(new SpinnerEditor(spin));
359                 spin.setToolTipText(tip);
360                 panel.add(spin, "w :30lp:65lp");
361                 
362                 unit = new UnitSelector(toField);
363                 unit.setToolTipText(tip);
364                 panel.add(unit, "w 30lp, wrap para*2");
365                 
366
367                 // Scale override
368                 scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass"));
369                 scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip"));
370                 scaleMassValues.setSelected(true);
371                 boolean overridden = false;
372                 for (RocketComponent c : document.getRocket()) {
373                         if (c instanceof MassComponent || c.isMassOverridden()) {
374                                 overridden = true;
375                                 break;
376                         }
377                 }
378                 scaleMassValues.setEnabled(overridden);
379                 panel.add(scaleMassValues, "span, wrap para*3");
380                 
381
382                 // Buttons
383                 
384                 JButton scale = new JButton(trans.get("button.scale"));
385                 scale.addActionListener(new ActionListener() {
386                         @Override
387                         public void actionPerformed(ActionEvent e) {
388                                 doScale();
389                                 ScaleDialog.this.setVisible(false);
390                         }
391                 });
392                 panel.add(scale, "span, split, right, gap para");
393                 
394                 JButton cancel = new JButton(trans.get("button.cancel"));
395                 cancel.addActionListener(new ActionListener() {
396                         @Override
397                         public void actionPerformed(ActionEvent e) {
398                                 ScaleDialog.this.setVisible(false);
399                         }
400                 });
401                 panel.add(cancel, "right, gap para");
402                 
403
404
405                 GUIUtil.setDisposableDialogOptions(this, scale);
406         }
407         
408         
409
410         private void doScale() {
411                 double mul = multiplier.getValue();
412                 if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) {
413                         ExceptionHandler.handleErrorCondition("Illegal multiplier value, mul=" + mul);
414                         return;
415                 }
416                 
417                 if (MathUtil.equals(mul, 1.0)) {
418                         // Nothing to do
419                         log.user("Scaling by value 1.0 - nothing to do");
420                         return;
421                 }
422                 
423                 boolean scaleMass = scaleMassValues.isSelected();
424                 
425                 Object item = selectionOption.getSelectedItem();
426                 log.user("Scaling design by factor " + mul + ", option=" + item);
427                 if (SCALE_ROCKET.equals(item)) {
428                         
429                         // Scale the entire rocket design
430                         try {
431                                 document.startUndo(trans.get("undo.scaleRocket"));
432                                 for (RocketComponent c : document.getRocket()) {
433                                         scale(c, mul, scaleMass);
434                                 }
435                         } finally {
436                                 document.stopUndo();
437                         }
438                         
439                 } else if (SCALE_SUBSELECTION.equals(item)) {
440                         
441                         // Scale component and subcomponents
442                         try {
443                                 document.startUndo(trans.get("undo.scaleComponents"));
444                                 for (RocketComponent c : selection) {
445                                         scale(c, mul, scaleMass);
446                                 }
447                         } finally {
448                                 document.stopUndo();
449                         }
450                         
451                 } else if (SCALE_SELECTION.equals(item)) {
452                         
453                         // Scale only the selected component
454                         try {
455                                 document.startUndo(trans.get("undo.scaleComponent"));
456                                 scale(selection, mul, scaleMass);
457                         } finally {
458                                 document.stopUndo();
459                         }
460                         
461                 } else {
462                         throw new BugException("Unknown item selected, item=" + item);
463                 }
464         }
465         
466         
467         /**
468          * Perform scaling on a single component.
469          */
470         private void scale(RocketComponent component, double mul, boolean scaleMass) {
471                 
472                 Class<?> clazz = component.getClass();
473                 while (clazz != null) {
474                         List<Scaler> list = SCALERS.get(clazz);
475                         if (list != null) {
476                                 for (Scaler s : list) {
477                                         s.scale(component, mul, scaleMass);
478                                 }
479                         }
480                         
481                         clazz = clazz.getSuperclass();
482                 }
483         }
484         
485         
486         private void updateToField() {
487                 double mul = multiplier.getValue();
488                 double from = fromField.getValue();
489                 double to = from * mul;
490                 toField.setValue(to);
491         }
492         
493         private void updateMultiplier() {
494                 double from = fromField.getValue();
495                 double to = toField.getValue();
496                 double mul = to / from;
497                 
498                 if (!MathUtil.equals(from, 0)) {
499                         mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
500                         multiplier.setValue(mul);
501                 }
502                 updateToField();
503         }
504         
505         
506
507         /**
508          * Interface for scaling a specific component/value.
509          */
510         private interface Scaler {
511                 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
512         }
513         
514         /**
515          * General scaler implementation that uses reflection to get/set a specific value.
516          */
517         private static class GeneralScaler implements Scaler {
518                 
519                 private final Method getter;
520                 private final Method setter;
521                 private final Method autoMethod;
522                 
523                 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
524                         
525                         getter = Reflection.findMethod(componentClass, "get" + methodName);
526                         setter = Reflection.findMethod(componentClass, "set" + methodName, double.class);
527                         if (autoMethodName != null) {
528                                 autoMethod = Reflection.findMethod(componentClass, autoMethodName);
529                         } else {
530                                 autoMethod = null;
531                         }
532                         
533                 }
534                 
535                 @Override
536                 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
537                         
538                         // Do not scale if set to automatic
539                         if (autoMethod != null) {
540                                 boolean auto = (Boolean) autoMethod.invoke(c);
541                                 if (auto) {
542                                         return;
543                                 }
544                         }
545                         
546                         // Scale value
547                         double value = (Double) getter.invoke(c);
548                         value = value * multiplier;
549                         setter.invoke(c, value);
550                 }
551                 
552         }
553         
554         
555         private static class OverrideScaler implements Scaler {
556                 
557                 @Override
558                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
559                         
560                         if (component.isCGOverridden()) {
561                                 double cgx = component.getOverrideCGX();
562                                 cgx = cgx * multiplier;
563                                 component.setOverrideCGX(cgx);
564                         }
565                         
566                         if (scaleMass && component.isMassOverridden()) {
567                                 double mass = component.getOverrideMass();
568                                 mass = mass * MathUtil.pow3(multiplier);
569                                 component.setOverrideMass(mass);
570                         }
571                 }
572                 
573         }
574         
575         private static class MassComponentScaler implements Scaler {
576                 
577                 @Override
578                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
579                         if (scaleMass) {
580                                 MassComponent c = (MassComponent) component;
581                                 double mass = c.getComponentMass();
582                                 mass = mass * MathUtil.pow3(multiplier);
583                                 c.setComponentMass(mass);
584                         }
585                 }
586                 
587         }
588         
589         private static class FreeformFinSetScaler implements Scaler {
590                 
591                 @Override
592                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
593                         FreeformFinSet finset = (FreeformFinSet) component;
594                         Coordinate[] points = finset.getFinPoints();
595                         for (int i = 0; i < points.length; i++) {
596                                 points[i] = points[i].multiply(multiplier);
597                         }
598                         try {
599                                 finset.setPoints(points);
600                         } catch (IllegalFinPointException e) {
601                                 throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e);
602                         }
603                 }
604                 
605         }
606         
607 }