Support for opening recovery device on stage separation
[debian/openrocket] / core / src / net / sf / openrocket / file / openrocket / OpenRocketSaver.java
1 package net.sf.openrocket.file.openrocket;
2
3 import java.io.BufferedWriter;
4 import java.io.IOException;
5 import java.io.OutputStream;
6 import java.io.OutputStreamWriter;
7 import java.io.Writer;
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;
13
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;
38
39 public class OpenRocketSaver extends RocketSaver {
40         private static final LogHelper log = Application.getLogger();
41         
42         
43         /**
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".
47          */
48         public static final int FILE_VERSION_DIVISOR = 100;
49         
50         
51         private static final String OPENROCKET_CHARSET = "UTF-8";
52         
53         private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
54         private static final String METHOD_SUFFIX = "Saver";
55         
56         
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;
65         
66         
67         private int indent;
68         private Writer dest;
69         
70         @Override
71         public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
72                         throws IOException {
73                 
74                 log.info("Saving .ork file");
75                 
76                 if (options.isCompressionEnabled()) {
77                         log.debug("Enabling compression");
78                         output = new GZIPOutputStream(output);
79                 }
80                 
81                 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
82                 
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);
88                 
89                 
90                 this.indent = 0;
91                 
92                 
93                 writeln("<?xml version='1.0' encoding='utf-8'?>");
94                 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
95                                 + BuildProperties.getVersion() + "\">");
96                 indent++;
97                 
98                 // Recursively save the rocket structure
99                 saveComponent(document.getRocket());
100                 
101                 writeln("");
102                 
103                 // Save all simulations
104                 writeln("<simulations>");
105                 indent++;
106                 boolean first = true;
107                 for (Simulation s : document.getSimulations()) {
108                         if (!first)
109                                 writeln("");
110                         first = false;
111                         saveSimulation(s, options.getSimulationTimeSkip());
112                 }
113                 indent--;
114                 writeln("</simulations>");
115                 
116                 indent--;
117                 writeln("</openrocket>");
118                 
119                 log.debug("Writing complete, flushing buffers");
120                 dest.flush();
121                 if (options.isCompressionEnabled()) {
122                         ((GZIPOutputStream) output).finish();
123                 }
124         }
125         
126         
127         
128         @Override
129         public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
130                 
131                 long size = 0;
132                 
133                 // Size per component
134                 int componentCount = 0;
135                 Rocket rocket = doc.getRocket();
136                 Iterator<RocketComponent> iterator = rocket.iterator(true);
137                 while (iterator.hasNext()) {
138                         iterator.next();
139                         componentCount++;
140                 }
141                 
142                 if (options.isCompressionEnabled())
143                         size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
144                 else
145                         size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
146                 
147                 
148                 // Size per simulation
149                 if (options.isCompressionEnabled())
150                         size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
151                 else
152                         size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
153                 
154                 
155                 // Size per flight data point
156                 int pointCount = 0;
157                 double timeSkip = options.getSimulationTimeSkip();
158                 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
159                         for (Simulation s : doc.getSimulations()) {
160                                 FlightData data = s.getSimulatedData();
161                                 if (data != null) {
162                                         for (int i = 0; i < data.getBranchCount(); i++) {
163                                                 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
164                                         }
165                                 }
166                         }
167                 }
168                 
169                 if (options.isCompressionEnabled())
170                         size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
171                 else
172                         size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
173                 
174                 return size;
175         }
176         
177         
178         /**
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
181          * will be used.
182          * 
183          * @param document      the document to output.
184          * @param opts          the storage options.
185          * @return                      the integer file version to use.
186          */
187         private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
188                 /*
189                  * File version 1.5 is requires for:
190                  *  - saving designs using ComponentPrests
191                  *  - recovery device deployment on lower stage separation
192                  *  
193                  * File version 1.4 is required for:
194                  *  - saving simulation data
195                  *  - saving motor data
196                  * 
197                  * File version 1.1 is required for:
198                  *  - fin tabs
199                  *  - components attached to tube coupler
200                  * 
201                  * Otherwise use version 1.0.
202                  */
203                 
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;
208                         }
209                 }
210                 
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;
216                                 }
217                         }
218                 }
219                 
220                 // Check if design has simulations defined (version 1.4)
221                 if (document.getSimulationCount() > 0) {
222                         return FILE_VERSION_DIVISOR + 4;
223                 }
224                 
225                 // Check for motor definitions (version 1.4)
226                 for (RocketComponent c : document.getRocket()) {
227                         if (!(c instanceof MotorMount))
228                                 continue;
229                         
230                         MotorMount mount = (MotorMount) c;
231                         for (String id : document.getRocket().getMotorConfigurationIDs()) {
232                                 if (mount.getMotor(id) != null) {
233                                         return FILE_VERSION_DIVISOR + 4;
234                                 }
235                         }
236                 }
237                 
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;
246                                 }
247                         }
248                         
249                         // Check for components attached to tube coupler
250                         if (c instanceof TubeCoupler) {
251                                 if (c.getChildCount() > 0) {
252                                         return FILE_VERSION_DIVISOR + 1;
253                                 }
254                         }
255                 }
256                 
257                 // Default (version 1.0)
258                 return FILE_VERSION_DIVISOR + 0;
259         }
260         
261         
262         
263         @SuppressWarnings("unchecked")
264         private void saveComponent(RocketComponent component) throws IOException {
265                 
266                 log.debug("Saving component " + component.getComponentName());
267                 
268                 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
269                                 "getElements", RocketComponent.class);
270                 if (m == null) {
271                         throw new BugException("Unable to find saving class for component " +
272                                         component.getComponentName());
273                 }
274                 
275                 // Get the strings to save
276                 List<String> list = (List<String>) m.invokeStatic(component);
277                 int length = list.size();
278                 
279                 if (length == 0) // Nothing to do
280                         return;
281                 
282                 if (length < 2) {
283                         throw new RuntimeException("BUG, component data length less than two lines.");
284                 }
285                 
286                 // Open element
287                 writeln(list.get(0));
288                 indent++;
289                 
290                 // Write parameters
291                 for (int i = 1; i < length - 1; i++) {
292                         writeln(list.get(i));
293                 }
294                 
295                 // Recursively write subcomponents
296                 if (component.getChildCount() > 0) {
297                         writeln("");
298                         writeln("<subcomponents>");
299                         indent++;
300                         boolean emptyline = false;
301                         for (RocketComponent subcomponent : component.getChildren()) {
302                                 if (emptyline)
303                                         writeln("");
304                                 emptyline = true;
305                                 saveComponent(subcomponent);
306                         }
307                         indent--;
308                         writeln("</subcomponents>");
309                 }
310                 
311                 // Close element
312                 indent--;
313                 writeln(list.get(length - 1));
314         }
315         
316         
317         private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
318                 SimulationOptions cond = simulation.getOptions();
319                 
320                 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
321                 indent++;
322                 
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>");
328                 indent++;
329                 
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));
340                 
341                 if (cond.isISAAtmosphere()) {
342                         writeln("<atmosphere model=\"isa\"/>");
343                 } else {
344                         writeln("<atmosphere model=\"extendedisa\">");
345                         indent++;
346                         writeElement("basetemperature", cond.getLaunchTemperature());
347                         writeElement("basepressure", cond.getLaunchPressure());
348                         indent--;
349                         writeln("</atmosphere>");
350                 }
351                 
352                 writeElement("timestep", cond.getTimeStep());
353                 
354                 indent--;
355                 writeln("</conditions>");
356                 
357                 
358                 for (String s : simulation.getSimulationListeners()) {
359                         writeElement("listener", escapeXML(s));
360                 }
361                 
362                 
363                 // Write basic simulation data
364                 
365                 FlightData data = simulation.getSimulatedData();
366                 if (data != null) {
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()) + "\"";
386                         str += ">";
387                         writeln(str);
388                         indent++;
389                         
390                         for (Warning w : data.getWarningSet()) {
391                                 writeElement("warning", escapeXML(w.toString()));
392                         }
393                         
394                         // Check whether to store data
395                         if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
396                                 timeSkip = 0;
397                         
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);
402                                 }
403                         }
404                         
405                         indent--;
406                         writeln("</flightdata>");
407                 }
408                 
409                 indent--;
410                 writeln("</simulation>");
411                 
412         }
413         
414         
415         
416         private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
417                         throws IOException {
418                 double previousTime = -100000;
419                 
420                 if (branch == null)
421                         return;
422                 
423                 // Retrieve the types from the branch
424                 FlightDataType[] types = branch.getTypes();
425                 
426                 if (types.length == 0)
427                         return;
428                 
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]));
433                 }
434                 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
435                 
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++) {
442                         if (i > 0)
443                                 sb.append(",");
444                         sb.append(escapeXML(types[i].getName()));
445                 }
446                 sb.append("\">");
447                 writeln(sb.toString());
448                 indent++;
449                 
450                 // Write events
451                 for (FlightEvent event : branch.getEvents()) {
452                         writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
453                                         + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
454                 }
455                 
456                 // Write the data
457                 int length = branch.getLength();
458                 if (length > 0) {
459                         writeDataPointString(data, 0, sb);
460                         previousTime = timeData.get(0);
461                 }
462                 
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);
468                                 }
469                         } else {
470                                 // If time data is not available, write all points
471                                 writeDataPointString(data, i, sb);
472                         }
473                 }
474                 
475                 if (length > 1) {
476                         writeDataPointString(data, length - 1, sb);
477                 }
478                 
479                 indent--;
480                 writeln("</databranch>");
481         }
482         
483         
484         
485         /* TODO: LOW: This is largely duplicated from above! */
486         private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
487                 int count = 0;
488                 
489                 double previousTime = -100000;
490                 
491                 if (branch == null)
492                         return 0;
493                 
494                 // Retrieve the types from the branch
495                 FlightDataType[] types = branch.getTypes();
496                 
497                 if (types.length == 0)
498                         return 0;
499                 
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();
504                 }
505                 
506                 // Write the data
507                 int length = branch.getLength();
508                 if (length > 0) {
509                         count++;
510                         previousTime = timeData.get(0);
511                 }
512                 
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)) {
515                                 count++;
516                                 previousTime = timeData.get(i);
517                         }
518                 }
519                 
520                 if (length > 1) {
521                         count++;
522                 }
523                 
524                 return count;
525         }
526         
527         
528         
529         private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
530                         throws IOException {
531                 sb.setLength(0);
532                 sb.append("<datapoint>");
533                 for (int j = 0; j < data.size(); j++) {
534                         if (j > 0)
535                                 sb.append(",");
536                         sb.append(TextUtil.doubleToString(data.get(j).get(index)));
537                 }
538                 sb.append("</datapoint>");
539                 writeln(sb.toString());
540         }
541         
542         
543         
544         private void writeElement(String element, Object content) throws IOException {
545                 if (content == null)
546                         content = "";
547                 writeln("<" + element + ">" + content + "</" + element + ">");
548         }
549         
550         
551         
552         private void writeln(String str) throws IOException {
553                 if (str.length() == 0) {
554                         dest.write("\n");
555                         return;
556                 }
557                 String s = "";
558                 for (int i = 0; i < indent; i++)
559                         s = s + "  ";
560                 s = s + str + "\n";
561                 dest.write(s);
562         }
563         
564         
565         
566         
567         /**
568          * Return the XML equivalent of an enum name.
569          * 
570          * @param e             the enum to save.
571          * @return              the corresponding XML name.
572          */
573         public static String enumToXMLName(Enum<?> e) {
574                 return e.name().toLowerCase(Locale.ENGLISH).replace("_", "");
575         }
576         
577 }