package net.sf.openrocket.file.openrocket; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import net.sf.openrocket.aerodynamics.Warning; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.document.StorageOptions; import net.sf.openrocket.file.RocketSaver; import net.sf.openrocket.rocketcomponent.DeploymentConfiguration.DeployEvent; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.FlightConfigurableComponent; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; import net.sf.openrocket.rocketcomponent.TubeCoupler; import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.extension.SimulationExtension; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BuildProperties; import net.sf.openrocket.util.Config; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Reflection; import net.sf.openrocket.util.TextUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OpenRocketSaver extends RocketSaver { private static final Logger log = LoggerFactory.getLogger(OpenRocketSaver.class); /** * Divisor used in converting an integer version to the point-represented version. * The integer version divided by this value is the major version and the remainder is * the minor version. For example 101 corresponds to file version "1.1". */ public static final int FILE_VERSION_DIVISOR = 100; private static final String OPENROCKET_CHARSET = "UTF-8"; private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers"; private static final String METHOD_SUFFIX = "Saver"; // Estimated storage used by different portions // These have been hand-estimated from saved files private static final int BYTES_PER_COMPONENT_COMPRESSED = 80; private static final int BYTES_PER_SIMULATION_COMPRESSED = 100; private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100; private int indent; private Writer dest; @Override public void save(OutputStream output, OpenRocketDocument document, StorageOptions options) throws IOException { log.info("Saving .ork file"); dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET)); // Select file version number final int fileVersion = calculateNecessaryFileVersion(document, options); final String fileVersionString = (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR); log.debug("Storing file version " + fileVersionString); this.indent = 0; writeln("<?xml version='1.0' encoding='utf-8'?>"); writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket " + BuildProperties.getVersion() + "\">"); indent++; // Recursively save the rocket structure saveComponent(document.getRocket()); writeln(""); // Save custom expressions; saveCustomDatatypes(document); // Save all simulations writeln("<simulations>"); indent++; boolean first = true; for (Simulation s : document.getSimulations()) { if (!first) writeln(""); first = false; saveSimulation(s, options.getSimulationTimeSkip()); } indent--; writeln("</simulations>"); indent--; writeln("</openrocket>"); log.debug("Writing complete, flushing buffers"); dest.flush(); } /* * Save all the custom expressions */ private void saveCustomDatatypes(OpenRocketDocument doc) throws IOException { if (doc.getCustomExpressions().isEmpty()) return; writeln("<datatypes>"); indent++; for (CustomExpression exp : doc.getCustomExpressions()) { saveCustomExpressionDatatype(exp); } indent--; writeln("</datatypes>"); writeln(""); } /* * Save one custom expression datatype */ private void saveCustomExpressionDatatype(CustomExpression exp) throws IOException { // Write out custom expression writeln("<type source=\"customexpression\">"); indent++; writeln("<name>" + exp.getName() + "</name>"); writeln("<symbol>" + exp.getSymbol() + "</symbol>"); writeln("<unit unittype=\"auto\">" + exp.getUnit() + "</unit>"); // auto unit type means it will be determined from string writeln("<expression>" + exp.getExpressionString() + "</expression>"); indent--; writeln("</type>"); } @Override public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { long size = 0; // TODO - estimate decals // Size per component int componentCount = 0; Rocket rocket = doc.getRocket(); Iterator<RocketComponent> iterator = rocket.iterator(true); while (iterator.hasNext()) { iterator.next(); componentCount++; } size += componentCount * BYTES_PER_COMPONENT_COMPRESSED; // Size per simulation size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED; // Size per flight data point int pointCount = 0; double timeSkip = options.getSimulationTimeSkip(); if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) { for (Simulation s : doc.getSimulations()) { FlightData data = s.getSimulatedData(); if (data != null) { for (int i = 0; i < data.getBranchCount(); i++) { pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip); } } } } size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED; return size; } /** * Public test accessor method for calculateNecessaryFileVersion, used by unit tests. * * @param document the document to output. * @param opts the storage options. * @return the integer file version to use. */ public int testAccessor_calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) { // TODO: should check for test context here and fail if not running junit return calculateNecessaryFileVersion(document, opts); } /** * Determine which file version is required in order to store all the features of the * current design. By default the oldest version that supports all the necessary features * will be used. * * @param document the document to output. * @param opts the storage options. * @return the integer file version to use. */ private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) { /* * NOTE: Remember to update the supported versions in DocumentConfig as well! * * File version 1.7 is required for: * - simulation extensions * - saving tube fins. * * File version 1.6 is required for: * - saving files using appearances and textures, flight configurations. * * File version 1.5 is requires for: * - saving designs using ComponentPrests * - recovery device deployment on lower stage separation * - custom expressions * * File version 1.4 is required for: * - saving simulation data * - saving motor data * * File version 1.1 is required for: * - fin tabs * - components attached to tube coupler * * Otherwise use version 1.0. */ ///////////////// // Version 1.7 // ///////////////// for (Simulation sim : document.getSimulations()) { if (!sim.getSimulationExtensions().isEmpty()) { return FILE_VERSION_DIVISOR + 7; } } // Search the rocket for any TubeFinSet objects (version 1.7) for (RocketComponent c : document.getRocket()) { if (c instanceof TubeFinSet) { return FILE_VERSION_DIVISOR + 7; } } ///////////////// // Version 1.6 // ///////////////// // Search the rocket for any Appearances or non-motor flight configurations (version 1.6) for (RocketComponent c : document.getRocket()) { if (c.getAppearance() != null) { return FILE_VERSION_DIVISOR + 6; } if (c instanceof FlightConfigurableComponent) { if (c instanceof MotorMount) { MotorMount mmt = (MotorMount) c; if (mmt.getIgnitionConfiguration().size() > 0) { return FILE_VERSION_DIVISOR + 6; } } if (c instanceof RecoveryDevice) { RecoveryDevice recovery = (RecoveryDevice) c; if (recovery.getDeploymentConfiguration().size() > 0) { return FILE_VERSION_DIVISOR + 6; } } if (c instanceof Stage) { Stage stage = (Stage) c; if (stage.getStageSeparationConfiguration().size() > 0) { return FILE_VERSION_DIVISOR + 6; } } } } ///////////////// // Version 1.5 // ///////////////// // Search the rocket for any ComponentPresets (version 1.5) for (RocketComponent c : document.getRocket()) { if (c.getPresetComponent() != null) { return FILE_VERSION_DIVISOR + 5; } } // Search for recovery device deployment type LOWER_STAGE_SEPARATION (version 1.5) for (RocketComponent c : document.getRocket()) { if (c instanceof RecoveryDevice) { if (((RecoveryDevice) c).getDeploymentConfiguration().getDefault().getDeployEvent() == DeployEvent.LOWER_STAGE_SEPARATION) { return FILE_VERSION_DIVISOR + 5; } } } // Check for custom expressions (version 1.5) if (!document.getCustomExpressions().isEmpty()) { return FILE_VERSION_DIVISOR + 5; } ///////////////// // Version 1.4 // ///////////////// // Check if design has simulations defined (version 1.4) if (document.getSimulationCount() > 0) { return FILE_VERSION_DIVISOR + 4; } // Check for motor definitions (version 1.4) for (RocketComponent c : document.getRocket()) { if (!(c instanceof MotorMount)) continue; MotorMount mount = (MotorMount) c; for (String id : document.getRocket().getFlightConfigurationIDs()) { if (mount.getMotor(id) != null) { return FILE_VERSION_DIVISOR + 4; } } } ///////////////// // Version 1.3 // ///////////////// // no version 1.3 file type exists ///////////////// // Version 1.2 // ///////////////// // no version 1.2 file type exists ///////////////// // Version 1.1 // ///////////////// // Check for fin tabs or tube coupler children (version 1.1) for (RocketComponent c : document.getRocket()) { // Check for fin tabs if (c instanceof FinSet) { FinSet fin = (FinSet) c; if (!MathUtil.equals(fin.getTabHeight(), 0) && !MathUtil.equals(fin.getTabLength(), 0)) { return FILE_VERSION_DIVISOR + 1; } } // Check for components attached to tube coupler if (c instanceof TubeCoupler) { if (c.getChildCount() > 0) { return FILE_VERSION_DIVISOR + 1; } } } ///////////////// // Version 1.0 // ///////////////// // Default (version 1.0) return FILE_VERSION_DIVISOR + 0; } @SuppressWarnings("unchecked") private void saveComponent(RocketComponent component) throws IOException { log.debug("Saving component " + component.getComponentName()); Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX, "getElements", RocketComponent.class); if (m == null) { throw new BugException("Unable to find saving class for component " + component.getComponentName()); } // Get the strings to save List<String> list = (List<String>) m.invokeStatic(component); int length = list.size(); if (length == 0) // Nothing to do return; if (length < 2) { throw new RuntimeException("BUG, component data length less than two lines."); } // Open element writeln(list.get(0)); indent++; // Write parameters for (int i = 1; i < length - 1; i++) { writeln(list.get(i)); } // Recursively write subcomponents if (component.getChildCount() > 0) { writeln(""); writeln("<subcomponents>"); indent++; boolean emptyline = false; for (RocketComponent subcomponent : component.getChildren()) { if (emptyline) writeln(""); emptyline = true; saveComponent(subcomponent); } indent--; writeln("</subcomponents>"); } // Close element indent--; writeln(list.get(length - 1)); } private void saveSimulation(Simulation simulation, double timeSkip) throws IOException { SimulationOptions cond = simulation.getOptions(); writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">"); indent++; writeln("<name>" + TextUtil.escapeXML(simulation.getName()) + "</name>"); // TODO: MEDIUM: Other simulators/calculators writeln("<simulator>RK4Simulator</simulator>"); writeln("<calculator>BarrowmanCalculator</calculator>"); writeln("<conditions>"); indent++; writeElement("configid", cond.getMotorConfigurationID()); writeElement("launchrodlength", cond.getLaunchRodLength()); writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI); writeElement("launchroddirection", cond.getLaunchRodDirection() * 360.0 / (2.0 * Math.PI)); writeElement("windaverage", cond.getWindSpeedAverage()); writeElement("windturbulence", cond.getWindTurbulenceIntensity()); writeElement("launchaltitude", cond.getLaunchAltitude()); writeElement("launchlatitude", cond.getLaunchLatitude()); writeElement("launchlongitude", cond.getLaunchLongitude()); writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase(Locale.ENGLISH)); if (cond.isISAAtmosphere()) { writeln("<atmosphere model=\"isa\"/>"); } else { writeln("<atmosphere model=\"extendedisa\">"); indent++; writeElement("basetemperature", cond.getLaunchTemperature()); writeElement("basepressure", cond.getLaunchPressure()); indent--; writeln("</atmosphere>"); } writeElement("timestep", cond.getTimeStep()); indent--; writeln("</conditions>"); for (SimulationExtension extension : simulation.getSimulationExtensions()) { Config config = extension.getConfig(); writeln("<extension extensionid=\"" + TextUtil.escapeXML(extension.getId()) + "\">"); indent++; if (config != null) { for (String key : config.keySet()) { Object value = config.get(key, null); writeEntry(key, value); } } indent--; writeln("</extension>"); } // Write basic simulation data FlightData data = simulation.getSimulatedData(); if (data != null) { String str = "<flightdata"; if (!Double.isNaN(data.getMaxAltitude())) str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\""; if (!Double.isNaN(data.getMaxVelocity())) str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\""; if (!Double.isNaN(data.getMaxAcceleration())) str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\""; if (!Double.isNaN(data.getMaxMachNumber())) str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\""; if (!Double.isNaN(data.getTimeToApogee())) str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\""; if (!Double.isNaN(data.getFlightTime())) str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\""; if (!Double.isNaN(data.getGroundHitVelocity())) str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\""; if (!Double.isNaN(data.getLaunchRodVelocity())) str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\""; if (!Double.isNaN(data.getDeploymentVelocity())) str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\""; str += ">"; writeln(str); indent++; for (Warning w : data.getWarningSet()) { writeElement("warning", TextUtil.escapeXML(w.toString())); } // Check whether to store data if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data timeSkip = 0; if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) { for (int i = 0; i < data.getBranchCount(); i++) { FlightDataBranch branch = data.getBranch(i); saveFlightDataBranch(branch, timeSkip); } } indent--; writeln("</flightdata>"); } indent--; writeln("</simulation>"); } private void writeEntry(String key, Object value) throws IOException { if (value == null) { return; } String keyAttr; if (key != null) { keyAttr = "key=\"" + key + "\" "; } else { keyAttr = ""; } if (value instanceof Boolean) { writeln("<entry " + keyAttr + "type=\"boolean\">" + value + "</entry>"); } else if (value instanceof Number) { writeln("<entry " + keyAttr + "type=\"number\">" + value + "</entry>"); } else if (value instanceof String) { writeln("<entry " + keyAttr + "type=\"string\">" + TextUtil.escapeXML((String) value) + "</entry>"); } else if (value instanceof List) { List<?> list = (List<?>) value; writeln("<entry " + keyAttr + "type=\"list\">"); indent++; for (Object o : list) { writeEntry(null, o); } indent--; writeln("</entry>"); } else { // Unknown type log.error("Unknown configuration value type " + value.getClass() + " value=" + value); } } private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) throws IOException { double previousTime = -100000; if (branch == null) return; // Retrieve the types from the branch FlightDataType[] types = branch.getTypes(); if (types.length == 0) return; // Retrieve the data from the branch List<List<Double>> data = new ArrayList<List<Double>>(types.length); for (int i = 0; i < types.length; i++) { data.add(branch.get(types[i])); } List<Double> timeData = branch.get(FlightDataType.TYPE_TIME); // Build the <databranch> tag StringBuilder sb = new StringBuilder(); sb.append("<databranch name=\""); sb.append(TextUtil.escapeXML(branch.getBranchName())); sb.append("\" "); // Kevins version where typekeys are used /* sb.append("\" typekeys=\""); for (int i = 0; i < types.length; i++) { if (i > 0) sb.append(","); sb.append(escapeXML(types[i].getKey())); } */ if (!Double.isNaN(branch.getOptimumAltitude())) { sb.append("optimumAltitude=\""); sb.append(branch.getOptimumAltitude()); sb.append("\" "); } if (!Double.isNaN(branch.getTimeToOptimumAltitude())) { sb.append("timeToOptimumAltitude=\""); sb.append(branch.getTimeToOptimumAltitude()); sb.append("\" "); } sb.append("types=\""); for (int i = 0; i < types.length; i++) { if (i > 0) sb.append(","); sb.append(TextUtil.escapeXML(types[i].getName())); } sb.append("\">"); writeln(sb.toString()); indent++; // Write events for (FlightEvent event : branch.getEvents()) { writeln("<event time=\"" + TextUtil.doubleToString(event.getTime()) + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>"); } // Write the data int length = branch.getLength(); if (length > 0) { writeDataPointString(data, 0, sb); previousTime = timeData.get(0); } for (int i = 1; i < length - 1; i++) { if (timeData != null) { if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { writeDataPointString(data, i, sb); previousTime = timeData.get(i); } } else { // If time data is not available, write all points writeDataPointString(data, i, sb); } } if (length > 1) { writeDataPointString(data, length - 1, sb); } indent--; writeln("</databranch>"); } /* TODO: LOW: This is largely duplicated from above! */ private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) { int count = 0; double previousTime = -100000; if (branch == null) return 0; // Retrieve the types from the branch FlightDataType[] types = branch.getTypes(); if (types.length == 0) return 0; List<Double> timeData = branch.get(FlightDataType.TYPE_TIME); if (timeData == null) { // If time data not available, store all points return branch.getLength(); } // Write the data int length = branch.getLength(); if (length > 0) { count++; previousTime = timeData.get(0); } for (int i = 1; i < length - 1; i++) { if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { count++; previousTime = timeData.get(i); } } if (length > 1) { count++; } return count; } private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb) throws IOException { sb.setLength(0); sb.append("<datapoint>"); for (int j = 0; j < data.size(); j++) { if (j > 0) sb.append(","); sb.append(TextUtil.doubleToString(data.get(j).get(index))); } sb.append("</datapoint>"); writeln(sb.toString()); } private void writeElement(String element, Object content) throws IOException { if (content == null) content = ""; writeln("<" + element + ">" + content + "</" + element + ">"); } private void writeln(String str) throws IOException { if (str.length() == 0) { dest.write("\n"); return; } String s = ""; for (int i = 0; i < indent; i++) s = s + " "; s = s + str + "\n"; dest.write(s); } /** * Return the XML equivalent of an enum name. * * @param e the enum to save. * @return the corresponding XML name. */ public static String enumToXMLName(Enum<?> e) { return e.name().toLowerCase(Locale.ENGLISH).replace("_", ""); } }