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