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.rocketcomponent.FinSet;
19 import net.sf.openrocket.rocketcomponent.Rocket;
20 import net.sf.openrocket.rocketcomponent.RocketComponent;
21 import net.sf.openrocket.rocketcomponent.TubeCoupler;
22 import net.sf.openrocket.simulation.FlightData;
23 import net.sf.openrocket.simulation.FlightDataBranch;
24 import net.sf.openrocket.simulation.FlightDataType;
25 import net.sf.openrocket.simulation.FlightEvent;
26 import net.sf.openrocket.simulation.GUISimulationConditions;
27 import net.sf.openrocket.util.BugException;
28 import net.sf.openrocket.util.MathUtil;
29 import net.sf.openrocket.util.Prefs;
30 import net.sf.openrocket.util.Reflection;
31 import net.sf.openrocket.util.TextUtil;
33 public class OpenRocketSaver extends RocketSaver {
36 * Divisor used in converting an integer version to the point-represented version.
37 * The integer version divided by this value is the major version and the remainder is
38 * the minor version. For example 101 corresponds to file version "1.1".
40 public static final int FILE_VERSION_DIVISOR = 100;
43 private static final String OPENROCKET_CHARSET = "UTF-8";
45 private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
46 private static final String METHOD_SUFFIX = "Saver";
49 // Estimated storage used by different portions
50 // These have been hand-estimated from saved files
51 private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
52 private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
53 private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
54 private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
55 private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
56 private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
63 public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
66 if (options.isCompressionEnabled()) {
67 output = new GZIPOutputStream(output);
70 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
72 final int fileVersion = calculateNecessaryFileVersion(document, options);
73 final String fileVersionString =
74 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
79 System.out.println("Writing...");
81 writeln("<?xml version='1.0' encoding='utf-8'?>");
82 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
83 + Prefs.getVersion() + "\">");
86 // Recursively save the rocket structure
87 saveComponent(document.getRocket());
91 // Save all simulations
92 writeln("<simulations>");
95 for (Simulation s : document.getSimulations()) {
99 saveSimulation(s, options.getSimulationTimeSkip());
102 writeln("</simulations>");
105 writeln("</openrocket>");
108 if (options.isCompressionEnabled()) {
109 ((GZIPOutputStream) output).finish();
116 public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
120 // Size per component
121 int componentCount = 0;
122 Rocket rocket = doc.getRocket();
123 Iterator<RocketComponent> iterator = rocket.deepIterator(true);
124 while (iterator.hasNext()) {
129 if (options.isCompressionEnabled())
130 size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
132 size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
135 // Size per simulation
136 if (options.isCompressionEnabled())
137 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
139 size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
142 // Size per flight data point
144 double timeSkip = options.getSimulationTimeSkip();
145 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
146 for (Simulation s : doc.getSimulations()) {
147 FlightData data = s.getSimulatedData();
149 for (int i = 0; i < data.getBranchCount(); i++) {
150 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
156 if (options.isCompressionEnabled())
157 size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
159 size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
166 * Determine which file version is required in order to store all the features of the
167 * current design. By default the oldest version that supports all the necessary features
170 * @param document the document to output.
171 * @param opts the storage options.
172 * @return the integer file version to use.
174 private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
176 * File version 1.1 is required for:
178 * - components attached to tube coupler
180 * Otherwise use version 1.0.
183 // Check for fin tabs (version 1.1)
184 Iterator<RocketComponent> iterator = document.getRocket().deepIterator();
185 while (iterator.hasNext()) {
186 RocketComponent c = iterator.next();
188 // Check for fin tabs
189 if (c instanceof FinSet) {
190 FinSet fin = (FinSet) c;
191 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
192 !MathUtil.equals(fin.getTabLength(), 0)) {
193 return FILE_VERSION_DIVISOR + 1;
197 // Check for components attached to tube coupler
198 if (c instanceof TubeCoupler) {
199 if (c.getChildCount() > 0) {
200 return FILE_VERSION_DIVISOR + 1;
205 // Default (version 1.0)
206 return FILE_VERSION_DIVISOR + 0;
211 @SuppressWarnings("unchecked")
212 private void saveComponent(RocketComponent component) throws IOException {
214 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
215 "getElements", RocketComponent.class);
217 throw new BugException("Unable to find saving class for component " +
218 component.getComponentName());
221 // Get the strings to save
222 List<String> list = (List<String>) m.invokeStatic(component);
223 int length = list.size();
225 if (length == 0) // Nothing to do
229 throw new RuntimeException("BUG, component data length less than two lines.");
233 writeln(list.get(0));
237 for (int i = 1; i < length - 1; i++) {
238 writeln(list.get(i));
241 // Recursively write subcomponents
242 if (component.getChildCount() > 0) {
244 writeln("<subcomponents>");
246 boolean emptyline = false;
247 for (RocketComponent subcomponent : component) {
251 saveComponent(subcomponent);
254 writeln("</subcomponents>");
259 writeln(list.get(length - 1));
264 private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
265 GUISimulationConditions cond = simulation.getConditions();
267 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
270 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
271 // TODO: MEDIUM: Other simulators/calculators
272 writeln("<simulator>RK4Simulator</simulator>");
273 writeln("<calculator>BarrowmanCalculator</calculator>");
274 writeln("<conditions>");
277 writeElement("configid", cond.getMotorConfigurationID());
278 writeElement("launchrodlength", cond.getLaunchRodLength());
279 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
280 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
281 writeElement("windaverage", cond.getWindSpeedAverage());
282 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
283 writeElement("launchaltitude", cond.getLaunchAltitude());
284 writeElement("launchlatitude", cond.getLaunchLatitude());
286 if (cond.isISAAtmosphere()) {
287 writeln("<atmosphere model=\"isa\"/>");
289 writeln("<atmosphere model=\"extendedisa\">");
291 writeElement("basetemperature", cond.getLaunchTemperature());
292 writeElement("basepressure", cond.getLaunchPressure());
294 writeln("</atmosphere>");
297 writeElement("timestep", cond.getTimeStep());
300 writeln("</conditions>");
303 for (String s : simulation.getSimulationListeners()) {
304 writeElement("listener", escapeXML(s));
308 // Write basic simulation data
310 FlightData data = simulation.getSimulatedData();
312 String str = "<flightdata";
313 if (!Double.isNaN(data.getMaxAltitude()))
314 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
315 if (!Double.isNaN(data.getMaxVelocity()))
316 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
317 if (!Double.isNaN(data.getMaxAcceleration()))
318 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
319 if (!Double.isNaN(data.getMaxMachNumber()))
320 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
321 if (!Double.isNaN(data.getTimeToApogee()))
322 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
323 if (!Double.isNaN(data.getFlightTime()))
324 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
325 if (!Double.isNaN(data.getGroundHitVelocity()))
326 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
331 for (Warning w : data.getWarningSet()) {
332 writeElement("warning", escapeXML(w.toString()));
335 // Check whether to store data
336 if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
339 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
340 for (int i = 0; i < data.getBranchCount(); i++) {
341 FlightDataBranch branch = data.getBranch(i);
342 saveFlightDataBranch(branch, timeSkip);
347 writeln("</flightdata>");
351 writeln("</simulation>");
357 private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
359 double previousTime = -100000;
364 // Retrieve the types from the branch
365 FlightDataType[] types = branch.getTypes();
367 if (types.length == 0)
370 // Retrieve the data from the branch
371 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
372 for (int i = 0; i < types.length; i++) {
373 data.add(branch.get(types[i]));
375 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
376 if (timeData == null) {
377 // TODO: MEDIUM: External data may not have time data
378 throw new IllegalArgumentException("Data did not contain time data");
381 // Build the <databranch> tag
382 StringBuilder sb = new StringBuilder();
383 sb.append("<databranch name=\"");
384 sb.append(escapeXML(branch.getBranchName()));
385 sb.append("\" types=\"");
386 for (int i = 0; i < types.length; i++) {
389 sb.append(escapeXML(types[i].getName()));
392 writeln(sb.toString());
396 for (FlightEvent event : branch.getEvents()) {
397 writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
398 + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
402 int length = branch.getLength();
404 writeDataPointString(data, 0, sb);
405 previousTime = timeData.get(0);
408 for (int i = 1; i < length - 1; i++) {
409 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
410 writeDataPointString(data, i, sb);
411 previousTime = timeData.get(i);
416 writeDataPointString(data, length - 1, sb);
420 writeln("</databranch>");
425 /* TODO: LOW: This is largely duplicated from above! */
426 private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
429 double previousTime = -100000;
434 // Retrieve the types from the branch
435 FlightDataType[] types = branch.getTypes();
437 if (types.length == 0)
440 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
441 if (timeData == null) {
442 // TODO: MEDIUM: External data may not have time data
443 throw new IllegalArgumentException("Data did not contain time data");
447 int length = branch.getLength();
450 previousTime = timeData.get(0);
453 for (int i = 1; i < length - 1; i++) {
454 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
456 previousTime = timeData.get(i);
469 private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
472 sb.append("<datapoint>");
473 for (int j = 0; j < data.size(); j++) {
476 sb.append(TextUtil.doubleToString(data.get(j).get(index)));
478 sb.append("</datapoint>");
479 writeln(sb.toString());
484 private void writeElement(String element, Object content) throws IOException {
487 writeln("<" + element + ">" + content + "</" + element + ">");
492 private void writeln(String str) throws IOException {
493 if (str.length() == 0) {
498 for (int i = 0; i < indent; i++)
505 public static void main(String[] arg) {
506 double d = -0.000000123456789123;
509 for (int i = 0; i < 20; i++) {
510 String str = TextUtil.doubleToString(d);
511 System.out.println(str + " -> " + Double.parseDouble(str));
516 System.out.println("Value: " + Double.parseDouble("1.2345e9"));
522 * Return the XML equivalent of an enum name.
524 * @param e the enum to save.
525 * @return the corresponding XML name.
527 public static String enumToXMLName(Enum<?> e) {
528 return e.name().toLowerCase().replace("_", "");