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