diff --git a/l10n/messages.properties b/l10n/messages.properties index 4457bc7b1..73d0a9b75 100644 --- a/l10n/messages.properties +++ b/l10n/messages.properties @@ -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. diff --git a/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index da124327b..b1037a1d8 100644 --- a/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -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 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); - } - - + private JButton split = null; - 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"); - + public FinSetConfig(RocketComponent component) { + super(component); - //// 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"); - + //// 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); + } - //// 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 em = - new EnumModel(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); - } + 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 em = + new EnumModel(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 children = parent.getChildren(); + List rings = new ArrayList(); + + 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: + *

+ * 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 rings, Double finPositionFromTop, Double finLength, DoubleModel mts) { + List positionsFromTop = new ArrayList(); + + //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() { + @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; + } + } } diff --git a/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index 6b9065896..bbce869a5 100644 --- a/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -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 { 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 position + */ + 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 index 000000000..cbc6113f3 --- /dev/null +++ b/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java @@ -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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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 rings = new ArrayList(); + + 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); + } + +}