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