create changelog entry
[debian/openrocket] / core / 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.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;
22
23 import javax.swing.*;
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;
31
32
33 public abstract class FinSetConfig extends RocketComponentConfig {
34         private static final LogHelper log = Application.getLogger();
35         private static final Translator trans = Application.getTranslator();
36         
37         private JButton split = null;
38         
39         public FinSetConfig(OpenRocketDocument d, RocketComponent component) {
40                 super(d, component);
41                 
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);
45         }
46         
47         
48         protected void addFinSetButtons() {
49                 JButton convert = null;
50                 
51                 //// Convert buttons
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() {
58                                 @Override
59                                 public void actionPerformed(ActionEvent e) {
60                                         log.user("Converting " + component.getComponentName() + " into freeform fin set");
61                                         
62                                         // Do change in future for overall safety
63                                         SwingUtilities.invokeLater(new Runnable() {
64                                                 @Override
65                                                 public void run() {
66                                                         //// Convert fin set
67                                                         document.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
68                                                         RocketComponent freeform =
69                                                                         FreeformFinSet.convertFinSet((FinSet) component);
70                                                         ComponentConfigDialog.showDialog(freeform);
71                                                 }
72                                         });
73                                         
74                                         ComponentConfigDialog.hideDialog();
75                                 }
76                         });
77                 }
78                 
79                 //// Split fins
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() {
84                         @Override
85                         public void actionPerformed(ActionEvent e) {
86                                 log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
87                                                 ((FinSet) component).getFinCount());
88                                 
89                                 // Do change in future for overall safety
90                                 SwingUtilities.invokeLater(new Runnable() {
91                                         @Override
92                                         public void run() {
93                                                 RocketComponent parent = component.getParent();
94                                                 int index = parent.getChildPosition(component);
95                                                 int count = ((FinSet) component).getFinCount();
96                                                 double base = ((FinSet) component).getBaseRotation();
97                                                 if (count <= 1)
98                                                         return;
99                                                 
100                                                 document.addUndoPosition("Split fin set");
101                                                 parent.removeChild(index);
102                                                 for (int i = 0; i < count; i++) {
103                                                         FinSet copy = (FinSet) component.copy();
104                                                         copy.setFinCount(1);
105                                                         copy.setBaseRotation(base + i * 2 * Math.PI / count);
106                                                         copy.setName(copy.getName() + " #" + (i + 1));
107                                                         parent.addChild(copy, index + i);
108                                                 }
109                                         }
110                                 });
111                                 
112                                 ComponentConfigDialog.hideDialog();
113                         }
114                 });
115                 split.setEnabled(((FinSet) component).getFinCount() > 1);
116                 
117                 if (convert == null)
118                         addButtons(split);
119                 else
120                         addButtons(split, convert);
121                 
122         }
123         
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::]",""));
130                 
131                 //// Through-the-wall fin tabs:
132                 panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
133                                 "spanx, wrap 30lp");
134                 
135                 JLabel label;
136                 DoubleModel length;
137                 DoubleModel length2;
138                 DoubleModel length_2;
139                 JSpinner spin;
140                 JButton autoCalc;
141                 
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);
145                 
146                 register(length);
147                 register(length2);
148                 register(length_2);
149                 
150                 ////  Tab length
151                 //// Tab length:
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");
156                 
157                 final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
158                 
159                 spin = new JSpinner(mtl.getSpinnerModel());
160                 spin.setEditor(new SpinnerEditor(spin));
161                 panel.add(spin, "growx 1");
162                 
163                 panel.add(new UnitSelector(mtl), "growx 1");
164                 panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
165                                 "w 100lp, growx 5, wrap");
166                 
167
168                 ////  Tab length
169                 //// Tab height:
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");
174                 
175                 final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
176                 
177                 spin = new JSpinner(mth.getSpinnerModel());
178                 spin.setEditor(new SpinnerEditor(spin));
179                 panel.add(spin, "growx");
180                 
181                 panel.add(new UnitSelector(mth), "growx");
182                 panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
183                                 "w 100lp, growx 5, wrap");
184                 
185                 ////  Tab position:
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");
190                 
191                 final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
192                 
193                 spin = new JSpinner(mts.getSpinnerModel());
194                 spin.setEditor(new SpinnerEditor(spin));
195                 panel.add(spin, "growx");
196                 
197                 panel.add(new UnitSelector(mts), "growx");
198                 panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
199                 
200
201                 //// relative to
202                 label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
203                 panel.add(label, "right, gapright unrel");
204                 
205                 final EnumModel<FinSet.TabRelativePosition> em =
206                                 new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
207                 
208                 panel.add(new JComboBox(em), "spanx 3, growx, wrap para");
209                 
210
211                 // Calculate fin tab height, length, and position
212                 autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc"));
213                 
214                 autoCalc.addActionListener(new ActionListener() {
215                         @Override
216                         public void actionPerformed(ActionEvent e) {
217                                 log.user("Computing " + component.getComponentName() + " tab height.");
218                                 
219                                 RocketComponent parent = component.getParent();
220                                 if (parent instanceof Coaxial) {
221                                         try {
222                                                 document.startUndo("Compute fin tabs");
223                                                 
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();
233                                                                         //Set fin tab depth
234                                                                         if (depth >= 0.0d) {
235                                                                                 mth.setValue(depth);
236                                                                                 mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
237                                                                         }
238                                                                 }
239                                                         } else if (rocketComponent instanceof CenteringRing) {
240                                                                 rings.add((CenteringRing) rocketComponent);
241                                                         }
242                                                 }
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);
249                                                         mtl.setValue(len);
250                                                         //Be nice to the user and set the tab relative position enum back the way they had it.
251                                                         em.setSelectedItem(temp);
252                                                 }
253                                                 
254                                         } finally {
255                                                 document.stopUndo();
256                                         }
257                                 }
258                         }
259                 });
260                 panel.add(autoCalc, "skip 1, spanx");
261                 
262                 return panel;
263         }
264         
265         /**
266          * Scenarios:
267          * <p/>
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.
274          *
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
280      * 
281          * @return the length of the fin tab
282          */
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>();
286                 
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;
290                 
291                 if (rings != null) {
292                         //Sort rings from top of parent to bottom
293                         Collections.sort(rings, new Comparator<CenteringRing>() {
294                                 @Override
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)));
298                                                 }
299                         });
300                         
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);
309                                 } else {
310                                         positionsFromTop.add(new SortableRing(centeringRing, relativeTo));
311                                 }
312                         }
313                         
314                         for (int i = 0; i < positionsFromTop.size(); i++) {
315                                 SortableRing sortableRing = positionsFromTop.get(i);
316                                 if (top == null) {
317                                         top = sortableRing;
318                                 } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
319                                         top = sortableRing;
320                                         bottom = null;
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) {
325                                                         top = sortableRing;
326                                                 } else {
327                                                         bottom = sortableRing;
328                                                 }
329                                         }
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) {
333                                                 top = bottom;
334                                                 bottom = sortableRing;
335                                         }
336                                 } else {
337                                         if (bottom == null) {
338                                                 bottom = sortableRing;
339                                         }
340                                 }
341                         }
342                 }
343
344         double resultFinTabLength = 0d;
345
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) {
348                         mts.setValue(0);
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());
356                         } else {
357                                 mts.setValue(0);
358                 double diffLen = top.positionFromTop() - finPositionFromTop;
359                 if (diffLen < 0) {
360                 // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
361                 // root chord.
362                     resultFinTabLength = finLength;
363                 }
364                 else {
365                     // Otherwise there is one ring within the span. Return the length from the start of the fin to the top
366                     // side of the ring.
367                     resultFinTabLength = diffLen;
368                 }
369             }
370                 }
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) {
374                         mts.setValue(0);
375
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;
381             }
382             else {
383                 resultFinTabLength = lenToBottomRing;
384             }
385                 } else {
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());
390             }
391                         // The rings are within the span of the root chord.  Place the tab between them.
392             else {
393                             resultFinTabLength = (bottom.positionFromTop() - top.bottomSidePositionFromTop());
394             }
395         }
396         if (resultFinTabLength < 0) {
397             resultFinTabLength = 0d;
398         }
399         return resultFinTabLength;
400         }
401         
402         @Override
403         public void updateFields() {
404                 super.updateFields();
405                 if (split != null)
406                         split.setEnabled(((FinSet) component).getFinCount() > 1);
407         }
408         
409         /**
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.
412          */
413         static class SortableRing {
414                 
415                 /**
416                  * The length of the ring (more commonly called the thickness).
417                  */
418                 private double thickness;
419                 /**
420                  * The position of the ring from the top of the parent.
421                  */
422                 private double positionFromTop;
423                 
424                 /**
425                  * Constructor.
426                  *
427                  * @param r the source centering ring
428                  */
429                 SortableRing(CenteringRing r, RocketComponent relativeTo) {
430                         thickness = r.getLength();
431                         positionFromTop = r.asPositionValue(RocketComponent.Position.TOP, relativeTo);
432                 }
433                 
434                 /**
435                  * Merge an adjacent ring.
436                  *
437                  * @param adjacent the adjacent ring
438                  */
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;
443                         } else {
444                                 double tmp = positionFromTop + thickness;
445                                 positionFromTop = v;
446                                 thickness = tmp - v;
447                         }
448                 }
449                 
450                 /**
451                  * Compute the position of the bottom edge of the ring, relative to the top of the parent.
452                  *
453                  * @return the distance from the top of the parent to the bottom edge of the ring
454                  */
455                 public double bottomSidePositionFromTop() {
456                         return positionFromTop + thickness;
457                 }
458                 
459                 /**
460                  * Compute the position of the top edge of the ring, relative to the top of the parent.
461                  *
462                  * @return the distance from the top of the parent to the top edge of the ring
463                  */
464                 public double positionFromTop() {
465                         return positionFromTop;
466                 }
467         }
468 }