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