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.zip.GZIPOutputStream;
13 import net.sf.openrocket.aerodynamics.Warning;
14 import net.sf.openrocket.document.OpenRocketDocument;
15 import net.sf.openrocket.document.Simulation;
16 import net.sf.openrocket.document.StorageOptions;
17 import net.sf.openrocket.file.RocketSaver;
18 import net.sf.openrocket.logging.LogHelper;
19 import net.sf.openrocket.rocketcomponent.FinSet;
20 import net.sf.openrocket.rocketcomponent.MotorMount;
21 import net.sf.openrocket.rocketcomponent.Rocket;
22 import net.sf.openrocket.rocketcomponent.RocketComponent;
23 import net.sf.openrocket.rocketcomponent.TubeCoupler;
24 import net.sf.openrocket.simulation.FlightData;
25 import net.sf.openrocket.simulation.FlightDataBranch;
26 import net.sf.openrocket.simulation.FlightDataType;
27 import net.sf.openrocket.simulation.FlightEvent;
28 import net.sf.openrocket.simulation.SimulationOptions;
29 import net.sf.openrocket.startup.Application;
30 import net.sf.openrocket.util.BugException;
31 import net.sf.openrocket.util.BuildProperties;
32 import net.sf.openrocket.util.MathUtil;
33 import net.sf.openrocket.util.Reflection;
34 import net.sf.openrocket.util.TextUtil;
36 public class OpenRocketSaver extends RocketSaver {
37 private static final LogHelper log = Application.getLogger();
41 * Divisor used in converting an integer version to the point-represented version.
42 * The integer version divided by this value is the major version and the remainder is
43 * the minor version. For example 101 corresponds to file version "1.1".
45 public static final int FILE_VERSION_DIVISOR = 100;
48 private static final String OPENROCKET_CHARSET = "UTF-8";
50 private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
51 private static final String METHOD_SUFFIX = "Saver";
54 // Estimated storage used by different portions
55 // These have been hand-estimated from saved files
56 private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
57 private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
58 private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
59 private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
60 private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
61 private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
68 public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
71 log.info("Saving .ork file");
73 if (options.isCompressionEnabled()) {
74 log.debug("Enabling compression");
75 output = new GZIPOutputStream(output);
78 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
80 // Select file version number
81 final int fileVersion = calculateNecessaryFileVersion(document, options);
82 final String fileVersionString =
83 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
84 log.debug("Storing file version " + fileVersionString);
90 writeln("<?xml version='1.0' encoding='utf-8'?>");
91 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
92 + BuildProperties.getVersion() + "\">");
95 // Recursively save the rocket structure
96 saveComponent(document.getRocket());
100 // Save all simulations
101 writeln("<simulations>");
103 boolean first = true;
104 for (Simulation s : document.getSimulations()) {
108 saveSimulation(s, options.getSimulationTimeSkip());
111 writeln("</simulations>");
114 writeln("</openrocket>");
116 log.debug("Writing complete, flushing buffers");
118 if (options.isCompressionEnabled()) {
119 ((GZIPOutputStream) output).finish();
126 public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
130 // Size per component
131 int componentCount = 0;
132 Rocket rocket = doc.getRocket();
133 Iterator<RocketComponent> iterator = rocket.iterator(true);
134 while (iterator.hasNext()) {
139 if (options.isCompressionEnabled())
140 size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
142 size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
145 // Size per simulation
146 if (options.isCompressionEnabled())
147 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
149 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
152 // Size per flight data point
154 double timeSkip = options.getSimulationTimeSkip();
155 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
156 for (Simulation s : doc.getSimulations()) {
157 FlightData data = s.getSimulatedData();
159 for (int i = 0; i < data.getBranchCount(); i++) {
160 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
166 if (options.isCompressionEnabled())
167 size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
169 size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
176 * Determine which file version is required in order to store all the features of the
177 * current design. By default the oldest version that supports all the necessary features
180 * @param document the document to output.
181 * @param opts the storage options.
182 * @return the integer file version to use.
184 private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
186 * File version 1.4 is required for:
187 * - saving simulation data
188 * - saving motor data
190 * File version 1.1 is required for:
192 * - components attached to tube coupler
194 * Otherwise use version 1.0.
197 // Check if design has simulations defined (version 1.4)
198 if (document.getSimulationCount() > 0) {
199 return FILE_VERSION_DIVISOR + 4;
202 // Check for motor definitions (version 1.4)
203 Iterator<RocketComponent> iterator = document.getRocket().iterator();
204 while (iterator.hasNext()) {
205 RocketComponent c = iterator.next();
206 if (!(c instanceof MotorMount))
209 MotorMount mount = (MotorMount) c;
210 for (String id : document.getRocket().getMotorConfigurationIDs()) {
211 if (mount.getMotor(id) != null) {
212 return FILE_VERSION_DIVISOR + 4;
217 // Check for fin tabs (version 1.1)
218 iterator = document.getRocket().iterator();
219 while (iterator.hasNext()) {
220 RocketComponent c = iterator.next();
222 // Check for fin tabs
223 if (c instanceof FinSet) {
224 FinSet fin = (FinSet) c;
225 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
226 !MathUtil.equals(fin.getTabLength(), 0)) {
227 return FILE_VERSION_DIVISOR + 1;
231 // Check for components attached to tube coupler
232 if (c instanceof TubeCoupler) {
233 if (c.getChildCount() > 0) {
234 return FILE_VERSION_DIVISOR + 1;
239 // Default (version 1.0)
240 return FILE_VERSION_DIVISOR + 0;
245 @SuppressWarnings("unchecked")
246 private void saveComponent(RocketComponent component) throws IOException {
248 log.debug("Saving component " + component.getComponentName());
250 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
251 "getElements", RocketComponent.class);
253 throw new BugException("Unable to find saving class for component " +
254 component.getComponentName());
257 // Get the strings to save
258 List<String> list = (List<String>) m.invokeStatic(component);
259 int length = list.size();
261 if (length == 0) // Nothing to do
265 throw new RuntimeException("BUG, component data length less than two lines.");
269 writeln(list.get(0));
273 for (int i = 1; i < length - 1; i++) {
274 writeln(list.get(i));
277 // Recursively write subcomponents
278 if (component.getChildCount() > 0) {
280 writeln("<subcomponents>");
282 boolean emptyline = false;
283 for (RocketComponent subcomponent : component.getChildren()) {
287 saveComponent(subcomponent);
290 writeln("</subcomponents>");
295 writeln(list.get(length - 1));
299 private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
300 SimulationOptions cond = simulation.getOptions();
302 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
305 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
306 // TODO: MEDIUM: Other simulators/calculators
307 writeln("<simulator>RK4Simulator</simulator>");
308 writeln("<calculator>BarrowmanCalculator</calculator>");
309 writeln("<conditions>");
312 writeElement("configid", cond.getMotorConfigurationID());
313 writeElement("launchrodlength", cond.getLaunchRodLength());
314 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
315 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
316 writeElement("windaverage", cond.getWindSpeedAverage());
317 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
318 writeElement("launchaltitude", cond.getLaunchAltitude());
319 writeElement("launchlatitude", cond.getLaunchLatitude());
320 writeElement("launchlongitude", cond.getLaunchLongitude());
321 writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase());
323 if (cond.isISAAtmosphere()) {
324 writeln("<atmosphere model=\"isa\"/>");
326 writeln("<atmosphere model=\"extendedisa\">");
328 writeElement("basetemperature", cond.getLaunchTemperature());
329 writeElement("basepressure", cond.getLaunchPressure());
331 writeln("</atmosphere>");
334 writeElement("timestep", cond.getTimeStep());
337 writeln("</conditions>");
340 for (String s : simulation.getSimulationListeners()) {
341 writeElement("listener", escapeXML(s));
345 // Write basic simulation data
347 FlightData data = simulation.getSimulatedData();
349 String str = "<flightdata";
350 if (!Double.isNaN(data.getMaxAltitude()))
351 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
352 if (!Double.isNaN(data.getMaxVelocity()))
353 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
354 if (!Double.isNaN(data.getMaxAcceleration()))
355 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
356 if (!Double.isNaN(data.getMaxMachNumber()))
357 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
358 if (!Double.isNaN(data.getTimeToApogee()))
359 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
360 if (!Double.isNaN(data.getFlightTime()))
361 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
362 if (!Double.isNaN(data.getGroundHitVelocity()))
363 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
364 if (!Double.isNaN(data.getLaunchRodVelocity()))
365 str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\"";
366 if (!Double.isNaN(data.getDeploymentVelocity()))
367 str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\"";
372 for (Warning w : data.getWarningSet()) {
373 writeElement("warning", escapeXML(w.toString()));
376 // Check whether to store data
377 if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
380 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
381 for (int i = 0; i < data.getBranchCount(); i++) {
382 FlightDataBranch branch = data.getBranch(i);
383 saveFlightDataBranch(branch, timeSkip);
388 writeln("</flightdata>");
392 writeln("</simulation>");
398 private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
400 double previousTime = -100000;
405 // Retrieve the types from the branch
406 FlightDataType[] types = branch.getTypes();
408 if (types.length == 0)
411 // Retrieve the data from the branch
412 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
413 for (int i = 0; i < types.length; i++) {
414 data.add(branch.get(types[i]));
416 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
418 // Build the <databranch> tag
419 StringBuilder sb = new StringBuilder();
420 sb.append("<databranch name=\"");
421 sb.append(escapeXML(branch.getBranchName()));
422 sb.append("\" types=\"");
423 for (int i = 0; i < types.length; i++) {
426 sb.append(escapeXML(types[i].getName()));
429 writeln(sb.toString());
433 for (FlightEvent event : branch.getEvents()) {
434 writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
435 + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
439 int length = branch.getLength();
441 writeDataPointString(data, 0, sb);
442 previousTime = timeData.get(0);
445 for (int i = 1; i < length - 1; i++) {
446 if (timeData != null) {
447 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
448 writeDataPointString(data, i, sb);
449 previousTime = timeData.get(i);
452 // If time data is not available, write all points
453 writeDataPointString(data, i, sb);
458 writeDataPointString(data, length - 1, sb);
462 writeln("</databranch>");
467 /* TODO: LOW: This is largely duplicated from above! */
468 private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
471 double previousTime = -100000;
476 // Retrieve the types from the branch
477 FlightDataType[] types = branch.getTypes();
479 if (types.length == 0)
482 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
483 if (timeData == null) {
484 // If time data not available, store all points
485 return branch.getLength();
489 int length = branch.getLength();
492 previousTime = timeData.get(0);
495 for (int i = 1; i < length - 1; i++) {
496 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
498 previousTime = timeData.get(i);
511 private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
514 sb.append("<datapoint>");
515 for (int j = 0; j < data.size(); j++) {
518 sb.append(TextUtil.doubleToString(data.get(j).get(index)));
520 sb.append("</datapoint>");
521 writeln(sb.toString());
526 private void writeElement(String element, Object content) throws IOException {
529 writeln("<" + element + ">" + content + "</" + element + ">");
534 private void writeln(String str) throws IOException {
535 if (str.length() == 0) {
540 for (int i = 0; i < indent; i++)
550 * Return the XML equivalent of an enum name.
552 * @param e the enum to save.
553 * @return the corresponding XML name.
555 public static String enumToXMLName(Enum<?> e) {
556 return e.name().toLowerCase().replace("_", "");