1 package net.sf.openrocket.gui.configdialog;
3 import java.awt.event.ActionEvent;
4 import java.awt.event.ActionListener;
5 import java.util.ArrayList;
6 import java.util.Collections;
7 import java.util.Comparator;
10 import javax.swing.JButton;
11 import javax.swing.JComboBox;
12 import javax.swing.JLabel;
13 import javax.swing.JPanel;
14 import javax.swing.JSpinner;
15 import javax.swing.SwingUtilities;
17 import net.miginfocom.swing.MigLayout;
18 import net.sf.openrocket.document.OpenRocketDocument;
19 import net.sf.openrocket.gui.SpinnerEditor;
20 import net.sf.openrocket.gui.adaptors.DoubleModel;
21 import net.sf.openrocket.gui.adaptors.EnumModel;
22 import net.sf.openrocket.gui.components.BasicSlider;
23 import net.sf.openrocket.gui.components.StyledLabel;
24 import net.sf.openrocket.gui.components.StyledLabel.Style;
25 import net.sf.openrocket.gui.components.UnitSelector;
26 import net.sf.openrocket.l10n.Translator;
27 import net.sf.openrocket.logging.LogHelper;
28 import net.sf.openrocket.rocketcomponent.CenteringRing;
29 import net.sf.openrocket.rocketcomponent.Coaxial;
30 import net.sf.openrocket.rocketcomponent.FinSet;
31 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
32 import net.sf.openrocket.rocketcomponent.InnerTube;
33 import net.sf.openrocket.rocketcomponent.RocketComponent;
34 import net.sf.openrocket.startup.Application;
35 import net.sf.openrocket.unit.UnitGroup;
38 public abstract class FinSetConfig extends RocketComponentConfig {
39 private static final LogHelper log = Application.getLogger();
40 private static final Translator trans = Application.getTranslator();
42 private JButton split = null;
44 public FinSetConfig(OpenRocketDocument d, RocketComponent component) {
47 //// Fin tabs and Through-the-wall fin tabs
48 tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(),
49 trans.get("FinSetConfig.tab.Through-the-wall"), 0);
53 protected void addFinSetButtons() {
54 JButton convert = null;
57 if (!(component instanceof FreeformFinSet)) {
58 //// Convert to freeform
59 convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
60 //// Convert this fin set into a freeform fin set
61 convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
62 convert.addActionListener(new ActionListener() {
64 public void actionPerformed(ActionEvent e) {
65 log.user("Converting " + component.getComponentName() + " into freeform fin set");
67 // Do change in future for overall safety
68 SwingUtilities.invokeLater(new Runnable() {
72 document.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
73 RocketComponent freeform =
74 FreeformFinSet.convertFinSet((FinSet) component);
75 ComponentConfigDialog.showDialog(freeform);
79 ComponentConfigDialog.hideDialog();
85 split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
86 //// Split the fin set into separate fins
87 split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
88 split.addActionListener(new ActionListener() {
90 public void actionPerformed(ActionEvent e) {
91 log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
92 ((FinSet) component).getFinCount());
94 // Do change in future for overall safety
95 SwingUtilities.invokeLater(new Runnable() {
98 RocketComponent parent = component.getParent();
99 int index = parent.getChildPosition(component);
100 int count = ((FinSet) component).getFinCount();
101 double base = ((FinSet) component).getBaseRotation();
105 document.addUndoPosition("Split fin set");
106 parent.removeChild(index);
107 for (int i = 0; i < count; i++) {
108 FinSet copy = (FinSet) component.copy();
110 copy.setBaseRotation(base + i * 2 * Math.PI / count);
111 copy.setName(copy.getName() + " #" + (i + 1));
112 parent.addChild(copy, index + i);
117 ComponentConfigDialog.hideDialog();
120 split.setEnabled(((FinSet) component).getFinCount() > 1);
125 addButtons(split, convert);
129 public JPanel finTabPanel() {
130 JPanel panel = new JPanel(
131 new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
132 "[150lp::][65lp::][30lp::][200lp::]", ""));
133 // JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
134 // "[40lp][80lp::][30lp::][100lp::]",""));
136 //// Through-the-wall fin tabs:
137 panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
143 DoubleModel length_2;
147 length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
148 length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
149 length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
157 label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
158 //// The length of the fin tab.
159 label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
160 panel.add(label, "gapleft para, gapright 40lp, growx 1");
162 final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
164 spin = new JSpinner(mtl.getSpinnerModel());
165 spin.setEditor(new SpinnerEditor(spin));
166 panel.add(spin, "growx 1");
168 panel.add(new UnitSelector(mtl), "growx 1");
169 panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
170 "w 100lp, growx 5, wrap");
175 label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
176 //// The spanwise height of the fin tab.
177 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
178 panel.add(label, "gapleft para");
180 final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
182 spin = new JSpinner(mth.getSpinnerModel());
183 spin.setEditor(new SpinnerEditor(spin));
184 panel.add(spin, "growx");
186 panel.add(new UnitSelector(mth), "growx");
187 panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
188 "w 100lp, growx 5, wrap");
191 label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
192 //// The position of the fin tab.
193 label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
194 panel.add(label, "gapleft para");
196 final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
198 spin = new JSpinner(mts.getSpinnerModel());
199 spin.setEditor(new SpinnerEditor(spin));
200 panel.add(spin, "growx");
202 panel.add(new UnitSelector(mts), "growx");
203 panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
207 label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
208 panel.add(label, "right, gapright unrel");
210 final EnumModel<FinSet.TabRelativePosition> em =
211 new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
213 panel.add(new JComboBox(em), "spanx 3, growx, wrap para");
216 // Calculate fin tab height, length, and position
217 autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc"));
219 autoCalc.addActionListener(new ActionListener() {
221 public void actionPerformed(ActionEvent e) {
222 log.user("Computing " + component.getComponentName() + " tab height.");
224 RocketComponent parent = component.getParent();
225 if (parent instanceof Coaxial) {
227 document.startUndo("Compute fin tabs");
229 List<RocketComponent> children = parent.getChildren();
230 List<CenteringRing> rings = new ArrayList<CenteringRing>();
232 for (int i = 0; i < children.size(); i++) {
233 RocketComponent rocketComponent = children.get(i);
234 if (rocketComponent instanceof InnerTube) {
235 InnerTube it = (InnerTube) rocketComponent;
236 if (it.isMotorMount()) {
237 double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius();
241 mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
244 } else if (rocketComponent instanceof CenteringRing) {
245 rings.add((CenteringRing) rocketComponent);
248 //Figure out position and length of the fin tab
249 if (!rings.isEmpty()) {
250 FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem();
251 em.setSelectedItem(FinSet.TabRelativePosition.FRONT);
252 double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP),
253 component.getLength(), mts);
255 //Be nice to the user and set the tab relative position enum back the way they had it.
256 em.setSelectedItem(temp);
265 panel.add(autoCalc, "skip 1, spanx");
273 * 1. All rings ahead of start of fin.
274 * 2. First ring ahead of start of fin. Second ring ahead of end of fin.
275 * 3. First ring ahead of start of fin. Second ring behind end of fin.
276 * 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin.
277 * 5. First ring equal or behind start of fin. Second ring behind end of fin.
278 * 6. All rings behind end of fin.
280 * @param rings an unordered list of centering rings attached to the parent of the fin set
281 * @param finPositionFromTop the position from the top of the parent of the start of the fin set root
282 * @param finLength the length of the root chord
283 * @param mts the model for the tab shift (position); the model's value is modified as a result of this method call
284 * @return the length of the fin tab
286 private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts) {
287 List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
289 //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here.
290 SortableRing top = null;
291 SortableRing bottom = null;
294 //Sort rings from top of parent to bottom
295 Collections.sort(rings, new Comparator<CenteringRing>() {
297 public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
298 return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP) -
299 centeringRing1.asPositionValue(RocketComponent.Position.TOP)));
303 for (int i = 0; i < rings.size(); i++) {
304 CenteringRing centeringRing = rings.get(i);
305 //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring.
306 if (!positionsFromTop.isEmpty() &&
307 positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >= centeringRing.asPositionValue(RocketComponent.Position.TOP)) {
308 SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1);
309 adjacent.merge(centeringRing);
311 positionsFromTop.add(new SortableRing(centeringRing));
315 for (int i = 0; i < positionsFromTop.size(); i++) {
316 SortableRing sortableRing = positionsFromTop.get(i);
319 } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
322 } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) {
323 if (bottom == null) {
324 //If the current ring is in the upper half of the root chord, make it the top ring
325 if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) {
328 bottom = sortableRing;
331 //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring,
332 //and the current ring the bottom
333 else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) {
335 bottom = sortableRing;
338 if (bottom == null) {
339 bottom = sortableRing;
345 // Edge case where there are no centering rings or for some odd reason top and bottom are identical.
346 if (top == null || top == bottom) {
351 if (bottom == null) {
352 // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then
353 // set the position of the fin tab starting at the bottom side of the top ring.
354 if (top.bottomSidePositionFromTop() >= finPositionFromTop) {
355 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
356 return (finPositionFromTop + finLength - top.bottomSidePositionFromTop());
358 // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
364 // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the
365 // fin tab align with the start of the root chord.
366 if (top.bottomSidePositionFromTop() < finPositionFromTop) {
368 return bottom.positionFromTop - finPositionFromTop;
370 // Otherwise the rings are within the span of the root chord. Place the tab between them.
371 mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
372 return (bottom.positionFromTop() - top.bottomSidePositionFromTop());
377 public void updateFields() {
378 super.updateFields();
380 split.setEnabled(((FinSet) component).getFinCount() > 1);
384 * A container class to store pertinent info about centering rings. This is used in the computation to figure
385 * out tab length and position.
387 static class SortableRing {
390 * The length of the ring (more commonly called the thickness).
392 private double thickness;
394 * The position of the ring from the top of the parent.
396 private double positionFromTop;
401 * @param r the source centering ring
403 SortableRing(CenteringRing r) {
404 thickness = r.getLength();
405 positionFromTop = r.asPositionValue(RocketComponent.Position.TOP);
409 * Merge an adjacent ring.
411 * @param adjacent the adjacent ring
413 public void merge(CenteringRing adjacent) {
414 double v = adjacent.asPositionValue(RocketComponent.Position.TOP);
415 if (positionFromTop < v) {
416 thickness = (v + adjacent.getLength()) - positionFromTop;
418 double tmp = positionFromTop + thickness;
425 * Compute the position of the bottom edge of the ring, relative to the top of the parent.
427 * @return the distance from the top of the parent to the bottom edge of the ring
429 public double bottomSidePositionFromTop() {
430 return positionFromTop + thickness;
434 * Compute the position of the top edge of the ring, relative to the top of the parent.
436 * @return the distance from the top of the parent to the top edge of the ring
438 public double positionFromTop() {
439 return positionFromTop;