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.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;
62 * Dialog that allows scaling the rocket design.
64 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
66 public class ScaleDialog extends JDialog {
68 private static final LogHelper log = Application.getLogger();
69 private static final Translator trans = Application.getTranslator();
73 * Scaler implementations
75 * Each scaled value (except override cg/mass) is defined using a Scaler instance.
77 private static final Map<Class<? extends RocketComponent>, List<Scaler>> SCALERS =
78 new HashMap<Class<? extends RocketComponent>, List<Scaler>>();
83 addScaler(RocketComponent.class, "PositionValue");
84 SCALERS.get(RocketComponent.class).add(new OverrideScaler());
87 addScaler(BodyComponent.class, "Length");
90 addScaler(SymmetricComponent.class, "Thickness", "isFilled");
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");
103 addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic");
104 addScaler(BodyTube.class, "MotorOverhang");
107 addScaler(LaunchLug.class, "OuterRadius");
108 addScaler(LaunchLug.class, "Thickness");
109 addScaler(LaunchLug.class, "Length");
112 addScaler(FinSet.class, "Thickness");
113 addScaler(FinSet.class, "TabHeight");
114 addScaler(FinSet.class, "TabLength");
115 addScaler(FinSet.class, "TabShift");
118 addScaler(TrapezoidFinSet.class, "Sweep");
119 addScaler(TrapezoidFinSet.class, "RootChord");
120 addScaler(TrapezoidFinSet.class, "TipChord");
121 addScaler(TrapezoidFinSet.class, "Height");
124 addScaler(EllipticalFinSet.class, "Length");
125 addScaler(EllipticalFinSet.class, "Height");
128 list = new ArrayList<ScaleDialog.Scaler>(1);
129 list.add(new FreeformFinSetScaler());
130 SCALERS.put(FreeformFinSet.class, list);
133 addScaler(MassObject.class, "Length");
134 addScaler(MassObject.class, "Radius");
135 addScaler(MassObject.class, "RadialPosition");
138 list = new ArrayList<ScaleDialog.Scaler>(1);
139 list.add(new MassComponentScaler());
140 SCALERS.put(MassComponent.class, list);
143 addScaler(Parachute.class, "Diameter");
144 addScaler(Parachute.class, "LineLength");
147 addScaler(Streamer.class, "StripLength");
148 addScaler(Streamer.class, "StripWidth");
151 addScaler(ShockCord.class, "CordLength");
154 addScaler(RingComponent.class, "Length");
155 addScaler(RingComponent.class, "RadialPosition");
157 // ThicknessRingComponent
158 addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
159 addScaler(ThicknessRingComponent.class, "Thickness");
162 addScaler(InnerTube.class, "MotorOverhang");
164 // RadiusRingComponent
165 addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic");
166 addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic");
169 private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName) {
170 addScaler(componentClass, methodName, null);
173 private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
174 List<Scaler> list = SCALERS.get(componentClass);
176 list = new ArrayList<ScaleDialog.Scaler>();
177 SCALERS.put(componentClass, list);
179 list.add(new GeneralScaler(componentClass, methodName, autoMethodName));
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;
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");
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);
201 private final OpenRocketDocument document;
202 private final RocketComponent selection;
204 private final JComboBox selectionOption;
205 private final 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 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
219 this.document = document;
220 this.selection = selection;
222 // Generate options for scaling
223 List<String> options = new ArrayList<String>();
224 options.add(SCALE_ROCKET);
225 if (selection != null && selection.getChildCount() > 0) {
226 options.add(SCALE_SUBSELECTION);
228 if (selection != null) {
229 options.add(SCALE_SELECTION);
234 * Select initial size for "from" field.
236 * If a component is selected, either its diameter (for SymmetricComponents) or length is selected.
237 * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used.
240 double initialSize = 0;
241 if (selection != null) {
242 if (selection instanceof SymmetricComponent) {
243 SymmetricComponent s = (SymmetricComponent) selection;
244 initialSize = s.getForeRadius() * 2;
245 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
247 initialSize = selection.getLength();
250 for (RocketComponent c : document.getRocket()) {
251 if (c instanceof SymmetricComponent) {
252 SymmetricComponent s = (SymmetricComponent) c;
253 initialSize = s.getForeRadius() * 2;
254 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
258 if (initialSize < 0.001) {
259 Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
260 initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
263 fromField.setValue(initialSize);
264 toField.setValue(initialSize);
267 // Add actions to the values
268 multiplier.addChangeListener(new ChangeListener() {
270 public void stateChanged(ChangeEvent e) {
278 fromField.addChangeListener(new ChangeListener() {
280 public void stateChanged(ChangeEvent e) {
288 toField.addChangeListener(new ChangeListener() {
290 public void stateChanged(ChangeEvent e) {
302 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
307 tip = trans.get("lbl.scale.ttip");
308 JLabel label = new JLabel(trans.get("lbl.scale"));
309 label.setToolTipText(tip);
310 panel.add(label, "span, split, gapright unrel");
312 selectionOption = new JComboBox(options.toArray());
313 selectionOption.setEditable(false);
314 selectionOption.setToolTipText(tip);
315 panel.add(selectionOption, "growx, wrap para*2");
319 tip = trans.get("lbl.scaling.ttip");
320 label = new JLabel(trans.get("lbl.scaling"));
321 label.setToolTipText(tip);
322 panel.add(label, "gapright unrel");
325 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
326 spin.setEditor(new SpinnerEditor(spin));
327 spin.setToolTipText(tip);
328 panel.add(spin, "w :30lp:65lp");
330 UnitSelector unit = new UnitSelector(multiplier);
331 unit.setToolTipText(tip);
332 panel.add(unit, "w 30lp");
333 BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0));
334 slider.setToolTipText(tip);
335 panel.add(slider, "w 100lp, growx, wrap para");
338 // Scale from ... to ...
339 tip = trans.get("lbl.scaleFromTo.ttip");
340 label = new JLabel(trans.get("lbl.scaleFrom"));
341 label.setToolTipText(tip);
342 panel.add(label, "gapright unrel, right");
344 spin = new JSpinner(fromField.getSpinnerModel());
345 spin.setEditor(new SpinnerEditor(spin));
346 spin.setToolTipText(tip);
347 panel.add(spin, "span, split, w :30lp:65lp");
349 unit = new UnitSelector(fromField);
350 unit.setToolTipText(tip);
351 panel.add(unit, "w 30lp");
353 label = new JLabel(trans.get("lbl.scaleTo"));
354 label.setToolTipText(tip);
355 panel.add(label, "gap unrel");
357 spin = new JSpinner(toField.getSpinnerModel());
358 spin.setEditor(new SpinnerEditor(spin));
359 spin.setToolTipText(tip);
360 panel.add(spin, "w :30lp:65lp");
362 unit = new UnitSelector(toField);
363 unit.setToolTipText(tip);
364 panel.add(unit, "w 30lp, wrap para*2");
368 scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass"));
369 scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip"));
370 scaleMassValues.setSelected(true);
371 boolean overridden = false;
372 for (RocketComponent c : document.getRocket()) {
373 if (c instanceof MassComponent || c.isMassOverridden()) {
378 scaleMassValues.setEnabled(overridden);
379 panel.add(scaleMassValues, "span, wrap para*3");
384 JButton scale = new JButton(trans.get("button.scale"));
385 scale.addActionListener(new ActionListener() {
387 public void actionPerformed(ActionEvent e) {
389 ScaleDialog.this.setVisible(false);
392 panel.add(scale, "span, split, right, gap para");
394 JButton cancel = new JButton(trans.get("button.cancel"));
395 cancel.addActionListener(new ActionListener() {
397 public void actionPerformed(ActionEvent e) {
398 ScaleDialog.this.setVisible(false);
401 panel.add(cancel, "right, gap para");
405 GUIUtil.setDisposableDialogOptions(this, scale);
410 private void doScale() {
411 double mul = multiplier.getValue();
412 if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) {
413 ExceptionHandler.handleErrorCondition("Illegal multiplier value, mul=" + mul);
417 if (MathUtil.equals(mul, 1.0)) {
419 log.user("Scaling by value 1.0 - nothing to do");
423 boolean scaleMass = scaleMassValues.isSelected();
425 Object item = selectionOption.getSelectedItem();
426 log.user("Scaling design by factor " + mul + ", option=" + item);
427 if (SCALE_ROCKET.equals(item)) {
429 // Scale the entire rocket design
431 document.startUndo(trans.get("undo.scaleRocket"));
432 for (RocketComponent c : document.getRocket()) {
433 scale(c, mul, scaleMass);
439 } else if (SCALE_SUBSELECTION.equals(item)) {
441 // Scale component and subcomponents
443 document.startUndo(trans.get("undo.scaleComponents"));
444 for (RocketComponent c : selection) {
445 scale(c, mul, scaleMass);
451 } else if (SCALE_SELECTION.equals(item)) {
453 // Scale only the selected component
455 document.startUndo(trans.get("undo.scaleComponent"));
456 scale(selection, mul, scaleMass);
462 throw new BugException("Unknown item selected, item=" + item);
468 * Perform scaling on a single component.
470 private void scale(RocketComponent component, double mul, boolean scaleMass) {
472 Class<?> clazz = component.getClass();
473 while (clazz != null) {
474 List<Scaler> list = SCALERS.get(clazz);
476 for (Scaler s : list) {
477 s.scale(component, mul, scaleMass);
481 clazz = clazz.getSuperclass();
486 private void updateToField() {
487 double mul = multiplier.getValue();
488 double from = fromField.getValue();
489 double to = from * mul;
490 toField.setValue(to);
493 private void updateMultiplier() {
494 double from = fromField.getValue();
495 double to = toField.getValue();
496 double mul = to / from;
498 if (!MathUtil.equals(from, 0)) {
499 mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
500 multiplier.setValue(mul);
508 * Interface for scaling a specific component/value.
510 private interface Scaler {
511 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
515 * General scaler implementation that uses reflection to get/set a specific value.
517 private static class GeneralScaler implements Scaler {
519 private final Method getter;
520 private final Method setter;
521 private final Method autoMethod;
523 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
525 getter = Reflection.findMethod(componentClass, "get" + methodName);
526 setter = Reflection.findMethod(componentClass, "set" + methodName, double.class);
527 if (autoMethodName != null) {
528 autoMethod = Reflection.findMethod(componentClass, autoMethodName);
536 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
538 // Do not scale if set to automatic
539 if (autoMethod != null) {
540 boolean auto = (Boolean) autoMethod.invoke(c);
547 double value = (Double) getter.invoke(c);
548 value = value * multiplier;
549 setter.invoke(c, value);
555 private static class OverrideScaler implements Scaler {
558 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
560 if (component.isCGOverridden()) {
561 double cgx = component.getOverrideCGX();
562 cgx = cgx * multiplier;
563 component.setOverrideCGX(cgx);
566 if (scaleMass && component.isMassOverridden()) {
567 double mass = component.getOverrideMass();
568 mass = mass * MathUtil.pow3(multiplier);
569 component.setOverrideMass(mass);
575 private static class MassComponentScaler implements Scaler {
578 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
580 MassComponent c = (MassComponent) component;
581 double mass = c.getComponentMass();
582 mass = mass * MathUtil.pow3(multiplier);
583 c.setComponentMass(mass);
589 private static class FreeformFinSetScaler implements Scaler {
592 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
593 FreeformFinSet finset = (FreeformFinSet) component;
594 Coordinate[] points = finset.getFinPoints();
595 for (int i = 0; i < points.length; i++) {
596 points[i] = points[i].multiply(multiplier);
599 finset.setPoints(points);
600 } catch (IllegalFinPointException e) {
601 throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e);