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