1 package net.sf.openrocket.gui.dialogs;
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;
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;
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;
61 * Dialog that allows scaling the rocket design.
63 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
65 public class ScaleDialog extends JDialog {
67 private static final LogHelper log = Application.getLogger();
68 private static final Translator trans = Application.getTranslator();
72 * Scaler implementations
74 * Each scaled value (except override cg/mass) is defined using a Scaler instance.
76 private static final Map<Class<? extends RocketComponent>, List<Scaler>> SCALERS =
77 new HashMap<Class<? extends RocketComponent>, List<Scaler>>();
82 addScaler(RocketComponent.class, "PositionValue");
83 SCALERS.get(RocketComponent.class).add(new OverrideScaler());
86 addScaler(BodyComponent.class, "Length");
89 addScaler(SymmetricComponent.class, "Thickness", "isFilled");
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");
102 addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic");
103 addScaler(BodyTube.class, "MotorOverhang");
106 addScaler(LaunchLug.class, "OuterRadius");
107 addScaler(LaunchLug.class, "Thickness");
108 addScaler(LaunchLug.class, "Length");
111 addScaler(FinSet.class, "Thickness");
112 addScaler(FinSet.class, "TabHeight");
113 addScaler(FinSet.class, "TabLength");
114 addScaler(FinSet.class, "TabShift");
117 addScaler(TrapezoidFinSet.class, "Sweep");
118 addScaler(TrapezoidFinSet.class, "RootChord");
119 addScaler(TrapezoidFinSet.class, "TipChord");
120 addScaler(TrapezoidFinSet.class, "Height");
123 addScaler(EllipticalFinSet.class, "Length");
124 addScaler(EllipticalFinSet.class, "Height");
127 list = new ArrayList<ScaleDialog.Scaler>(1);
128 list.add(new FreeformFinSetScaler());
129 SCALERS.put(FreeformFinSet.class, list);
132 addScaler(MassObject.class, "Length");
133 addScaler(MassObject.class, "Radius");
134 addScaler(MassObject.class, "RadialPosition");
137 list = new ArrayList<ScaleDialog.Scaler>(1);
138 list.add(new MassComponentScaler());
139 SCALERS.put(MassComponent.class, list);
142 addScaler(Parachute.class, "Diameter");
143 addScaler(Parachute.class, "LineLength");
146 addScaler(Streamer.class, "StripLength");
147 addScaler(Streamer.class, "StripWidth");
150 addScaler(ShockCord.class, "CordLength");
153 addScaler(RingComponent.class, "Length");
154 addScaler(RingComponent.class, "RadialPosition");
156 // ThicknessRingComponent
157 addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
158 addScaler(ThicknessRingComponent.class, "Thickness");
161 addScaler(InnerTube.class, "MotorOverhang");
163 // RadiusRingComponent
164 addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
165 addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic");
168 private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName) {
169 addScaler(componentClass, methodName, null);
172 private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
173 List<Scaler> list = SCALERS.get(componentClass);
175 list = new ArrayList<ScaleDialog.Scaler>();
176 SCALERS.put(componentClass, list);
178 list.add(new GeneralScaler(componentClass, methodName, autoMethodName));
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;
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");
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);
200 private final OpenRocketDocument document;
201 private final RocketComponent selection;
202 private final boolean onlySelection;
204 private JComboBox selectionOption;
205 private JCheckBox scaleMassValues;
207 private boolean changing = false;
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.
216 public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) {
217 this(document, selection, parent, false);
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)
228 public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent, Boolean onlySelection) {
229 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
231 this.document = document;
232 this.selection = selection;
233 this.onlySelection = onlySelection;
238 private void init() {
239 // Generate options for scaling
240 List<String> options = new ArrayList<String>();
242 options.add(SCALE_ROCKET);
243 if (selection != null && selection.getChildCount() > 0) {
244 options.add(SCALE_SUBSELECTION);
246 if (selection != null) {
247 options.add(SCALE_SELECTION);
252 * Select initial size for "from" field.
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.
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);
265 initialSize = selection.getLength();
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);
276 if (initialSize < 0.001) {
277 Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
278 initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
281 fromField.setValue(initialSize);
282 toField.setValue(initialSize);
285 // Add actions to the values
286 multiplier.addChangeListener(new ChangeListener() {
288 public void stateChanged(ChangeEvent e) {
296 fromField.addChangeListener(new ChangeListener() {
298 public void stateChanged(ChangeEvent e) {
306 toField.addChangeListener(new ChangeListener() {
308 public void stateChanged(ChangeEvent e) {
320 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
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");
330 selectionOption = new JComboBox(options.toArray());
331 selectionOption.setEditable(false);
332 selectionOption.setToolTipText(tip);
333 panel.add(selectionOption, "growx, wrap para*2");
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");
343 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
344 spin.setEditor(new SpinnerEditor(spin));
345 spin.setToolTipText(tip);
346 panel.add(spin, "w :30lp:65lp");
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");
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");
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");
367 unit = new UnitSelector(fromField);
368 unit.setToolTipText(tip);
369 panel.add(unit, "w 30lp");
371 label = new JLabel(trans.get("lbl.scaleTo"));
372 label.setToolTipText(tip);
373 panel.add(label, "gap unrel");
375 spin = new JSpinner(toField.getSpinnerModel());
376 spin.setEditor(new SpinnerEditor(spin));
377 spin.setToolTipText(tip);
378 panel.add(spin, "w :30lp:65lp");
380 unit = new UnitSelector(toField);
381 unit.setToolTipText(tip);
382 panel.add(unit, "w 30lp, wrap para*2");
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()) {
396 scaleMassValues.setEnabled(overridden);
397 panel.add(scaleMassValues, "span, wrap para*3");
402 JButton scale = new JButton(trans.get("button.scale"));
403 scale.addActionListener(new ActionListener() {
405 public void actionPerformed(ActionEvent e) {
407 ScaleDialog.this.setVisible(false);
410 panel.add(scale, "span, split, right, gap para");
412 JButton cancel = new JButton(trans.get("button.cancel"));
413 cancel.addActionListener(new ActionListener() {
415 public void actionPerformed(ActionEvent e) {
416 ScaleDialog.this.setVisible(false);
419 panel.add(cancel, "right, gap para");
423 GUIUtil.setDisposableDialogOptions(this, scale);
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);
435 if (MathUtil.equals(mul, 1.0)) {
437 log.user("Scaling by value 1.0 - nothing to do");
441 boolean scaleMass = scaleMassValues.isSelected();
443 Object item = selectionOption.getSelectedItem();
444 log.user("Scaling design by factor " + mul + ", option=" + item);
445 if (SCALE_ROCKET.equals(item)) {
447 // Scale the entire rocket design
449 document.startUndo(trans.get("undo.scaleRocket"));
450 for (RocketComponent c : document.getRocket()) {
451 scale(c, mul, scaleMass);
457 } else if (SCALE_SUBSELECTION.equals(item)) {
459 // Scale component and subcomponents
461 document.startUndo(trans.get("undo.scaleComponents"));
462 for (RocketComponent c : selection) {
463 scale(c, mul, scaleMass);
469 } else if (SCALE_SELECTION.equals(item)) {
471 // Scale only the selected component
473 document.startUndo(trans.get("undo.scaleComponent"));
474 scale(selection, mul, scaleMass);
480 throw new BugException("Unknown item selected, item=" + item);
486 * Perform scaling on a single component.
488 private void scale(RocketComponent component, double mul, boolean scaleMass) {
490 Class<?> clazz = component.getClass();
491 while (clazz != null) {
492 List<Scaler> list = SCALERS.get(clazz);
494 for (Scaler s : list) {
495 s.scale(component, mul, scaleMass);
499 clazz = clazz.getSuperclass();
504 private void updateToField() {
505 double mul = multiplier.getValue();
506 double from = fromField.getValue();
507 double to = from * mul;
508 toField.setValue(to);
511 private void updateMultiplier() {
512 double from = fromField.getValue();
513 double to = toField.getValue();
514 double mul = to / from;
516 if (!MathUtil.equals(from, 0)) {
517 mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
518 multiplier.setValue(mul);
526 * Interface for scaling a specific component/value.
528 private interface Scaler {
529 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
533 * General scaler implementation that uses reflection to get/set a specific value.
535 private static class GeneralScaler implements Scaler {
537 private final Method getter;
538 private final Method setter;
539 private final Method autoMethod;
541 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
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);
554 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
556 // Do not scale if set to automatic
557 if (autoMethod != null) {
558 boolean auto = (Boolean) autoMethod.invoke(c);
565 double value = (Double) getter.invoke(c);
566 value = value * multiplier;
567 setter.invoke(c, value);
573 private static class OverrideScaler implements Scaler {
576 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
578 if (component.isCGOverridden()) {
579 double cgx = component.getOverrideCGX();
580 cgx = cgx * multiplier;
581 component.setOverrideCGX(cgx);
584 if (scaleMass && component.isMassOverridden()) {
585 double mass = component.getOverrideMass();
586 mass = mass * MathUtil.pow3(multiplier);
587 component.setOverrideMass(mass);
593 private static class MassComponentScaler implements Scaler {
596 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
598 MassComponent c = (MassComponent) component;
599 double mass = c.getComponentMass();
600 mass = mass * MathUtil.pow3(multiplier);
601 c.setComponentMass(mass);
607 private static class FreeformFinSetScaler implements Scaler {
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);
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);