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