1 package net.sf.openrocket.gui.configdialog;
3 import net.miginfocom.swing.MigLayout;
4 import net.sf.openrocket.gui.SpinnerEditor;
5 import net.sf.openrocket.gui.adaptors.DoubleModel;
6 import net.sf.openrocket.gui.adaptors.EnumModel;
7 import net.sf.openrocket.gui.components.BasicSlider;
8 import net.sf.openrocket.gui.components.StyledLabel;
9 import net.sf.openrocket.gui.components.StyledLabel.Style;
10 import net.sf.openrocket.gui.components.UnitSelector;
11 import net.sf.openrocket.l10n.Translator;
12 import net.sf.openrocket.logging.LogHelper;
13 import net.sf.openrocket.rocketcomponent.CenteringRing;
14 import net.sf.openrocket.rocketcomponent.Coaxial;
15 import net.sf.openrocket.rocketcomponent.FinSet;
16 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
17 import net.sf.openrocket.rocketcomponent.InnerTube;
18 import net.sf.openrocket.rocketcomponent.RocketComponent;
19 import net.sf.openrocket.startup.Application;
20 import net.sf.openrocket.unit.UnitGroup;
23 import java.awt.event.ActionEvent;
24 import java.awt.event.ActionListener;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.List;
31 public abstract class FinSetConfig extends RocketComponentConfig {
32 private static final LogHelper log = Application.getLogger();
33 private static final Translator trans = Application.getTranslator();
35 private JButton split = null;
37 public FinSetConfig(RocketComponent component) {
40 //// Fin tabs and Through-the-wall fin tabs
41 tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(),
42 trans.get("FinSetConfig.tab.Through-the-wall"), 0);
46 protected void addFinSetButtons() {
47 JButton convert = null;
50 if (!(component instanceof FreeformFinSet)) {
51 //// Convert to freeform
52 convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
53 //// Convert this fin set into a freeform fin set
54 convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
55 convert.addActionListener(new ActionListener() {
57 public void actionPerformed(ActionEvent e) {
58 log.user("Converting " + component.getComponentName() + " into freeform fin set");
60 // Do change in future for overall safety
61 SwingUtilities.invokeLater(new Runnable() {
65 ComponentConfigDialog.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
66 RocketComponent freeform =
67 FreeformFinSet.convertFinSet((FinSet) component);
68 ComponentConfigDialog.showDialog(freeform);
72 ComponentConfigDialog.hideDialog();
78 split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
79 //// Split the fin set into separate fins
80 split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
81 split.addActionListener(new ActionListener() {
83 public void actionPerformed(ActionEvent e) {
84 log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
85 ((FinSet) component).getFinCount());
87 // Do change in future for overall safety
88 SwingUtilities.invokeLater(new Runnable() {
91 RocketComponent parent = component.getParent();
92 int index = parent.getChildPosition(component);
93 int count = ((FinSet) component).getFinCount();
94 double base = ((FinSet) component).getBaseRotation();
98 ComponentConfigDialog.addUndoPosition("Split fin set");
99 parent.removeChild(index);
100 for (int i = 0; i < count; i++) {
101 FinSet copy = (FinSet) component.copy();
103 copy.setBaseRotation(base + i * 2 * Math.PI / count);
104 copy.setName(copy.getName() + " #" + (i + 1));
105 parent.addChild(copy, index + i);
110 ComponentConfigDialog.hideDialog();
113 split.setEnabled(((FinSet) component).getFinCount() > 1);
118 addButtons(split, convert);
122 public JPanel finTabPanel() {
123 JPanel panel = new JPanel(
124 new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
125 "[150lp::][65lp::][30lp::][200lp::]", ""));
126 // JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
127 // "[40lp][80lp::][30lp::][100lp::]",""));
129 //// Through-the-wall fin tabs:
130 panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
136 DoubleModel length_2;
140 length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
141 length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
142 length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
150 label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
151 //// The length of the fin tab.
152 label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
153 panel.add(label, "gapleft para, gapright 40lp, growx 1");
155 final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
157 spin = new JSpinner(mtl.getSpinnerModel());
158 spin.setEditor(new SpinnerEditor(spin));
159 panel.add(spin, "growx 1");
161 panel.add(new UnitSelector(mtl), "growx 1");
162 panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
163 "w 100lp, growx 5, wrap");
168 label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
169 //// The spanwise height of the fin tab.
170 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
171 panel.add(label, "gapleft para");
173 final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
175 spin = new JSpinner(mth.getSpinnerModel());
176 spin.setEditor(new SpinnerEditor(spin));
177 panel.add(spin, "growx");
179 panel.add(new UnitSelector(mth), "growx");
180 panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
181 "w 100lp, growx 5, wrap para");
184 label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
185 //// The position of the fin tab.
186 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
187 panel.add(label, "gapleft para");
189 final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
191 spin = new JSpinner(mts.getSpinnerModel());
192 spin.setEditor(new SpinnerEditor(spin));
193 panel.add(spin, "growx");
195 panel.add(new UnitSelector(mts), "growx");
196 panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
200 label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
201 panel.add(label, "right, gapright unrel");
203 final EnumModel<FinSet.TabRelativePosition> em =
204 new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
206 panel.add(new JComboBox(em), "spanx 3, growx");
208 calcHeight = new JButton(trans.get("FinSetConfig.but.Calcheight"));
210 // Calculate fin tab height, length, and position
211 calcHeight.addActionListener(new ActionListener() {
213 public void actionPerformed(ActionEvent e) {
214 log.user("Computing " + component.getComponentName() + " tab height.");
216 // Do change in future for overall safety
217 SwingUtilities.invokeLater(new Runnable() {
220 RocketComponent parent = component.getParent();
221 if (parent instanceof Coaxial) {
222 List<RocketComponent> children = parent.getChildren();
223 List<CenteringRing> rings = new ArrayList<CenteringRing>();
225 ComponentConfigDialog.addUndoPosition("Compute fin tab height");
226 for (int i = 0; i < children.size(); i++) {
227 RocketComponent rocketComponent = children.get(i);
228 if (rocketComponent instanceof InnerTube) {
229 InnerTube it = (InnerTube) rocketComponent;
230 if (it.isMotorMount()) {
231 double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius();
235 mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
238 } else if (rocketComponent instanceof CenteringRing) {
239 rings.add((CenteringRing) rocketComponent);
242 //Figure out position and length of the fin tab
243 if (!rings.isEmpty()) {
244 FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem();
245 em.setSelectedItem(FinSet.TabRelativePosition.FRONT);
246 double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP),
247 component.getLength(), mts);
249 //Be nice to the user and set the tab relative position enum back the way they had it.
250 em.setSelectedItem(temp);
257 panel.add(calcHeight, "right, gapright unrel");
265 * 1. All rings ahead of start of fin.
266 * 2. First ring ahead of start of fin. Second ring ahead of end of fin.
267 * 3. First ring ahead of start of fin. Second ring behind end of fin.
268 * 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin.
269 * 5. First ring equal or behind start of fin. Second ring behind end of fin.
270 * 6. All rings behind end of fin.
272 * @param rings an unordered list of centering rings attached to the parent of the fin set
273 * @param finPositionFromTop the position from the top of the parent of the start of the fin set root
274 * @param finLength the length of the root chord
275 * @param mts the model for the tab shift (position); the model's value is modified as a result of this method call
276 * @return the length of the fin tab
278 private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts) {
279 List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
281 //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here.
282 SortableRing top = null;
283 SortableRing bottom = null;
286 //Sort rings from top of parent to bottom
287 Collections.sort(rings, new Comparator<CenteringRing>() {
289 public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
290 return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP) -
291 centeringRing1.asPositionValue(RocketComponent.Position.TOP)));
295 for (int i = 0; i < rings.size(); i++) {
296 CenteringRing centeringRing = rings.get(i);
297 //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring.
298 if (!positionsFromTop.isEmpty() &&
299 positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >= centeringRing.asPositionValue(RocketComponent.Position.TOP)) {
300 SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1);
301 adjacent.merge(centeringRing);
303 positionsFromTop.add(new SortableRing(centeringRing));
307 for (int i = 0; i < positionsFromTop.size(); i++) {
308 SortableRing sortableRing = positionsFromTop.get(i);
311 } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
314 } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) {
315 if (bottom == null) {
316 //If the current ring is in the upper half of the root chord, make it the top ring
317 if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) {
320 bottom = sortableRing;
323 //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring,
324 //and the current ring the bottom
325 else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) {
327 bottom = sortableRing;
330 if (bottom == null) {
331 bottom = sortableRing;
337 // Edge case where there are no centering rings or for some odd reason top and bottom are identical.
338 if (top == null || top == bottom) {
343 if (bottom == null) {
344 // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then
345 // set the position of the fin tab starting at the bottom side of the top ring.
346 if (top.bottomSidePositionFromTop() >= finPositionFromTop) {
347 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
348 return (finPositionFromTop + finLength - top.bottomSidePositionFromTop());
350 // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
356 // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the
357 // fin tab align with the start of the root chord.
358 if (top.bottomSidePositionFromTop() < finPositionFromTop) {
360 return bottom.positionFromTop - finPositionFromTop;
362 // Otherwise the rings are within the span of the root chord. Place the tab between them.
363 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
364 return (bottom.positionFromTop() - top.bottomSidePositionFromTop());
369 public void updateFields() {
370 super.updateFields();
372 split.setEnabled(((FinSet) component).getFinCount() > 1);
376 * A container class to store pertinent info about centering rings. This is used in the computation to figure
377 * out tab length and position.
379 static class SortableRing {
382 * The length of the ring (more commonly called the thickness).
384 private double thickness;
386 * The position of the ring from the top of the parent.
388 private double positionFromTop;
393 * @param r the source centering ring
395 SortableRing(CenteringRing r) {
396 thickness = r.getLength();
397 positionFromTop = r.asPositionValue(RocketComponent.Position.TOP);
401 * Merge an adjacent ring.
403 * @param adjacent the adjacent ring
405 public void merge(CenteringRing adjacent) {
406 double v = adjacent.asPositionValue(RocketComponent.Position.TOP);
407 if (positionFromTop < v) {
408 thickness = (v + adjacent.getLength()) - positionFromTop;
410 double tmp = positionFromTop + thickness;
417 * Compute the position of the bottom edge of the ring, relative to the top of the parent.
419 * @return the distance from the top of the parent to the bottom edge of the ring
421 public double bottomSidePositionFromTop() {
422 return positionFromTop + thickness;
426 * Compute the position of the top edge of the ring, relative to the top of the parent.
428 * @return the distance from the top of the parent to the top edge of the ring
430 public double positionFromTop() {
431 return positionFromTop;