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;
214 * @param document the document to modify.
215 * @param selection the currently selected component (or <code>null</code> if none selected).
216 * @param parent the parent window.
218 public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) {
219 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
221 this.document = document;
222 this.selection = selection;
224 // Generate options for scaling
225 List<String> options = new ArrayList<String>();
226 options.add(SCALE_ROCKET);
227 if (selection != null && selection.getChildCount() > 0) {
228 options.add(SCALE_SUBSELECTION);
230 if (selection != null) {
231 options.add(SCALE_SELECTION);
236 * Select initial size for "from" field.
238 * If a component is selected, either its diameter (for SymmetricComponents) or length is selected.
239 * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used.
242 double initialSize = 0;
243 if (selection != null) {
244 if (selection instanceof SymmetricComponent) {
245 SymmetricComponent s = (SymmetricComponent) selection;
246 initialSize = s.getForeRadius() * 2;
247 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
249 initialSize = selection.getLength();
252 for (RocketComponent c : document.getRocket()) {
253 if (c instanceof SymmetricComponent) {
254 SymmetricComponent s = (SymmetricComponent) c;
255 initialSize = s.getForeRadius() * 2;
256 initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2);
260 if (initialSize < 0.001) {
261 Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
262 initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
265 fromField.setValue(initialSize);
266 toField.setValue(initialSize);
269 // Add actions to the values
270 multiplier.addChangeListener(new ChangeListener() {
272 public void stateChanged(ChangeEvent e) {
280 fromField.addChangeListener(new ChangeListener() {
282 public void stateChanged(ChangeEvent e) {
290 toField.addChangeListener(new ChangeListener() {
292 public void stateChanged(ChangeEvent e) {
304 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
309 tip = trans.get("lbl.scale.ttip");
310 JLabel label = new JLabel(trans.get("lbl.scale"));
311 label.setToolTipText(tip);
312 panel.add(label, "span, split, gapright unrel");
314 selectionOption = new JComboBox(options.toArray());
315 selectionOption.setEditable(false);
316 selectionOption.setToolTipText(tip);
317 panel.add(selectionOption, "growx, wrap para*2");
321 tip = trans.get("lbl.scaling.ttip");
322 label = new JLabel(trans.get("lbl.scaling"));
323 label.setToolTipText(tip);
324 panel.add(label, "gapright unrel");
327 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
328 spin.setEditor(new SpinnerEditor(spin));
329 spin.setToolTipText(tip);
330 panel.add(spin, "w :30lp:65lp");
332 UnitSelector unit = new UnitSelector(multiplier);
333 unit.setToolTipText(tip);
334 panel.add(unit, "w 30lp");
335 BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0));
336 slider.setToolTipText(tip);
337 panel.add(slider, "w 100lp, growx, wrap para");
340 // Scale from ... to ...
341 tip = trans.get("lbl.scaleFromTo.ttip");
342 label = new JLabel(trans.get("lbl.scaleFrom"));
343 label.setToolTipText(tip);
344 panel.add(label, "gapright unrel, right");
346 spin = new JSpinner(fromField.getSpinnerModel());
347 spin.setEditor(new SpinnerEditor(spin));
348 spin.setToolTipText(tip);
349 panel.add(spin, "span, split, w :30lp:65lp");
351 unit = new UnitSelector(fromField);
352 unit.setToolTipText(tip);
353 panel.add(unit, "w 30lp");
355 label = new JLabel(trans.get("lbl.scaleTo"));
356 label.setToolTipText(tip);
357 panel.add(label, "gap unrel");
359 spin = new JSpinner(toField.getSpinnerModel());
360 spin.setEditor(new SpinnerEditor(spin));
361 spin.setToolTipText(tip);
362 panel.add(spin, "w :30lp:65lp");
364 unit = new UnitSelector(toField);
365 unit.setToolTipText(tip);
366 panel.add(unit, "w 30lp, wrap para*2");
370 scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass"));
371 scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip"));
372 scaleMassValues.setSelected(true);
373 boolean overridden = false;
374 for (RocketComponent c : document.getRocket()) {
375 if (c instanceof MassComponent || c.isMassOverridden()) {
380 scaleMassValues.setEnabled(overridden);
381 panel.add(scaleMassValues, "span, wrap para*3");
386 JButton scale = new JButton(trans.get("button.scale"));
387 scale.addActionListener(new ActionListener() {
389 public void actionPerformed(ActionEvent e) {
391 ScaleDialog.this.setVisible(false);
394 panel.add(scale, "span, split, right, gap para");
396 JButton cancel = new JButton(trans.get("button.cancel"));
397 cancel.addActionListener(new ActionListener() {
399 public void actionPerformed(ActionEvent e) {
400 ScaleDialog.this.setVisible(false);
403 panel.add(cancel, "right, gap para");
407 GUIUtil.setDisposableDialogOptions(this, scale);
412 private void doScale() {
413 double mul = multiplier.getValue();
414 if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) {
415 ExceptionHandler.handleErrorCondition("Illegal multiplier value, mul=" + mul);
419 if (MathUtil.equals(mul, 1.0)) {
421 log.user("Scaling by value 1.0 - nothing to do");
425 boolean scaleMass = scaleMassValues.isSelected();
427 Object item = selectionOption.getSelectedItem();
428 log.user("Scaling design by factor " + mul + ", option=" + item);
429 if (SCALE_ROCKET.equals(item)) {
431 // Scale the entire rocket design
433 document.startUndo("Scale rocket");
434 for (RocketComponent c : document.getRocket()) {
435 scale(c, mul, scaleMass);
441 } else if (SCALE_SUBSELECTION.equals(item)) {
443 // Scale component and subcomponents
445 document.startUndo("Scale components");
446 for (RocketComponent c : selection) {
447 scale(c, mul, scaleMass);
453 } else if (SCALE_SELECTION.equals(item)) {
455 // Scale only the selected component
457 document.startUndo("Scale component");
458 scale(selection, mul, scaleMass);
464 throw new BugException("Unknown item selected, item=" + item);
470 * Perform scaling on a single component.
472 private void scale(RocketComponent component, double mul, boolean scaleMass) {
474 Class<?> clazz = component.getClass();
475 while (clazz != null) {
476 List<Scaler> list = SCALERS.get(clazz);
478 for (Scaler s : list) {
479 s.scale(component, mul, scaleMass);
483 clazz = clazz.getSuperclass();
488 private void updateToField() {
489 double mul = multiplier.getValue();
490 double from = fromField.getValue();
491 double to = from * mul;
492 toField.setValue(to);
495 private void updateMultiplier() {
496 double from = fromField.getValue();
497 double to = toField.getValue();
498 double mul = to / from;
500 if (!MathUtil.equals(from, 0)) {
501 mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
502 multiplier.setValue(mul);
510 * Interface for scaling a specific component/value.
512 private interface Scaler {
513 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
517 * General scaler implementation that uses reflection to get/set a specific value.
519 private static class GeneralScaler implements Scaler {
521 private final Method getter;
522 private final Method setter;
523 private final Method autoMethod;
525 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
527 getter = Reflection.findMethod(componentClass, "get" + methodName);
528 setter = Reflection.findMethod(componentClass, "set" + methodName, double.class);
529 if (autoMethodName != null) {
530 autoMethod = Reflection.findMethod(componentClass, autoMethodName);
538 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
540 // Do not scale if set to automatic
541 if (autoMethod != null) {
542 boolean auto = (Boolean) autoMethod.invoke(c);
549 double value = (Double) getter.invoke(c);
550 value = value * multiplier;
551 setter.invoke(c, value);
557 private static class OverrideScaler implements Scaler {
560 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
562 if (component.isCGOverridden()) {
563 double cgx = component.getOverrideCGX();
564 cgx = cgx * multiplier;
565 component.setOverrideCGX(cgx);
568 if (scaleMass && component.isMassOverridden()) {
569 double mass = component.getOverrideMass();
570 mass = mass * MathUtil.pow3(multiplier);
571 component.setOverrideMass(mass);
577 private static class MassComponentScaler implements Scaler {
580 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
582 MassComponent c = (MassComponent) component;
583 double mass = c.getComponentMass();
584 mass = mass * MathUtil.pow3(multiplier);
585 c.setComponentMass(mass);
591 private static class FreeformFinSetScaler implements Scaler {
594 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
595 FreeformFinSet finset = (FreeformFinSet) component;
596 Coordinate[] points = finset.getFinPoints();
597 for (int i = 0; i < points.length; i++) {
598 points[i] = points[i].multiply(multiplier);
601 finset.setPoints(points);
602 } catch (IllegalFinPointException e) {
603 throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e);