1 package net.sf.openrocket.file.openrocket;
3 import java.io.BufferedWriter;
4 import java.io.IOException;
5 import java.io.OutputStream;
6 import java.io.OutputStreamWriter;
8 import java.util.ArrayList;
9 import java.util.Iterator;
10 import java.util.List;
11 import java.util.Locale;
12 import java.util.zip.GZIPOutputStream;
14 import net.sf.openrocket.aerodynamics.Warning;
15 import net.sf.openrocket.document.OpenRocketDocument;
16 import net.sf.openrocket.document.Simulation;
17 import net.sf.openrocket.document.StorageOptions;
18 import net.sf.openrocket.file.RocketSaver;
19 import net.sf.openrocket.logging.LogHelper;
20 import net.sf.openrocket.rocketcomponent.FinSet;
21 import net.sf.openrocket.rocketcomponent.MotorMount;
22 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
23 import net.sf.openrocket.rocketcomponent.RecoveryDevice.DeployEvent;
24 import net.sf.openrocket.rocketcomponent.Rocket;
25 import net.sf.openrocket.rocketcomponent.RocketComponent;
26 import net.sf.openrocket.rocketcomponent.TubeCoupler;
27 import net.sf.openrocket.simulation.FlightData;
28 import net.sf.openrocket.simulation.FlightDataBranch;
29 import net.sf.openrocket.simulation.FlightDataType;
30 import net.sf.openrocket.simulation.FlightEvent;
31 import net.sf.openrocket.simulation.SimulationOptions;
32 import net.sf.openrocket.simulation.customexpression.CustomExpression;
33 import net.sf.openrocket.startup.Application;
34 import net.sf.openrocket.util.BugException;
35 import net.sf.openrocket.util.BuildProperties;
36 import net.sf.openrocket.util.MathUtil;
37 import net.sf.openrocket.util.Reflection;
38 import net.sf.openrocket.util.TextUtil;
40 public class OpenRocketSaver extends RocketSaver {
41 private static final LogHelper log = Application.getLogger();
45 * Divisor used in converting an integer version to the point-represented version.
46 * The integer version divided by this value is the major version and the remainder is
47 * the minor version. For example 101 corresponds to file version "1.1".
49 public static final int FILE_VERSION_DIVISOR = 100;
52 private static final String OPENROCKET_CHARSET = "UTF-8";
54 private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
55 private static final String METHOD_SUFFIX = "Saver";
58 // Estimated storage used by different portions
59 // These have been hand-estimated from saved files
60 private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
61 private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
62 private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
63 private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
64 private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
65 private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
72 public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
75 log.info("Saving .ork file");
77 if (options.isCompressionEnabled()) {
78 log.debug("Enabling compression");
79 output = new GZIPOutputStream(output);
82 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
84 // Select file version number
85 final int fileVersion = calculateNecessaryFileVersion(document, options);
86 final String fileVersionString =
87 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
88 log.debug("Storing file version " + fileVersionString);
94 writeln("<?xml version='1.0' encoding='utf-8'?>");
95 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
96 + BuildProperties.getVersion() + "\">");
99 // Recursively save the rocket structure
100 saveComponent(document.getRocket());
104 // Save custom expressions;
105 saveCustomDatatypes(document);
107 // Save all simulations
108 writeln("<simulations>");
110 boolean first = true;
111 for (Simulation s : document.getSimulations()) {
115 saveSimulation(s, options.getSimulationTimeSkip());
118 writeln("</simulations>");
121 writeln("</openrocket>");
123 log.debug("Writing complete, flushing buffers");
125 if (options.isCompressionEnabled()) {
126 ((GZIPOutputStream) output).finish();
131 * Save all the custom expressions
133 private void saveCustomDatatypes(OpenRocketDocument doc) throws IOException {
135 if (doc.getCustomExpressions().isEmpty())
138 writeln("<datatypes>");
141 for (CustomExpression exp : doc.getCustomExpressions()) {
142 saveCustomExpressionDatatype(exp);
146 writeln("</datatypes>");
151 * Save one custom expression datatype
153 private void saveCustomExpressionDatatype(CustomExpression exp) throws IOException {
154 // Write out custom expression
156 writeln("<type source=\"customexpression\">");
158 writeln("<name>" + exp.getName() + "</name>");
159 writeln("<symbol>" + exp.getSymbol() + "</symbol>");
160 writeln("<unit unittype=\"auto\">" + exp.getUnit() + "</unit>"); // auto unit type means it will be determined from string
161 writeln("<expression>" + exp.getExpressionString() + "</expression>");
167 public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
171 // Size per component
172 int componentCount = 0;
173 Rocket rocket = doc.getRocket();
174 Iterator<RocketComponent> iterator = rocket.iterator(true);
175 while (iterator.hasNext()) {
180 if (options.isCompressionEnabled())
181 size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
183 size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
186 // Size per simulation
187 if (options.isCompressionEnabled())
188 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
190 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
193 // Size per flight data point
195 double timeSkip = options.getSimulationTimeSkip();
196 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
197 for (Simulation s : doc.getSimulations()) {
198 FlightData data = s.getSimulatedData();
200 for (int i = 0; i < data.getBranchCount(); i++) {
201 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
207 if (options.isCompressionEnabled())
208 size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
210 size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
217 * Determine which file version is required in order to store all the features of the
218 * current design. By default the oldest version that supports all the necessary features
221 * @param document the document to output.
222 * @param opts the storage options.
223 * @return the integer file version to use.
225 private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
227 * File version 1.5 is requires for:
228 * - saving designs using ComponentPrests
229 * - recovery device deployment on lower stage separation
230 * - custom expressions
232 * File version 1.4 is required for:
233 * - saving simulation data
234 * - saving motor data
236 * File version 1.1 is required for:
238 * - components attached to tube coupler
240 * Otherwise use version 1.0.
243 // Search the rocket for any ComponentPresets (version 1.5)
244 for (RocketComponent c : document.getRocket()) {
245 if (c.getPresetComponent() != null) {
246 return FILE_VERSION_DIVISOR + 5;
250 // Search for recovery device deployment type LOWER_STAGE_SEPARATION (version 1.5)
251 for (RocketComponent c : document.getRocket()) {
252 if (c instanceof RecoveryDevice) {
253 if (((RecoveryDevice) c).getDeployEvent() == DeployEvent.LOWER_STAGE_SEPARATION) {
254 return FILE_VERSION_DIVISOR + 5;
259 // Check for custom expressions
260 if (!document.getCustomExpressions().isEmpty()) {
261 return FILE_VERSION_DIVISOR + 5;
264 // Check if design has simulations defined (version 1.4)
265 if (document.getSimulationCount() > 0) {
266 return FILE_VERSION_DIVISOR + 4;
269 // Check for motor definitions (version 1.4)
270 for (RocketComponent c : document.getRocket()) {
271 if (!(c instanceof MotorMount))
274 MotorMount mount = (MotorMount) c;
275 for (String id : document.getRocket().getMotorConfigurationIDs()) {
276 if (mount.getMotor(id) != null) {
277 return FILE_VERSION_DIVISOR + 4;
282 // Check for fin tabs (version 1.1)
283 for (RocketComponent c : document.getRocket()) {
284 // Check for fin tabs
285 if (c instanceof FinSet) {
286 FinSet fin = (FinSet) c;
287 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
288 !MathUtil.equals(fin.getTabLength(), 0)) {
289 return FILE_VERSION_DIVISOR + 1;
293 // Check for components attached to tube coupler
294 if (c instanceof TubeCoupler) {
295 if (c.getChildCount() > 0) {
296 return FILE_VERSION_DIVISOR + 1;
301 // Default (version 1.0)
302 return FILE_VERSION_DIVISOR + 0;
307 @SuppressWarnings("unchecked")
308 private void saveComponent(RocketComponent component) throws IOException {
310 log.debug("Saving component " + component.getComponentName());
312 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
313 "getElements", RocketComponent.class);
315 throw new BugException("Unable to find saving class for component " +
316 component.getComponentName());
319 // Get the strings to save
320 List<String> list = (List<String>) m.invokeStatic(component);
321 int length = list.size();
323 if (length == 0) // Nothing to do
327 throw new RuntimeException("BUG, component data length less than two lines.");
331 writeln(list.get(0));
335 for (int i = 1; i < length - 1; i++) {
336 writeln(list.get(i));
339 // Recursively write subcomponents
340 if (component.getChildCount() > 0) {
342 writeln("<subcomponents>");
344 boolean emptyline = false;
345 for (RocketComponent subcomponent : component.getChildren()) {
349 saveComponent(subcomponent);
352 writeln("</subcomponents>");
357 writeln(list.get(length - 1));
361 private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
362 SimulationOptions cond = simulation.getOptions();
364 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
367 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
368 // TODO: MEDIUM: Other simulators/calculators
370 writeln("<simulator>RK4Simulator</simulator>");
371 writeln("<calculator>BarrowmanCalculator</calculator>");
373 writeln("<conditions>");
376 writeElement("configid", cond.getMotorConfigurationID());
377 writeElement("launchrodlength", cond.getLaunchRodLength());
378 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
379 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
380 writeElement("windaverage", cond.getWindSpeedAverage());
381 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
382 writeElement("launchaltitude", cond.getLaunchAltitude());
383 writeElement("launchlatitude", cond.getLaunchLatitude());
384 writeElement("launchlongitude", cond.getLaunchLongitude());
385 writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase(Locale.ENGLISH));
387 if (cond.isISAAtmosphere()) {
388 writeln("<atmosphere model=\"isa\"/>");
390 writeln("<atmosphere model=\"extendedisa\">");
392 writeElement("basetemperature", cond.getLaunchTemperature());
393 writeElement("basepressure", cond.getLaunchPressure());
395 writeln("</atmosphere>");
398 writeElement("timestep", cond.getTimeStep());
401 writeln("</conditions>");
404 for (String s : simulation.getSimulationListeners()) {
405 writeElement("listener", escapeXML(s));
408 // Write basic simulation data
410 FlightData data = simulation.getSimulatedData();
412 String str = "<flightdata";
413 if (!Double.isNaN(data.getMaxAltitude()))
414 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
415 if (!Double.isNaN(data.getMaxVelocity()))
416 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
417 if (!Double.isNaN(data.getMaxAcceleration()))
418 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
419 if (!Double.isNaN(data.getMaxMachNumber()))
420 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
421 if (!Double.isNaN(data.getTimeToApogee()))
422 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
423 if (!Double.isNaN(data.getFlightTime()))
424 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
425 if (!Double.isNaN(data.getGroundHitVelocity()))
426 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
427 if (!Double.isNaN(data.getLaunchRodVelocity()))
428 str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\"";
429 if (!Double.isNaN(data.getDeploymentVelocity()))
430 str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\"";
435 for (Warning w : data.getWarningSet()) {
436 writeElement("warning", escapeXML(w.toString()));
439 // Check whether to store data
440 if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
443 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
444 for (int i = 0; i < data.getBranchCount(); i++) {
445 FlightDataBranch branch = data.getBranch(i);
446 saveFlightDataBranch(branch, timeSkip);
451 writeln("</flightdata>");
455 writeln("</simulation>");
461 private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
463 double previousTime = -100000;
468 // Retrieve the types from the branch
469 FlightDataType[] types = branch.getTypes();
471 if (types.length == 0)
474 // Retrieve the data from the branch
475 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
476 for (int i = 0; i < types.length; i++) {
477 data.add(branch.get(types[i]));
479 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
481 // Build the <databranch> tag
482 StringBuilder sb = new StringBuilder();
483 sb.append("<databranch name=\"");
484 sb.append(escapeXML(branch.getBranchName()));
486 // Kevins version where typekeys are used
488 sb.append("\" typekeys=\"");
489 for (int i = 0; i < types.length; i++) {
492 sb.append(escapeXML(types[i].getKey()));
496 sb.append("\" types=\"");
497 for (int i = 0; i < types.length; i++) {
500 sb.append(escapeXML(types[i].getName()));
503 writeln(sb.toString());
507 for (FlightEvent event : branch.getEvents()) {
508 writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
509 + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
513 int length = branch.getLength();
515 writeDataPointString(data, 0, sb);
516 previousTime = timeData.get(0);
519 for (int i = 1; i < length - 1; i++) {
520 if (timeData != null) {
521 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
522 writeDataPointString(data, i, sb);
523 previousTime = timeData.get(i);
526 // If time data is not available, write all points
527 writeDataPointString(data, i, sb);
532 writeDataPointString(data, length - 1, sb);
536 writeln("</databranch>");
541 /* TODO: LOW: This is largely duplicated from above! */
542 private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
545 double previousTime = -100000;
550 // Retrieve the types from the branch
551 FlightDataType[] types = branch.getTypes();
553 if (types.length == 0)
556 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
557 if (timeData == null) {
558 // If time data not available, store all points
559 return branch.getLength();
563 int length = branch.getLength();
566 previousTime = timeData.get(0);
569 for (int i = 1; i < length - 1; i++) {
570 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
572 previousTime = timeData.get(i);
585 private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
588 sb.append("<datapoint>");
589 for (int j = 0; j < data.size(); j++) {
592 sb.append(TextUtil.doubleToString(data.get(j).get(index)));
594 sb.append("</datapoint>");
595 writeln(sb.toString());
600 private void writeElement(String element, Object content) throws IOException {
603 writeln("<" + element + ">" + content + "</" + element + ">");
608 private void writeln(String str) throws IOException {
609 if (str.length() == 0) {
614 for (int i = 0; i < indent; i++)
624 * Return the XML equivalent of an enum name.
626 * @param e the enum to save.
627 * @return the corresponding XML name.
629 public static String enumToXMLName(Enum<?> e) {
630 return e.name().toLowerCase(Locale.ENGLISH).replace("_", "");