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