--- /dev/null
+package net.sf.openrocket.file.motor;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.simplesax.ElementHandler;
+import net.sf.openrocket.file.simplesax.NullElementHandler;
+import net.sf.openrocket.file.simplesax.PlainTextHandler;
+import net.sf.openrocket.file.simplesax.SimpleSAX;
+import net.sf.openrocket.motor.Manufacturer;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorDigest;
+import net.sf.openrocket.motor.ThrustCurveMotor;
+import net.sf.openrocket.motor.MotorDigest.DataType;
+import net.sf.openrocket.util.Coordinate;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class RockSimMotorLoader extends MotorLoader {
+
+ public static final String CHARSET_NAME = "UTF-8";
+
+ public static final Charset CHARSET = Charset.forName(CHARSET_NAME);
+
+
+ /** Any delay longed than this will be interpreted as a plugged motor. */
+ private static final int DELAY_LIMIT = 90;
+
+
+
+ @Override
+ protected Charset getDefaultCharset() {
+ return CHARSET;
+ }
+
+
+
+ /**
+ * Load a <code>Motor</code> from a RockSim motor definition file specified by the
+ * <code>Reader</code>. The <code>Reader</code> is responsible for using the correct
+ * charset.
+ * <p>
+ * If automatic CG/mass calculation is used, then the CG is assumed to be located at
+ * the center of the motor casing and the mass is calculated from the thrust curve
+ * by assuming a constant exhaust velocity.
+ *
+ * @param reader the source of the file.
+ * @return a list of the {@link Motor} objects defined in the file.
+ * @throws IOException if an I/O error occurs or if the file format is invalid.
+ */
+ @Override
+ public List<Motor> load(Reader reader, String filename) throws IOException {
+ InputSource source = new InputSource(reader);
+ RSEHandler handler = new RSEHandler();
+ WarningSet warnings = new WarningSet();
+
+ try {
+ SimpleSAX.readXML(source, handler, warnings);
+ return handler.getMotors();
+ } catch (SAXException e) {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+
+
+ /**
+ * Initial handler for the RockSim engine files.
+ */
+ private static class RSEHandler extends ElementHandler {
+ private final List<Motor> motors = new ArrayList<Motor>();
+
+ private RSEMotorHandler motorHandler;
+
+ public List<Motor> getMotors() {
+ return motors;
+ }
+
+ @Override
+ public ElementHandler openElement(String element,
+ HashMap<String, String> attributes, WarningSet warnings) throws SAXException {
+
+ if (element.equals("engine-database") ||
+ element.equals("engine-list")) {
+ // Ignore <engine-database> and <engine-list> elements
+ return this;
+ }
+
+ if (element.equals("version")) {
+ // Ignore <version> elements completely
+ return null;
+ }
+
+ if (element.equals("engine")) {
+ motorHandler = new RSEMotorHandler(attributes);
+ return motorHandler;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) throws SAXException {
+
+ if (element.equals("engine")) {
+ Motor motor = motorHandler.getMotor();
+ motors.add(motor);
+ }
+ }
+ }
+
+
+ /**
+ * Handler for a RockSim engine file <motor> element.
+ */
+ private static class RSEMotorHandler extends ElementHandler {
+
+ private final String manufacturer;
+ private final String designation;
+ private final double[] delays;
+ private final double diameter;
+ private final double length;
+ private final double initMass;
+ private final double propMass;
+ private final Motor.Type type;
+ private boolean calculateMass;
+ private boolean calculateCG;
+
+ private String description = "";
+
+ private List<Double> time;
+ private List<Double> force;
+ private List<Double> mass;
+ private List<Double> cg;
+
+ private RSEMotorDataHandler dataHandler = null;
+
+
+ public RSEMotorHandler(HashMap<String, String> attributes) throws SAXException {
+ String str;
+
+ // Manufacturer
+ str = attributes.get("mfg");
+ if (str == null)
+ throw new SAXException("Manufacturer missing");
+ manufacturer = str;
+
+ // Designation
+ str = attributes.get("code");
+ if (str == null)
+ throw new SAXException("Designation missing");
+ designation = removeDelay(str);
+
+ // Delays
+ ArrayList<Double> delayList = new ArrayList<Double>();
+ str = attributes.get("delays");
+ if (str != null) {
+ String[] split = str.split(",");
+ for (String delay: split) {
+ try {
+
+ double d = Double.parseDouble(delay);
+ if (d >= DELAY_LIMIT)
+ d = Motor.PLUGGED;
+ delayList.add(d);
+
+ } catch (NumberFormatException e) {
+ if (str.equalsIgnoreCase("P") || str.equalsIgnoreCase("plugged")) {
+ delayList.add(Motor.PLUGGED);
+ }
+ }
+ }
+ }
+ delays = new double[delayList.size()];
+ for (int i=0; i<delayList.size(); i++) {
+ delays[i] = delayList.get(i);
+ }
+
+ // Diameter
+ str = attributes.get("dia");
+ if (str == null)
+ throw new SAXException("Diameter missing");
+ try {
+ diameter = Double.parseDouble(str) / 1000.0;
+ } catch (NumberFormatException e) {
+ throw new SAXException("Invalid diameter " + str);
+ }
+
+ // Length
+ str = attributes.get("len");
+ if (str == null)
+ throw new SAXException("Length missing");
+ try {
+ length = Double.parseDouble(str) / 1000.0;
+ } catch (NumberFormatException e) {
+ throw new SAXException("Invalid length " + str);
+ }
+
+ // Initial mass
+ str = attributes.get("initWt");
+ if (str == null)
+ throw new SAXException("Initial mass missing");
+ try {
+ initMass = Double.parseDouble(str) / 1000.0;
+ } catch (NumberFormatException e) {
+ throw new SAXException("Invalid initial mass " + str);
+ }
+
+ // Propellant mass
+ str = attributes.get("propWt");
+ if (str == null)
+ throw new SAXException("Propellant mass missing");
+ try {
+ propMass = Double.parseDouble(str) / 1000.0;
+ } catch (NumberFormatException e) {
+ throw new SAXException("Invalid propellant mass " + str);
+ }
+
+ if (propMass > initMass) {
+ throw new SAXException("Propellant weight exceeds total weight in " +
+ "RockSim engine format");
+ }
+
+ // Motor type
+ str = attributes.get("Type");
+ if (str != null && str.equalsIgnoreCase("single-use")) {
+ type = Motor.Type.SINGLE;
+ } else if (str != null && str.equalsIgnoreCase("hybrid")) {
+ type = Motor.Type.HYBRID;
+ } else if (str != null && str.equalsIgnoreCase("reloadable")) {
+ type = Motor.Type.RELOAD;
+ } else {
+ type = Motor.Type.UNKNOWN;
+ }
+
+ // Calculate mass
+ str = attributes.get("auto-calc-mass");
+ if ("0".equals(str) || "false".equalsIgnoreCase(str)) {
+ calculateMass = false;
+ } else {
+ calculateMass = true;
+ }
+
+ // Calculate CG
+ str = attributes.get("auto-calc-cg");
+ if ("0".equals(str) || "false".equalsIgnoreCase(str)) {
+ calculateCG = false;
+ } else {
+ calculateCG = true;
+ }
+ }
+
+ @Override
+ public ElementHandler openElement(String element,
+ HashMap<String, String> attributes, WarningSet warnings) throws SAXException {
+
+ if (element.equals("comments")) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+ if (element.equals("data")) {
+ if (dataHandler != null) {
+ throw new SAXException("Multiple data elements encountered in motor " +
+ "definition");
+ }
+ dataHandler = new RSEMotorDataHandler();
+ return dataHandler;
+ }
+
+ warnings.add("Unknown element '" + element + "' encountered, ignoring.");
+ return null;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("comments")) {
+ if (description.length() > 0) {
+ description = description + "\n\n" + content.trim();
+ } else {
+ description = content.trim();
+ }
+ return;
+ }
+
+ if (element.equals("data")) {
+ time = dataHandler.getTime();
+ force = dataHandler.getForce();
+ mass = dataHandler.getMass();
+ cg = dataHandler.getCG();
+
+ sortLists(time, force, mass, cg);
+
+ for (double d: mass) {
+ if (Double.isNaN(d)) {
+ calculateMass = true;
+ break;
+ }
+ }
+ for (double d: cg) {
+ if (Double.isNaN(d)) {
+ calculateCG = true;
+ break;
+ }
+ }
+ return;
+ }
+ }
+
+ public Motor getMotor() throws SAXException {
+ if (time == null || time.size() == 0)
+ throw new SAXException("Illegal motor data");
+
+
+ finalizeThrustCurve(time, force, mass, cg);
+ final int n = time.size();
+
+ if (hasIllegalValue(mass))
+ calculateMass = true;
+ if (hasIllegalValue(cg))
+ calculateCG = true;
+
+ if (calculateMass) {
+ mass = calculateMass(time, force, initMass, propMass);
+ }
+ if (calculateCG) {
+ for (int i=0; i < n; i++) {
+ cg.set(i, length/2);
+ }
+ }
+
+ double[] timeArray = toArray(time);
+ double[] thrustArray = toArray(force);
+ Coordinate[] cgArray = new Coordinate[n];
+ for (int i=0; i < n; i++) {
+ cgArray[i] = new Coordinate(cg.get(i),0,0,mass.get(i));
+ }
+
+
+ // Create the motor digest from all data available in the file
+ MotorDigest motorDigest = new MotorDigest();
+ motorDigest.update(DataType.TIME_ARRAY, timeArray);
+ if (!calculateMass) {
+ motorDigest.update(DataType.MASS_PER_TIME, toArray(mass));
+ } else {
+ motorDigest.update(DataType.MASS_SPECIFIC, initMass, initMass-propMass);
+ }
+ if (!calculateCG) {
+ motorDigest.update(DataType.CG_PER_TIME, toArray(cg));
+ }
+ motorDigest.update(DataType.FORCE_PER_TIME, thrustArray);
+ final String digest = motorDigest.getDigest();
+
+
+ try {
+ return new ThrustCurveMotor(Manufacturer.getManufacturer(manufacturer),
+ designation, description, type,
+ delays, diameter, length, timeArray, thrustArray, cgArray, digest);
+ } catch (IllegalArgumentException e) {
+ throw new SAXException("Illegal motor data", e);
+ }
+ }
+ }
+
+
+ /**
+ * Handler for the <data> element in a RockSim engine file motor definition.
+ */
+ private static class RSEMotorDataHandler extends ElementHandler {
+
+ private final List<Double> time = new ArrayList<Double>();
+ private final List<Double> force = new ArrayList<Double>();
+ private final List<Double> mass = new ArrayList<Double>();
+ private final List<Double> cg = new ArrayList<Double>();
+
+
+ public List<Double> getTime() {
+ return time;
+ }
+ public List<Double> getForce() {
+ return force;
+ }
+ public List<Double> getMass() {
+ return mass;
+ }
+ public List<Double> getCG() {
+ return cg;
+ }
+
+
+ @Override
+ public ElementHandler openElement(String element,
+ HashMap<String, String> attributes, WarningSet warnings) {
+
+ if (element.equals("eng-data")) {
+ return NullElementHandler.INSTANCE;
+ }
+
+ warnings.add("Unknown element '" + element + "' encountered, ignoring.");
+ return null;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) throws SAXException {
+
+ double t = parseDouble(attributes.get("t"));
+ double f = parseDouble(attributes.get("f"));
+ double m = parseDouble(attributes.get("m")) / 1000.0;
+ double g = parseDouble(attributes.get("cg")) / 1000.0;
+
+ if (Double.isNaN(t) || Double.isNaN(f)) {
+ throw new SAXException("Illegal motor data point encountered");
+ }
+
+ time.add(t);
+ force.add(f);
+ mass.add(m);
+ cg.add(g);
+ }
+
+
+ private double parseDouble(String str) {
+ if (str == null)
+ return Double.NaN;
+ try {
+ return Double.parseDouble(str);
+ } catch (NumberFormatException e) {
+ return Double.NaN;
+ }
+ }
+ }
+
+
+
+ private static boolean hasIllegalValue(List<Double> list) {
+ for (Double d: list) {
+ if (d == null || d.isNaN() || d.isInfinite()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static double[] toArray(List<Double> list) {
+ final int n = list.size();
+ double[] array = new double[n];
+ for (int i=0; i < n; i++) {
+ array[i] = list.get(i);
+ }
+ return array;
+ }
+}