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.simplesax.AbstractElementHandler; 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.MotorDigest.DataType; import net.sf.openrocket.motor.ThrustCurveMotor; import net.sf.openrocket.util.Coordinate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class RockSimMotorLoader extends AbstractMotorLoader { private static final Logger log = LoggerFactory.getLogger(RockSimMotorLoader.class); public static final String CHARSET_NAME = "UTF-8"; public static final Charset CHARSET = Charset.forName(CHARSET_NAME); /** Any delay longer 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 AbstractElementHandler { 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 AbstractElementHandler { 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 = false; private boolean calculateCG = false; 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 ("single-use".equalsIgnoreCase(str)) { type = Motor.Type.SINGLE; } else if ("hybrid".equalsIgnoreCase(str)) { type = Motor.Type.HYBRID; } else if ("reloadable".equalsIgnoreCase(str)) { 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 { Manufacturer m = Manufacturer.getManufacturer(manufacturer); Motor.Type t = type; if (t == Motor.Type.UNKNOWN) { t = m.getMotorType(); } else { if (m.getMotorType() != Motor.Type.UNKNOWN && m.getMotorType() != t) { log.warn("Loaded motor type inconsistent with manufacturer," + " loaded type=" + t + " manufacturer=" + m + " manufacturer type=" + m.getMotorType() + " designation=" + designation); } } return new ThrustCurveMotor(m, designation, description, t, 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 AbstractElementHandler { 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; } }