4 package net.sf.openrocket.file.rocksim.importt;
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;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
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.
35 class FinSetHandler extends AbstractElementHandler {
37 * The parent component.
39 private final RocketComponent component;
42 * The name of the fin.
46 * The Rocksim fin shape code.
48 private int shapeCode;
50 * The location of the fin on its parent.
52 private double location = 0.0d;
54 * The OpenRocket Position which gives the absolute/relative positioning for location.
56 private RocketComponent.Position position;
58 * The number of fins in this fin set.
62 * The length of the root chord.
64 private double rootChord = 0.0d;
66 * The length of the tip chord.
68 private double tipChord = 0.0d;
70 * The length of the mid-chord (aka height).
72 private double midChordLen = 0.0d;
74 * The distance of the leading edge from root to top.
76 private double sweepDistance = 0.0d;
78 * The angle the fins have been rotated from the y-axis, if looking down the tube, in radians.
80 private double radialAngle = 0.0d;
82 * The thickness of the fins.
84 private double thickness;
86 * The finish of the fins.
88 private ExternalComponent.Finish finish;
90 * The shape of the tip.
92 private int tipShapeCode;
94 * The length of the TTW tab.
96 private double tabLength = 0.0d;
98 * The depth of the TTW tab.
100 private double tabDepth = 0.0d;
102 * The offset of the tab, from the front of the fin.
104 private double taboffset = 0.0d;
106 * The elliptical semi-span (height).
108 private double semiSpan;
110 * The list of custom points.
112 private String pointList;
114 * Override the Cg and mass.
116 private boolean override = false;
118 * The overridden mass.
120 private Double mass = 0d;
124 private Double cg = 0d;
126 * The density of the material in the component.
128 private Double density = 0d;
132 private String materialName = "";
134 * The Rocksim calculated mass.
136 private Double calcMass = 0d;
138 * The Rocksim calculated cg.
140 private Double calcCg = 0d;
146 * @param c the parent
148 * @throws IllegalArgumentException thrown if <code>c</code> is null
150 public FinSetHandler (RocketComponent c) throws IllegalArgumentException {
152 throw new IllegalArgumentException("The parent component of a fin set may not be null.");
158 public ElementHandler openElement (String element, HashMap<String, String> attributes, WarningSet warnings) {
159 return PlainTextHandler.INSTANCE;
163 public void closeElement (String element, HashMap<String, String> attributes, String content, WarningSet warnings)
164 throws SAXException {
166 if (RocksimCommonConstants.NAME.equals(element)) {
169 if (RocksimCommonConstants.MATERIAL.equals(element)) {
170 materialName = content;
172 if (RocksimCommonConstants.FINISH_CODE.equals(element)) {
173 finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket();
175 if (RocksimCommonConstants.XB.equals(element)) {
176 location = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
178 if (RocksimCommonConstants.LOCATION_MODE.equals(element)) {
179 position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket();
181 if (RocksimCommonConstants.FIN_COUNT.equals(element)) {
182 finCount = Integer.parseInt(content);
184 if (RocksimCommonConstants.ROOT_CHORD.equals(element)) {
185 rootChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
187 if (RocksimCommonConstants.TIP_CHORD.equals(element)) {
188 tipChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
190 if (RocksimCommonConstants.SEMI_SPAN.equals(element)) {
191 semiSpan = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
193 if ("MidChordLen".equals(element)) {
194 midChordLen = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
196 if (RocksimCommonConstants.SWEEP_DISTANCE.equals(element)) {
197 sweepDistance = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
199 if (RocksimCommonConstants.THICKNESS.equals(element)) {
200 thickness = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
202 if (RocksimCommonConstants.TIP_SHAPE_CODE.equals(element)) {
203 tipShapeCode = Integer.parseInt(content);
205 if (RocksimCommonConstants.TAB_LENGTH.equals(element)) {
206 tabLength = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
208 if (RocksimCommonConstants.TAB_DEPTH.equals(element)) {
209 tabDepth = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
211 if (RocksimCommonConstants.TAB_OFFSET.equals(element)) {
212 taboffset = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
214 if (RocksimCommonConstants.RADIAL_ANGLE.equals(element)) {
215 radialAngle = Double.parseDouble(content);
217 if (RocksimCommonConstants.SHAPE_CODE.equals(element)) {
218 shapeCode = Integer.parseInt(content);
220 if (RocksimCommonConstants.POINT_LIST.equals(element)) {
223 if (RocksimCommonConstants.KNOWN_MASS.equals(element)) {
224 mass = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
226 if (RocksimCommonConstants.DENSITY.equals(element)) {
227 density = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_BULK_DENSITY);
229 if (RocksimCommonConstants.KNOWN_CG.equals(element)) {
230 cg = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
232 if (RocksimCommonConstants.USE_KNOWN_CG.equals(element)) {
233 override = "1".equals(content);
235 if (RocksimCommonConstants.CALC_MASS.equals(element)) {
236 calcMass = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS;
238 if (RocksimCommonConstants.CALC_CG.equals(element)) {
239 calcCg = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
242 catch (NumberFormatException nfe) {
243 warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number.");
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);
261 BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density);
262 component.addChild(finSet);
265 warnings.add(finSet.getComponentName() + " can not be attached to "
266 + component.getComponentName() + ", ignoring component.");
272 * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet.
274 * @param warnings the warning set to convey incompatibilities to the user
276 * @return a FinSet instance
278 public FinSet asOpenRocket (WarningSet warnings) {
281 if (shapeCode == 0) {
283 result = new TrapezoidFinSet();
284 ((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness);
286 else if (shapeCode == 1) {
288 result = new EllipticalFinSet();
289 ((EllipticalFinSet) result).setHeight(semiSpan);
290 ((EllipticalFinSet) result).setLength(rootChord);
292 else if (shapeCode == 2) {
294 result = new FreeformFinSet();
296 ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings));
298 catch (IllegalFinPointException e) {
299 warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring.");
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);
323 * Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates.
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
329 * @return an array of OpenRocket Coordinates
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(",");
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);
345 warnings.add("Invalid fin point pair.");
348 catch (NumberFormatException nfe) {
349 warnings.add("Fin point not in numeric format.");
352 if (!result.isEmpty()) {
353 //OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the
355 Coordinate last = result.get(result.size() - 1);
356 if (last.x == 0 && last.y == 0) {
357 Collections.reverse(result);
361 final Coordinate[] coords = new Coordinate[result.size()];
362 return result.toArray(coords);
367 * Convert a Rocksim tip shape to an OpenRocket CrossSection.
369 * @param tipShape the tip shape code from Rocksim
371 * @return a CrossSection instance
373 public static FinSet.CrossSection convertTipShapeCode (int tipShape) {
376 return FinSet.CrossSection.SQUARE;
378 return FinSet.CrossSection.ROUNDED;
380 return FinSet.CrossSection.AIRFOIL;
382 return FinSet.CrossSection.SQUARE;
386 public static int convertTipShapeCode (FinSet.CrossSection cs) {
387 if (FinSet.CrossSection.ROUNDED.equals(cs)) {
390 if (FinSet.CrossSection.AIRFOIL.equals(cs)) {