--- /dev/null
+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.zip.GZIPOutputStream;
+
+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.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.TextUtil;
+
+public class OpenRocketSaver extends RocketSaver {
+
+ /**
+ * 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_UNCOMPRESSED = 590;
+ private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
+ private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
+ private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
+ private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
+ 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 {
+
+ if (options.isCompressionEnabled()) {
+ output = new GZIPOutputStream(output);
+ }
+
+ dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
+
+ final int fileVersion = calculateNecessaryFileVersion(document, options);
+ final String fileVersionString =
+ (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
+
+
+ this.indent = 0;
+
+ System.out.println("Writing...");
+
+ writeln("<?xml version='1.0' encoding='utf-8'?>");
+ writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
+ + Prefs.getVersion() + "\">");
+ indent++;
+
+ // Recursively save the rocket structure
+ saveComponent(document.getRocket());
+
+ writeln("");
+
+ // 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>");
+
+ dest.flush();
+ if (options.isCompressionEnabled()) {
+ ((GZIPOutputStream)output).finish();
+ }
+ }
+
+
+
+ @Override
+ public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
+
+ long size = 0;
+
+ // Size per component
+ int componentCount = 0;
+ Rocket rocket = doc.getRocket();
+ Iterator<RocketComponent> iterator = rocket.deepIterator(true);
+ while (iterator.hasNext()) {
+ iterator.next();
+ componentCount++;
+ }
+
+ if (options.isCompressionEnabled())
+ size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
+ else
+ size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
+
+
+ // Size per simulation
+ if (options.isCompressionEnabled())
+ size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
+ else
+ size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
+
+
+ // 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);
+ }
+ }
+ }
+ }
+
+ if (options.isCompressionEnabled())
+ size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
+ else
+ size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
+
+ return size;
+ }
+
+
+ /**
+ * 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) {
+ /*
+ * File version 1.1 is required for:
+ * - fin tabs
+ * - components attached to tube coupler
+ *
+ * Otherwise use version 1.0.
+ */
+
+ // Check for fin tabs (version 1.1)
+ Iterator<RocketComponent> iterator = document.getRocket().deepIterator();
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+
+ // 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;
+ }
+ }
+ }
+
+ // Default (version 1.0)
+ return FILE_VERSION_DIVISOR + 0;
+ }
+
+
+
+ @SuppressWarnings("unchecked")
+ private void saveComponent(RocketComponent component) throws IOException {
+
+ Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
+ "getElements", RocketComponent.class);
+ if (m==null) {
+ throw new RuntimeException("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) {
+ 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 {
+ SimulationConditions cond = simulation.getConditions();
+
+ writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) +"\">");
+ indent++;
+
+ writeln("<name>" + 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() * 180.0/Math.PI);
+ writeElement("windaverage", cond.getWindSpeedAverage());
+ writeElement("windturbulence", cond.getWindTurbulenceIntensity());
+ writeElement("launchaltitude", cond.getLaunchAltitude());
+ writeElement("launchlatitude", cond.getLaunchLatitude());
+
+ 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 (String s: simulation.getSimulationListeners()) {
+ writeElement("listener", escapeXML(s));
+ }
+
+
+ // 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()) + "\"";
+ str += ">";
+ writeln(str);
+ indent++;
+
+ for (Warning w: data.getWarningSet()) {
+ writeElement("warning", 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 saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
+ throws IOException {
+ double previousTime = -100000;
+
+ if (branch == null)
+ return;
+
+ // Retrieve the types from the branch
+ FlightDataBranch.Type[] 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(FlightDataBranch.TYPE_TIME);
+ if (timeData == null) {
+ // TODO: MEDIUM: External data may not have time data
+ throw new IllegalArgumentException("Data did not contain time data");
+ }
+
+ // Build the <databranch> tag
+ StringBuilder sb = new StringBuilder();
+ sb.append("<databranch name=\"");
+ sb.append(escapeXML(branch.getBranchName()));
+ sb.append("\" types=\"");
+ for (int i=0; i<types.length; i++) {
+ if (i > 0)
+ sb.append(",");
+ sb.append(escapeXML(types[i].getName()));
+ }
+ sb.append("\">");
+ writeln(sb.toString());
+ indent++;
+
+ // Write events
+ for (Pair<Double,FlightEvent> p: branch.getEvents()) {
+ writeln("<event time=\"" + TextUtil.doubleToString(p.getU())
+ + "\" type=\"" + enumToXMLName(p.getV().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 (Math.abs(timeData.get(i) - previousTime - timeSkip) <
+ Math.abs(timeData.get(i+1) - previousTime - timeSkip)) {
+ writeDataPointString(data, i, sb);
+ previousTime = timeData.get(i);
+ }
+ }
+
+ 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
+ FlightDataBranch.Type[] types = branch.getTypes();
+
+ if (types.length == 0)
+ return 0;
+
+ List<Double> timeData = branch.get(FlightDataBranch.TYPE_TIME);
+ if (timeData == null) {
+ // TODO: MEDIUM: External data may not have time data
+ throw new IllegalArgumentException("Data did not contain time data");
+ }
+
+ // 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);
+ }
+
+
+ public static void main(String[] arg) {
+ double d = -0.000000123456789123;
+
+
+ for (int i=0; i< 20; i++) {
+ String str = TextUtil.doubleToString(d);
+ System.out.println(str + " -> " + Double.parseDouble(str));
+ d *= 10;
+ }
+
+
+ System.out.println("Value: "+ Double.parseDouble("1.2345e9"));
+
+ }
+
+
+ /**
+ * 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().replace("_", "");
+ }
+
+}