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;
203 private final JComboBox selectionOption;
204 private final JCheckBox scaleMassValues;
206 private boolean changing = false;
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.
215 public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) {
216 super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL);
218 this.document = document;
219 this.selection = selection;
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);
227 if (selection != null) {
228 options.add(SCALE_SELECTION);
233 * Select initial size for "from" field.
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.
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);
246 initialSize = selection.getLength();
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);
257 if (initialSize < 0.001) {
258 Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
259 initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE)));
262 fromField.setValue(initialSize);
263 toField.setValue(initialSize);
266 // Add actions to the values
267 multiplier.addChangeListener(new ChangeListener() {
269 public void stateChanged(ChangeEvent e) {
277 fromField.addChangeListener(new ChangeListener() {
279 public void stateChanged(ChangeEvent e) {
287 toField.addChangeListener(new ChangeListener() {
289 public void stateChanged(ChangeEvent e) {
301 JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", ""));
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");
311 selectionOption = new JComboBox(options.toArray());
312 selectionOption.setEditable(false);
313 selectionOption.setToolTipText(tip);
314 panel.add(selectionOption, "growx, wrap para*2");
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");
324 JSpinner spin = new JSpinner(multiplier.getSpinnerModel());
325 spin.setEditor(new SpinnerEditor(spin));
326 spin.setToolTipText(tip);
327 panel.add(spin, "w :30lp:65lp");
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");
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");
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");
348 unit = new UnitSelector(fromField);
349 unit.setToolTipText(tip);
350 panel.add(unit, "w 30lp");
352 label = new JLabel(trans.get("lbl.scaleTo"));
353 label.setToolTipText(tip);
354 panel.add(label, "gap unrel");
356 spin = new JSpinner(toField.getSpinnerModel());
357 spin.setEditor(new SpinnerEditor(spin));
358 spin.setToolTipText(tip);
359 panel.add(spin, "w :30lp:65lp");
361 unit = new UnitSelector(toField);
362 unit.setToolTipText(tip);
363 panel.add(unit, "w 30lp, wrap para*2");
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()) {
377 scaleMassValues.setEnabled(overridden);
378 panel.add(scaleMassValues, "span, wrap para*3");
383 JButton scale = new JButton(trans.get("button.scale"));
384 scale.addActionListener(new ActionListener() {
386 public void actionPerformed(ActionEvent e) {
388 ScaleDialog.this.setVisible(false);
391 panel.add(scale, "span, split, right, gap para");
393 JButton cancel = new JButton(trans.get("button.cancel"));
394 cancel.addActionListener(new ActionListener() {
396 public void actionPerformed(ActionEvent e) {
397 ScaleDialog.this.setVisible(false);
400 panel.add(cancel, "right, gap para");
404 GUIUtil.setDisposableDialogOptions(this, scale);
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);
416 if (MathUtil.equals(mul, 1.0)) {
418 log.user("Scaling by value 1.0 - nothing to do");
422 boolean scaleMass = scaleMassValues.isSelected();
424 Object item = selectionOption.getSelectedItem();
425 log.user("Scaling design by factor " + mul + ", option=" + item);
426 if (SCALE_ROCKET.equals(item)) {
428 // Scale the entire rocket design
430 document.startUndo(trans.get("undo.scaleRocket"));
431 for (RocketComponent c : document.getRocket()) {
432 scale(c, mul, scaleMass);
438 } else if (SCALE_SUBSELECTION.equals(item)) {
440 // Scale component and subcomponents
442 document.startUndo(trans.get("undo.scaleComponents"));
443 for (RocketComponent c : selection) {
444 scale(c, mul, scaleMass);
450 } else if (SCALE_SELECTION.equals(item)) {
452 // Scale only the selected component
454 document.startUndo(trans.get("undo.scaleComponent"));
455 scale(selection, mul, scaleMass);
461 throw new BugException("Unknown item selected, item=" + item);
467 * Perform scaling on a single component.
469 private void scale(RocketComponent component, double mul, boolean scaleMass) {
471 Class<?> clazz = component.getClass();
472 while (clazz != null) {
473 List<Scaler> list = SCALERS.get(clazz);
475 for (Scaler s : list) {
476 s.scale(component, mul, scaleMass);
480 clazz = clazz.getSuperclass();
485 private void updateToField() {
486 double mul = multiplier.getValue();
487 double from = fromField.getValue();
488 double to = from * mul;
489 toField.setValue(to);
492 private void updateMultiplier() {
493 double from = fromField.getValue();
494 double to = toField.getValue();
495 double mul = to / from;
497 if (!MathUtil.equals(from, 0)) {
498 mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX);
499 multiplier.setValue(mul);
507 * Interface for scaling a specific component/value.
509 private interface Scaler {
510 public void scale(RocketComponent c, double multiplier, boolean scaleMass);
514 * General scaler implementation that uses reflection to get/set a specific value.
516 private static class GeneralScaler implements Scaler {
518 private final Method getter;
519 private final Method setter;
520 private final Method autoMethod;
522 public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) {
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);
535 public void scale(RocketComponent c, double multiplier, boolean scaleMass) {
537 // Do not scale if set to automatic
538 if (autoMethod != null) {
539 boolean auto = (Boolean) autoMethod.invoke(c);
546 double value = (Double) getter.invoke(c);
547 value = value * multiplier;
548 setter.invoke(c, value);
554 private static class OverrideScaler implements Scaler {
557 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
559 if (component.isCGOverridden()) {
560 double cgx = component.getOverrideCGX();
561 cgx = cgx * multiplier;
562 component.setOverrideCGX(cgx);
565 if (scaleMass && component.isMassOverridden()) {
566 double mass = component.getOverrideMass();
567 mass = mass * MathUtil.pow3(multiplier);
568 component.setOverrideMass(mass);
574 private static class MassComponentScaler implements Scaler {
577 public void scale(RocketComponent component, double multiplier, boolean scaleMass) {
579 MassComponent c = (MassComponent) component;
580 double mass = c.getComponentMass();
581 mass = mass * MathUtil.pow3(multiplier);
582 c.setComponentMass(mass);
588 private static class FreeformFinSetScaler implements Scaler {
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);
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);