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.startup.Application;
33 import net.sf.openrocket.util.BugException;
34 import net.sf.openrocket.util.BuildProperties;
35 import net.sf.openrocket.util.MathUtil;
36 import net.sf.openrocket.util.Reflection;
37 import net.sf.openrocket.util.TextUtil;
39 public class OpenRocketSaver extends RocketSaver {
40 private static final LogHelper log = Application.getLogger();
44 * Divisor used in converting an integer version to the point-represented version.
45 * The integer version divided by this value is the major version and the remainder is
46 * the minor version. For example 101 corresponds to file version "1.1".
48 public static final int FILE_VERSION_DIVISOR = 100;
51 private static final String OPENROCKET_CHARSET = "UTF-8";
53 private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
54 private static final String METHOD_SUFFIX = "Saver";
57 // Estimated storage used by different portions
58 // These have been hand-estimated from saved files
59 private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
60 private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
61 private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
62 private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
63 private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
64 private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
71 public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
74 log.info("Saving .ork file");
76 if (options.isCompressionEnabled()) {
77 log.debug("Enabling compression");
78 output = new GZIPOutputStream(output);
81 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
83 // Select file version number
84 final int fileVersion = calculateNecessaryFileVersion(document, options);
85 final String fileVersionString =
86 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
87 log.debug("Storing file version " + fileVersionString);
93 writeln("<?xml version='1.0' encoding='utf-8'?>");
94 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
95 + BuildProperties.getVersion() + "\">");
98 // Recursively save the rocket structure
99 saveComponent(document.getRocket());
103 // Save all simulations
104 writeln("<simulations>");
106 boolean first = true;
107 for (Simulation s : document.getSimulations()) {
111 saveSimulation(s, options.getSimulationTimeSkip());
114 writeln("</simulations>");
117 writeln("</openrocket>");
119 log.debug("Writing complete, flushing buffers");
121 if (options.isCompressionEnabled()) {
122 ((GZIPOutputStream) output).finish();
129 public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
133 // Size per component
134 int componentCount = 0;
135 Rocket rocket = doc.getRocket();
136 Iterator<RocketComponent> iterator = rocket.iterator(true);
137 while (iterator.hasNext()) {
142 if (options.isCompressionEnabled())
143 size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
145 size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
148 // Size per simulation
149 if (options.isCompressionEnabled())
150 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
152 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
155 // Size per flight data point
157 double timeSkip = options.getSimulationTimeSkip();
158 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
159 for (Simulation s : doc.getSimulations()) {
160 FlightData data = s.getSimulatedData();
162 for (int i = 0; i < data.getBranchCount(); i++) {
163 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
169 if (options.isCompressionEnabled())
170 size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
172 size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
179 * Determine which file version is required in order to store all the features of the
180 * current design. By default the oldest version that supports all the necessary features
183 * @param document the document to output.
184 * @param opts the storage options.
185 * @return the integer file version to use.
187 private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
189 * File version 1.5 is requires for:
190 * - saving designs using ComponentPrests
191 * - recovery device deployment on lower stage separation
193 * File version 1.4 is required for:
194 * - saving simulation data
195 * - saving motor data
197 * File version 1.1 is required for:
199 * - components attached to tube coupler
201 * Otherwise use version 1.0.
204 // Search the rocket for any ComponentPresets (version 1.5)
205 for (RocketComponent c : document.getRocket()) {
206 if (c.getPresetComponent() != null) {
207 return FILE_VERSION_DIVISOR + 5;
211 // Search for recovery device deployment type LOWER_STAGE_SEPARATION (version 1.5)
212 for (RocketComponent c : document.getRocket()) {
213 if (c instanceof RecoveryDevice) {
214 if (((RecoveryDevice) c).getDeployEvent() == DeployEvent.LOWER_STAGE_SEPARATION) {
215 return FILE_VERSION_DIVISOR + 5;
220 // Check if design has simulations defined (version 1.4)
221 if (document.getSimulationCount() > 0) {
222 return FILE_VERSION_DIVISOR + 4;
225 // Check for motor definitions (version 1.4)
226 for (RocketComponent c : document.getRocket()) {
227 if (!(c instanceof MotorMount))
230 MotorMount mount = (MotorMount) c;
231 for (String id : document.getRocket().getMotorConfigurationIDs()) {
232 if (mount.getMotor(id) != null) {
233 return FILE_VERSION_DIVISOR + 4;
238 // Check for fin tabs (version 1.1)
239 for (RocketComponent c : document.getRocket()) {
240 // Check for fin tabs
241 if (c instanceof FinSet) {
242 FinSet fin = (FinSet) c;
243 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
244 !MathUtil.equals(fin.getTabLength(), 0)) {
245 return FILE_VERSION_DIVISOR + 1;
249 // Check for components attached to tube coupler
250 if (c instanceof TubeCoupler) {
251 if (c.getChildCount() > 0) {
252 return FILE_VERSION_DIVISOR + 1;
257 // Default (version 1.0)
258 return FILE_VERSION_DIVISOR + 0;
263 @SuppressWarnings("unchecked")
264 private void saveComponent(RocketComponent component) throws IOException {
266 log.debug("Saving component " + component.getComponentName());
268 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
269 "getElements", RocketComponent.class);
271 throw new BugException("Unable to find saving class for component " +
272 component.getComponentName());
275 // Get the strings to save
276 List<String> list = (List<String>) m.invokeStatic(component);
277 int length = list.size();
279 if (length == 0) // Nothing to do
283 throw new RuntimeException("BUG, component data length less than two lines.");
287 writeln(list.get(0));
291 for (int i = 1; i < length - 1; i++) {
292 writeln(list.get(i));
295 // Recursively write subcomponents
296 if (component.getChildCount() > 0) {
298 writeln("<subcomponents>");
300 boolean emptyline = false;
301 for (RocketComponent subcomponent : component.getChildren()) {
305 saveComponent(subcomponent);
308 writeln("</subcomponents>");
313 writeln(list.get(length - 1));
317 private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
318 SimulationOptions cond = simulation.getOptions();
320 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
323 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
324 // TODO: MEDIUM: Other simulators/calculators
325 writeln("<simulator>RK4Simulator</simulator>");
326 writeln("<calculator>BarrowmanCalculator</calculator>");
327 writeln("<conditions>");
330 writeElement("configid", cond.getMotorConfigurationID());
331 writeElement("launchrodlength", cond.getLaunchRodLength());
332 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
333 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
334 writeElement("windaverage", cond.getWindSpeedAverage());
335 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
336 writeElement("launchaltitude", cond.getLaunchAltitude());
337 writeElement("launchlatitude", cond.getLaunchLatitude());
338 writeElement("launchlongitude", cond.getLaunchLongitude());
339 writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase(Locale.ENGLISH));
341 if (cond.isISAAtmosphere()) {
342 writeln("<atmosphere model=\"isa\"/>");
344 writeln("<atmosphere model=\"extendedisa\">");
346 writeElement("basetemperature", cond.getLaunchTemperature());
347 writeElement("basepressure", cond.getLaunchPressure());
349 writeln("</atmosphere>");
352 writeElement("timestep", cond.getTimeStep());
355 writeln("</conditions>");
358 for (String s : simulation.getSimulationListeners()) {
359 writeElement("listener", escapeXML(s));
363 // Write basic simulation data
365 FlightData data = simulation.getSimulatedData();
367 String str = "<flightdata";
368 if (!Double.isNaN(data.getMaxAltitude()))
369 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
370 if (!Double.isNaN(data.getMaxVelocity()))
371 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
372 if (!Double.isNaN(data.getMaxAcceleration()))
373 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
374 if (!Double.isNaN(data.getMaxMachNumber()))
375 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
376 if (!Double.isNaN(data.getTimeToApogee()))
377 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
378 if (!Double.isNaN(data.getFlightTime()))
379 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
380 if (!Double.isNaN(data.getGroundHitVelocity()))
381 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
382 if (!Double.isNaN(data.getLaunchRodVelocity()))
383 str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\"";
384 if (!Double.isNaN(data.getDeploymentVelocity()))
385 str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\"";
390 for (Warning w : data.getWarningSet()) {
391 writeElement("warning", escapeXML(w.toString()));
394 // Check whether to store data
395 if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
398 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
399 for (int i = 0; i < data.getBranchCount(); i++) {
400 FlightDataBranch branch = data.getBranch(i);
401 saveFlightDataBranch(branch, timeSkip);
406 writeln("</flightdata>");
410 writeln("</simulation>");
416 private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
418 double previousTime = -100000;
423 // Retrieve the types from the branch
424 FlightDataType[] types = branch.getTypes();
426 if (types.length == 0)
429 // Retrieve the data from the branch
430 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
431 for (int i = 0; i < types.length; i++) {
432 data.add(branch.get(types[i]));
434 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
436 // Build the <databranch> tag
437 StringBuilder sb = new StringBuilder();
438 sb.append("<databranch name=\"");
439 sb.append(escapeXML(branch.getBranchName()));
440 sb.append("\" types=\"");
441 for (int i = 0; i < types.length; i++) {
444 sb.append(escapeXML(types[i].getName()));
447 writeln(sb.toString());
451 for (FlightEvent event : branch.getEvents()) {
452 writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
453 + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
457 int length = branch.getLength();
459 writeDataPointString(data, 0, sb);
460 previousTime = timeData.get(0);
463 for (int i = 1; i < length - 1; i++) {
464 if (timeData != null) {
465 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
466 writeDataPointString(data, i, sb);
467 previousTime = timeData.get(i);
470 // If time data is not available, write all points
471 writeDataPointString(data, i, sb);
476 writeDataPointString(data, length - 1, sb);
480 writeln("</databranch>");
485 /* TODO: LOW: This is largely duplicated from above! */
486 private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
489 double previousTime = -100000;
494 // Retrieve the types from the branch
495 FlightDataType[] types = branch.getTypes();
497 if (types.length == 0)
500 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
501 if (timeData == null) {
502 // If time data not available, store all points
503 return branch.getLength();
507 int length = branch.getLength();
510 previousTime = timeData.get(0);
513 for (int i = 1; i < length - 1; i++) {
514 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
516 previousTime = timeData.get(i);
529 private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
532 sb.append("<datapoint>");
533 for (int j = 0; j < data.size(); j++) {
536 sb.append(TextUtil.doubleToString(data.get(j).get(index)));
538 sb.append("</datapoint>");
539 writeln(sb.toString());
544 private void writeElement(String element, Object content) throws IOException {
547 writeln("<" + element + ">" + content + "</" + element + ">");
552 private void writeln(String str) throws IOException {
553 if (str.length() == 0) {
558 for (int i = 0; i < indent; i++)
568 * Return the XML equivalent of an enum name.
570 * @param e the enum to save.
571 * @return the corresponding XML name.
573 public static String enumToXMLName(Enum<?> e) {
574 return e.name().toLowerCase(Locale.ENGLISH).replace("_", "");