/*
* FinSetHandler.java
*/
package net.sf.openrocket.file.rocksim.importt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.file.DocumentLoadingContext;
import net.sf.openrocket.file.rocksim.RocksimCommonConstants;
import net.sf.openrocket.file.rocksim.RocksimFinishCode;
import net.sf.openrocket.file.rocksim.RocksimLocationMode;
import net.sf.openrocket.file.simplesax.AbstractElementHandler;
import net.sf.openrocket.file.simplesax.ElementHandler;
import net.sf.openrocket.file.simplesax.PlainTextHandler;
import net.sf.openrocket.material.Material;
import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
import net.sf.openrocket.rocketcomponent.ExternalComponent;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.FreeformFinSet;
import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
import net.sf.openrocket.util.Coordinate;
import org.xml.sax.SAXException;
/**
* A SAX handler for Rocksim fin sets. Because the type of fin may not be known first (in Rocksim file format, the fin
* shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin
* characteristics are kept here until the closing FinSet tag. At that point, <code>asOpenRocket</code> method is called
* to construct the corresponding OpenRocket FinSet.
*/
class FinSetHandler extends AbstractElementHandler {
/**
* The parent component.
*/
private final RocketComponent component;
/**
* The name of the fin.
*/
private String name;
/**
* The Rocksim fin shape code.
*/
private int shapeCode;
/**
* The location of the fin on its parent.
*/
private double location = 0.0d;
/**
* The OpenRocket Position which gives the absolute/relative positioning for location.
*/
private RocketComponent.Position position;
/**
* The number of fins in this fin set.
*/
private int finCount;
/**
* The length of the root chord.
*/
private double rootChord = 0.0d;
/**
* The length of the tip chord.
*/
private double tipChord = 0.0d;
/**
* The length of the mid-chord (aka height).
*/
private double midChordLen = 0.0d;
/**
* The distance of the leading edge from root to top.
*/
private double sweepDistance = 0.0d;
/**
* The angle the fins have been rotated from the y-axis, if looking down the tube, in radians.
*/
private double radialAngle = 0.0d;
/**
* The thickness of the fins.
*/
private double thickness;
/**
* The finish of the fins.
*/
private ExternalComponent.Finish finish;
/**
* The shape of the tip.
*/
private int tipShapeCode;
/**
* The length of the TTW tab.
*/
private double tabLength = 0.0d;
/**
* The depth of the TTW tab.
*/
private double tabDepth = 0.0d;
/**
* The offset of the tab, from the front of the fin.
*/
private double taboffset = 0.0d;
/**
* The elliptical semi-span (height).
*/
private double semiSpan;
/**
* The list of custom points.
*/
private String pointList;
/**
* Override the Cg and mass.
*/
private boolean override = false;
/**
* The overridden mass.
*/
private Double mass = 0d;
/**
* The overridden Cg.
*/
private Double cg = 0d;
/**
* The density of the material in the component.
*/
private Double density = 0d;
/**
* The material name.
*/
private String materialName = "";
/**
* The Rocksim calculated mass.
*/
private Double calcMass = 0d;
/**
* The Rocksim calculated cg.
*/
private Double calcCg = 0d;
private final RockSimAppearanceBuilder appearanceBuilder;
/**
* Constructor.
*
* @param c the parent
*
* @throws IllegalArgumentException thrown if <code>c</code> is null
*/
public FinSetHandler(DocumentLoadingContext context, RocketComponent c) throws IllegalArgumentException {
if (c == null) {
throw new IllegalArgumentException("The parent component of a fin set may not be null.");
}
appearanceBuilder = new RockSimAppearanceBuilder(context);
component = c;
}
@Override
public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) {
return PlainTextHandler.INSTANCE;
}
@Override
public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings)
throws SAXException {
try {
if (RocksimCommonConstants.NAME.equals(element)) {
name = content;
}
if (RocksimCommonConstants.MATERIAL.equals(element)) {
materialName = content;
}
if (RocksimCommonConstants.FINISH_CODE.equals(element)) {
finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket();
}
if (RocksimCommonConstants.XB.equals(element)) {
location = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.LOCATION_MODE.equals(element)) {
position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket();
}
if (RocksimCommonConstants.FIN_COUNT.equals(element)) {
finCount = Integer.parseInt(content);
}
if (RocksimCommonConstants.ROOT_CHORD.equals(element)) {
rootChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.TIP_CHORD.equals(element)) {
tipChord = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.SEMI_SPAN.equals(element)) {
semiSpan = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if ("MidChordLen".equals(element)) {
midChordLen = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.SWEEP_DISTANCE.equals(element)) {
sweepDistance = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.THICKNESS.equals(element)) {
thickness = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.TIP_SHAPE_CODE.equals(element)) {
tipShapeCode = Integer.parseInt(content);
}
if (RocksimCommonConstants.TAB_LENGTH.equals(element)) {
tabLength = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.TAB_DEPTH.equals(element)) {
tabDepth = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.TAB_OFFSET.equals(element)) {
taboffset = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
if (RocksimCommonConstants.RADIAL_ANGLE.equals(element)) {
radialAngle = Double.parseDouble(content);
}
if (RocksimCommonConstants.SHAPE_CODE.equals(element)) {
shapeCode = Integer.parseInt(content);
}
if (RocksimCommonConstants.POINT_LIST.equals(element)) {
pointList = content;
}
if (RocksimCommonConstants.KNOWN_MASS.equals(element)) {
mass = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
}
if (RocksimCommonConstants.DENSITY.equals(element)) {
density = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_BULK_DENSITY);
}
if (RocksimCommonConstants.KNOWN_CG.equals(element)) {
cg = Math.max(0d, Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS);
}
if (RocksimCommonConstants.USE_KNOWN_CG.equals(element)) {
override = "1".equals(content);
}
if (RocksimCommonConstants.CALC_MASS.equals(element)) {
calcMass = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_MASS;
}
if (RocksimCommonConstants.CALC_CG.equals(element)) {
calcCg = Double.parseDouble(content) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH;
}
appearanceBuilder.processElement(element, content, warnings);
} catch (NumberFormatException nfe) {
warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number.");
}
}
@Override
public void endHandler(String element, HashMap<String, String> attributes,
String content, WarningSet warnings) throws SAXException {
//Create the fin set and correct for overrides and actual material densities
final FinSet finSet = asOpenRocket(warnings);
finSet.setAppearance(appearanceBuilder.getAppearance());
if (component.isCompatible(finSet)) {
BaseHandler.setOverride(finSet, override, mass, cg);
if (!override && finSet.getCrossSection().equals(FinSet.CrossSection.AIRFOIL)) {
//Override mass anyway. This is done only for AIRFOIL because Rocksim does not compute different
//mass/cg for different cross sections, but OpenRocket does. This can lead to drastic differences
//in mass. To counteract that, the cross section value is retained but the mass/cg is overridden
//with the calculated values from Rocksim. This will best approximate the Rocksim design in OpenRocket.
BaseHandler.setOverride(finSet, true, calcMass, calcCg);
}
BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density);
component.addChild(finSet);
}
else {
warnings.add(finSet.getComponentName() + " can not be attached to "
+ component.getComponentName() + ", ignoring component.");
}
}
/**
* Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet.
*
* @param warnings the warning set to convey incompatibilities to the user
*
* @return a FinSet instance
*/
public FinSet asOpenRocket(WarningSet warnings) {
FinSet result;
if (shapeCode == 0) {
//Trapezoidal
result = new TrapezoidFinSet();
((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness);
}
else if (shapeCode == 1) {
//Elliptical
result = new EllipticalFinSet();
((EllipticalFinSet) result).setHeight(semiSpan);
((EllipticalFinSet) result).setLength(rootChord);
}
else if (shapeCode == 2) {
result = new FreeformFinSet();
try {
((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings));
} catch (IllegalFinPointException e) {
warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring.");
}
}
else {
return null;
}
result.setThickness(thickness);
result.setName(name);
result.setFinCount(finCount);
result.setFinish(finish);
//All TTW tabs in Rocksim are relative to the front of the fin.
result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT);
result.setTabHeight(tabDepth);
result.setTabLength(tabLength);
result.setTabShift(taboffset);
result.setBaseRotation(radialAngle);
result.setCrossSection(convertTipShapeCode(tipShapeCode));
result.setRelativePosition(position);
PositionDependentHandler.setLocation(result, position, location);
return result;
}
/**
* Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates.
*
* @param pointList a comma and pipe delimited string of X,Y coordinates from Rocksim. This is of the format:
* <pre>x0,y0|x1,y1|x2,y2|... </pre>
* @param warnings the warning set to convey incompatibilities to the user
*
* @return an array of OpenRocket Coordinates
*/
private Coordinate[] toCoordinates(String pointList, WarningSet warnings) {
List<Coordinate> result = new ArrayList<Coordinate>();
if (pointList != null && pointList.length() > 0) {
String[] points = pointList.split("\\Q|\\E");
for (String point : points) {
String[] aPoint = point.split(",");
try {
if (aPoint.length > 1) {
Coordinate c = new Coordinate(
Double.parseDouble(aPoint[0]) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH,
Double.parseDouble(aPoint[1]) / RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH);
result.add(c);
}
else {
warnings.add("Invalid fin point pair.");
}
} catch (NumberFormatException nfe) {
warnings.add("Fin point not in numeric format.");
}
}
if (!result.isEmpty()) {
//OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the
//Coordinate array.
Coordinate last = result.get(result.size() - 1);
if (last.x == 0 && last.y == 0) {
Collections.reverse(result);
}
}
}
final Coordinate[] coords = new Coordinate[result.size()];
return result.toArray(coords);
}
/**
* Convert a Rocksim tip shape to an OpenRocket CrossSection.
*
* @param tipShape the tip shape code from Rocksim
*
* @return a CrossSection instance
*/
public static FinSet.CrossSection convertTipShapeCode(int tipShape) {
switch (tipShape) {
case 0:
return FinSet.CrossSection.SQUARE;
case 1:
return FinSet.CrossSection.ROUNDED;
case 2:
return FinSet.CrossSection.AIRFOIL;
default:
return FinSet.CrossSection.SQUARE;
}
}
public static int convertTipShapeCode(FinSet.CrossSection cs) {
if (FinSet.CrossSection.ROUNDED.equals(cs)) {
return 1;
}
if (FinSet.CrossSection.AIRFOIL.equals(cs)) {
return 2;
}
return 0;
}
}