package net.sf.openrocket.gui.dialogs; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.SpinnerEditor; import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.components.BasicSlider; import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.rocketcomponent.BodyComponent; import net.sf.openrocket.rocketcomponent.BodyTube; import net.sf.openrocket.rocketcomponent.EllipticalFinSet; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.FreeformFinSet; import net.sf.openrocket.rocketcomponent.IllegalFinPointException; import net.sf.openrocket.rocketcomponent.InnerTube; import net.sf.openrocket.rocketcomponent.LaunchLug; import net.sf.openrocket.rocketcomponent.MassComponent; import net.sf.openrocket.rocketcomponent.MassObject; import net.sf.openrocket.rocketcomponent.Parachute; import net.sf.openrocket.rocketcomponent.RadiusRingComponent; import net.sf.openrocket.rocketcomponent.RingComponent; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.ShockCord; import net.sf.openrocket.rocketcomponent.Streamer; import net.sf.openrocket.rocketcomponent.SymmetricComponent; import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Reflection; import net.sf.openrocket.util.Reflection.Method; /** * Dialog that allows scaling the rocket design. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class ScaleDialog extends JDialog { private static final Logger log = LoggerFactory.getLogger(ScaleDialog.class); private static final Translator trans = Application.getTranslator(); /* * Scaler implementations * * Each scaled value (except override cg/mass) is defined using a Scaler instance. */ private static final Map<Class<? extends RocketComponent>, List<Scaler>> SCALERS = new HashMap<Class<? extends RocketComponent>, List<Scaler>>(); static { List<Scaler> list; // RocketComponent addScaler(RocketComponent.class, "PositionValue"); SCALERS.get(RocketComponent.class).add(new OverrideScaler()); // BodyComponent addScaler(BodyComponent.class, "Length"); // SymmetricComponent addScaler(SymmetricComponent.class, "Thickness", "isFilled"); // Transition + Nose cone addScaler(Transition.class, "ForeRadius", "isForeRadiusAutomatic"); addScaler(Transition.class, "AftRadius", "isAftRadiusAutomatic"); addScaler(Transition.class, "ForeShoulderRadius"); addScaler(Transition.class, "ForeShoulderThickness"); addScaler(Transition.class, "ForeShoulderLength"); addScaler(Transition.class, "AftShoulderRadius"); addScaler(Transition.class, "AftShoulderThickness"); addScaler(Transition.class, "AftShoulderLength"); // Body tube addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic"); addScaler(BodyTube.class, "MotorOverhang"); // Launch lug addScaler(LaunchLug.class, "OuterRadius"); addScaler(LaunchLug.class, "Thickness"); addScaler(LaunchLug.class, "Length"); // FinSet addScaler(FinSet.class, "Thickness"); addScaler(FinSet.class, "TabHeight"); addScaler(FinSet.class, "TabLength"); addScaler(FinSet.class, "TabShift"); // TrapezoidFinSet addScaler(TrapezoidFinSet.class, "Sweep"); addScaler(TrapezoidFinSet.class, "RootChord"); addScaler(TrapezoidFinSet.class, "TipChord"); addScaler(TrapezoidFinSet.class, "Height"); // EllipticalFinSet addScaler(EllipticalFinSet.class, "Length"); addScaler(EllipticalFinSet.class, "Height"); // FreeformFinSet list = new ArrayList<ScaleDialog.Scaler>(1); list.add(new FreeformFinSetScaler()); SCALERS.put(FreeformFinSet.class, list); // MassObject addScaler(MassObject.class, "Length"); addScaler(MassObject.class, "Radius"); addScaler(MassObject.class, "RadialPosition"); // MassComponent list = new ArrayList<ScaleDialog.Scaler>(1); list.add(new MassComponentScaler()); SCALERS.put(MassComponent.class, list); // Parachute addScaler(Parachute.class, "Diameter"); addScaler(Parachute.class, "LineLength"); // Streamer addScaler(Streamer.class, "StripLength"); addScaler(Streamer.class, "StripWidth"); // ShockCord addScaler(ShockCord.class, "CordLength"); // RingComponent addScaler(RingComponent.class, "Length"); addScaler(RingComponent.class, "RadialPosition"); // ThicknessRingComponent addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); addScaler(ThicknessRingComponent.class, "Thickness"); // InnerTube addScaler(InnerTube.class, "MotorOverhang"); // RadiusRingComponent addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic"); } private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName) { addScaler(componentClass, methodName, null); } private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) { List<Scaler> list = SCALERS.get(componentClass); if (list == null) { list = new ArrayList<ScaleDialog.Scaler>(); SCALERS.put(componentClass, list); } list.add(new GeneralScaler(componentClass, methodName, autoMethodName)); } private static final double DEFAULT_INITIAL_SIZE = 0.1; // meters private static final double SCALE_MIN = 0.01; private static final double SCALE_MAX = 100.0; private static final String SCALE_ROCKET = trans.get("lbl.scaleRocket"); private static final String SCALE_SUBSELECTION = trans.get("lbl.scaleSubselection"); private static final String SCALE_SELECTION = trans.get("lbl.scaleSelection"); private final DoubleModel multiplier = new DoubleModel(1.0, UnitGroup.UNITS_RELATIVE, SCALE_MIN, SCALE_MAX); private final DoubleModel fromField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); private final DoubleModel toField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); private final OpenRocketDocument document; private final RocketComponent selection; private final boolean onlySelection; private JComboBox selectionOption; private JCheckBox scaleMassValues; private boolean changing = false; /** * Sole constructor. * * @param document the document to modify. * @param selection the currently selected component (or <code>null</code> if none selected). * @param parent the parent window. */ public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) { this(document, selection, parent, false); } /** * Sole constructor. * * @param document the document to modify. * @param selection the currently selected component (or <code>null</code> if none selected). * @param parent the parent window. * @param onlySelection true to only allow scaling on the selected component (not the whole rocket) */ public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent, Boolean onlySelection) { super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); this.document = document; this.selection = selection; this.onlySelection = onlySelection; init(); } private void init() { // Generate options for scaling List<String> options = new ArrayList<String>(); if (!onlySelection) options.add(SCALE_ROCKET); if (selection != null && selection.getChildCount() > 0) { options.add(SCALE_SUBSELECTION); } if (selection != null) { options.add(SCALE_SELECTION); } /* * Select initial size for "from" field. * * If a component is selected, either its diameter (for SymmetricComponents) or length is selected. * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used. */ // double initialSize = 0; if (selection != null) { if (selection instanceof SymmetricComponent) { SymmetricComponent s = (SymmetricComponent) selection; initialSize = s.getForeRadius() * 2; initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); } else { initialSize = selection.getLength(); } } else { for (RocketComponent c : document.getRocket()) { if (c instanceof SymmetricComponent) { SymmetricComponent s = (SymmetricComponent) c; initialSize = s.getForeRadius() * 2; initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); } } } if (initialSize < 0.001) { Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE))); } fromField.setValue(initialSize); toField.setValue(initialSize); // Add actions to the values multiplier.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (!changing) { changing = true; updateToField(); changing = false; } } }); fromField.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (!changing) { changing = true; updateToField(); changing = false; } } }); toField.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (!changing) { changing = true; updateMultiplier(); changing = false; } } }); String tip; JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); this.add(panel); // Scaling selection tip = trans.get("lbl.scale.ttip"); JLabel label = new JLabel(trans.get("lbl.scale")); label.setToolTipText(tip); panel.add(label, "span, split, gapright unrel"); selectionOption = new JComboBox(options.toArray()); selectionOption.setEditable(false); selectionOption.setToolTipText(tip); panel.add(selectionOption, "growx, wrap para*2"); // Scale multiplier tip = trans.get("lbl.scaling.ttip"); label = new JLabel(trans.get("lbl.scaling")); label.setToolTipText(tip); panel.add(label, "gapright unrel"); JSpinner spin = new JSpinner(multiplier.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); panel.add(spin, "w :30lp:65lp"); UnitSelector unit = new UnitSelector(multiplier); unit.setToolTipText(tip); panel.add(unit, "w 30lp"); BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0)); slider.setToolTipText(tip); panel.add(slider, "w 100lp, growx, wrap para"); // Scale from ... to ... tip = trans.get("lbl.scaleFromTo.ttip"); label = new JLabel(trans.get("lbl.scaleFrom")); label.setToolTipText(tip); panel.add(label, "gapright unrel, right"); spin = new JSpinner(fromField.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); panel.add(spin, "span, split, w :30lp:65lp"); unit = new UnitSelector(fromField); unit.setToolTipText(tip); panel.add(unit, "w 30lp"); label = new JLabel(trans.get("lbl.scaleTo")); label.setToolTipText(tip); panel.add(label, "gap unrel"); spin = new JSpinner(toField.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); panel.add(spin, "w :30lp:65lp"); unit = new UnitSelector(toField); unit.setToolTipText(tip); panel.add(unit, "w 30lp, wrap para*2"); // Scale override scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass")); scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip")); scaleMassValues.setSelected(true); boolean overridden = false; for (RocketComponent c : document.getRocket()) { if (c instanceof MassComponent || c.isMassOverridden()) { overridden = true; break; } } scaleMassValues.setEnabled(overridden); panel.add(scaleMassValues, "span, wrap para*3"); // Buttons JButton scale = new JButton(trans.get("button.scale")); scale.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doScale(); ScaleDialog.this.setVisible(false); } }); panel.add(scale, "span, split, right, gap para"); JButton cancel = new JButton(trans.get("button.cancel")); cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { ScaleDialog.this.setVisible(false); } }); panel.add(cancel, "right, gap para"); GUIUtil.setDisposableDialogOptions(this, scale); } private void doScale() { double mul = multiplier.getValue(); if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) { Application.getExceptionHandler().handleErrorCondition("Illegal multiplier value, mul=" + mul); return; } if (MathUtil.equals(mul, 1.0)) { // Nothing to do log.info(Markers.USER_MARKER, "Scaling by value 1.0 - nothing to do"); return; } boolean scaleMass = scaleMassValues.isSelected(); Object item = selectionOption.getSelectedItem(); log.info(Markers.USER_MARKER, "Scaling design by factor " + mul + ", option=" + item); if (SCALE_ROCKET.equals(item)) { // Scale the entire rocket design try { document.startUndo(trans.get("undo.scaleRocket")); for (RocketComponent c : document.getRocket()) { scale(c, mul, scaleMass); } } finally { document.stopUndo(); } } else if (SCALE_SUBSELECTION.equals(item)) { // Scale component and subcomponents try { document.startUndo(trans.get("undo.scaleComponents")); for (RocketComponent c : selection) { scale(c, mul, scaleMass); } } finally { document.stopUndo(); } } else if (SCALE_SELECTION.equals(item)) { // Scale only the selected component try { document.startUndo(trans.get("undo.scaleComponent")); scale(selection, mul, scaleMass); } finally { document.stopUndo(); } } else { throw new BugException("Unknown item selected, item=" + item); } } /** * Perform scaling on a single component. */ private void scale(RocketComponent component, double mul, boolean scaleMass) { Class<?> clazz = component.getClass(); while (clazz != null) { List<Scaler> list = SCALERS.get(clazz); if (list != null) { for (Scaler s : list) { s.scale(component, mul, scaleMass); } } clazz = clazz.getSuperclass(); } } private void updateToField() { double mul = multiplier.getValue(); double from = fromField.getValue(); double to = from * mul; toField.setValue(to); } private void updateMultiplier() { double from = fromField.getValue(); double to = toField.getValue(); double mul = to / from; if (!MathUtil.equals(from, 0)) { mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX); multiplier.setValue(mul); } updateToField(); } /** * Interface for scaling a specific component/value. */ private interface Scaler { public void scale(RocketComponent c, double multiplier, boolean scaleMass); } /** * General scaler implementation that uses reflection to get/set a specific value. */ private static class GeneralScaler implements Scaler { private final Method getter; private final Method setter; private final Method autoMethod; public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) { getter = Reflection.findMethod(componentClass, "get" + methodName); setter = Reflection.findMethod(componentClass, "set" + methodName, double.class); if (autoMethodName != null) { autoMethod = Reflection.findMethod(componentClass, autoMethodName); } else { autoMethod = null; } } @Override public void scale(RocketComponent c, double multiplier, boolean scaleMass) { // Do not scale if set to automatic if (autoMethod != null) { boolean auto = (Boolean) autoMethod.invoke(c); if (auto) { return; } } // Scale value double value = (Double) getter.invoke(c); value = value * multiplier; setter.invoke(c, value); } } private static class OverrideScaler implements Scaler { @Override public void scale(RocketComponent component, double multiplier, boolean scaleMass) { if (component.isCGOverridden()) { double cgx = component.getOverrideCGX(); cgx = cgx * multiplier; component.setOverrideCGX(cgx); } if (scaleMass && component.isMassOverridden()) { double mass = component.getOverrideMass(); mass = mass * MathUtil.pow3(multiplier); component.setOverrideMass(mass); } } } private static class MassComponentScaler implements Scaler { @Override public void scale(RocketComponent component, double multiplier, boolean scaleMass) { if (scaleMass) { MassComponent c = (MassComponent) component; double mass = c.getComponentMass(); mass = mass * MathUtil.pow3(multiplier); c.setComponentMass(mass); } } } private static class FreeformFinSetScaler implements Scaler { @Override public void scale(RocketComponent component, double multiplier, boolean scaleMass) { FreeformFinSet finset = (FreeformFinSet) component; Coordinate[] points = finset.getFinPoints(); for (int i = 0; i < points.length; i++) { points[i] = points[i].multiply(multiplier); } try { finset.setPoints(points); } catch (IllegalFinPointException e) { throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e); } } } }