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