create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / file / motor / RockSimMotorLoader.java
1 package net.sf.openrocket.file.motor;
2
3 import java.io.IOException;
4 import java.io.Reader;
5 import java.nio.charset.Charset;
6 import java.util.ArrayList;
7 import java.util.HashMap;
8 import java.util.List;
9
10 import net.sf.openrocket.aerodynamics.WarningSet;
11 import net.sf.openrocket.file.simplesax.AbstractElementHandler;
12 import net.sf.openrocket.file.simplesax.ElementHandler;
13 import net.sf.openrocket.file.simplesax.NullElementHandler;
14 import net.sf.openrocket.file.simplesax.PlainTextHandler;
15 import net.sf.openrocket.file.simplesax.SimpleSAX;
16 import net.sf.openrocket.logging.LogHelper;
17 import net.sf.openrocket.motor.Manufacturer;
18 import net.sf.openrocket.motor.Motor;
19 import net.sf.openrocket.motor.MotorDigest;
20 import net.sf.openrocket.motor.MotorDigest.DataType;
21 import net.sf.openrocket.motor.ThrustCurveMotor;
22 import net.sf.openrocket.startup.Application;
23 import net.sf.openrocket.util.Coordinate;
24
25 import org.xml.sax.InputSource;
26 import org.xml.sax.SAXException;
27
28 public class RockSimMotorLoader extends AbstractMotorLoader {
29         
30         private static final LogHelper log = Application.getLogger();
31         
32         public static final String CHARSET_NAME = "UTF-8";
33         
34         public static final Charset CHARSET = Charset.forName(CHARSET_NAME);
35         
36         
37         /** Any delay longer than this will be interpreted as a plugged motor. */
38         private static final int DELAY_LIMIT = 90;
39         
40         
41         
42         @Override
43         protected Charset getDefaultCharset() {
44                 return CHARSET;
45         }
46         
47         
48         
49         /**
50          * Load a <code>Motor</code> from a RockSim motor definition file specified by the 
51          * <code>Reader</code>. The <code>Reader</code> is responsible for using the correct 
52          * charset.
53          * <p>
54          * If automatic CG/mass calculation is used, then the CG is assumed to be located at 
55          * the center of the motor casing and the mass is calculated from the thrust curve 
56          * by assuming a constant exhaust velocity.
57          * 
58          * @param reader  the source of the file.
59          * @return                a list of the {@link Motor} objects defined in the file.
60          * @throws IOException  if an I/O error occurs or if the file format is invalid.
61          */
62         @Override
63         public List<Motor> load(Reader reader, String filename) throws IOException {
64                 InputSource source = new InputSource(reader);
65                 RSEHandler handler = new RSEHandler();
66                 WarningSet warnings = new WarningSet();
67                 
68                 try {
69                         SimpleSAX.readXML(source, handler, warnings);
70                         return handler.getMotors();
71                 } catch (SAXException e) {
72                         throw new IOException(e.getMessage(), e);
73                 }
74         }
75         
76         
77         
78         /**
79          * Initial handler for the RockSim engine files.
80          */
81         private static class RSEHandler extends AbstractElementHandler {
82                 private final List<Motor> motors = new ArrayList<Motor>();
83                 
84                 private RSEMotorHandler motorHandler;
85                 
86                 public List<Motor> getMotors() {
87                         return motors;
88                 }
89                 
90                 @Override
91                 public ElementHandler openElement(String element,
92                                 HashMap<String, String> attributes, WarningSet warnings) throws SAXException {
93                         
94                         if (element.equals("engine-database") ||
95                                         element.equals("engine-list")) {
96                                 // Ignore <engine-database> and <engine-list> elements
97                                 return this;
98                         }
99                         
100                         if (element.equals("version")) {
101                                 // Ignore <version> elements completely
102                                 return null;
103                         }
104                         
105                         if (element.equals("engine")) {
106                                 motorHandler = new RSEMotorHandler(attributes);
107                                 return motorHandler;
108                         }
109                         
110                         return null;
111                 }
112                 
113                 @Override
114                 public void closeElement(String element, HashMap<String, String> attributes,
115                                 String content, WarningSet warnings) throws SAXException {
116                         
117                         if (element.equals("engine")) {
118                                 Motor motor = motorHandler.getMotor();
119                                 motors.add(motor);
120                         }
121                 }
122         }
123         
124         
125         /**
126          * Handler for a RockSim engine file <motor> element.
127          */
128         private static class RSEMotorHandler extends AbstractElementHandler {
129                 
130                 private final String manufacturer;
131                 private final String designation;
132                 private final double[] delays;
133                 private final double diameter;
134                 private final double length;
135                 private final double initMass;
136                 private final double propMass;
137                 private final Motor.Type type;
138                 private boolean calculateMass;
139                 private boolean calculateCG;
140                 
141                 private String description = "";
142                 
143                 private List<Double> time;
144                 private List<Double> force;
145                 private List<Double> mass;
146                 private List<Double> cg;
147                 
148                 private RSEMotorDataHandler dataHandler = null;
149                 
150                 
151                 public RSEMotorHandler(HashMap<String, String> attributes) throws SAXException {
152                         String str;
153                         
154                         // Manufacturer
155                         str = attributes.get("mfg");
156                         if (str == null)
157                                 throw new SAXException("Manufacturer missing");
158                         manufacturer = str;
159                         
160                         // Designation
161                         str = attributes.get("code");
162                         if (str == null)
163                                 throw new SAXException("Designation missing");
164                         designation = removeDelay(str);
165                         
166                         // Delays
167                         ArrayList<Double> delayList = new ArrayList<Double>();
168                         str = attributes.get("delays");
169                         if (str != null) {
170                                 String[] split = str.split(",");
171                                 for (String delay : split) {
172                                         try {
173                                                 
174                                                 double d = Double.parseDouble(delay);
175                                                 if (d >= DELAY_LIMIT)
176                                                         d = Motor.PLUGGED;
177                                                 delayList.add(d);
178                                                 
179                                         } catch (NumberFormatException e) {
180                                                 if (str.equalsIgnoreCase("P") || str.equalsIgnoreCase("plugged")) {
181                                                         delayList.add(Motor.PLUGGED);
182                                                 }
183                                         }
184                                 }
185                         }
186                         delays = new double[delayList.size()];
187                         for (int i = 0; i < delayList.size(); i++) {
188                                 delays[i] = delayList.get(i);
189                         }
190                         
191                         // Diameter
192                         str = attributes.get("dia");
193                         if (str == null)
194                                 throw new SAXException("Diameter missing");
195                         try {
196                                 diameter = Double.parseDouble(str) / 1000.0;
197                         } catch (NumberFormatException e) {
198                                 throw new SAXException("Invalid diameter " + str);
199                         }
200                         
201                         // Length
202                         str = attributes.get("len");
203                         if (str == null)
204                                 throw new SAXException("Length missing");
205                         try {
206                                 length = Double.parseDouble(str) / 1000.0;
207                         } catch (NumberFormatException e) {
208                                 throw new SAXException("Invalid length " + str);
209                         }
210                         
211                         // Initial mass
212                         str = attributes.get("initWt");
213                         if (str == null)
214                                 throw new SAXException("Initial mass missing");
215                         try {
216                                 initMass = Double.parseDouble(str) / 1000.0;
217                         } catch (NumberFormatException e) {
218                                 throw new SAXException("Invalid initial mass " + str);
219                         }
220                         
221                         // Propellant mass
222                         str = attributes.get("propWt");
223                         if (str == null)
224                                 throw new SAXException("Propellant mass missing");
225                         try {
226                                 propMass = Double.parseDouble(str) / 1000.0;
227                         } catch (NumberFormatException e) {
228                                 throw new SAXException("Invalid propellant mass " + str);
229                         }
230                         
231                         if (propMass > initMass) {
232                                 throw new SAXException("Propellant weight exceeds total weight in " +
233                                                 "RockSim engine format");
234                         }
235                         
236                         // Motor type
237                         str = attributes.get("Type");
238                         if ("single-use".equalsIgnoreCase(str)) {
239                                 type = Motor.Type.SINGLE;
240                         } else if ("hybrid".equalsIgnoreCase(str)) {
241                                 type = Motor.Type.HYBRID;
242                         } else if ("reloadable".equalsIgnoreCase(str)) {
243                                 type = Motor.Type.RELOAD;
244                         } else {
245                                 type = Motor.Type.UNKNOWN;
246                         }
247                         
248                         // Calculate mass
249                         str = attributes.get("auto-calc-mass");
250                         if ("0".equals(str) || "false".equalsIgnoreCase(str)) {
251                                 calculateMass = false;
252                         } else {
253                                 calculateMass = true;
254                         }
255                         
256                         // Calculate CG
257                         str = attributes.get("auto-calc-cg");
258                         if ("0".equals(str) || "false".equalsIgnoreCase(str)) {
259                                 calculateCG = false;
260                         } else {
261                                 calculateCG = true;
262                         }
263                 }
264                 
265                 @Override
266                 public ElementHandler openElement(String element,
267                                 HashMap<String, String> attributes, WarningSet warnings) throws SAXException {
268                         
269                         if (element.equals("comments")) {
270                                 return PlainTextHandler.INSTANCE;
271                         }
272                         
273                         if (element.equals("data")) {
274                                 if (dataHandler != null) {
275                                         throw new SAXException("Multiple data elements encountered in motor " +
276                                                         "definition");
277                                 }
278                                 dataHandler = new RSEMotorDataHandler();
279                                 return dataHandler;
280                         }
281                         
282                         warnings.add("Unknown element '" + element + "' encountered, ignoring.");
283                         return null;
284                 }
285                 
286                 @Override
287                 public void closeElement(String element, HashMap<String, String> attributes,
288                                 String content, WarningSet warnings) {
289                         
290                         if (element.equals("comments")) {
291                                 if (description.length() > 0) {
292                                         description = description + "\n\n" + content.trim();
293                                 } else {
294                                         description = content.trim();
295                                 }
296                                 return;
297                         }
298                         
299                         if (element.equals("data")) {
300                                 time = dataHandler.getTime();
301                                 force = dataHandler.getForce();
302                                 mass = dataHandler.getMass();
303                                 cg = dataHandler.getCG();
304                                 
305                                 sortLists(time, force, mass, cg);
306                                 
307                                 for (double d : mass) {
308                                         if (Double.isNaN(d)) {
309                                                 calculateMass = true;
310                                                 break;
311                                         }
312                                 }
313                                 for (double d : cg) {
314                                         if (Double.isNaN(d)) {
315                                                 calculateCG = true;
316                                                 break;
317                                         }
318                                 }
319                                 return;
320                         }
321                 }
322                 
323                 public Motor getMotor() throws SAXException {
324                         if (time == null || time.size() == 0)
325                                 throw new SAXException("Illegal motor data");
326                         
327                         
328                         finalizeThrustCurve(time, force, mass, cg);
329                         final int n = time.size();
330                         
331                         if (hasIllegalValue(mass))
332                                 calculateMass = true;
333                         if (hasIllegalValue(cg))
334                                 calculateCG = true;
335                         
336                         if (calculateMass) {
337                                 mass = calculateMass(time, force, initMass, propMass);
338                         }
339                         if (calculateCG) {
340                                 for (int i = 0; i < n; i++) {
341                                         cg.set(i, length / 2);
342                                 }
343                         }
344                         
345                         double[] timeArray = toArray(time);
346                         double[] thrustArray = toArray(force);
347                         Coordinate[] cgArray = new Coordinate[n];
348                         for (int i = 0; i < n; i++) {
349                                 cgArray[i] = new Coordinate(cg.get(i), 0, 0, mass.get(i));
350                         }
351                         
352                         
353                         // Create the motor digest from all data available in the file
354                         MotorDigest motorDigest = new MotorDigest();
355                         motorDigest.update(DataType.TIME_ARRAY, timeArray);
356                         if (!calculateMass) {
357                                 motorDigest.update(DataType.MASS_PER_TIME, toArray(mass));
358                         } else {
359                                 motorDigest.update(DataType.MASS_SPECIFIC, initMass, initMass - propMass);
360                         }
361                         if (!calculateCG) {
362                                 motorDigest.update(DataType.CG_PER_TIME, toArray(cg));
363                         }
364                         motorDigest.update(DataType.FORCE_PER_TIME, thrustArray);
365                         final String digest = motorDigest.getDigest();
366                         
367                         
368                         try {
369                                 Manufacturer m = Manufacturer.getManufacturer(manufacturer);
370                                 Motor.Type t = type;
371                                 if (t == Motor.Type.UNKNOWN) {
372                                         t = m.getMotorType();
373                                 } else {
374                                         if (m.getMotorType() != Motor.Type.UNKNOWN && m.getMotorType() != t) {
375                                                 log.warn("Loaded motor type inconsistent with manufacturer," +
376                                                                 " loaded type=" + t + " manufacturer=" + m +
377                                                                 " manufacturer type=" + m.getMotorType() +
378                                                                 " designation=" + designation);
379                                         }
380                                 }
381                                 
382                                 return new ThrustCurveMotor(m, designation, description, t,
383                                                 delays, diameter, length, timeArray, thrustArray, cgArray, digest);
384                         } catch (IllegalArgumentException e) {
385                                 throw new SAXException("Illegal motor data", e);
386                         }
387                 }
388         }
389         
390         
391         /**
392          * Handler for the <data> element in a RockSim engine file motor definition.
393          */
394         private static class RSEMotorDataHandler extends AbstractElementHandler {
395                 
396                 private final List<Double> time = new ArrayList<Double>();
397                 private final List<Double> force = new ArrayList<Double>();
398                 private final List<Double> mass = new ArrayList<Double>();
399                 private final List<Double> cg = new ArrayList<Double>();
400                 
401                 
402                 public List<Double> getTime() {
403                         return time;
404                 }
405                 
406                 public List<Double> getForce() {
407                         return force;
408                 }
409                 
410                 public List<Double> getMass() {
411                         return mass;
412                 }
413                 
414                 public List<Double> getCG() {
415                         return cg;
416                 }
417                 
418                 
419                 @Override
420                 public ElementHandler openElement(String element,
421                                 HashMap<String, String> attributes, WarningSet warnings) {
422                         
423                         if (element.equals("eng-data")) {
424                                 return NullElementHandler.INSTANCE;
425                         }
426                         
427                         warnings.add("Unknown element '" + element + "' encountered, ignoring.");
428                         return null;
429                 }
430                 
431                 @Override
432                 public void closeElement(String element, HashMap<String, String> attributes,
433                                 String content, WarningSet warnings) throws SAXException {
434                         
435                         double t = parseDouble(attributes.get("t"));
436                         double f = parseDouble(attributes.get("f"));
437                         double m = parseDouble(attributes.get("m")) / 1000.0;
438                         double g = parseDouble(attributes.get("cg")) / 1000.0;
439                         
440                         if (Double.isNaN(t) || Double.isNaN(f)) {
441                                 throw new SAXException("Illegal motor data point encountered");
442                         }
443                         
444                         time.add(t);
445                         force.add(f);
446                         mass.add(m);
447                         cg.add(g);
448                 }
449                 
450                 
451                 private double parseDouble(String str) {
452                         if (str == null)
453                                 return Double.NaN;
454                         try {
455                                 return Double.parseDouble(str);
456                         } catch (NumberFormatException e) {
457                                 return Double.NaN;
458                         }
459                 }
460         }
461         
462         
463         
464         private static boolean hasIllegalValue(List<Double> list) {
465                 for (Double d : list) {
466                         if (d == null || d.isNaN() || d.isInfinite()) {
467                                 return true;
468                         }
469                 }
470                 return false;
471         }
472         
473         private static double[] toArray(List<Double> list) {
474                 final int n = list.size();
475                 double[] array = new double[n];
476                 for (int i = 0; i < n; i++) {
477                         array[i] = list.get(i);
478                 }
479                 return array;
480         }
481 }