]> git.gag.com Git - debian/openrocket/commitdiff
DGP - convenience computation of fin tab depth, length, and offset
authorrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Wed, 5 Oct 2011 04:00:51 +0000 (04:00 +0000)
committerrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Wed, 5 Oct 2011 04:00:51 +0000 (04:00 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@176 180e2498-e6e9-4542-8430-84ac67f01cd8

l10n/messages.properties
src/net/sf/openrocket/gui/configdialog/FinSetConfig.java
src/net/sf/openrocket/rocketcomponent/RocketComponent.java
test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java [new file with mode: 0644]

index 4457bc7b12c61135305ca69e19ddd4c754428f65..73d0a9b751fe16076209f35e246ff8a4312bec46 100644 (file)
@@ -583,6 +583,7 @@ FinSetConfig.but.Converttofreeform.ttip = Convert this fin set into a freeform f
 FinSetConfig.Convertfinset = Convert fin set
 FinSetConfig.but.Splitfins = Split fins
 FinSetConfig.but.Splitfins.ttip = Split the fin set into separate fins
+FinSetConfig.but.Calcheight = Calculate Height
 FinSetConfig.lbl.Through-the-wall  = Through-the-wall fin tabs:
 FinSetConfig.lbl.Tablength = Tab length:
 FinSetConfig.ttip.Tablength = The length of the fin tab.
index da124327be44710148e50f888b2eebe1f09295f4..b1037a1d8f14303d670924e5c79e00ede32cc8ec 100644 (file)
@@ -1,15 +1,5 @@
 package net.sf.openrocket.gui.configdialog;
 
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-import javax.swing.JButton;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSpinner;
-import javax.swing.SwingUtilities;
-
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.gui.SpinnerEditor;
 import net.sf.openrocket.gui.adaptors.DoubleModel;
@@ -20,199 +10,425 @@ import net.sf.openrocket.gui.components.StyledLabel.Style;
 import net.sf.openrocket.gui.components.UnitSelector;
 import net.sf.openrocket.l10n.Translator;
 import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.Coaxial;
 import net.sf.openrocket.rocketcomponent.FinSet;
 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.unit.UnitGroup;
 
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
 
 public abstract class FinSetConfig extends RocketComponentConfig {
-       private static final LogHelper log = Application.getLogger();
-       private static final Translator trans = Application.getTranslator();
-
-       private JButton split = null;
-       
-       public FinSetConfig(RocketComponent component) {
-               super(component);
-               
-               //// Fin tabs and Through-the-wall fin tabs
-               tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(), 
-                               trans.get("FinSetConfig.tab.Through-the-wall"), 0);
-       }
-       
-       
-
-       protected void addFinSetButtons() {
-               JButton convert = null;
-               
-               //// Convert buttons
-               if (!(component instanceof FreeformFinSet)) {
-                       //// Convert to freeform
-                       convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
-                       //// Convert this fin set into a freeform fin set
-                       convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
-                       convert.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       log.user("Converting " + component.getComponentName() + " into freeform fin set");
-                                       
-                                       // Do change in future for overall safety
-                                       SwingUtilities.invokeLater(new Runnable() {
-                                               @Override
-                                               public void run() {
-                                                       //// Convert fin set
-                                                       ComponentConfigDialog.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
-                                                       RocketComponent freeform =
-                                                                       FreeformFinSet.convertFinSet((FinSet) component);
-                                                       ComponentConfigDialog.showDialog(freeform);
-                                               }
-                                       });
-                                       
-                                       ComponentConfigDialog.hideDialog();
-                               }
-                       });
-               }
-               
-               //// Split fins
-               split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
-               //// Split the fin set into separate fins
-               split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
-               split.addActionListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
-                                               ((FinSet) component).getFinCount());
-                               
-                               // Do change in future for overall safety
-                               SwingUtilities.invokeLater(new Runnable() {
-                                       @Override
-                                       public void run() {
-                                               RocketComponent parent = component.getParent();
-                                               int index = parent.getChildPosition(component);
-                                               int count = ((FinSet) component).getFinCount();
-                                               double base = ((FinSet) component).getBaseRotation();
-                                               if (count <= 1)
-                                                       return;
-                                               
-                                               ComponentConfigDialog.addUndoPosition("Split fin set");
-                                               parent.removeChild(index);
-                                               for (int i = 0; i < count; i++) {
-                                                       FinSet copy = (FinSet) component.copy();
-                                                       copy.setFinCount(1);
-                                                       copy.setBaseRotation(base + i * 2 * Math.PI / count);
-                                                       copy.setName(copy.getName() + " #" + (i + 1));
-                                                       parent.addChild(copy, index + i);
-                                               }
-                                       }
-                               });
-                               
-                               ComponentConfigDialog.hideDialog();
-                       }
-               });
-               split.setEnabled(((FinSet) component).getFinCount() > 1);
-               
-               if (convert == null)
-                       addButtons(split);
-               else
-                       addButtons(split, convert);
-               
-       }
-       
-       public JPanel finTabPanel() {
-               JPanel panel = new JPanel(
-                               new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
-                                               "[150lp::][65lp::][30lp::][200lp::]", ""));
-               //              JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
-               //                              "[40lp][80lp::][30lp::][100lp::]",""));
-               
-               //// Through-the-wall fin tabs:
-               panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
-                               "spanx, wrap 30lp");
-               
-               JLabel label;
-               DoubleModel m;
-               DoubleModel length;
-               DoubleModel length2;
-               DoubleModel length_2;
-               JSpinner spin;
-               
-               length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
-               length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
-               length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
-               
-               register(length);
-               register(length2);
-               register(length_2);
-               
-               ////  Tab length
-               //// Tab length:
-               label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
-               //// The length of the fin tab.
-               label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
-               panel.add(label, "gapleft para, gapright 40lp, growx 1");
-               
-               m = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
-               
-               spin = new JSpinner(m.getSpinnerModel());
-               spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin, "growx 1");
-               
-               panel.add(new UnitSelector(m), "growx 1");
-               panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length)),
-                               "w 100lp, growx 5, wrap");
-               
-
-               ////  Tab length
-               //// Tab height:
-               label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
-               //// The spanwise height of the fin tab.
-               label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
-               panel.add(label, "gapleft para");
-               
-               m = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
-               
-               spin = new JSpinner(m.getSpinnerModel());
-               spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin, "growx");
-               
-               panel.add(new UnitSelector(m), "growx");
-               panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length2)),
-                               "w 100lp, growx 5, wrap para");
-               
-
-               ////  Tab position:
-               label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
-               //// The position of the fin tab.
-               label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
-               panel.add(label, "gapleft para");
-               
-               m = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
-               
-               spin = new JSpinner(m.getSpinnerModel());
-               spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin, "growx");
-               
-               panel.add(new UnitSelector(m), "growx");
-               panel.add(new BasicSlider(m.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
-               
-
-               //// relative to
-               label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
-               panel.add(label, "right, gapright unrel");
-               
-               EnumModel<FinSet.TabRelativePosition> em =
-                               new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
-               
-               panel.add(new JComboBox(em), "spanx 3, growx");
-               
-               return panel;
-       }
-       
-       @Override
-       public void updateFields() {
-               super.updateFields();
-               if (split != null)
-                       split.setEnabled(((FinSet) component).getFinCount() > 1);
-       }
+    private static final LogHelper log = Application.getLogger();
+    private static final Translator trans = Application.getTranslator();
+
+    private JButton split = null;
+
+    public FinSetConfig(RocketComponent component) {
+        super(component);
+
+        //// Fin tabs and Through-the-wall fin tabs
+        tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(),
+                trans.get("FinSetConfig.tab.Through-the-wall"), 0);
+    }
+
+
+    protected void addFinSetButtons() {
+        JButton convert = null;
+
+        //// Convert buttons
+        if (!(component instanceof FreeformFinSet)) {
+            //// Convert to freeform
+            convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
+            //// Convert this fin set into a freeform fin set
+            convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
+            convert.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    log.user("Converting " + component.getComponentName() + " into freeform fin set");
+
+                    // Do change in future for overall safety
+                    SwingUtilities.invokeLater(new Runnable() {
+                        @Override
+                        public void run() {
+                            //// Convert fin set
+                            ComponentConfigDialog.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
+                            RocketComponent freeform =
+                                    FreeformFinSet.convertFinSet((FinSet) component);
+                            ComponentConfigDialog.showDialog(freeform);
+                        }
+                    });
+
+                    ComponentConfigDialog.hideDialog();
+                }
+            });
+        }
+
+        //// Split fins
+        split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
+        //// Split the fin set into separate fins
+        split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
+        split.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
+                        ((FinSet) component).getFinCount());
+
+                // Do change in future for overall safety
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        RocketComponent parent = component.getParent();
+                        int index = parent.getChildPosition(component);
+                        int count = ((FinSet) component).getFinCount();
+                        double base = ((FinSet) component).getBaseRotation();
+                        if (count <= 1)
+                            return;
+
+                        ComponentConfigDialog.addUndoPosition("Split fin set");
+                        parent.removeChild(index);
+                        for (int i = 0; i < count; i++) {
+                            FinSet copy = (FinSet) component.copy();
+                            copy.setFinCount(1);
+                            copy.setBaseRotation(base + i * 2 * Math.PI / count);
+                            copy.setName(copy.getName() + " #" + (i + 1));
+                            parent.addChild(copy, index + i);
+                        }
+                    }
+                });
+
+                ComponentConfigDialog.hideDialog();
+            }
+        });
+        split.setEnabled(((FinSet) component).getFinCount() > 1);
+
+        if (convert == null)
+            addButtons(split);
+        else
+            addButtons(split, convert);
+
+    }
+
+    public JPanel finTabPanel() {
+        JPanel panel = new JPanel(
+                new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
+                        "[150lp::][65lp::][30lp::][200lp::]", ""));
+        //             JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
+        //                             "[40lp][80lp::][30lp::][100lp::]",""));
+
+        //// Through-the-wall fin tabs:
+        panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
+                "spanx, wrap 30lp");
+
+        JLabel label;
+        DoubleModel length;
+        DoubleModel length2;
+        DoubleModel length_2;
+        JSpinner spin;
+        JButton calcHeight;
+
+        length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
+        length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
+        length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
+
+        register(length);
+        register(length2);
+        register(length_2);
+
+        ////  Tab length
+        //// Tab length:
+        label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
+        //// The length of the fin tab.
+        label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
+        panel.add(label, "gapleft para, gapright 40lp, growx 1");
+
+        final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
+
+        spin = new JSpinner(mtl.getSpinnerModel());
+        spin.setEditor(new SpinnerEditor(spin));
+        panel.add(spin, "growx 1");
+
+        panel.add(new UnitSelector(mtl), "growx 1");
+        panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
+                "w 100lp, growx 5, wrap");
+
+
+        ////  Tab length
+        //// Tab height:
+        label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
+        //// The spanwise height of the fin tab.
+        label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
+        panel.add(label, "gapleft para");
+
+        final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
+
+        spin = new JSpinner(mth.getSpinnerModel());
+        spin.setEditor(new SpinnerEditor(spin));
+        panel.add(spin, "growx");
+
+        panel.add(new UnitSelector(mth), "growx");
+        panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
+                "w 100lp, growx 5, wrap para");
+
+        ////  Tab position:
+        label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
+        //// The position of the fin tab.
+        label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
+        panel.add(label, "gapleft para");
+
+        final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
+
+        spin = new JSpinner(mts.getSpinnerModel());
+        spin.setEditor(new SpinnerEditor(spin));
+        panel.add(spin, "growx");
+
+        panel.add(new UnitSelector(mts), "growx");
+        panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
+
+
+        //// relative to
+        label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
+        panel.add(label, "right, gapright unrel");
+
+        final EnumModel<FinSet.TabRelativePosition> em =
+                new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
+
+        panel.add(new JComboBox(em), "spanx 3, growx");
+
+        calcHeight = new JButton(trans.get("FinSetConfig.but.Calcheight"));
+
+        // Calculate fin tab height, length, and position
+        calcHeight.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                log.user("Computing " + component.getComponentName() + " tab height.");
+
+                // Do change in future for overall safety
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        RocketComponent parent = component.getParent();
+                        if (parent instanceof Coaxial) {
+                            List<RocketComponent> children = parent.getChildren();
+                            List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+                            ComponentConfigDialog.addUndoPosition("Compute fin tab height");
+                            for (int i = 0; i < children.size(); i++) {
+                                RocketComponent rocketComponent = children.get(i);
+                                if (rocketComponent instanceof InnerTube) {
+                                    InnerTube it = (InnerTube) rocketComponent;
+                                    if (it.isMotorMount()) {
+                                        double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius();
+                                        //Set fin tab depth
+                                        if (depth >= 0.0d) {
+                                            mth.setValue(depth);
+                                            mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
+                                        }
+                                    }
+                                } else if (rocketComponent instanceof CenteringRing) {
+                                    rings.add((CenteringRing) rocketComponent);
+                                }
+                            }
+                            //Figure out position and length of the fin tab
+                            if (!rings.isEmpty()) {
+                                FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem();
+                                em.setSelectedItem(FinSet.TabRelativePosition.FRONT);
+                                double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP),
+                                        component.getLength(), mts);
+                                mtl.setValue(len);
+                                //Be nice to the user and set the tab relative position enum back the way they had it.
+                                em.setSelectedItem(temp);
+                            }
+                        }
+                    }
+                });
+            }
+        });
+        panel.add(calcHeight, "right, gapright unrel");
+
+        return panel;
+    }
+
+    /**
+     * Scenarios:
+     * <p/>
+     * 1. All rings ahead of start of fin.
+     * 2. First ring ahead of start of fin.  Second ring ahead of end of fin.
+     * 3. First ring ahead of start of fin.  Second ring behind end of fin.
+     * 4. First ring equal or behind start of fin.  Second ring ahead of, or equal to, end of fin.
+     * 5. First ring equal or behind start of fin. Second ring behind end of fin.
+     * 6. All rings behind end of fin.
+     *
+     * @param rings              an unordered list of centering rings attached to the parent of the fin set
+     * @param finPositionFromTop the position from the top of the parent of the start of the fin set root
+     * @param finLength          the length of the root chord
+     * @param mts                the model for the tab shift (position); the model's value is modified as a result of this method call
+     * @return the length of the fin tab
+     */
+    private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts) {
+        List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
+
+        //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here.
+        SortableRing top = null;
+        SortableRing bottom = null;
+
+        if (rings != null) {
+            //Sort rings from top of parent to bottom
+            Collections.sort(rings, new Comparator<CenteringRing>() {
+                @Override
+                public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
+                    return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP) -
+                            centeringRing1.asPositionValue(RocketComponent.Position.TOP)));
+                }
+            });
+
+            for (int i = 0; i < rings.size(); i++) {
+                CenteringRing centeringRing = rings.get(i);
+                //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring.
+                if (!positionsFromTop.isEmpty() &&
+                        positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >= centeringRing.asPositionValue(RocketComponent.Position.TOP)) {
+                    SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1);
+                    adjacent.merge(centeringRing);
+                } else {
+                    positionsFromTop.add(new SortableRing(centeringRing));
+                }
+            }
+
+            for (int i = 0; i < positionsFromTop.size(); i++) {
+                SortableRing sortableRing = positionsFromTop.get(i);
+                if (top == null) {
+                    top = sortableRing;
+                } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
+                    top = sortableRing;
+                    bottom = null;
+                } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) {
+                    if (bottom == null) {
+                        //If the current ring is in the upper half of the root chord, make it the top ring
+                        if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) {
+                            top = sortableRing;
+                        } else {
+                            bottom = sortableRing;
+                        }
+                    }
+                    //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring,
+                    //and the current ring the bottom
+                    else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) {
+                        top = bottom;
+                        bottom = sortableRing;
+                    }
+                } else {
+                    if (bottom == null) {
+                        bottom = sortableRing;
+                    }
+                }
+            }
+        }
+
+        // Edge case where there are no centering rings or for some odd reason top and bottom are identical.
+        if (top == null || top == bottom) {
+            mts.setValue(0);
+            return finLength;
+        }
+
+        if (bottom == null) {
+            // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then
+            // set the position of the fin tab starting at the bottom side of the top ring.
+            if (top.bottomSidePositionFromTop() >= finPositionFromTop) {
+                mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
+                return (finPositionFromTop + finLength - top.bottomSidePositionFromTop());
+            } else {
+                // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
+                // root chord.
+                mts.setValue(0);
+                return finLength;
+            }
+        }
+        // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the
+        // fin tab align with the start of the root chord.
+        if (top.bottomSidePositionFromTop() < finPositionFromTop) {
+            mts.setValue(0);
+            return bottom.positionFromTop - finPositionFromTop;
+        } else {
+            // Otherwise the rings are within the span of the root chord.  Place the tab between them.
+            mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
+            return (bottom.positionFromTop() - top.bottomSidePositionFromTop());
+        }
+    }
+
+    @Override
+    public void updateFields() {
+        super.updateFields();
+        if (split != null)
+            split.setEnabled(((FinSet) component).getFinCount() > 1);
+    }
+
+    /**
+     * A container class to store pertinent info about centering rings.  This is used in the computation to figure
+     * out tab length and position.
+     */
+    static class SortableRing {
+
+        /**
+         * The length of the ring (more commonly called the thickness).
+         */
+        private double thickness;
+        /**
+         * The position of the ring from the top of the parent.
+         */
+        private double positionFromTop;
+
+        /**
+         * Constructor.
+         *
+         * @param r the source centering ring
+         */
+        SortableRing(CenteringRing r) {
+            thickness = r.getLength();
+            positionFromTop = r.asPositionValue(RocketComponent.Position.TOP);
+        }
+
+        /**
+         * Merge an adjacent ring.
+         *
+         * @param adjacent the adjacent ring
+         */
+        public void merge(CenteringRing adjacent) {
+            double v = adjacent.asPositionValue(RocketComponent.Position.TOP);
+            if (positionFromTop < v) {
+                thickness = (v + adjacent.getLength()) - positionFromTop;
+            } else {
+                double tmp = positionFromTop + thickness;
+                positionFromTop = v;
+                thickness = tmp - v;
+            }
+        }
+
+        /**
+         * Compute the position of the bottom edge of the ring, relative to the top of the parent.
+         *
+         * @return the distance from the top of the parent to the bottom edge of the ring
+         */
+        public double bottomSidePositionFromTop() {
+            return positionFromTop + thickness;
+        }
+
+        /**
+         * Compute the position of the top edge of the ring, relative to the top of the parent.
+         *
+         * @return the distance from the top of the parent to the top edge of the ring
+         */
+        public double positionFromTop() {
+            return positionFromTop;
+        }
+    }
 }
