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.Rocket;
23 import net.sf.openrocket.rocketcomponent.RocketComponent;
24 import net.sf.openrocket.rocketcomponent.TubeCoupler;
25 import net.sf.openrocket.simulation.FlightData;
26 import net.sf.openrocket.simulation.FlightDataBranch;
27 import net.sf.openrocket.simulation.FlightDataType;
28 import net.sf.openrocket.simulation.FlightEvent;
29 import net.sf.openrocket.simulation.SimulationOptions;
30 import net.sf.openrocket.startup.Application;
31 import net.sf.openrocket.util.BugException;
32 import net.sf.openrocket.util.BuildProperties;
33 import net.sf.openrocket.util.MathUtil;
34 import net.sf.openrocket.util.Reflection;
35 import net.sf.openrocket.util.TextUtil;
37 public class OpenRocketSaver extends RocketSaver {
38 private static final LogHelper log = Application.getLogger();
42 * Divisor used in converting an integer version to the point-represented version.
43 * The integer version divided by this value is the major version and the remainder is
44 * the minor version. For example 101 corresponds to file version "1.1".
46 public static final int FILE_VERSION_DIVISOR = 100;
49 private static final String OPENROCKET_CHARSET = "UTF-8";
51 private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
52 private static final String METHOD_SUFFIX = "Saver";
55 // Estimated storage used by different portions
56 // These have been hand-estimated from saved files
57 private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
58 private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
59 private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
60 private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
61 private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
62 private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
69 public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
72 log.info("Saving .ork file");
74 if (options.isCompressionEnabled()) {
75 log.debug("Enabling compression");
76 output = new GZIPOutputStream(output);
79 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
81 // Select file version number
82 final int fileVersion = calculateNecessaryFileVersion(document, options);
83 final String fileVersionString =
84 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
85 log.debug("Storing file version " + fileVersionString);
91 writeln("<?xml version='1.0' encoding='utf-8'?>");
92 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
93 + BuildProperties.getVersion() + "\">");
96 // Recursively save the rocket structure
97 saveComponent(document.getRocket());
101 // Save all simulations
102 writeln("<simulations>");
104 boolean first = true;
105 for (Simulation s : document.getSimulations()) {
109 saveSimulation(s, options.getSimulationTimeSkip());
112 writeln("</simulations>");
115 writeln("</openrocket>");
117 log.debug("Writing complete, flushing buffers");
119 if (options.isCompressionEnabled()) {
120 ((GZIPOutputStream) output).finish();
127 public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
131 // Size per component
132 int componentCount = 0;
133 Rocket rocket = doc.getRocket();
134 Iterator<RocketComponent> iterator = rocket.iterator(true);
135 while (iterator.hasNext()) {
140 if (options.isCompressionEnabled())
141 size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
143 size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
146 // Size per simulation
147 if (options.isCompressionEnabled())
148 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
150 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
153 // Size per flight data point
155 double timeSkip = options.getSimulationTimeSkip();
156 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
157 for (Simulation s : doc.getSimulations()) {
158 FlightData data = s.getSimulatedData();
160 for (int i = 0; i < data.getBranchCount(); i++) {
161 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
167 if (options.isCompressionEnabled())
168 size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
170 size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
177 * Determine which file version is required in order to store all the features of the
178 * current design. By default the oldest version that supports all the necessary features
181 * @param document the document to output.
182 * @param opts the storage options.
183 * @return the integer file version to use.
185 private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
187 * File version 1.4 is required for:
188 * - saving simulation data
189 * - saving motor data
191 * File version 1.1 is required for:
193 * - components attached to tube coupler
195 * Otherwise use version 1.0.
198 // Check if design has simulations defined (version 1.4)
199 if (document.getSimulationCount() > 0) {
200 return FILE_VERSION_DIVISOR + 4;
203 // Check for motor definitions (version 1.4)
204 Iterator<RocketComponent> iterator = document.getRocket().iterator();
205 while (iterator.hasNext()) {
206 RocketComponent c = iterator.next();
207 if (!(c instanceof MotorMount))
210 MotorMount mount = (MotorMount) c;
211 for (String id : document.getRocket().getMotorConfigurationIDs()) {
212 if (mount.getMotor(id) != null) {
213 return FILE_VERSION_DIVISOR + 4;
218 // Check for fin tabs (version 1.1)
219 iterator = document.getRocket().iterator();
220 while (iterator.hasNext()) {
221 RocketComponent c = iterator.next();
223 // Check for fin tabs
224 if (c instanceof FinSet) {
225 FinSet fin = (FinSet) c;
226 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
227 !MathUtil.equals(fin.getTabLength(), 0)) {
228 return FILE_VERSION_DIVISOR + 1;
232 // Check for components attached to tube coupler
233 if (c instanceof TubeCoupler) {
234 if (c.getChildCount() > 0) {
235 return FILE_VERSION_DIVISOR + 1;
240 // Default (version 1.0)
241 return FILE_VERSION_DIVISOR + 0;
246 @SuppressWarnings("unchecked")
247 private void saveComponent(RocketComponent component) throws IOException {
249 log.debug("Saving component " + component.getComponentName());
251 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
252 "getElements", RocketComponent.class);
254 throw new BugException("Unable to find saving class for component " +
255 component.getComponentName());
258 // Get the strings to save
259 List<String> list = (List<String>) m.invokeStatic(component);
260 int length = list.size();
262 if (length == 0) // Nothing to do
266 throw new RuntimeException("BUG, component data length less than two lines.");
270 writeln(list.get(0));
274 for (int i = 1; i < length - 1; i++) {
275 writeln(list.get(i));
278 // Recursively write subcomponents
279 if (component.getChildCount() > 0) {
281 writeln("<subcomponents>");
283 boolean emptyline = false;
284 for (RocketComponent subcomponent : component.getChildren()) {
288 saveComponent(subcomponent);
291 writeln("</subcomponents>");
296 writeln(list.get(length - 1));
300 private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
301 SimulationOptions cond = simulation.getOptions();
303 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
306 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
307 // TODO: MEDIUM: Other simulators/calculators
308 writeln("<simulator>RK4Simulator</simulator>");
309 writeln("<calculator>BarrowmanCalculator</calculator>");
310 writeln("<conditions>");
313 writeElement("configid", cond.getMotorConfigurationID());
314 writeElement("launchrodlength", cond.getLaunchRodLength());
315 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
316 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
317 writeElement("windaverage", cond.getWindSpeedAverage());
318 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
319 writeElement("launchaltitude", cond.getLaunchAltitude());
320 writeElement("launchlatitude", cond.getLaunchLatitude());
321 writeElement("launchlongitude", cond.getLaunchLongitude());
322 writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase(Locale.ENGLISH));
324 if (cond.isISAAtmosphere()) {
325 writeln("<atmosphere model=\"isa\"/>");
327 writeln("<atmosphere model=\"extendedisa\">");
329 writeElement("basetemperature", cond.getLaunchTemperature());
330 writeElement("basepressure", cond.getLaunchPressure());
332 writeln("</atmosphere>");
335 writeElement("timestep", cond.getTimeStep());
338 writeln("</conditions>");
341 for (String s : simulation.getSimulationListeners()) {
342 writeElement("listener", escapeXML(s));
346 // Write basic simulation data
348 FlightData data = simulation.getSimulatedData();
350 String str = "<flightdata";
351 if (!Double.isNaN(data.getMaxAltitude()))
352 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
353 if (!Double.isNaN(data.getMaxVelocity()))
354 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
355 if (!Double.isNaN(data.getMaxAcceleration()))
356 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
357 if (!Double.isNaN(data.getMaxMachNumber()))
358 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
359 if (!Double.isNaN(data.getTimeToApogee()))
360 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
361 if (!Double.isNaN(data.getFlightTime()))
362 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
363 if (!Double.isNaN(data.getGroundHitVelocity()))
364 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
365 if (!Double.isNaN(data.getLaunchRodVelocity()))
366 str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\"";
367 if (!Double.isNaN(data.getDeploymentVelocity()))
368 str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\"";
373 for (Warning w : data.getWarningSet()) {
374 writeElement("warning", escapeXML(w.toString()));
377 // Check whether to store data
378 if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
381 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
382 for (int i = 0; i < data.getBranchCount(); i++) {
383 FlightDataBranch branch = data.getBranch(i);
384 saveFlightDataBranch(branch, timeSkip);
389 writeln("</flightdata>");
393 writeln("</simulation>");
399 private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
401 double previousTime = -100000;
406 // Retrieve the types from the branch
407 FlightDataType[] types = branch.getTypes();
409 if (types.length == 0)
412 // Retrieve the data from the branch
413 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
414 for (int i = 0; i < types.length; i++) {
415 data.add(branch.get(types[i]));
417 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
419 // Build the <databranch> tag
420 StringBuilder sb = new StringBuilder();
421 sb.append("<databranch name=\"");
422 sb.append(escapeXML(branch.getBranchName()));
423 sb.append("\" types=\"");
424 for (int i = 0; i < types.length; i++) {
427 sb.append(escapeXML(types[i].getName()));
430 writeln(sb.toString());
434 for (FlightEvent event : branch.getEvents()) {
435 writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
436 + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
440 int length = branch.getLength();
442 writeDataPointString(data, 0, sb);
443 previousTime = timeData.get(0);
446 for (int i = 1; i < length - 1; i++) {
447 if (timeData != null) {
448 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
449 writeDataPointString(data, i, sb);
450 previousTime = timeData.get(i);
453 // If time data is not available, write all points
454 writeDataPointString(data, i, sb);
459 writeDataPointString(data, length - 1, sb);
463 writeln("</databranch>");
468 /* TODO: LOW: This is largely duplicated from above! */
469 private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
472 double previousTime = -100000;
477 // Retrieve the types from the branch
478 FlightDataType[] types = branch.getTypes();
480 if (types.length == 0)
483 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
484 if (timeData == null) {
485 // If time data not available, store all points
486 return branch.getLength();
490 int length = branch.getLength();
493 previousTime = timeData.get(0);
496 for (int i = 1; i < length - 1; i++) {
497 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
499 previousTime = timeData.get(i);
512 private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
515 sb.append("<datapoint>");
516 for (int j = 0; j < data.size(); j++) {
519 sb.append(TextUtil.doubleToString(data.get(j).get(index)));
521 sb.append("</datapoint>");
522 writeln(sb.toString());
527 private void writeElement(String element, Object content) throws IOException {
530 writeln("<" + element + ">" + content + "</" + element + ">");
535 private void writeln(String str) throws IOException {
536 if (str.length() == 0) {
541 for (int i = 0; i < indent; i++)
551 * Return the XML equivalent of an enum name.
553 * @param e the enum to save.
554 * @return the corresponding XML name.
556 public static String enumToXMLName(Enum<?> e) {
557 return e.name().toLowerCase(Locale.ENGLISH).replace("_", "");