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