create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / file / rocksim / importt / FinSetHandler.java
1 /*
2  * FinSetHandler.java
3  */
4 package net.sf.openrocket.file.rocksim.importt;
5
6 import net.sf.openrocket.aerodynamics.WarningSet;
7 import net.sf.openrocket.file.rocksim.RocksimCommonConstants;
8 import net.sf.openrocket.file.rocksim.RocksimFinishCode;
9 import net.sf.openrocket.file.rocksim.RocksimLocationMode;
10 import net.sf.openrocket.file.simplesax.AbstractElementHandler;
11 import net.sf.openrocket.file.simplesax.ElementHandler;
12 import net.sf.openrocket.file.simplesax.PlainTextHandler;
13 import net.sf.openrocket.material.Material;
14 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
15 import net.sf.openrocket.rocketcomponent.ExternalComponent;
16 import net.sf.openrocket.rocketcomponent.FinSet;
17 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
18 import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
19 import net.sf.openrocket.rocketcomponent.RocketComponent;
20 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
21 import net.sf.openrocket.util.Coordinate;
22 import org.xml.sax.SAXException;
23
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
28
29 /**
30  * A SAX handler for Rocksim fin sets.  Because the type of fin may not be known first (in Rocksim file format, the fin
31  * shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin
32  * characteristics are kept here until the closing FinSet tag. At that point, <code>asOpenRocket</code> method is called
33  * to construct the corresponding OpenRocket FinSet.
34  */
35 class FinSetHandler extends AbstractElementHandler {
36     /**
37      * The parent component.
38      */
39     private final RocketComponent component;
40
41     /**
42      * The name of the fin.
43      */
44     private String name;
45     /**
46      * The Rocksim fin shape code.
47      */
48     private int shapeCode;
49     /**
50      * The location of the fin on its parent.
51      */
52     private double location = 0.0d;
53     /**
54      * The OpenRocket Position which gives the absolute/relative positioning for location.
55      */
56     private RocketComponent.Position position;
57     /**
58      * The number of fins in this fin set.
59      */
60     private int finCount;
61     /**
62      * The length of the root chord.
63      */
64     private double rootChord = 0.0d;
65     /**
66      * The length of the tip chord.
67      */
68     private double tipChord = 0.0d;
69     /**
70      * The length of the mid-chord (aka height).
71      */
72     private double midChordLen = 0.0d;
73     /**
74      * The distance of the leading edge from root to top.
75      */
76     private double sweepDistance = 0.0d;
77     /**
78      * The angle the fins have been rotated from the y-axis, if looking down the tube, in radians.
79      */
80     private double radialAngle = 0.0d;
81     /**
82      * The thickness of the fins.
83      */
84     private double thickness;
85     /**
86      * The finish of the fins.
87      */
88     private ExternalComponent.Finish finish;
89     /**
90      * The shape of the tip.
91      */
92     private int tipShapeCode;
93     /**
94      * The length of the TTW tab.
95      */
96     private double tabLength = 0.0d;
97     /**
98      * The depth of the TTW tab.
99      */
100     private double tabDepth = 0.0d;
101     /**
102      * The offset of the tab, from the front of the fin.
103      */
104     private double taboffset = 0.0d;
105     /**
106      * The elliptical semi-span (height).
107      */
108     private double semiSpan;
109     /**
110      * The list of custom points.
111      */
112     private String pointList;
113     /**
114      * Override the Cg and mass.
115      */
116     private boolean override = false;
117     /**
118      * The overridden mass.
119      */
120     private Double mass = 0d;
121     /**
122      * The overridden Cg.
123      */
124     private Double cg = 0d;
125     /**
126      * The density of the material in the component.
127      */
128     private Double density = 0d;
129     /**
130      * The material name.
131      */
132     private String materialName = "";
133     /**
134      * The Rocksim calculated mass.
135      */
136     private Double calcMass = 0d;
137     /**
138      * The Rocksim calculated cg.
139      */
140     private Double calcCg = 0d;
141
142
143     /**
144      * Constructor.
145      *
146      * @param c the parent
147      *
148      * @throws IllegalArgumentException thrown if <code>c</code> is null
149      */
150     public FinSetHandler (RocketComponent c) throws IllegalArgumentException {
151         if (c == null) {
152             throw new IllegalArgumentException("The parent component of a fin set may not be null.");
153         }
154         component = c;
155     }
156
157     @Override
158     public ElementHandler openElement (String element, HashMap<String, String> attributes, WarningSet warnings) {
159         return PlainTextHandler.INSTANCE;
160     }
161
162     @Override
163     public void closeElement (String element, HashMap<String, String> attributes, String content, WarningSet warnings)
164             throws SAXException {
165         try {
166             if (RocksimCommonConstants.NAME.equals(element)) {
167                 name = content;
168             }
169             if (RocksimCommonConstants.MATERIAL.equals(element)) {
170                 materialName = content;
171             }
172             if (RocksimCommonConstants.FINISH_CODE.equals(element)) {
173                 finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket();
174             }
175             if (RocksimCommonConstants.XB.equals(element)) {
176                 location = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
177             }
178             if (RocksimCommonConstants.LOCATION_MODE.equals(element)) {
179                 position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket();
180             }
181             if (RocksimCommonConstants.FIN_COUNT.equals(element)) {
182                 finCount = Integer.parseInt(content);
183             }
184             if (RocksimCommonConstants.ROOT_CHORD.equals(element)) {
185                 rootChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
186             }
187             if (RocksimCommonConstants.TIP_CHORD.equals(element)) {
188                 tipChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
189             }
190             if (RocksimCommonConstants.SEMI_SPAN.equals(element)) {
191                 semiSpan = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
192             }
193             if ("MidChordLen".equals(element)) {
194                 midChordLen = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
195             }
196             if (RocksimCommonConstants.SWEEP_DISTANCE.equals(element)) {
197                 sweepDistance = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
198             }
199             if (RocksimCommonConstants.THICKNESS.equals(element)) {
200                 thickness = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
201             }
202             if (RocksimCommonConstants.TIP_SHAPE_CODE.equals(element)) {
203                 tipShapeCode = Integer.parseInt(content);
204             }
205             if (RocksimCommonConstants.TAB_LENGTH.equals(element)) {
206                 tabLength = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
207             }
208             if (RocksimCommonConstants.TAB_DEPTH.equals(element)) {
209                 tabDepth = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
210             }
211             if (RocksimCommonConstants.TAB_OFFSET.equals(element)) {
212                 taboffset = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
213             }
214             if (RocksimCommonConstants.RADIAL_ANGLE.equals(element)) {
215                 radialAngle = Double.parseDouble(content);
216             }
217             if (RocksimCommonConstants.SHAPE_CODE.equals(element)) {
218                 shapeCode = Integer.parseInt(content);
219             }
220             if (RocksimCommonConstants.POINT_LIST.equals(element)) {
221                 pointList = content;
222             }
223             if (RocksimCommonConstants.KNOWN_MASS.equals(element)) {
224                 mass = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
225             }
226             if (RocksimCommonConstants.DENSITY.equals(element)) {
227                 density = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_BULK_DENSITY);
228             }
229             if (RocksimCommonConstants.KNOWN_CG.equals(element)) {
230                 cg = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
231             }
232             if (RocksimCommonConstants.USE_KNOWN_CG.equals(element)) {
233                 override = "1".equals(content);
234             }
235             if (RocksimCommonConstants.CALC_MASS.equals(element)) {
236                 calcMass = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS;
237             }
238             if (RocksimCommonConstants.CALC_CG.equals(element)) {
239                 calcCg = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
240             }
241         }
242         catch (NumberFormatException nfe) {
243             warnings.add("Could not convert " + element + " value of " + content + ".  It is expected to be a number.");
244         }
245     }
246
247     @Override
248     public void endHandler (String element, HashMap<String, String> attributes,
249             String content, WarningSet warnings) throws SAXException {
250         //Create the fin set and correct for overrides and actual material densities
251         final FinSet finSet = asOpenRocket(warnings);
252         if (component.isCompatible(finSet)) {
253             BaseHandler.setOverride(finSet, override, mass, cg);
254             if (!override && finSet.getCrossSection().equals(FinSet.CrossSection.AIRFOIL)) {
255                 //Override mass anyway.  This is done only for AIRFOIL because Rocksim does not compute different
256                 //mass/cg for different cross sections, but OpenRocket does.  This can lead to drastic differences
257                 //in mass.  To counteract that, the cross section value is retained but the mass/cg is overridden
258                 //with the calculated values from Rocksim.  This will best approximate the Rocksim design in OpenRocket.
259                 BaseHandler.setOverride(finSet, true, calcMass, calcCg);
260             }
261             BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density);
262             component.addChild(finSet);
263         }
264         else {
265             warnings.add(finSet.getComponentName() + " can not be attached to "
266                          + component.getComponentName() + ", ignoring component.");
267         }
268     }
269
270
271     /**
272      * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet.
273      *
274      * @param warnings the warning set to convey incompatibilities to the user
275      *
276      * @return a FinSet instance
277      */
278     public FinSet asOpenRocket (WarningSet warnings) {
279         FinSet result;
280
281         if (shapeCode == 0) {
282             //Trapezoidal
283             result = new TrapezoidFinSet();
284             ((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness);
285         }
286         else if (shapeCode == 1) {
287             //Elliptical
288             result = new EllipticalFinSet();
289             ((EllipticalFinSet) result).setHeight(semiSpan);
290             ((EllipticalFinSet) result).setLength(rootChord);
291         }
292         else if (shapeCode == 2) {
293
294             result = new FreeformFinSet();
295             try {
296                 ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings));
297             }
298             catch (IllegalFinPointException e) {
299                 warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring.");
300             }
301         }
302         else {
303             return null;
304         }
305         result.setThickness(thickness);
306         result.setName(name);
307         result.setFinCount(finCount);
308         result.setFinish(finish);
309         //All TTW tabs in Rocksim are relative to the front of the fin.
310         result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT);
311         result.setTabHeight(tabDepth);
312         result.setTabLength(tabLength);
313         result.setTabShift(taboffset);
314         result.setBaseRotation(radialAngle);
315         result.setCrossSection(convertTipShapeCode(tipShapeCode));
316         result.setRelativePosition(position);
317         PositionDependentHandler.setLocation(result, position, location);
318         return result;
319
320     }
321
322     /**
323      * Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates.
324      *
325      * @param pointList a comma and pipe delimited string of X,Y coordinates from Rocksim.  This is of the format:
326      *                  <pre>x0,y0|x1,y1|x2,y2|... </pre>
327      * @param warnings  the warning set to convey incompatibilities to the user
328      *
329      * @return an array of OpenRocket Coordinates
330      */
331     private Coordinate[] toCoordinates (String pointList, WarningSet warnings) {
332         List<Coordinate> result = new ArrayList<Coordinate>();
333         if (pointList != null && pointList.length() > 0) {
334             String[] points = pointList.split("\\Q|\\E");
335             for (String point : points) {
336                 String[] aPoint = point.split(",");
337                 try {
338                     if (aPoint.length > 1) {
339                         Coordinate c = new Coordinate(
340                                 Double.parseDouble(aPoint[0]) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH,
341                                 Double.parseDouble(aPoint[1]) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH);
342                         result.add(c);
343                     }
344                     else {
345                         warnings.add("Invalid fin point pair.");
346                     }
347                 }
348                 catch (NumberFormatException nfe) {
349                     warnings.add("Fin point not in numeric format.");
350                 }
351             }
352             if (!result.isEmpty()) {
353                 //OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the
354                 //Coordinate array.
355                 Coordinate last = result.get(result.size() - 1);
356                 if (last.x == 0 && last.y == 0) {
357                     Collections.reverse(result);
358                 }
359             }
360         }
361         final Coordinate[] coords = new Coordinate[result.size()];
362         return result.toArray(coords);
363     }
364
365
366     /**
367      * Convert a Rocksim tip shape to an OpenRocket CrossSection.
368      *
369      * @param tipShape the tip shape code from Rocksim
370      *
371      * @return a CrossSection instance
372      */
373     public static FinSet.CrossSection convertTipShapeCode (int tipShape) {
374         switch (tipShape) {
375             case 0:
376                 return FinSet.CrossSection.SQUARE;
377             case 1:
378                 return FinSet.CrossSection.ROUNDED;
379             case 2:
380                 return FinSet.CrossSection.AIRFOIL;
381             default:
382                 return FinSet.CrossSection.SQUARE;
383         }
384     }
385     
386     public static int convertTipShapeCode (FinSet.CrossSection cs) {
387         if (FinSet.CrossSection.ROUNDED.equals(cs)) {
388             return 1;
389         }
390         if (FinSet.CrossSection.AIRFOIL.equals(cs)) {
391             return 2;
392         }
393         return 0;
394     }
395
396 }
397