index 6b906589615dc0fc62ae807a0ebfaba743b1838c..bbce869a56cc88de4a5f3a791539fb161aabd0a9 100644 (file)
@@ -1,15 +1,5 @@
 package net.sf.openrocket.rocketcomponent;
 
-import java.awt.Color;
-import java.util.ArrayDeque;
-import java.util.Collection;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import javax.swing.event.ChangeListener;
-
 import net.sf.openrocket.l10n.Translator;
 import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.preset.ComponentPreset;
@@ -24,6 +14,15 @@ import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.SafetyMutex;
 import net.sf.openrocket.util.UniqueID;
 
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
 
 public abstract class RocketComponent implements ChangeSource, Cloneable, Iterable<RocketComponent> {
        private static final LogHelper log = Application.getLogger();
@@ -839,10 +838,44 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
                this.relativePosition = position;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-       
-       
 
 
+    /**
+     * Determine position relative to given position argument.  Note: This is a side-effect free method.  No state
+     * is modified.  It's exactly like setRelativePosition without the 'set'.
+     *
+     * @param thePosition the relative position to be used as the basis for the computation
+     *
+     * @return double position of the component relative to the parent, with respect to <code>position</code>
+     */
+    public double asPositionValue (Position thePosition) {
+        if (this.relativePosition == thePosition) {
+            return this.position;
+        }
+        double result = this.position;
+        if (this.parent != null) {
+            double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x;
+
+            switch (thePosition) {
+            case ABSOLUTE:
+                result = this.toAbsolute(Coordinate.NUL)[0].x;
+                break;
+            case TOP:
+                result = thisPos;
+                break;
+            case MIDDLE:
+                result = thisPos - (this.parent.length - this.length) / 2;
+                break;
+            case BOTTOM:
+                result = thisPos - (this.parent.length - this.length);
+                break;
+            default:
+                throw new BugException("Unknown position type: " + thePosition);
+            }
+        }
+        return result;
+    }
+
        /**
         * Get the position value of the component.  The exact meaning of the value is
         * dependent on the current relative positioning.
diff --git a/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java b/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java
new file mode 100644 (file)
index 0000000..cbc6113
--- /dev/null
@@ -0,0 +1,210 @@
+package net.sf.openrocket.gui.configdialog;
+
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FinSetConfigTest {
+
+    static Method method;
+
+    @BeforeClass
+    public static void classSetup() throws Exception {
+        method = FinSetConfig.class.getDeclaredMethod("computeFinTabLength", List.class, Double.class, Double.class, DoubleModel.class);
+        Assert.assertNotNull(method);
+        method.setAccessible(true);
+    }
+
+    /**
+     * Test no centering rings.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testComputeFinTabLength() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+        
+        Double result = (Double)method.invoke(null, rings, 10d, 11d, dm);
+        Assert.assertEquals(0.0001, 11d, result.doubleValue());
+        result = (Double)method.invoke(null, null, 10d, 11d, dm);
+        Assert.assertEquals(11d, result.doubleValue(), 0.0001);
+    }
+
+    /**
+     * Test 2 rings both ahead of the fin.
+     */
+    @Test
+    public void testCompute2LeadingRings() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.43);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.45);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.01, result.doubleValue(), 0.0001);
+        
+    }
+
+    /**
+     * Test one ring, ahead of the fin.
+     */
+    @Test
+    public void testCompute1Ring() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.43);
+        rings.add(ring1);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.01, result.doubleValue(), 0.0001);
+    }
+
+    /**
+     * Test one ring ahead of the fin, one ring within the root chord.
+     */
+    @Test
+    public void testComputeOneLeadingOneRingWithinRoot() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.43);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.45);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.45d, 0.01, dm);
+        Assert.assertEquals(0.01 - 0.004, result.doubleValue(), 0.0001);
+    }
+
+    /**
+     * Test one ring ahead of the fin, one ring beyond the root chord.
+     */
+    @Test
+    public void testComputeOneLeadingOneTrailingRing() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.43);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.48);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.01, result.doubleValue(), 0.0001);
+    }
+
+    /**
+     * Test one ring within the root chord, another ring beyond the root chord.
+     */
+    @Test
+    public void testComputeOneWithinRootOneTrailingRing() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.4701);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.48);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.0059, result.doubleValue(), 0.0001);
+    }
+    
+    /**
+     * Test both rings within the root chord.
+     */
+    @Test
+    public void testBothRingsWithinRootChord() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.4701);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.4750);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.0009, result.doubleValue(), 0.0001);
+    }
+
+
+    /**
+     * Test both rings beyond the root chord.
+     */
+    @Test
+    public void testBothRingsBeyondRootChord() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.48);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.49);
+        rings.add(ring1);
+        rings.add(ring2);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.006, result.doubleValue(), 0.0001);
+    }
+
+    /**
+     * Test both rings within the root chord - the top ring has an adjacent ring (so 3 rings total).
+     */
+    @Test
+    public void test3RingsWithinRootChord() throws Exception {
+        DoubleModel dm = new DoubleModel(1d);
+        List<CenteringRing> rings = new ArrayList<CenteringRing>();
+
+        CenteringRing ring1 = new CenteringRing();
+        ring1.setLength(0.004);
+        ring1.setPositionValue(0.47);
+        CenteringRing ring2 = new CenteringRing();
+        ring2.setLength(0.004);
+        ring2.setPositionValue(0.4702);
+        CenteringRing ring3 = new CenteringRing();
+        ring3.setLength(0.004);
+        ring3.setPositionValue(0.4770);
+        rings.add(ring1);
+        rings.add(ring2);
+        rings.add(ring3);
+
+        Double result = (Double)method.invoke(null, rings, 0.47d, 0.01, dm);
+        Assert.assertEquals(0.0028, result.doubleValue(), 0.0001);
+    }
+
+}