b1037a1d8f14303d670924e5c79e00ede32cc8ec
[debian/openrocket] / src / net / sf / openrocket / gui / configdialog / FinSetConfig.java
1 package net.sf.openrocket.gui.configdialog;
2
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;
21
22 import javax.swing.*;
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;
29
30
31 public abstract class FinSetConfig extends RocketComponentConfig {
32     private static final LogHelper log = Application.getLogger();
33     private static final Translator trans = Application.getTranslator();
34
35     private JButton split = null;
36
37     public FinSetConfig(RocketComponent component) {
38         super(component);
39
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);
43     }
44
45
46     protected void addFinSetButtons() {
47         JButton convert = null;
48
49         //// Convert buttons
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() {
56                 @Override
57                 public void actionPerformed(ActionEvent e) {
58                     log.user("Converting " + component.getComponentName() + " into freeform fin set");
59
60                     // Do change in future for overall safety
61                     SwingUtilities.invokeLater(new Runnable() {
62                         @Override
63                         public void run() {
64                             //// Convert fin set
65                             ComponentConfigDialog.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
66                             RocketComponent freeform =
67                                     FreeformFinSet.convertFinSet((FinSet) component);
68                             ComponentConfigDialog.showDialog(freeform);
69                         }
70                     });
71
72                     ComponentConfigDialog.hideDialog();
73                 }
74             });
75         }
76
77         //// Split fins
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() {
82             @Override
83             public void actionPerformed(ActionEvent e) {
84                 log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
85                         ((FinSet) component).getFinCount());
86
87                 // Do change in future for overall safety
88                 SwingUtilities.invokeLater(new Runnable() {
89                     @Override
90                     public void run() {
91                         RocketComponent parent = component.getParent();
92                         int index = parent.getChildPosition(component);
93                         int count = ((FinSet) component).getFinCount();
94                         double base = ((FinSet) component).getBaseRotation();
95                         if (count <= 1)
96                             return;
97
98                         ComponentConfigDialog.addUndoPosition("Split fin set");
99                         parent.removeChild(index);
100                         for (int i = 0; i < count; i++) {
101                             FinSet copy = (FinSet) component.copy();
102                             copy.setFinCount(1);
103                             copy.setBaseRotation(base + i * 2 * Math.PI / count);
104                             copy.setName(copy.getName() + " #" + (i + 1));
105                             parent.addChild(copy, index + i);
106                         }
107                     }
108                 });
109
110                 ComponentConfigDialog.hideDialog();
111             }
112         });
113         split.setEnabled(((FinSet) component).getFinCount() > 1);
114
115         if (convert == null)
116             addButtons(split);
117         else
118             addButtons(split, convert);
119
120     }
121
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::]",""));
128
129         //// Through-the-wall fin tabs:
130         panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
131                 "spanx, wrap 30lp");
132
133         JLabel label;
134         DoubleModel length;
135         DoubleModel length2;
136         DoubleModel length_2;
137         JSpinner spin;
138         JButton calcHeight;
139
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);
143
144         register(length);
145         register(length2);
146         register(length_2);
147
148         ////  Tab length
149         //// Tab length:
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");
154
155         final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
156
157         spin = new JSpinner(mtl.getSpinnerModel());
158         spin.setEditor(new SpinnerEditor(spin));
159         panel.add(spin, "growx 1");
160
161         panel.add(new UnitSelector(mtl), "growx 1");
162         panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
163                 "w 100lp, growx 5, wrap");
164
165
166         ////  Tab length
167         //// Tab height:
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");
172
173         final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
174
175         spin = new JSpinner(mth.getSpinnerModel());
176         spin.setEditor(new SpinnerEditor(spin));
177         panel.add(spin, "growx");
178
179         panel.add(new UnitSelector(mth), "growx");
180         panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
181                 "w 100lp, growx 5, wrap para");
182
183         ////  Tab position:
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");
188
189         final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
190
191         spin = new JSpinner(mts.getSpinnerModel());
192         spin.setEditor(new SpinnerEditor(spin));
193         panel.add(spin, "growx");
194
195         panel.add(new UnitSelector(mts), "growx");
196         panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
197
198
199         //// relative to
200         label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
201         panel.add(label, "right, gapright unrel");
202
203         final EnumModel<FinSet.TabRelativePosition> em =
204                 new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
205
206         panel.add(new JComboBox(em), "spanx 3, growx");
207
208         calcHeight = new JButton(trans.get("FinSetConfig.but.Calcheight"));
209
210         // Calculate fin tab height, length, and position
211         calcHeight.addActionListener(new ActionListener() {
212             @Override
213             public void actionPerformed(ActionEvent e) {
214                 log.user("Computing " + component.getComponentName() + " tab height.");
215
216                 // Do change in future for overall safety
217                 SwingUtilities.invokeLater(new Runnable() {
218                     @Override
219                     public void run() {
220                         RocketComponent parent = component.getParent();
221                         if (parent instanceof Coaxial) {
222                             List<RocketComponent> children = parent.getChildren();
223                             List<CenteringRing> rings = new ArrayList<CenteringRing>();
224
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();
232                                         //Set fin tab depth
233                                         if (depth >= 0.0d) {
234                                             mth.setValue(depth);
235                                             mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
236                                         }
237                                     }
238                                 } else if (rocketComponent instanceof CenteringRing) {
239                                     rings.add((CenteringRing) rocketComponent);
240                                 }
241                             }
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);
248                                 mtl.setValue(len);
249                                 //Be nice to the user and set the tab relative position enum back the way they had it.
250                                 em.setSelectedItem(temp);
251                             }
252                         }
253                     }
254                 });
255             }
256         });
257         panel.add(calcHeight, "right, gapright unrel");
258
259         return panel;
260     }
261
262     /**
263      * Scenarios:
264      * <p/>
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.
271      *
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
277      */
278     private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts) {
279         List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
280
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;
284
285         if (rings != null) {
286             //Sort rings from top of parent to bottom
287             Collections.sort(rings, new Comparator<CenteringRing>() {
288                 @Override
289                 public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
290                     return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP) -
291                             centeringRing1.asPositionValue(RocketComponent.Position.TOP)));
292                 }
293             });
294
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);
302                 } else {
303                     positionsFromTop.add(new SortableRing(centeringRing));
304                 }
305             }
306
307             for (int i = 0; i < positionsFromTop.size(); i++) {
308                 SortableRing sortableRing = positionsFromTop.get(i);
309                 if (top == null) {
310                     top = sortableRing;
311                 } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
312                     top = sortableRing;
313                     bottom = null;
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) {
318                             top = sortableRing;
319                         } else {
320                             bottom = sortableRing;
321                         }
322                     }
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) {
326                         top = bottom;
327                         bottom = sortableRing;
328                     }
329                 } else {
330                     if (bottom == null) {
331                         bottom = sortableRing;
332                     }
333                 }
334             }
335         }
336
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) {
339             mts.setValue(0);
340             return finLength;
341         }
342
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());
349             } else {
350                 // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
351                 // root chord.
352                 mts.setValue(0);
353                 return finLength;
354             }
355         }
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) {
359             mts.setValue(0);
360             return bottom.positionFromTop - finPositionFromTop;
361         } else {
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());
365         }
366     }
367
368     @Override
369     public void updateFields() {
370         super.updateFields();
371         if (split != null)
372             split.setEnabled(((FinSet) component).getFinCount() > 1);
373     }
374
375     /**
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.
378      */
379     static class SortableRing {
380
381         /**
382          * The length of the ring (more commonly called the thickness).
383          */
384         private double thickness;
385         /**
386          * The position of the ring from the top of the parent.
387          */
388         private double positionFromTop;
389
390         /**
391          * Constructor.
392          *
393          * @param r the source centering ring
394          */
395         SortableRing(CenteringRing r) {
396             thickness = r.getLength();
397             positionFromTop = r.asPositionValue(RocketComponent.Position.TOP);
398         }
399
400         /**
401          * Merge an adjacent ring.
402          *
403          * @param adjacent the adjacent ring
404          */
405         public void merge(CenteringRing adjacent) {
406             double v = adjacent.asPositionValue(RocketComponent.Position.TOP);
407             if (positionFromTop < v) {
408                 thickness = (v + adjacent.getLength()) - positionFromTop;
409             } else {
410                 double tmp = positionFromTop + thickness;
411                 positionFromTop = v;
412                 thickness = tmp - v;
413             }
414         }
415
416         /**
417          * Compute the position of the bottom edge of the ring, relative to the top of the parent.
418          *
419          * @return the distance from the top of the parent to the bottom edge of the ring
420          */
421         public double bottomSidePositionFromTop() {
422             return positionFromTop + thickness;
423         }
424
425         /**
426          * Compute the position of the top edge of the ring, relative to the top of the parent.
427          *
428          * @return the distance from the top of the parent to the top edge of the ring
429          */
430         public double positionFromTop() {
431             return positionFromTop;
432         }
433     }
434 }