bf23728448922b375910e2b25ee1ac37b64acdc3
[debian/openrocket] / 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.MathUtil;
32 import net.sf.openrocket.util.Prefs;
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                                 + Prefs.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.2 is required for:
187                  *  - saving motor data
188                  * 
189                  * File version 1.1 is required for:
190                  *  - fin tabs
191                  *  - components attached to tube coupler
192                  * 
193                  * Otherwise use version 1.0.
194                  */
195
196                 // Check for motor definitions (version 1.2)
197                 Iterator<RocketComponent> iterator = document.getRocket().iterator();
198                 while (iterator.hasNext()) {
199                         RocketComponent c = iterator.next();
200                         if (!(c instanceof MotorMount))
201                                 continue;
202                         
203                         MotorMount mount = (MotorMount) c;
204                         for (String id : document.getRocket().getMotorConfigurationIDs()) {
205                                 if (mount.getMotor(id) != null) {
206                                         return FILE_VERSION_DIVISOR + 2;
207                                 }
208                         }
209                 }
210                 
211                 // Check for fin tabs (version 1.1)
212                 iterator = document.getRocket().iterator();
213                 while (iterator.hasNext()) {
214                         RocketComponent c = iterator.next();
215                         
216                         // Check for fin tabs
217                         if (c instanceof FinSet) {
218                                 FinSet fin = (FinSet) c;
219                                 if (!MathUtil.equals(fin.getTabHeight(), 0) &&
220                                                 !MathUtil.equals(fin.getTabLength(), 0)) {
221                                         return FILE_VERSION_DIVISOR + 1;
222                                 }
223                         }
224                         
225                         // Check for components attached to tube coupler
226                         if (c instanceof TubeCoupler) {
227                                 if (c.getChildCount() > 0) {
228                                         return FILE_VERSION_DIVISOR + 1;
229                                 }
230                         }
231                 }
232                 
233                 // Default (version 1.0)
234                 return FILE_VERSION_DIVISOR + 0;
235         }
236         
237         
238
239         @SuppressWarnings("unchecked")
240         private void saveComponent(RocketComponent component) throws IOException {
241                 
242                 log.debug("Saving component " + component.getComponentName());
243                 
244                 Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
245                                 "getElements", RocketComponent.class);
246                 if (m == null) {
247                         throw new BugException("Unable to find saving class for component " +
248                                         component.getComponentName());
249                 }
250                 
251                 // Get the strings to save
252                 List<String> list = (List<String>) m.invokeStatic(component);
253                 int length = list.size();
254                 
255                 if (length == 0) // Nothing to do
256                         return;
257                 
258                 if (length < 2) {
259                         throw new RuntimeException("BUG, component data length less than two lines.");
260                 }
261                 
262                 // Open element
263                 writeln(list.get(0));
264                 indent++;
265                 
266                 // Write parameters
267                 for (int i = 1; i < length - 1; i++) {
268                         writeln(list.get(i));
269                 }
270                 
271                 // Recursively write subcomponents
272                 if (component.getChildCount() > 0) {
273                         writeln("");
274                         writeln("<subcomponents>");
275                         indent++;
276                         boolean emptyline = false;
277                         for (RocketComponent subcomponent : component.getChildren()) {
278                                 if (emptyline)
279                                         writeln("");
280                                 emptyline = true;
281                                 saveComponent(subcomponent);
282                         }
283                         indent--;
284                         writeln("</subcomponents>");
285                 }
286                 
287                 // Close element
288                 indent--;
289                 writeln(list.get(length - 1));
290         }
291         
292         
293         private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
294                 SimulationOptions cond = simulation.getOptions();
295                 
296                 writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">");
297                 indent++;
298                 
299                 writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
300                 // TODO: MEDIUM: Other simulators/calculators
301                 writeln("<simulator>RK4Simulator</simulator>");
302                 writeln("<calculator>BarrowmanCalculator</calculator>");
303                 writeln("<conditions>");
304                 indent++;
305                 
306                 writeElement("configid", cond.getMotorConfigurationID());
307                 writeElement("launchrodlength", cond.getLaunchRodLength());
308                 writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
309                 writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI);
310                 writeElement("windaverage", cond.getWindSpeedAverage());
311                 writeElement("windturbulence", cond.getWindTurbulenceIntensity());
312                 writeElement("launchaltitude", cond.getLaunchAltitude());
313                 writeElement("launchlatitude", cond.getLaunchLatitude());
314                 
315                 if (cond.isISAAtmosphere()) {
316                         writeln("<atmosphere model=\"isa\"/>");
317                 } else {
318                         writeln("<atmosphere model=\"extendedisa\">");
319                         indent++;
320                         writeElement("basetemperature", cond.getLaunchTemperature());
321                         writeElement("basepressure", cond.getLaunchPressure());
322                         indent--;
323                         writeln("</atmosphere>");
324                 }
325                 
326                 writeElement("timestep", cond.getTimeStep());
327                 
328                 indent--;
329                 writeln("</conditions>");
330                 
331
332                 for (String s : simulation.getSimulationListeners()) {
333                         writeElement("listener", escapeXML(s));
334                 }
335                 
336
337                 // Write basic simulation data
338                 
339                 FlightData data = simulation.getSimulatedData();
340                 if (data != null) {
341                         String str = "<flightdata";
342                         if (!Double.isNaN(data.getMaxAltitude()))
343                                 str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\"";
344                         if (!Double.isNaN(data.getMaxVelocity()))
345                                 str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\"";
346                         if (!Double.isNaN(data.getMaxAcceleration()))
347                                 str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\"";
348                         if (!Double.isNaN(data.getMaxMachNumber()))
349                                 str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\"";
350                         if (!Double.isNaN(data.getTimeToApogee()))
351                                 str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\"";
352                         if (!Double.isNaN(data.getFlightTime()))
353                                 str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\"";
354                         if (!Double.isNaN(data.getGroundHitVelocity()))
355                                 str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\"";
356                         str += ">";
357                         writeln(str);
358                         indent++;
359                         
360                         for (Warning w : data.getWarningSet()) {
361                                 writeElement("warning", escapeXML(w.toString()));
362                         }
363                         
364                         // Check whether to store data
365                         if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
366                                 timeSkip = 0;
367                         
368                         if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
369                                 for (int i = 0; i < data.getBranchCount(); i++) {
370                                         FlightDataBranch branch = data.getBranch(i);
371                                         saveFlightDataBranch(branch, timeSkip);
372                                 }
373                         }
374                         
375                         indent--;
376                         writeln("</flightdata>");
377                 }
378                 
379                 indent--;
380                 writeln("</simulation>");
381                 
382         }
383         
384         
385
386         private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
387                         throws IOException {
388                 double previousTime = -100000;
389                 
390                 if (branch == null)
391                         return;
392                 
393                 // Retrieve the types from the branch
394                 FlightDataType[] types = branch.getTypes();
395                 
396                 if (types.length == 0)
397                         return;
398                 
399                 // Retrieve the data from the branch
400                 List<List<Double>> data = new ArrayList<List<Double>>(types.length);
401                 for (int i = 0; i < types.length; i++) {
402                         data.add(branch.get(types[i]));
403                 }
404                 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
405                 if (timeData == null) {
406                         // TODO: MEDIUM: External data may not have time data
407                         throw new IllegalArgumentException("Data did not contain time data");
408                 }
409                 
410                 // Build the <databranch> tag
411                 StringBuilder sb = new StringBuilder();
412                 sb.append("<databranch name=\"");
413                 sb.append(escapeXML(branch.getBranchName()));
414                 sb.append("\" types=\"");
415                 for (int i = 0; i < types.length; i++) {
416                         if (i > 0)
417                                 sb.append(",");
418                         sb.append(escapeXML(types[i].getName()));
419                 }
420                 sb.append("\">");
421                 writeln(sb.toString());
422                 indent++;
423                 
424                 // Write events
425                 for (FlightEvent event : branch.getEvents()) {
426                         writeln("<event time=\"" + TextUtil.doubleToString(event.getTime())
427                                         + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>");
428                 }
429                 
430                 // Write the data
431                 int length = branch.getLength();
432                 if (length > 0) {
433                         writeDataPointString(data, 0, sb);
434                         previousTime = timeData.get(0);
435                 }
436                 
437                 for (int i = 1; i < length - 1; i++) {
438                         if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
439                                 writeDataPointString(data, i, sb);
440                                 previousTime = timeData.get(i);
441                         }
442                 }
443                 
444                 if (length > 1) {
445                         writeDataPointString(data, length - 1, sb);
446                 }
447                 
448                 indent--;
449                 writeln("</databranch>");
450         }
451         
452         
453
454         /* TODO: LOW: This is largely duplicated from above! */
455         private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) {
456                 int count = 0;
457                 
458                 double previousTime = -100000;
459                 
460                 if (branch == null)
461                         return 0;
462                 
463                 // Retrieve the types from the branch
464                 FlightDataType[] types = branch.getTypes();
465                 
466                 if (types.length == 0)
467                         return 0;
468                 
469                 List<Double> timeData = branch.get(FlightDataType.TYPE_TIME);
470                 if (timeData == null) {
471                         // TODO: MEDIUM: External data may not have time data
472                         throw new IllegalArgumentException("Data did not contain time data");
473                 }
474                 
475                 // Write the data
476                 int length = branch.getLength();
477                 if (length > 0) {
478                         count++;
479                         previousTime = timeData.get(0);
480                 }
481                 
482                 for (int i = 1; i < length - 1; i++) {
483                         if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) {
484                                 count++;
485                                 previousTime = timeData.get(i);
486                         }
487                 }
488                 
489                 if (length > 1) {
490                         count++;
491                 }
492                 
493                 return count;
494         }
495         
496         
497
498         private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
499                         throws IOException {
500                 sb.setLength(0);
501                 sb.append("<datapoint>");
502                 for (int j = 0; j < data.size(); j++) {
503                         if (j > 0)
504                                 sb.append(",");
505                         sb.append(TextUtil.doubleToString(data.get(j).get(index)));
506                 }
507                 sb.append("</datapoint>");
508                 writeln(sb.toString());
509         }
510         
511         
512
513         private void writeElement(String element, Object content) throws IOException {
514                 if (content == null)
515                         content = "";
516                 writeln("<" + element + ">" + content + "</" + element + ">");
517         }
518         
519         
520
521         private void writeln(String str) throws IOException {
522                 if (str.length() == 0) {
523                         dest.write("\n");
524                         return;
525                 }
526                 String s = "";
527                 for (int i = 0; i < indent; i++)
528                         s = s + "  ";
529                 s = s + str + "\n";
530                 dest.write(s);
531         }
532         
533         
534
535
536         /**
537          * Return the XML equivalent of an enum name.
538          * 
539          * @param e             the enum to save.
540          * @return              the corresponding XML name.
541          */
542         public static String enumToXMLName(Enum<?> e) {
543                 return e.name().toLowerCase().replace("_", "");
544         }
545         
546 }