e7e42c85b5b3f3c0ccfe34edca88ebc18d035d14
[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.CustomExpression;
28 import net.sf.openrocket.simulation.FlightData;
29 import net.sf.openrocket.simulation.FlightDataBranch;
30 import net.sf.openrocket.simulation.FlightDataType;
31 import net.sf.openrocket.simulation.FlightEvent;
32 import net.sf.openrocket.simulation.SimulationOptions;
33 import net.sf.openrocket.startup.Application;
34 import net.sf.openrocket.util.BugException;
35 import net.sf.openrocket.util.BuildProperties;
36 import net.sf.openrocket.util.MathUtil;
37 import net.sf.openrocket.util.Reflection;
38 import net.sf.openrocket.util.TextUtil;
39
40 public class OpenRocketSaver extends RocketSaver {
41         private static final LogHelper log = Application.getLogger();
42         
43         
44         /**
45          * Divisor used in converting an integer version to the point-represented version.
46          * The integer version divided by this value is the major version and the remainder is
47          * the minor version.  For example 101 corresponds to file version "1.1".
48          */
49         public static final int FILE_VERSION_DIVISOR = 100;
50         
51         
52         private static final String OPENROCKET_CHARSET = "UTF-8";
53         
54         private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers";
55         private static final String METHOD_SUFFIX = "Saver";
56         
57         
58         // Estimated storage used by different portions
59         // These have been hand-estimated from saved files
60         private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590;
61         private static final int BYTES_PER_COMPONENT_COMPRESSED = 80;
62         private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000;
63         private static final int BYTES_PER_SIMULATION_COMPRESSED = 100;
64         private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350;
65         private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100;
66         
67         
68         private int indent;
69         private Writer dest;
70         
71         @Override
72         public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
73                         throws IOException {
74                 
75                 log.info("Saving .ork file");
76                 
77                 if (options.isCompressionEnabled()) {
78                         log.debug("Enabling compression");
79                         output = new GZIPOutputStream(output);
80                 }
81                 
82                 dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
83                 
84                 // Select file version number
85                 final int fileVersion = calculateNecessaryFileVersion(document, options);
86                 final String fileVersionString =
87                                 (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR);
88                 log.debug("Storing file version " + fileVersionString);
89                 
90                 
91                 this.indent = 0;
92                 
93                 
94                 writeln("<?xml version='1.0' encoding='utf-8'?>");
95                 writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
96                                 + BuildProperties.getVersion() + "\">");
97                 indent++;
98                 
99                 // Recursively save the rocket structure
100                 saveComponent(document.getRocket());
101                 
102                 writeln("");
103                 
104                 // Save all simulations
105                 writeln("<simulations>");
106                 indent++;
107                 boolean first = true;
108                 for (Simulation s : document.getSimulations()) {
109                         if (!first)
110                                 writeln("");
111                         first = false;
112                         saveSimulation(s, options.getSimulationTimeSkip());
113                 }
114                 indent--;
115                 writeln("</simulations>");
116                 
117                 indent--;
118                 writeln("</openrocket>");
119                 
120                 log.debug("Writing complete, flushing buffers");
121                 dest.flush();
122                 if (options.isCompressionEnabled()) {
123                         ((GZIPOutputStream) output).finish();
124                 }
125         }
126         
127         
128         
129         @Override
130         public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
131                 
132                 long size = 0;
133                 
134                 // Size per component
135                 int componentCount = 0;
136                 Rocket rocket = doc.getRocket();
137                 Iterator<RocketComponent> iterator = rocket.iterator(true);
138                 while (iterator.hasNext()) {
139                         iterator.next();
140                         componentCount++;
141                 }
142                 
143                 if (options.isCompressionEnabled())
144                         size += componentCount * BYTES_PER_COMPONENT_COMPRESSED;
145                 else
146                         size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED;
147                 
148                 
149                 // Size per simulation
150                 if (options.isCompressionEnabled())
151                         size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED;
152                 else
153                         size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED;
154                 
155                 
156                 // Size per flight data point
157                 int pointCount = 0;
158                 double timeSkip = options.getSimulationTimeSkip();
159                 if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
160                         for (Simulation s : doc.getSimulations()) {
161                                 FlightData data = s.getSimulatedData();
162                                 if (data != null) {
163                                         for (int i = 0; i < data.getBranchCount(); i++) {
164                                                 pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip);
165                                         }
166                                 }
167                         }
168                 }
169                 
170                 if (options.isCompressionEnabled())
171                         size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED;
172                 else
173                         size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED;
174                 
175                 return size;
176         }
177         
178         
179         /**
180          * Determine which file version is required in order to store all the features of the
181          * current design.  By default the oldest version that supports all the necessary features
182          * will be used.
183          * 
184          * @param document      the document to output.
185          * @param opts          the storage options.
186          * @return                      the integer file version to use.
187          */
188         private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
189                 /*
190                  * File version 1.5 is requires for:
191                  *  - saving designs using ComponentPrests
192                  *  - recovery device deployment on lower stage separation
193                  *  
194                  * File version 1.4 is required for:
195                  *  - saving simulation data
196                  *  - saving motor data
197                  * 
198                  * File version 1.1 is required for:
199                  *  - fin tabs
200                  *  - components attached to tube coupler
201                  * 
202                  * Otherwise use version 1.0.
203                  */
204                 
205                 // Search the rocket for any ComponentPresets (version 1.5)
206                 for (RocketComponent c : document.getRocket()) {
207                         if (c.getPresetComponent() != null) {
208                                 return FILE_VERSION_DIVISOR + 5;
209                         }
210                 }
211                 
212                 // Search for recovery device deployment type LOWER_STAGE_SEPARATION (version 1.5)
213                 for (RocketComponent c : document.getRocket()) {
214                         if (c instanceof RecoveryDevice) {
215                                 if (((RecoveryDevice) c).getDeployEvent() == DeployEvent.LOWER_STAGE_SEPARATION) {
216                                         return FILE_VERSION_DIVISOR + 5;
217                                 }
218                         }
219                 }
220                 
221                 // Check if design has simulations defined (version 1.4)
222                 if (document.getSimulationCount() > 0) {
223                         return FILE_VERSION_DIVISOR + 4;
224                 }
225                 
226                 // Check for motor definitions (version 1.4)
227                 for (RocketComponent c : document.getRocket()) {
228                         if (!(c instanceof MotorMount))
229                                 continue;
230                         
231                         MotorMount mount = (MotorMount) c;
232                         for (String id : document.getRocket().getMotorConfigurationIDs()) {
233                                 if (mount.getMotor(id) != null) {
234                                         return FILE_VERSION_DIVISOR + 4;
235                                 }
236                         }
237                 }
238                 
239                 // Check for fin tabs (version 1.1)
240                 for (RocketComponent c : document.getRocket()) {
241                         // Check for fin tabs
242                         if (c instanceof FinSet) {
243                                 FinSet fin = (FinSet) c;
244                                 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
245                                                 !MathUtil.equals(fin.getTabLength(), 0)) {
246                                         return FILE_VERSION_DIVISOR + 1;
247                                 }
248                         }
249                         
250                         // Check for components attached to tube coupler
251                         if (c instanceof TubeCoupler) {
252                                 if (c.getChildCount() > 0) {
253                                         return FILE_VERSION_DIVISOR + 1;
254                                 }
255                         }
256                 }
257                 
258                 // Default (version 1.0)
259                 return FILE_VERSION_DIVISOR + 0;
260         }
261         
262         
263         
264         @SuppressWarnings("unchecked")
265         private void saveComponent(RocketComponent component) throws IOException {
266                 
267                 log.debug("Saving component " + component.getComponentName());
268                 
269                 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
270                                 "getElements", RocketComponent.class);
271                 if (m == null) {
272                         throw new BugException("Unable to find saving class for component " +
273                                         component.getComponentName());
274                 }
275                 
276                 // Get the strings to save
277                 List<String> list = (List<String>) m.invokeStatic(component);
278                 int length = list.size();
279                 
280                 if (length == 0) // Nothing to do
281                         return;
282                 
283                 if (length < 2) {
284                         throw new RuntimeException("BUG, component data length less than two lines.");
285                 }
286                 
287                 // Open element
288                 writeln(list.get(0));
289                 indent++;
290                 
291                 // Write parameters
292                 for (int i = 1; i < length - 1; i++) {
293                         writeln(list.get(i));
294                 }
295                 
296                 // Recursively write subcomponents
297                 if (component.getChildCount() > 0) {
298                         writeln("");
299                         writeln("<subcomponents>");
300                         indent++;
301                         boolean emptyline = false;
302                         for (RocketComponent subcomponent : component.getChildren()) {
303                                 if (emptyline)
304                                         writeln("");
305                                 emptyline = true;
306                                 saveComponent(subcomponent);
307                         }
308                         indent--;
309                         writeln("</subcomponents>");
310                 }
311                 
312                 // Close element
313                 indent--;
314                 writeln(list.get(length - 1));
315         }
316         
317         
318         private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
319                 SimulationOptions cond = simulation.getOptions();
320                 
321                 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
322                 indent++;
323                 
324                 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
325                 // TODO: MEDIUM: Other simulators/calculators
326                 
327                 writeln("<simulator>RK4Simulator</simulator>");
328                 writeln("<calculator>BarrowmanCalculator</calculator>");
329                 
330                 // Write out custom expressions
331                 if (!simulation.getCustomExpressions().isEmpty()){
332                         writeln("<customexpressions>"); indent++;
333                         for (CustomExpression expression : simulation.getCustomExpressions()){
334                                 writeln("<expression>"); indent++;
335                                 writeElement("name", expression.getName());
336                                 writeElement("symbol", expression.getSymbol());
337                                 writeElement("unit", expression.getUnit());
338                                 writeElement("expressionstring", expression.getExpressionString());
339                                 indent--; writeln("</expression>");
340                         }
341                         indent--; writeln("</customexpressions>");
342                 }
343                 
344                 writeln("<conditions>"); indent++;
345                 
346                 writeElement("configid", cond.getMotorConfigurationID());
347                 writeElement("launchrodlength", cond.getLaunchRodLength());
348                 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
349                 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
350                 writeElement("windaverage", cond.getWindSpeedAverage());
351                 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
352                 writeElement("launchaltitude", cond.getLaunchAltitude());
353                 writeElement("launchlatitude", cond.getLaunchLatitude());
354                 writeElement("launchlongitude", cond.getLaunchLongitude());
355                 writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase(Locale.ENGLISH));
356                 
357                 if (cond.isISAAtmosphere()) {
358                         writeln("<atmosphere model=\"isa\"/>");
359                 } else {
360                         writeln("<atmosphere model=\"extendedisa\">");
361                         indent++;
362                         writeElement("basetemperature", cond.getLaunchTemperature());
363                         writeElement("basepressure", cond.getLaunchPressure());
364                         indent--;
365                         writeln("</atmosphere>");
366                 }
367                 
368                 writeElement("timestep", cond.getTimeStep());
369                 
370                 indent--;
371                 writeln("</conditions>");
372                 
373                 
374                 for (String s : simulation.getSimulationListeners()) {
375                         writeElement("listener", escapeXML(s));
376                 }
377                 
378                 // Write basic simulation data
379                 
380                 FlightData data = simulation.getSimulatedData();
381                 if (data != null) {
382                         String str = "<flightdata";
383                         if (!Double.isNaN(data.getMaxAltitude()))
384                                 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
385                         if (!Double.isNaN(data.getMaxVelocity()))
386                                 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
387                         if (!Double.isNaN(data.getMaxAcceleration()))
388                                 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
389                         if (!Double.isNaN(data.getMaxMachNumber()))
390                                 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
391                         if (!Double.isNaN(data.getTimeToApogee()))
392                                 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
393                         if (!Double.isNaN(data.getFlightTime()))
394                                 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
395                         if (!Double.isNaN(data.getGroundHitVelocity()))
396                                 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
397                         if (!Double.isNaN(data.getLaunchRodVelocity()))
398                                 str += " launchrodvelocity=\"" + TextUtil.doubleToString(data.getLaunchRodVelocity()) + "\"";
399                         if (!Double.isNaN(data.getDeploymentVelocity()))
400                                 str += " deploymentvelocity=\"" + TextUtil.doubleToString(data.getDeploymentVelocity()) + "\"";
401                         str += ">";
402                         writeln(str);
403                         indent++;
404                         
405                         for (Warning w : data.getWarningSet()) {
406                                 writeElement("warning", escapeXML(w.toString()));
407                         }
408                         
409                         // Check whether to store data
410                         if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
411                                 timeSkip = 0;
412                         
413                         if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
414                                 for (int i = 0; i < data.getBranchCount(); i++) {
415                                         FlightDataBranch branch = data.getBranch(i);
416                                         saveFlightDataBranch(branch, timeSkip);
417                                 }
418                         }
419                         
420                         indent--;
421                         writeln("</flightdata>");
422                 }
423                 
424                 indent--;
425                 writeln("</simulation>");
426                 
427         }
428         
429         
430         
431         private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
432                         throws IOException {
433                 double previousTime = -100000;
434                 
435                 if (branch == null)
436                         return;
437                 
438                 // Retrieve the types from the branch
439                 FlightDataType[] types = branch.getTypes();
440                 
441                 if (types.length == 0)
442                         return;
443                 
444                 // Retrieve the data from the branch
445                 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
446                 for (int i = 0; i < types.length; i++) {
447                         data.add(branch.get(types[i]));
448                 }
449                 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
450                 
451                 // Build the <databranch> tag
452                 StringBuilder sb = new StringBuilder();
453                 sb.append("<databranch name=\"");
454                 sb.append(escapeXML(branch.getBranchName()));
455                 
456                 
457                 sb.append("\" typekeys=\"");
458                 for (int i = 0; i < types.length; i++) {
459                         if (i > 0)
460                                 sb.append(",");
461                         sb.append(types[i].getKey());
462                 }
463                 sb.append("\" types=\"");
464                 for (int i = 0; i < types.length; i++) {
465                         if (i > 0)
466                                 sb.append(",");
467                         sb.append(escapeXML(types[i].getName()));
468                 }
469                 sb.append("\">");
470                 writeln(sb.toString());
471                 indent++;
472                 
473                 // Write events
474                 for (FlightEvent event : branch.getEvents()) {
475                         writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
476                                         + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
477                 }
478                 
479                 // Write the data
480                 int length = branch.getLength();
481                 if (length > 0) {
482                         writeDataPointString(data, 0, sb);
483                         previousTime = timeData.get(0);
484                 }
485                 
486                 for (int i = 1; i < length - 1; i++) {
487                         if (timeData != null) {
488                                 if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
489                                         writeDataPointString(data, i, sb);
490                                         previousTime = timeData.get(i);
491                                 }
492                         } else {
493                                 // If time data is not available, write all points
494                                 writeDataPointString(data, i, sb);
495                         }
496                 }
497                 
498                 if (length > 1) {
499                         writeDataPointString(data, length - 1, sb);
500                 }
501                 
502                 indent--;
503                 writeln("</databranch>");
504         }
505         
506         
507         
508         /* TODO: LOW: This is largely duplicated from above! */
509         private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
510                 int count = 0;
511                 
512                 double previousTime = -100000;
513                 
514                 if (branch == null)
515                         return 0;
516                 
517                 // Retrieve the types from the branch
518                 FlightDataType[] types = branch.getTypes();
519                 
520                 if (types.length == 0)
521                         return 0;
522                 
523                 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
524                 if (timeData == null) {
525                         // If time data not available, store all points
526                         return branch.getLength();
527                 }
528                 
529                 // Write the data
530                 int length = branch.getLength();
531                 if (length > 0) {
532                         count++;
533                         previousTime = timeData.get(0);
534                 }
535                 
536                 for (int i = 1; i < length - 1; i++) {
537                         if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
538                                 count++;
539                                 previousTime = timeData.get(i);
540                         }
541                 }
542                 
543                 if (length > 1) {
544                         count++;
545                 }
546                 
547                 return count;
548         }
549         
550         
551         
552         private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
553                         throws IOException {
554                 sb.setLength(0);
555                 sb.append("<datapoint>");
556                 for (int j = 0; j < data.size(); j++) {
557                         if (j > 0)
558                                 sb.append(",");
559                         sb.append(TextUtil.doubleToString(data.get(j).get(index)));
560                 }
561                 sb.append("</datapoint>");
562                 writeln(sb.toString());
563         }
564         
565         
566         
567         private void writeElement(String element, Object content) throws IOException {
568                 if (content == null)
569                         content = "";
570                 writeln("<" + element + ">" + content + "</" + element + ">");
571         }
572         
573         
574         
575         private void writeln(String str) throws IOException {
576                 if (str.length() == 0) {
577                         dest.write("\n");
578                         return;
579                 }
580                 String s = "";
581                 for (int i = 0; i < indent; i++)
582                         s = s + "  ";
583                 s = s + str + "\n";
584                 dest.write(s);
585         }
586         
587         
588         
589         
590         /**
591          * Return the XML equivalent of an enum name.
592          * 
593          * @param e             the enum to save.
594          * @return              the corresponding XML name.
595          */
596         public static String enumToXMLName(Enum<?> e) {
597                 return e.name().toLowerCase(Locale.ENGLISH).replace("_", "");
598         }
599         
600 }