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