1 package net.sf.openrocket.gui.configdialog;
3 import net.miginfocom.swing.MigLayout;
4 import net.sf.openrocket.document.OpenRocketDocument;
5 import net.sf.openrocket.gui.SpinnerEditor;
6 import net.sf.openrocket.gui.adaptors.DoubleModel;
7 import net.sf.openrocket.gui.adaptors.EnumModel;
8 import net.sf.openrocket.gui.components.BasicSlider;
9 import net.sf.openrocket.gui.components.StyledLabel;
10 import net.sf.openrocket.gui.components.StyledLabel.Style;
11 import net.sf.openrocket.gui.components.UnitSelector;
12 import net.sf.openrocket.l10n.Translator;
13 import net.sf.openrocket.logging.LogHelper;
14 import net.sf.openrocket.rocketcomponent.CenteringRing;
15 import net.sf.openrocket.rocketcomponent.Coaxial;
16 import net.sf.openrocket.rocketcomponent.FinSet;
17 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
18 import net.sf.openrocket.rocketcomponent.InnerTube;
19 import net.sf.openrocket.rocketcomponent.RocketComponent;
20 import net.sf.openrocket.startup.Application;
21 import net.sf.openrocket.unit.UnitGroup;
24 import java.awt.event.ActionEvent;
25 import java.awt.event.ActionListener;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.Iterator;
30 import java.util.List;
33 public abstract class FinSetConfig extends RocketComponentConfig {
34 private static final LogHelper log = Application.getLogger();
35 private static final Translator trans = Application.getTranslator();
37 private JButton split = null;
39 public FinSetConfig(OpenRocketDocument d, RocketComponent component) {
42 //// Fin tabs and Through-the-wall fin tabs
43 tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(),
44 trans.get("FinSetConfig.tab.Through-the-wall"), 0);
48 protected void addFinSetButtons() {
49 JButton convert = null;
52 if (!(component instanceof FreeformFinSet)) {
53 //// Convert to freeform
54 convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
55 //// Convert this fin set into a freeform fin set
56 convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
57 convert.addActionListener(new ActionListener() {
59 public void actionPerformed(ActionEvent e) {
60 log.user("Converting " + component.getComponentName() + " into freeform fin set");
62 // Do change in future for overall safety
63 SwingUtilities.invokeLater(new Runnable() {
67 document.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
68 RocketComponent freeform =
69 FreeformFinSet.convertFinSet((FinSet) component);
70 ComponentConfigDialog.showDialog(freeform);
74 ComponentConfigDialog.hideDialog();
80 split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
81 //// Split the fin set into separate fins
82 split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
83 split.addActionListener(new ActionListener() {
85 public void actionPerformed(ActionEvent e) {
86 log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
87 ((FinSet) component).getFinCount());
89 // Do change in future for overall safety
90 SwingUtilities.invokeLater(new Runnable() {
93 RocketComponent parent = component.getParent();
94 int index = parent.getChildPosition(component);
95 int count = ((FinSet) component).getFinCount();
96 double base = ((FinSet) component).getBaseRotation();
100 document.addUndoPosition("Split fin set");
101 parent.removeChild(index);
102 for (int i = 0; i < count; i++) {
103 FinSet copy = (FinSet) component.copy();
105 copy.setBaseRotation(base + i * 2 * Math.PI / count);
106 copy.setName(copy.getName() + " #" + (i + 1));
107 parent.addChild(copy, index + i);
112 ComponentConfigDialog.hideDialog();
115 split.setEnabled(((FinSet) component).getFinCount() > 1);
120 addButtons(split, convert);
124 public JPanel finTabPanel() {
125 JPanel panel = new JPanel(
126 new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
127 "[150lp::][65lp::][30lp::][200lp::]", ""));
128 // JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
129 // "[40lp][80lp::][30lp::][100lp::]",""));
131 //// Through-the-wall fin tabs:
132 panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
138 DoubleModel length_2;
142 length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
143 length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
144 length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
152 label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
153 //// The length of the fin tab.
154 label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
155 panel.add(label, "gapleft para, gapright 40lp, growx 1");
157 final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
159 spin = new JSpinner(mtl.getSpinnerModel());
160 spin.setEditor(new SpinnerEditor(spin));
161 panel.add(spin, "growx 1");
163 panel.add(new UnitSelector(mtl), "growx 1");
164 panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
165 "w 100lp, growx 5, wrap");
170 label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
171 //// The spanwise height of the fin tab.
172 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
173 panel.add(label, "gapleft para");
175 final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
177 spin = new JSpinner(mth.getSpinnerModel());
178 spin.setEditor(new SpinnerEditor(spin));
179 panel.add(spin, "growx");
181 panel.add(new UnitSelector(mth), "growx");
182 panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
183 "w 100lp, growx 5, wrap");
186 label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
187 //// The position of the fin tab.
188 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
189 panel.add(label, "gapleft para");
191 final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
193 spin = new JSpinner(mts.getSpinnerModel());
194 spin.setEditor(new SpinnerEditor(spin));
195 panel.add(spin, "growx");
197 panel.add(new UnitSelector(mts), "growx");
198 panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
202 label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
203 panel.add(label, "right, gapright unrel");
205 final EnumModel<FinSet.TabRelativePosition> em =
206 new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
208 panel.add(new JComboBox(em), "spanx 3, growx, wrap para");
211 // Calculate fin tab height, length, and position
212 autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc"));
214 autoCalc.addActionListener(new ActionListener() {
216 public void actionPerformed(ActionEvent e) {
217 log.user("Computing " + component.getComponentName() + " tab height.");
219 RocketComponent parent = component.getParent();
220 if (parent instanceof Coaxial) {
222 document.startUndo("Compute fin tabs");
224 List<CenteringRing> rings = new ArrayList<CenteringRing>();
225 //Do deep recursive iteration
226 Iterator<RocketComponent> iter = parent.iterator(false);
227 while (iter.hasNext()) {
228 RocketComponent rocketComponent = iter.next();
229 if (rocketComponent instanceof InnerTube) {
230 InnerTube it = (InnerTube) rocketComponent;
231 if (it.isMotorMount()) {
232 double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius();
236 mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
239 } else if (rocketComponent instanceof CenteringRing) {
240 rings.add((CenteringRing) rocketComponent);
243 //Figure out position and length of the fin tab
244 if (!rings.isEmpty()) {
245 FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem();
246 em.setSelectedItem(FinSet.TabRelativePosition.FRONT);
247 double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP, parent),
248 component.getLength(), mts, parent);
250 //Be nice to the user and set the tab relative position enum back the way they had it.
251 em.setSelectedItem(temp);
260 panel.add(autoCalc, "skip 1, spanx");
268 * 1. All rings ahead of start of fin.
269 * 2. First ring ahead of start of fin. Second ring ahead of end of fin.
270 * 3. First ring ahead of start of fin. Second ring behind end of fin.
271 * 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin.
272 * 5. First ring equal or behind start of fin. Second ring behind end of fin.
273 * 6. All rings behind end of fin.
275 * @param rings an unordered list of centering rings attached to the parent of the fin set
276 * @param finPositionFromTop the position from the top of the parent of the start of the fin set root
277 * @param finLength the length of the root chord
278 * @param mts the model for the tab shift (position); the model's value is modified as a result of this method call
279 * @param relativeTo the parent component of the finset
281 * @return the length of the fin tab
283 private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts,
284 final RocketComponent relativeTo) {
285 List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
287 //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here.
288 SortableRing top = null;
289 SortableRing bottom = null;
292 //Sort rings from top of parent to bottom
293 Collections.sort(rings, new Comparator<CenteringRing>() {
295 public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
296 return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo) -
297 centeringRing1.asPositionValue(RocketComponent.Position.TOP, relativeTo)));
301 for (int i = 0; i < rings.size(); i++) {
302 CenteringRing centeringRing = rings.get(i);
303 //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring.
304 if (!positionsFromTop.isEmpty() &&
305 positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >=
306 centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo)) {
307 SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1);
308 adjacent.merge(centeringRing, relativeTo);
310 positionsFromTop.add(new SortableRing(centeringRing, relativeTo));
314 for (int i = 0; i < positionsFromTop.size(); i++) {
315 SortableRing sortableRing = positionsFromTop.get(i);
318 } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
321 } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) {
322 if (bottom == null) {
323 //If the current ring is in the upper half of the root chord, make it the top ring
324 if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) {
327 bottom = sortableRing;
330 //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring,
331 //and the current ring the bottom
332 else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) {
334 bottom = sortableRing;
337 if (bottom == null) {
338 bottom = sortableRing;
344 double resultFinTabLength = 0d;
346 // Edge case where there are no centering rings or for some odd reason top and bottom are identical.
347 if (top == null || top == bottom) {
349 resultFinTabLength = finLength;
350 } else if (bottom == null) {
351 // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then
352 // set the position of the fin tab starting at the bottom side of the top ring.
353 if (top.bottomSidePositionFromTop() >= finPositionFromTop) {
354 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
355 resultFinTabLength = (finPositionFromTop + finLength - top.bottomSidePositionFromTop());
358 double diffLen = top.positionFromTop() - finPositionFromTop;
360 // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
362 resultFinTabLength = finLength;
365 // Otherwise there is one ring within the span. Return the length from the start of the fin to the top
367 resultFinTabLength = diffLen;
371 // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the
372 // fin tab align with the start of the root chord.
373 else if (top.bottomSidePositionFromTop() < finPositionFromTop) {
376 double lenToBottomRing = bottom.positionFromTop - finPositionFromTop;
377 // If the bottom ring lies farther back (down) than the trailing edge of the fin, then the tab should
378 // only be as long as the fin.
379 if (lenToBottomRing > finLength) {
380 resultFinTabLength = finLength;
383 resultFinTabLength = lenToBottomRing;
386 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
387 // The bottom ring is beyond the trailing edge of the fin.
388 if (bottom.positionFromTop() > finLength + finPositionFromTop) {
389 resultFinTabLength = (finLength + finPositionFromTop - top.bottomSidePositionFromTop());
391 // The rings are within the span of the root chord. Place the tab between them.
393 resultFinTabLength = (bottom.positionFromTop() - top.bottomSidePositionFromTop());
396 if (resultFinTabLength < 0) {
397 resultFinTabLength = 0d;
399 return resultFinTabLength;
403 public void updateFields() {
404 super.updateFields();
406 split.setEnabled(((FinSet) component).getFinCount() > 1);
410 * A container class to store pertinent info about centering rings. This is used in the computation to figure
411 * out tab length and position.
413 static class SortableRing {
416 * The length of the ring (more commonly called the thickness).
418 private double thickness;
420 * The position of the ring from the top of the parent.
422 private double positionFromTop;
427 * @param r the source centering ring
429 SortableRing(CenteringRing r, RocketComponent relativeTo) {
430 thickness = r.getLength();
431 positionFromTop = r.asPositionValue(RocketComponent.Position.TOP, relativeTo);
435 * Merge an adjacent ring.
437 * @param adjacent the adjacent ring
439 public void merge(CenteringRing adjacent, RocketComponent relativeTo) {
440 double v = adjacent.asPositionValue(RocketComponent.Position.TOP, relativeTo);
441 if (positionFromTop < v) {
442 thickness = (v + adjacent.getLength()) - positionFromTop;
444 double tmp = positionFromTop + thickness;
451 * Compute the position of the bottom edge of the ring, relative to the top of the parent.
453 * @return the distance from the top of the parent to the bottom edge of the ring
455 public double bottomSidePositionFromTop() {
456 return positionFromTop + thickness;
460 * Compute the position of the top edge of the ring, relative to the top of the parent.
462 * @return the distance from the top of the parent to the top edge of the ring
464 public double positionFromTop() {
465 return positionFromTop;