Component scaling support
[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         // FIXME: Localize
210         
211         /**
212          * Sole constructor.
213          * 
214          * @param document              the document to modify.
215          * @param selection             the currently selected component (or <code>null</code> if none selected).
216          * @param parent                the parent window.
217          */
218         public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) {
219                 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
220                 
221                 this.document = document;
222                 this.selection = selection;
223                 
224                 // Generate options for scaling
225                 List<String> options = new ArrayList<String>();
226                 options.add(SCALE_ROCKET);
227                 if (selection != null && selection.getChildCount() > 0) {
228                         options.add(SCALE_SUBSELECTION);
229                 }
230                 if (selection != null) {
231                         options.add(SCALE_SELECTION);
232                 }
233                 
234
235                 /*
236                  * Select initial size for "from" field.
237                  * 
238                  * If a component is selected, either its diameter (for SymmetricComponents) or length is selected.
239                  * Otherwise the maximum body diameter is selected.  As a fallback DEFAULT_INITIAL_SIZE is used.
240                  */
241                 // 
242                 double initialSize = 0;
243                 if (selection != null) {
244                         if (selection instanceof SymmetricComponent) {
245                                 SymmetricComponent s = (SymmetricComponent) selection;
246                                 initialSize = s.getForeRadius() * 2;
247                                 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
248                         } else {
249                                 initialSize = selection.getLength();
250                         }
251                 } else {
252                         for (RocketComponent c : document.getRocket()) {
253                                 if (c instanceof SymmetricComponent) {
254                                         SymmetricComponent s = (SymmetricComponent) c;
255                                         initialSize = s.getForeRadius() * 2;
256                                         initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
257                                 }
258                         }
259                 }
260                 if (initialSize < 0.001) {
261                         Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
262                         initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
263                 }
264                 
265                 fromField.setValue(initialSize);
266                 toField.setValue(initialSize);
267                 
268
269                 // Add actions to the values
270                 multiplier.addChangeListener(new ChangeListener() {
271                         @Override
272                         public void stateChanged(ChangeEvent e) {
273                                 if (!changing) {
274                                         changing = true;
275                                         updateToField();
276                                         changing = false;
277                                 }
278                         }
279                 });
280                 fromField.addChangeListener(new ChangeListener() {
281                         @Override
282                         public void stateChanged(ChangeEvent e) {
283                                 if (!changing) {
284                                         changing = true;
285                                         updateToField();
286                                         changing = false;
287                                 }
288                         }
289                 });
290                 toField.addChangeListener(new ChangeListener() {
291                         @Override
292                         public void stateChanged(ChangeEvent e) {
293                                 if (!changing) {
294                                         changing = true;
295                                         updateMultiplier();
296                                         changing = false;
297                                 }
298                         }
299                 });
300                 
301
302
303                 String tip;
304                 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
305                 this.add(panel);
306                 
307
308                 // Scaling selection
309                 tip = trans.get("lbl.scale.ttip");
310                 JLabel label = new JLabel(trans.get("lbl.scale"));
311                 label.setToolTipText(tip);
312                 panel.add(label, "span, split, gapright unrel");
313                 
314                 selectionOption = new JComboBox(options.toArray());
315                 selectionOption.setEditable(false);
316                 selectionOption.setToolTipText(tip);
317                 panel.add(selectionOption, "growx, wrap para*2");
318                 
319
320                 // Scale multiplier
321                 tip = trans.get("lbl.scaling.ttip");
322                 label = new JLabel(trans.get("lbl.scaling"));
323                 label.setToolTipText(tip);
324                 panel.add(label, "gapright unrel");
325                 
326
327                 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
328                 spin.setEditor(new SpinnerEditor(spin));
329                 spin.setToolTipText(tip);
330                 panel.add(spin, "w :30lp:65lp");
331                 
332                 UnitSelector unit = new UnitSelector(multiplier);
333                 unit.setToolTipText(tip);
334                 panel.add(unit, "w 30lp");
335                 BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0));
336                 slider.setToolTipText(tip);
337                 panel.add(slider, "w 100lp, growx, wrap para");
338                 
339
340                 // Scale from ... to ...
341                 tip = trans.get("lbl.scaleFromTo.ttip");
342                 label = new JLabel(trans.get("lbl.scaleFrom"));
343                 label.setToolTipText(tip);
344                 panel.add(label, "gapright unrel, right");
345                 
346                 spin = new JSpinner(fromField.getSpinnerModel());
347                 spin.setEditor(new SpinnerEditor(spin));
348                 spin.setToolTipText(tip);
349                 panel.add(spin, "span, split, w :30lp:65lp");
350                 
351                 unit = new UnitSelector(fromField);
352                 unit.setToolTipText(tip);
353                 panel.add(unit, "w 30lp");
354                 
355                 label = new JLabel(trans.get("lbl.scaleTo"));
356                 label.setToolTipText(tip);
357                 panel.add(label, "gap unrel");
358                 
359                 spin = new JSpinner(toField.getSpinnerModel());
360                 spin.setEditor(new SpinnerEditor(spin));
361                 spin.setToolTipText(tip);
362                 panel.add(spin, "w :30lp:65lp");
363                 
364                 unit = new UnitSelector(toField);
365                 unit.setToolTipText(tip);
366                 panel.add(unit, "w 30lp, wrap para*2");
367                 
368
369                 // Scale override
370                 scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass"));
371                 scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip"));
372                 scaleMassValues.setSelected(true);
373                 boolean overridden = false;
374                 for (RocketComponent c : document.getRocket()) {
375                         if (c instanceof MassComponent || c.isMassOverridden()) {
376                                 overridden = true;
377                                 break;
378                         }
379                 }
380                 scaleMassValues.setEnabled(overridden);
381                 panel.add(scaleMassValues, "span, wrap para*3");
382                 
383
384                 // Buttons
385                 
386                 JButton scale = new JButton(trans.get("button.scale"));
387                 scale.addActionListener(new ActionListener() {
388                         @Override
389                         public void actionPerformed(ActionEvent e) {
390                                 doScale();
391                                 ScaleDialog.this.setVisible(false);
392                         }
393                 });
394                 panel.add(scale, "span, split, right, gap para");
395                 
396                 JButton cancel = new JButton(trans.get("button.cancel"));
397                 cancel.addActionListener(new ActionListener() {
398                         @Override
399                         public void actionPerformed(ActionEvent e) {
400                                 ScaleDialog.this.setVisible(false);
401                         }
402                 });
403                 panel.add(cancel, "right, gap para");
404                 
405
406
407                 GUIUtil.setDisposableDialogOptions(this, scale);
408         }
409         
410         
411
412         private void doScale() {
413                 double mul = multiplier.getValue();
414                 if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) {
415                         ExceptionHandler.handleErrorCondition("Illegal multiplier value, mul=" + mul);
416                         return;
417                 }
418                 
419                 if (MathUtil.equals(mul, 1.0)) {
420                         // Nothing to do
421                         log.user("Scaling by value 1.0 - nothing to do");
422                         return;
423                 }
424                 
425                 boolean scaleMass = scaleMassValues.isSelected();
426                 
427                 Object item = selectionOption.getSelectedItem();
428                 log.user("Scaling design by factor " + mul + ", option=" + item);
429                 if (SCALE_ROCKET.equals(item)) {
430                         
431                         // Scale the entire rocket design
432                         try {
433                                 document.startUndo("Scale rocket");
434                                 for (RocketComponent c : document.getRocket()) {
435                                         scale(c, mul, scaleMass);
436                                 }
437                         } finally {
438                                 document.stopUndo();
439                         }
440                         
441                 } else if (SCALE_SUBSELECTION.equals(item)) {
442                         
443                         // Scale component and subcomponents
444                         try {
445                                 document.startUndo("Scale components");
446                                 for (RocketComponent c : selection) {
447                                         scale(c, mul, scaleMass);
448                                 }
449                         } finally {
450                                 document.stopUndo();
451                         }
452                         
453                 } else if (SCALE_SELECTION.equals(item)) {
454                         
455                         // Scale only the selected component
456                         try {
457                                 document.startUndo("Scale component");
458                                 scale(selection, mul, scaleMass);
459                         } finally {
460                                 document.stopUndo();
461                         }
462                         
463                 } else {
464                         throw new BugException("Unknown item selected, item=" + item);
465                 }
466         }
467         
468         
469         /**
470          * Perform scaling on a single component.
471          */
472         private void scale(RocketComponent component, double mul, boolean scaleMass) {
473                 
474                 Class<?> clazz = component.getClass();
475                 while (clazz != null) {
476                         List<Scaler> list = SCALERS.get(clazz);
477                         if (list != null) {
478                                 for (Scaler s : list) {
479                                         s.scale(component, mul, scaleMass);
480                                 }
481                         }
482                         
483                         clazz = clazz.getSuperclass();
484                 }
485         }
486         
487         
488         private void updateToField() {
489                 double mul = multiplier.getValue();
490                 double from = fromField.getValue();
491                 double to = from * mul;
492                 toField.setValue(to);
493         }
494         
495         private void updateMultiplier() {
496                 double from = fromField.getValue();
497                 double to = toField.getValue();
498                 double mul = to / from;
499                 
500                 if (!MathUtil.equals(from, 0)) {
501                         mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
502                         multiplier.setValue(mul);
503                 }
504                 updateToField();
505         }
506         
507         
508
509         /**
510          * Interface for scaling a specific component/value.
511          */
512         private interface Scaler {
513                 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
514         }
515         
516         /**
517          * General scaler implementation that uses reflection to get/set a specific value.
518          */
519         private static class GeneralScaler implements Scaler {
520                 
521                 private final Method getter;
522                 private final Method setter;
523                 private final Method autoMethod;
524                 
525                 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
526                         
527                         getter = Reflection.findMethod(componentClass, "get" + methodName);
528                         setter = Reflection.findMethod(componentClass, "set" + methodName, double.class);
529                         if (autoMethodName != null) {
530                                 autoMethod = Reflection.findMethod(componentClass, autoMethodName);
531                         } else {
532                                 autoMethod = null;
533                         }
534                         
535                 }
536                 
537                 @Override
538                 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
539                         
540                         // Do not scale if set to automatic
541                         if (autoMethod != null) {
542                                 boolean auto = (Boolean) autoMethod.invoke(c);
543                                 if (auto) {
544                                         return;
545                                 }
546                         }
547                         
548                         // Scale value
549                         double value = (Double) getter.invoke(c);
550                         value = value * multiplier;
551                         setter.invoke(c, value);
552                 }
553                 
554         }
555         
556         
557         private static class OverrideScaler implements Scaler {
558                 
559                 @Override
560                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
561                         
562                         if (component.isCGOverridden()) {
563                                 double cgx = component.getOverrideCGX();
564                                 cgx = cgx * multiplier;
565                                 component.setOverrideCGX(cgx);
566                         }
567                         
568                         if (scaleMass && component.isMassOverridden()) {
569                                 double mass = component.getOverrideMass();
570                                 mass = mass * MathUtil.pow3(multiplier);
571                                 component.setOverrideMass(mass);
572                         }
573                 }
574                 
575         }
576         
577         private static class MassComponentScaler implements Scaler {
578                 
579                 @Override
580                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
581                         if (scaleMass) {
582                                 MassComponent c = (MassComponent) component;
583                                 double mass = c.getComponentMass();
584                                 mass = mass * MathUtil.pow3(multiplier);
585                                 c.setComponentMass(mass);
586                         }
587                 }
588                 
589         }
590         
591         private static class FreeformFinSetScaler implements Scaler {
592                 
593                 @Override
594                 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
595                         FreeformFinSet finset = (FreeformFinSet) component;
596                         Coordinate[] points = finset.getFinPoints();
597                         for (int i = 0; i < points.length; i++) {
598                                 points[i] = points[i].multiply(multiplier);
599                         }
600                         try {
601                                 finset.setPoints(points);
602                         } catch (IllegalFinPointException e) {
603                                 throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e);
604                         }
605                 }
606                 
607         }
608         
609 }