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