4 package net.sf.openrocket.file.rocksim.importt;
6 import net.sf.openrocket.aerodynamics.WarningSet;
7 import net.sf.openrocket.file.simplesax.ElementHandler;
8 import net.sf.openrocket.file.simplesax.PlainTextHandler;
9 import net.sf.openrocket.material.Material;
10 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
11 import net.sf.openrocket.rocketcomponent.ExternalComponent;
12 import net.sf.openrocket.rocketcomponent.FinSet;
13 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
14 import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
15 import net.sf.openrocket.rocketcomponent.RocketComponent;
16 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
17 import net.sf.openrocket.util.Coordinate;
18 import org.xml.sax.SAXException;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
26 * A SAX handler for Rocksim fin sets. Because the type of fin may not be known first (in Rocksim file format, the fin
27 * shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin
28 * characteristics are kept here until the closing FinSet tag. At that point, <code>asOpenRocket</code> method is called
29 * to construct the corresponding OpenRocket FinSet.
31 class FinSetHandler extends ElementHandler {
33 * The parent component.
35 private final RocketComponent component;
38 * The name of the fin.
42 * The Rocksim fin shape code.
44 private int shapeCode;
46 * The location of the fin on its parent.
48 private double location = 0.0d;
50 * The OpenRocket Position which gives the absolute/relative positioning for location.
52 private RocketComponent.Position position;
54 * The number of fins in this fin set.
58 * The length of the root chord.
60 private double rootChord = 0.0d;
62 * The length of the tip chord.
64 private double tipChord = 0.0d;
66 * The length of the mid-chord (aka height).
68 private double midChordLen = 0.0d;
70 * The distance of the leading edge from root to top.
72 private double sweepDistance = 0.0d;
74 * The angle the fins have been rotated from the y-axis, if looking down the tube, in radians.
76 private double radialAngle = 0.0d;
78 * The thickness of the fins.
80 private double thickness;
82 * The finish of the fins.
84 private ExternalComponent.Finish finish;
86 * The shape of the tip.
88 private int tipShapeCode;
90 * The length of the TTW tab.
92 private double tabLength = 0.0d;
94 * The depth of the TTW tab.
96 private double tabDepth = 0.0d;
98 * The offset of the tab, from the front of the fin.
100 private double taboffset = 0.0d;
102 * The elliptical semi-span (height).
104 private double semiSpan;
106 * The list of custom points.
108 private String pointList;
110 * Override the Cg and mass.
112 private boolean override = false;
114 * The overridden mass.
116 private Double mass = 0d;
120 private Double cg = 0d;
122 * The density of the material in the component.
124 private Double density = 0d;
128 private String materialName = "";
130 * The Rocksim calculated mass.
132 private Double calcMass = 0d;
134 * The Rocksim calculated cg.
136 private Double calcCg = 0d;
142 * @param c the parent
144 * @throws IllegalArgumentException thrown if <code>c</code> is null
146 public FinSetHandler (RocketComponent c) throws IllegalArgumentException {
148 throw new IllegalArgumentException("The parent component of a fin set may not be null.");
154 public ElementHandler openElement (String element, HashMap<String, String> attributes, WarningSet warnings) {
155 return PlainTextHandler.INSTANCE;
159 public void closeElement (String element, HashMap<String, String> attributes, String content, WarningSet warnings)
160 throws SAXException {
162 if ("Name".equals(element)) {
165 if ("Material".equals(element)) {
166 materialName = content;
168 if ("FinishCode".equals(element)) {
169 finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket();
171 if ("Xb".equals(element)) {
172 location = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
174 if ("LocationMode".equals(element)) {
175 position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket();
177 if ("FinCount".equals(element)) {
178 finCount = Integer.parseInt(content);
180 if ("RootChord".equals(element)) {
181 rootChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
183 if ("TipChord".equals(element)) {
184 tipChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
186 if ("SemiSpan".equals(element)) {
187 semiSpan = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
189 if ("MidChordLen".equals(element)) {
190 midChordLen = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
192 if ("SweepDistance".equals(element)) {
193 sweepDistance = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
195 if ("Thickness".equals(element)) {
196 thickness = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
198 if ("TipShapeCode".equals(element)) {
199 tipShapeCode = Integer.parseInt(content);
201 if ("TabLength".equals(element)) {
202 tabLength = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
204 if ("TabDepth".equals(element)) {
205 tabDepth = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
207 if ("TabOffset".equals(element)) {
208 taboffset = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
210 if ("RadialAngle".equals(element)) {
211 radialAngle = Double.parseDouble(content);
213 if ("ShapeCode".equals(element)) {
214 shapeCode = Integer.parseInt(content);
216 if ("PointList".equals(element)) {
219 if ("KnownMass".equals(element)) {
220 mass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS);
222 if ("Density".equals(element)) {
223 density = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY);
225 if ("KnownCG".equals(element)) {
226 cg = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS);
228 if ("UseKnownCG".equals(element)) {
229 override = "1".equals(content);
231 if ("CalcMass".equals(element)) {
232 calcMass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS;
234 if ("CalcCg".equals(element)) {
235 calcCg = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH;
238 catch (NumberFormatException nfe) {
239 warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number.");
244 public void endHandler (String element, HashMap<String, String> attributes,
245 String content, WarningSet warnings) throws SAXException {
246 //Create the fin set and correct for overrides and actual material densities
247 final FinSet finSet = asOpenRocket(warnings);
248 if (component.isCompatible(finSet)) {
249 BaseHandler.setOverride(finSet, override, mass, cg);
250 if (!override && finSet.getCrossSection().equals(FinSet.CrossSection.AIRFOIL)) {
251 //Override mass anyway. This is done only for AIRFOIL because Rocksim does not compute different
252 //mass/cg for different cross sections, but OpenRocket does. This can lead to drastic differences
253 //in mass. To counteract that, the cross section value is retained but the mass/cg is overridden
254 //with the calculated values from Rocksim. This will best approximate the Rocksim design in OpenRocket.
255 BaseHandler.setOverride(finSet, true, calcMass, calcCg);
257 BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density);
258 component.addChild(finSet);
261 warnings.add(finSet.getComponentName() + " can not be attached to "
262 + component.getComponentName() + ", ignoring component.");
268 * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet.
270 * @param warnings the warning set to convey incompatibilities to the user
272 * @return a FinSet instance
274 public FinSet asOpenRocket (WarningSet warnings) {
277 if (shapeCode == 0) {
279 result = new TrapezoidFinSet();
280 ((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness);
282 else if (shapeCode == 1) {
284 result = new EllipticalFinSet();
285 ((EllipticalFinSet) result).setHeight(semiSpan);
286 ((EllipticalFinSet) result).setLength(rootChord);
288 else if (shapeCode == 2) {
290 result = new FreeformFinSet();
292 ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings));
294 catch (IllegalFinPointException e) {
295 warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring.");
301 result.setThickness(thickness);
302 result.setName(name);
303 result.setFinCount(finCount);
304 result.setFinish(finish);
305 //All TTW tabs in Rocksim are relative to the front of the fin.
306 result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT);
307 result.setTabHeight(tabDepth);
308 result.setTabLength(tabLength);
309 result.setTabShift(taboffset);
310 result.setBaseRotation(radialAngle);
311 result.setCrossSection(convertTipShapeCode(tipShapeCode));
312 result.setRelativePosition(position);
313 PositionDependentHandler.setLocation(result, position, location);
319 * Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates.
321 * @param pointList a comma and pipe delimited string of X,Y coordinates from Rocksim. This is of the format:
322 * <pre>x0,y0|x1,y1|x2,y2|... </pre>
323 * @param warnings the warning set to convey incompatibilities to the user
325 * @return an array of OpenRocket Coordinates
327 private Coordinate[] toCoordinates (String pointList, WarningSet warnings) {
328 List<Coordinate> result = new ArrayList<Coordinate>();
329 if (pointList != null && !pointList.isEmpty()) {
330 String[] points = pointList.split("\\Q|\\E");
331 for (String point : points) {
332 String[] aPoint = point.split(",");
334 if (aPoint.length > 1) {
335 Coordinate c = new Coordinate(
336 Double.parseDouble(aPoint[0]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH,
337 Double.parseDouble(aPoint[1]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH);
341 warnings.add("Invalid fin point pair.");
344 catch (NumberFormatException nfe) {
345 warnings.add("Fin point not in numeric format.");
348 if (!result.isEmpty()) {
349 //OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the
351 Coordinate last = result.get(result.size() - 1);
352 if (last.x == 0 && last.y == 0) {
353 Collections.reverse(result);
357 final Coordinate[] coords = new Coordinate[result.size()];
358 return result.toArray(coords);
363 * Convert a Rocksim tip shape to an OpenRocket CrossSection.
365 * @param tipShape the tip shape code from Rocksim
367 * @return a CrossSection instance
369 public static FinSet.CrossSection convertTipShapeCode (int tipShape) {
372 return FinSet.CrossSection.SQUARE;
374 return FinSet.CrossSection.ROUNDED;
376 return FinSet.CrossSection.AIRFOIL;
378 return FinSet.CrossSection.SQUARE;
382 public static int convertTipShapeCode (FinSet.CrossSection cs) {
383 if (FinSet.CrossSection.ROUNDED.equals(cs)) {
386 if (FinSet.CrossSection.AIRFOIL.equals(cs)) {