1 // parseobject.cpp
2 //
3 // Copyright (C) 2004-2008 Chris Laurel <claurel@gmail.com>
4 //
5 // Functions for parsing objects common to star, solar system, and
6 // deep sky catalogs.
7 //
8 // This program is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU General Public License
10 // as published by the Free Software Foundation; either version 2
11 // of the License, or (at your option) any later version.
12 
13 #include <cassert>
14 #include <celutil/debug.h>
15 #include "parseobject.h"
16 #include "customorbit.h"
17 #include "customrotation.h"
18 #include "spiceorbit.h"
19 #include "spicerotation.h"
20 #include "scriptorbit.h"
21 #include "scriptrotation.h"
22 #include "frame.h"
23 #include "trajmanager.h"
24 #include "rotationmanager.h"
25 #include "universe.h"
26 
27 using namespace std;
28 
29 
30 bool
ParseDate(Hash * hash,const string & name,double & jd)31 ParseDate(Hash* hash, const string& name, double& jd)
32 {
33     // Check first for a number value representing a Julian date
34     if (hash->getNumber(name, jd))
35         return true;
36 
37     string dateString;
38     if (hash->getString(name, dateString))
39     {
40         astro::Date date(1, 1, 1);
41         if (astro::parseDate(dateString, date))
42         {
43             jd = (double) date;
44             return true;
45         }
46     }
47 
48     return false;
49 }
50 
51 
52 /*!
53  * Create a new Keplerian orbit from an ssc property table:
54  *
55  * EllipticalOrbit
56  * {
57  *     # One of the following is required to specify orbit size:
58  *     SemiMajorAxis <number>
59  *     PericenterDistance <number>
60  *
61  *     # Required
62  *     Period <number>
63  *
64  *     Eccentricity <number>   (default: 0.0)
65  *     Inclination <degrees>   (default: 0.0)
66  *     AscendingNode <degrees> (default: 0.0)
67  *
68  *     # One or none of the following:
69  *     ArgOfPericenter <degrees>  (default: 0.0)
70  *     LongOfPericenter <degrees> (default: 0.0)
71  *
72  *     Epoch <date> (default J2000.0)
73  *
74  *     # One or none of the following:
75  *     MeanAnomaly <degrees>     (default: 0.0)
76  *     MeanLongitude <degrees>   (default: 0.0)
77  * }
78  *
79  * If usePlanetUnits is true:
80  *     Period is in Julian years
81  *     SemiMajorAxis or PericenterDistance is in AU
82  * Otherwise:
83  *     Period is in Julian days
84  *     SemiMajorAxis or PericenterDistance is in kilometers.
85  */
86 static EllipticalOrbit*
CreateEllipticalOrbit(Hash * orbitData,bool usePlanetUnits)87 CreateEllipticalOrbit(Hash* orbitData,
88                       bool usePlanetUnits)
89 {
90     // SemiMajorAxis and Period are absolutely required; everything
91     // else has a reasonable default.
92     double pericenterDistance = 0.0;
93     double semiMajorAxis = 0.0;
94     if (!orbitData->getNumber("SemiMajorAxis", semiMajorAxis))
95     {
96         if (!orbitData->getNumber("PericenterDistance", pericenterDistance))
97         {
98             clog << "SemiMajorAxis/PericenterDistance missing!  Skipping planet . . .\n";
99             return NULL;
100         }
101     }
102 
103     double period = 0.0;
104     if (!orbitData->getNumber("Period", period))
105     {
106         clog << "Period missing!  Skipping planet . . .\n";
107         return NULL;
108     }
109 
110     double eccentricity = 0.0;
111     orbitData->getNumber("Eccentricity", eccentricity);
112 
113     double inclination = 0.0;
114     orbitData->getNumber("Inclination", inclination);
115 
116     double ascendingNode = 0.0;
117     orbitData->getNumber("AscendingNode", ascendingNode);
118 
119     double argOfPericenter = 0.0;
120     if (!orbitData->getNumber("ArgOfPericenter", argOfPericenter))
121     {
122         double longOfPericenter = 0.0;
123         if (orbitData->getNumber("LongOfPericenter", longOfPericenter))
124             argOfPericenter = longOfPericenter - ascendingNode;
125     }
126 
127     double epoch = astro::J2000;
128     ParseDate(orbitData, "Epoch", epoch);
129 
130     // Accept either the mean anomaly or mean longitude--use mean anomaly
131     // if both are specified.
132     double anomalyAtEpoch = 0.0;
133     if (!orbitData->getNumber("MeanAnomaly", anomalyAtEpoch))
134     {
135         double longAtEpoch = 0.0;
136         if (orbitData->getNumber("MeanLongitude", longAtEpoch))
137             anomalyAtEpoch = longAtEpoch - (argOfPericenter + ascendingNode);
138     }
139 
140     if (usePlanetUnits)
141     {
142         semiMajorAxis = astro::AUtoKilometers(semiMajorAxis);
143         pericenterDistance = astro::AUtoKilometers(pericenterDistance);
144         period = period * 365.25;
145     }
146 
147     // If we read the semi-major axis, use it to compute the pericenter
148     // distance.
149     if (semiMajorAxis != 0.0)
150         pericenterDistance = semiMajorAxis * (1.0 - eccentricity);
151 
152     return new EllipticalOrbit(pericenterDistance,
153                                eccentricity,
154                                degToRad(inclination),
155                                degToRad(ascendingNode),
156                                degToRad(argOfPericenter),
157                                degToRad(anomalyAtEpoch),
158                                period,
159                                epoch);
160 }
161 
162 
163 /*!
164  * Create a new sampled orbit from an ssc property table:
165  *
166  * SampledTrajectory
167  * {
168  *     Source <string>
169  *     Interpolation "Cubic" | "Linear"
170  *     DoublePrecision <boolean>
171  * }
172  *
173  * Source is the only required field. Interpolation defaults to cubic, and
174  * DoublePrecision defaults to true.
175  */
176 static Orbit*
CreateSampledTrajectory(Hash * trajData,const string & path)177 CreateSampledTrajectory(Hash* trajData, const string& path)
178 {
179     string sourceName;
180     if (!trajData->getString("Source", sourceName))
181     {
182         clog << "SampledTrajectory is missing a source.\n";
183         return NULL;
184     }
185 
186     // Read interpolation type; string value must be either "Linear" or "Cubic"
187     // Default interpolation type is cubic.
188     string interpolationString;
189     TrajectoryInterpolation interpolation = TrajectoryInterpolationCubic;
190     if (trajData->getString("Interpolation", interpolationString))
191     {
192         if (!compareIgnoringCase(interpolationString, "linear"))
193             interpolation = TrajectoryInterpolationLinear;
194         else if (!compareIgnoringCase(interpolationString, "cubic"))
195             interpolation = TrajectoryInterpolationCubic;
196         else
197             clog << "Unknown interpolation type " << interpolationString << endl; // non-fatal error
198     }
199 
200     // Double precision is true by default
201     bool useDoublePrecision = true;
202     trajData->getBoolean("DoublePrecision", useDoublePrecision);
203     TrajectoryPrecision precision = useDoublePrecision ? TrajectoryPrecisionDouble : TrajectoryPrecisionSingle;
204 
205     DPRINTF(1, "Attempting to load sampled trajectory from source '%s'\n", sourceName.c_str());
206     ResourceHandle orbitHandle = GetTrajectoryManager()->getHandle(TrajectoryInfo(sourceName, path, interpolation, precision));
207     Orbit* orbit = GetTrajectoryManager()->find(orbitHandle);
208     if (orbit == NULL)
209     {
210         clog << "Could not load sampled trajectory from '" << sourceName << "'\n";
211     }
212 
213     return orbit;
214 }
215 
216 
217 // Create a new FixedPosition trajectory. A FixedPosition is a property list
218 // with one of the following 3-vector properties:
219 //     Rectangular
220 //     Planetographic
221 //     Planetocentric
222 // Planetographic and planetocentric coordinates are given in the order longitude,
223 // latitude, altitude. Units of altitude are kilometers. Planetographic and
224 // and planetocentric coordinates are only practical when the coordinate system
225 // is BodyFixed.
226 static Orbit*
CreateFixedPosition(Hash * trajData,const Selection & centralObject,bool usePlanetUnits)227 CreateFixedPosition(Hash* trajData, const Selection& centralObject, bool usePlanetUnits)
228 {
229     Vec3d position(0.0, 0.0, 0.0);
230 
231     Vec3d v(0.0, 0.0, 0.0);
232     if (trajData->getVector("Rectangular", v))
233     {
234         if (usePlanetUnits)
235             v = v * astro::AUtoKilometers(1.0);
236         // Convert to Celestia's coordinate system
237         position = Vec3d(v.x, v.z, -v.y);
238     }
239     else if (trajData->getVector("Planetographic", v))
240     {
241         if (centralObject.getType() != Selection::Type_Body)
242         {
243             clog << "FixedPosition planetographic coordinates aren't valid for stars.\n";
244             return NULL;
245         }
246 
247         // TODO: Need function to calculate planetographic coordinates
248         // TODO: Change planetocentricToCartesian so that 180 degree offset isn't required
249         position = centralObject.body()->planetocentricToCartesian(180.0 + v.x, v.y, v.z);
250     }
251     else if (trajData->getVector("Planetocentric", v))
252     {
253         if (centralObject.getType() != Selection::Type_Body)
254         {
255             clog << "FixedPosition planetocentric coordinates aren't valid for stars.\n";
256             return NULL;
257         }
258 
259         // TODO: Change planetocentricToCartesian so that 180 degree offset isn't required
260         position = centralObject.body()->planetocentricToCartesian(180.0 + v.x, v.y, v.z);
261     }
262     else
263     {
264         clog << "Missing coordinates for FixedPosition\n";
265         return NULL;
266     }
267 
268     return new FixedOrbit(Point3d(position.x, position.y, position.z));
269 }
270 
271 
272 // Parse a string list--either a single string or an array of strings is permitted.
273 static bool
ParseStringList(Hash * table,const string & propertyName,list<string> & stringList)274 ParseStringList(Hash* table,
275 				const string& propertyName,
276 				list<string>& stringList)
277 {
278 	Value* v = table->getValue(propertyName);
279 	if (v == NULL)
280 		return false;
281 
282 	// Check for a single string first.
283 	if (v->getType() == Value::StringType)
284 	{
285 		stringList.push_back(v->getString());
286 		return true;
287 	}
288 	else if (v->getType() == Value::ArrayType)
289 	{
290 		Array* array = v->getArray();
291 		Array::const_iterator iter;
292 
293 		// Verify that all array entries are strings
294 		for (iter = array->begin(); iter != array->end(); iter++)
295 		{
296 			if ((*iter)->getType() != Value::StringType)
297 				return false;
298 		}
299 
300 		// Add strings to stringList
301 		for (iter = array->begin(); iter != array->end(); iter++)
302 			 stringList.push_back((*iter)->getString());
303 
304 		return true;
305 	}
306 	else
307 	{
308 		return false;
309 	}
310 }
311 
312 
313 #ifdef USE_SPICE
314 
315 /*! Create a new SPICE orbit. This is just a Celestia wrapper for a trajectory specified
316  *  in a SPICE SPK file.
317  *
318  *  SpiceOrbit
319  *  {
320  *      Kernel <string|string array>   # optional
321  *      Target <string>
322  *      Origin <string>
323  *      BoundingRadius <number>
324  *      Period <number>                # optional
325  *      Beginning <number>             # optional
326  *      Ending <number>                # optional
327  *  }
328  *
329  *  The Kernel property specifies one or more SPK files that must be loaded. Any
330  *  already loaded kernels will also be used if they contain trajectories for
331  *  the target or origin.
332  *  Target and origin are strings that give NAIF IDs for the target and origin
333  *  objects. Either names or integer IDs are valid, but integer IDs still must
334  *  be quoted.
335  *  BoundingRadius gives a conservative estimate of the maximum distance between
336  *  the target and origin objects. It is required by Celestia for visibility
337  *  culling when rendering.
338  *  Beginning and Ending specify the valid time range of the SPICE orbit. It is
339  *  an error to specify Beginning without Ending, and vice versa. If neither is
340  *  specified, the valid range is computed from the coverage window in the SPICE
341  *  kernel pool. If the coverage window is noncontiguous, the first interval is
342  *  used.
343  */
344 static SpiceOrbit*
CreateSpiceOrbit(Hash * orbitData,const string & path,bool usePlanetUnits)345 CreateSpiceOrbit(Hash* orbitData,
346                  const string& path,
347                  bool usePlanetUnits)
348 {
349     string targetBodyName;
350     string originName;
351 	list<string> kernelList;
352 
353 	if (orbitData->getValue("Kernel") != NULL)
354 	{
355 		// Kernel list is optional; a SPICE orbit may rely on kernels already loaded into
356 		// the kernel pool.
357 		if (!ParseStringList(orbitData, "Kernel", kernelList))
358 		{
359 			clog << "Kernel list for SPICE orbit is neither a string nor array of strings\n";
360 			return NULL;
361 		}
362 	}
363 
364     if (!orbitData->getString("Target", targetBodyName))
365     {
366         clog << "Target name missing from SPICE orbit\n";
367         return NULL;
368     }
369 
370     if (!orbitData->getString("Origin", originName))
371     {
372         clog << "Origin name missing from SPICE orbit\n";
373         return NULL;
374     }
375 
376     // A bounding radius for culling is required for SPICE orbits
377     double boundingRadius = 0.0;
378     if (!orbitData->getNumber("BoundingRadius", boundingRadius))
379     {
380         clog << "Bounding Radius missing from SPICE orbit\n";
381         return NULL;
382     }
383 
384     // The period of the orbit may be specified if appropriate; a value
385     // of zero for the period (the default), means that the orbit will
386     // be considered aperiodic.
387     double period = 0.0;
388     orbitData->getNumber("Period", period);
389 
390     if (usePlanetUnits)
391     {
392         boundingRadius = astro::AUtoKilometers(boundingRadius);
393         period = period * 365.25;
394     }
395 
396 	// Either a complete time interval must be specified with Beginning/Ending, or
397 	// else neither field can be present.
398 	Value* beginningDate = orbitData->getValue("Beginning");
399 	Value* endingDate = orbitData->getValue("Ending");
400 	if (beginningDate != NULL && endingDate == NULL)
401 	{
402 		clog << "Beginning specified for SPICE orbit, but ending is missing.\n";
403 		return NULL;
404 	}
405 
406 	if (endingDate != NULL && beginningDate == NULL)
407 	{
408 		clog << "Ending specified for SPICE orbit, but beginning is missing.\n";
409 		return NULL;
410 	}
411 
412 	SpiceOrbit* orbit = NULL;
413 	if (beginningDate != NULL && endingDate != NULL)
414 	{
415 		double beginningTDBJD = 0.0;
416 		if (!ParseDate(orbitData, "Beginning", beginningTDBJD))
417 		{
418 			clog << "Invalid beginning date specified for SPICE orbit.\n";
419 			return NULL;
420 		}
421 
422 		double endingTDBJD = 0.0;
423 		if (!ParseDate(orbitData, "Ending", endingTDBJD))
424 		{
425 			clog << "Invalid ending date specified for SPICE orbit.\n";
426 			return NULL;
427 		}
428 
429 		orbit = new SpiceOrbit(targetBodyName,
430 							   originName,
431 							   period,
432 							   boundingRadius,
433 							   beginningTDBJD,
434 							   endingTDBJD);
435 	}
436 	else
437 	{
438 		// No time interval given; we'll use whatever coverage window is given
439 		// in the SPICE kernel.
440 		orbit = new SpiceOrbit(targetBodyName,
441 							   originName,
442 							   period,
443 							   boundingRadius);
444 	}
445 
446     if (!orbit->init(path, &kernelList))
447     {
448         // Error using SPICE library; destroy the orbit; hopefully a
449         // fallback is defined in the SSC file.
450         delete orbit;
451         orbit = NULL;
452     }
453 
454     return orbit;
455 }
456 
457 
458 /*! Create a new rotation model based on a SPICE frame.
459  *
460  *  SpiceRotation
461  *  {
462  *      Kernel <string|string array>   # optional
463  *      Frame <string>
464  *      BaseFrame <string>             # optional (defaults to ecliptic)
465  *      Period <number>                # optional (units are hours)
466  *      Beginning <number>             # optional
467  *      Ending <number>                # optional
468  *  }
469  *
470  *  The Kernel property specifies one or more SPICE kernel files that must be
471  *  loaded in order for the frame to be defined over the required range. Any
472  *  already loaded kernels will be used if they contain information relevant
473  *  for defining the frame.
474  *  Frame and base name are strings that give SPICE names for the frames. The
475  *  orientation of the SpiceRotation is the orientation of the frame relative to
476  *  the base frame. By default, the base frame is eclipj2000.
477  *  Beginning and Ending specify the valid time range of the SPICE rotation.
478  *  If the Beginning and Ending are omitted, the rotation model is assumed to
479  *  be valid at any time. It is an error to specify Beginning without Ending,
480  *  and vice versa.
481  *  Period specifies the principal rotation period; it defaults to 0 indicating
482  *  that the rotation is aperiodic. It is not essential to provide the rotation
483  *  period; it is only used by Celestia for displaying object information such
484  *  as sidereal day length.
485  */
486 static SpiceRotation*
CreateSpiceRotation(Hash * rotationData,const string & path)487 CreateSpiceRotation(Hash* rotationData,
488                     const string& path)
489 {
490     string frameName;
491     string baseFrameName = "eclipj2000";
492 	list<string> kernelList;
493 
494 	if (rotationData->getValue("Kernel") != NULL)
495 	{
496 		// Kernel list is optional; a SPICE rotation may rely on kernels already loaded into
497 		// the kernel pool.
498 		if (!ParseStringList(rotationData, "Kernel", kernelList))
499 		{
500 			clog << "Kernel list for SPICE rotation is neither a string nor array of strings\n";
501 			return NULL;
502 		}
503 	}
504 
505     if (!rotationData->getString("Frame", frameName))
506     {
507         clog << "Frame name missing from SPICE rotation\n";
508         return NULL;
509     }
510 
511     rotationData->getString("BaseFrame", baseFrameName);
512 
513     // The period of the rotation may be specified if appropriate; a value
514     // of zero for the period (the default), means that the rotation will
515     // be considered aperiodic.
516     double period = 0.0;
517     rotationData->getNumber("Period", period);
518     period = period / 24.0;
519 
520 	// Either a complete time interval must be specified with Beginning/Ending, or
521 	// else neither field can be present.
522 	Value* beginningDate = rotationData->getValue("Beginning");
523 	Value* endingDate = rotationData->getValue("Ending");
524 	if (beginningDate != NULL && endingDate == NULL)
525 	{
526 		clog << "Beginning specified for SPICE rotation, but ending is missing.\n";
527 		return NULL;
528 	}
529 
530 	if (endingDate != NULL && beginningDate == NULL)
531 	{
532 		clog << "Ending specified for SPICE rotation, but beginning is missing.\n";
533 		return NULL;
534 	}
535 
536 	SpiceRotation* rotation = NULL;
537 	if (beginningDate != NULL && endingDate != NULL)
538 	{
539 		double beginningTDBJD = 0.0;
540 		if (!ParseDate(rotationData, "Beginning", beginningTDBJD))
541 		{
542 			clog << "Invalid beginning date specified for SPICE rotation.\n";
543 			return NULL;
544 		}
545 
546 		double endingTDBJD = 0.0;
547 		if (!ParseDate(rotationData, "Ending", endingTDBJD))
548 		{
549 			clog << "Invalid ending date specified for SPICE rotation.\n";
550 			return NULL;
551 		}
552 
553 		rotation = new SpiceRotation(frameName,
554 				  			         baseFrameName,
555 							         period,
556 							         beginningTDBJD,
557 							         endingTDBJD);
558 	}
559 	else
560 	{
561 		// No time interval given; rotation is valid at any time.
562 		rotation = new SpiceRotation(frameName,
563                                      baseFrameName,
564                                      period);
565 	}
566 
567     if (!rotation->init(path, &kernelList))
568     {
569         // Error using SPICE library; destroy the rotation.
570         delete rotation;
571         rotation = NULL;
572     }
573 
574     return rotation;
575 }
576 #endif
577 
578 
579 static ScriptedOrbit*
CreateScriptedOrbit(Hash * orbitData,const string & path)580 CreateScriptedOrbit(Hash* orbitData,
581                     const string& path)
582 {
583 #if !defined(CELX)
584     clog << "ScriptedOrbit not usable without scripting support.\n";
585     return NULL;
586 #else
587 
588     // Function name is required
589     string funcName;
590     if (!orbitData->getString("Function", funcName))
591     {
592         clog << "Function name missing from script orbit definition.\n";
593         return NULL;
594     }
595 
596     // Module name is optional
597     string moduleName;
598     orbitData->getString("Module", moduleName);
599 
600     string* pathCopy = new string(path);
601     Value* pathValue = new Value(*pathCopy);
602     orbitData->addValue("AddonPath", *pathValue);
603 
604     ScriptedOrbit* scriptedOrbit = new ScriptedOrbit();
605     if (scriptedOrbit != NULL)
606     {
607         if (!scriptedOrbit->initialize(moduleName, funcName, orbitData))
608         {
609             delete scriptedOrbit;
610             scriptedOrbit = NULL;
611         }
612     }
613 
614     return scriptedOrbit;
615 #endif
616 }
617 
618 
619 Orbit*
CreateOrbit(const Selection & centralObject,Hash * planetData,const string & path,bool usePlanetUnits)620 CreateOrbit(const Selection& centralObject,
621             Hash* planetData,
622             const string& path,
623             bool usePlanetUnits)
624 {
625     Orbit* orbit = NULL;
626 
627     string customOrbitName;
628     if (planetData->getString("CustomOrbit", customOrbitName))
629     {
630         orbit = GetCustomOrbit(customOrbitName);
631         if (orbit != NULL)
632         {
633             return orbit;
634         }
635         clog << "Could not find custom orbit named '" << customOrbitName <<
636             "'\n";
637     }
638 
639 #ifdef USE_SPICE
640     Value* spiceOrbitDataValue = planetData->getValue("SpiceOrbit");
641     if (spiceOrbitDataValue != NULL)
642     {
643         if (spiceOrbitDataValue->getType() != Value::HashType)
644         {
645             clog << "Object has incorrect spice orbit syntax.\n";
646             return NULL;
647         }
648         else
649         {
650             orbit = CreateSpiceOrbit(spiceOrbitDataValue->getHash(), path, usePlanetUnits);
651             if (orbit != NULL)
652             {
653                 return orbit;
654             }
655             clog << "Bad spice orbit\n";
656             DPRINTF(0, "Could not load SPICE orbit\n");
657         }
658     }
659 #endif
660 
661     // Trajectory calculated by Lua script
662     Value* scriptedOrbitValue = planetData->getValue("ScriptedOrbit");
663     if (scriptedOrbitValue != NULL)
664     {
665         if (scriptedOrbitValue->getType() != Value::HashType)
666         {
667             clog << "Object has incorrect scripted orbit syntax.\n";
668             return NULL;
669         }
670         else
671         {
672             orbit = CreateScriptedOrbit(scriptedOrbitValue->getHash(), path);
673             if (orbit != NULL)
674                 return orbit;
675         }
676     }
677 
678     // New 1.5.0 style for sampled trajectories. Permits specification of
679     // precision and interpolation type.
680     Value* sampledTrajDataValue = planetData->getValue("SampledTrajectory");
681     if (sampledTrajDataValue != NULL)
682     {
683         if (sampledTrajDataValue->getType() != Value::HashType)
684         {
685             clog << "Object has incorrect syntax for SampledTrajectory.\n";
686             return NULL;
687         }
688         else
689         {
690             return CreateSampledTrajectory(sampledTrajDataValue->getHash(), path);
691         }
692     }
693 
694     // Old style for sampled trajectories. Assumes cubic interpolation and
695     // single precision.
696     string sampOrbitFile;
697     if (planetData->getString("SampledOrbit", sampOrbitFile))
698     {
699         DPRINTF(1, "Attempting to load sampled orbit file '%s'\n",
700                 sampOrbitFile.c_str());
701         ResourceHandle orbitHandle =
702             GetTrajectoryManager()->getHandle(TrajectoryInfo(sampOrbitFile,
703                                                              path,
704                                                              TrajectoryInterpolationCubic,
705                                                              TrajectoryPrecisionSingle));
706         orbit = GetTrajectoryManager()->find(orbitHandle);
707         if (orbit != NULL)
708         {
709             return orbit;
710         }
711         clog << "Could not load sampled orbit file '" << sampOrbitFile << "'\n";
712     }
713 
714     Value* orbitDataValue = planetData->getValue("EllipticalOrbit");
715     if (orbitDataValue != NULL)
716     {
717         if (orbitDataValue->getType() != Value::HashType)
718         {
719             clog << "Object has incorrect elliptical orbit syntax.\n";
720             return NULL;
721         }
722         else
723         {
724             return CreateEllipticalOrbit(orbitDataValue->getHash(),
725                                          usePlanetUnits);
726         }
727     }
728 
729     // Create an 'orbit' that places the object at a fixed point in its
730     // reference frame. There are two forms for FixedPosition: a simple
731     // form with an 3-vector value, and complex form with a properlist
732     // value. The simple form:
733     //
734     // FixedPosition [ x y z ]
735     //
736     // is a shorthand for:
737     //
738     // FixedPosition { Rectangular [ x y z ] }
739     //
740     // In addition to Rectangular, other coordinate types for fixed position are
741     // Planetographic and Planetocentric.
742     Value* fixedPositionValue = planetData->getValue("FixedPosition");
743     if (fixedPositionValue != NULL)
744     {
745         Vec3d fixedPosition(0.0, 0.0, 0.0);
746         if (planetData->getVector("FixedPosition", fixedPosition))
747         {
748             // Convert to Celestia's coordinate system
749             fixedPosition = Vec3d(fixedPosition.x,
750                                   fixedPosition.z,
751                                   -fixedPosition.y);
752 
753             if (usePlanetUnits)
754                 fixedPosition = fixedPosition * astro::AUtoKilometers(1.0);
755 
756             return new FixedOrbit(Point3d(0.0, 0.0, 0.0) + fixedPosition);
757         }
758         else if (fixedPositionValue->getType() == Value::HashType)
759         {
760             return CreateFixedPosition(fixedPositionValue->getHash(), centralObject, usePlanetUnits);
761         }
762         else
763         {
764             clog << "Object has incorrect FixedPosition syntax.\n";
765         }
766     }
767 
768     // LongLat will make an object fixed relative to the surface of its center
769     // object. This is done by creating an orbit with a period equal to the
770     // rotation rate of the parent object. A body-fixed reference frame is a
771     // much better way to accomplish this.
772     Vec3d longlat(0.0, 0.0, 0.0);
773     if (planetData->getVector("LongLat", longlat))
774     {
775         Body* centralBody = centralObject.body();
776         if (centralBody != NULL)
777         {
778             Vec3d pos = centralBody->planetocentricToCartesian(longlat.x, longlat.y, longlat.z);
779             return new SynchronousOrbit(*centralBody, Point3d(pos.x, pos.y, pos.z));
780         }
781         else
782         {
783             // TODO: Allow fixing objects to the surface of stars.
784         }
785         return NULL;
786     }
787 
788     return NULL;
789 }
790 
791 
792 static ConstantOrientation*
CreateFixedRotationModel(double offset,double inclination,double ascendingNode)793 CreateFixedRotationModel(double offset,
794                          double inclination,
795                          double ascendingNode)
796 {
797     Quatd q = Quatd::yrotation(-PI - offset) *
798               Quatd::xrotation(-inclination) *
799               Quatd::yrotation(-ascendingNode);
800 
801     return new ConstantOrientation(q);
802 }
803 
804 
805 static RotationModel*
CreateUniformRotationModel(Hash * rotationData,double syncRotationPeriod)806 CreateUniformRotationModel(Hash* rotationData,
807                            double syncRotationPeriod)
808 {
809     // Default to synchronous rotation
810     double period = syncRotationPeriod;
811     if (rotationData->getNumber("Period", period))
812     {
813         period = period / 24.0;
814     }
815 
816     float offset = 0.0f;
817     if (rotationData->getNumber("MeridianAngle", offset))
818     {
819         offset = degToRad(offset);
820     }
821 
822     double epoch = astro::J2000;
823     ParseDate(rotationData, "Epoch", epoch);
824 
825     float inclination = 0.0f;
826     if (rotationData->getNumber("Inclination", inclination))
827     {
828         inclination = degToRad(inclination);
829     }
830 
831     float ascendingNode = 0.0f;
832     if (rotationData->getNumber("AscendingNode", ascendingNode))
833     {
834         ascendingNode = degToRad(ascendingNode);
835     }
836 
837     // No period was specified, and the default synchronous
838     // rotation period is zero, indicating that the object
839     // doesn't have a periodic orbit. Default to a constant
840     // orientation instead.
841     if (period == 0.0)
842     {
843         return CreateFixedRotationModel(offset, inclination, ascendingNode);
844     }
845     else
846     {
847         return new UniformRotationModel(period,
848                                         offset,
849                                         epoch,
850                                         inclination,
851                                         ascendingNode);
852     }
853 }
854 
855 
856 static ConstantOrientation*
CreateFixedRotationModel(Hash * rotationData)857 CreateFixedRotationModel(Hash* rotationData)
858 {
859     double offset = 0.0;
860     if (rotationData->getNumber("MeridianAngle", offset))
861     {
862         offset = degToRad(offset);
863     }
864 
865     double inclination = 0.0;
866     if (rotationData->getNumber("Inclination", inclination))
867     {
868         inclination = degToRad(inclination);
869     }
870 
871     double ascendingNode = 0.0;
872     if (rotationData->getNumber("AscendingNode", ascendingNode))
873     {
874         ascendingNode = degToRad(ascendingNode);
875     }
876 
877     Quatd q = Quatd::yrotation(-PI - offset) *
878               Quatd::xrotation(-inclination) *
879               Quatd::yrotation(-ascendingNode);
880 
881     return new ConstantOrientation(q);
882 }
883 
884 
885 static ConstantOrientation*
CreateFixedAttitudeRotationModel(Hash * rotationData)886 CreateFixedAttitudeRotationModel(Hash* rotationData)
887 {
888     double heading = 0.0;
889     if (rotationData->getNumber("Heading", heading))
890     {
891         heading = degToRad(heading);
892     }
893 
894     double tilt = 0.0;
895     if (rotationData->getNumber("Tilt", tilt))
896     {
897         tilt = degToRad(tilt);
898     }
899 
900     double roll = 0.0;
901     if (rotationData->getNumber("Roll", roll))
902     {
903         roll = degToRad(roll);
904     }
905 
906     Quatd q = Quatd::yrotation(-PI - heading) *
907               Quatd::xrotation(-tilt) *
908               Quatd::zrotation(-roll);
909 
910     return new ConstantOrientation(q);
911 }
912 
913 
914 static RotationModel*
CreatePrecessingRotationModel(Hash * rotationData,double syncRotationPeriod)915 CreatePrecessingRotationModel(Hash* rotationData,
916                               double syncRotationPeriod)
917 {
918     // Default to synchronous rotation
919     double period = syncRotationPeriod;
920     if (rotationData->getNumber("Period", period))
921     {
922         period = period / 24.0;
923     }
924 
925     float offset = 0.0f;
926     if (rotationData->getNumber("MeridianAngle", offset))
927     {
928         offset = degToRad(offset);
929     }
930 
931     double epoch = astro::J2000;
932     ParseDate(rotationData, "Epoch", epoch);
933 
934     float inclination = 0.0f;
935     if (rotationData->getNumber("Inclination", inclination))
936     {
937         inclination = degToRad(inclination);
938     }
939 
940     float ascendingNode = 0.0f;
941     if (rotationData->getNumber("AscendingNode", ascendingNode))
942     {
943         ascendingNode = degToRad(ascendingNode);
944     }
945 
946     // The default value of 0 is handled specially, interpreted to indicate
947     // that there's no precession.
948     double precessionPeriod = 0.0;
949     if (rotationData->getNumber("PrecessionPeriod", precessionPeriod))
950     {
951         // The precession period is specified in the ssc file in units
952         // of years, but internally Celestia uses days.
953         precessionPeriod = precessionPeriod * 365.25;
954     }
955 
956     // No period was specified, and the default synchronous
957     // rotation period is zero, indicating that the object
958     // doesn't have a periodic orbit. Default to a constant
959     // orientation instead.
960     if (period == 0.0)
961     {
962         return CreateFixedRotationModel(offset, inclination, ascendingNode);
963     }
964     else
965     {
966         return new PrecessingRotationModel(period,
967                                            offset,
968                                            epoch,
969                                            inclination,
970                                            ascendingNode,
971                                            precessionPeriod);
972     }
973 }
974 
975 
976 static ScriptedRotation*
CreateScriptedRotation(Hash * rotationData,const string & path)977 CreateScriptedRotation(Hash* rotationData,
978                        const string& path)
979 {
980 #if !defined(CELX)
981     clog << "ScriptedRotation not usable without scripting support.\n";
982     return NULL;
983 #else
984 
985     // Function name is required
986     string funcName;
987     if (!rotationData->getString("Function", funcName))
988     {
989         clog << "Function name missing from scripted rotation definition.\n";
990         return NULL;
991     }
992 
993     // Module name is optional
994     string moduleName;
995     rotationData->getString("Module", moduleName);
996 
997     string* pathCopy = new string(path);
998     Value* pathValue = new Value(*pathCopy);
999     rotationData->addValue("AddonPath", *pathValue);
1000 
1001     ScriptedRotation* scriptedRotation = new ScriptedRotation();
1002     if (scriptedRotation != NULL)
1003     {
1004         if (!scriptedRotation->initialize(moduleName, funcName, rotationData))
1005         {
1006             delete scriptedRotation;
1007             scriptedRotation = NULL;
1008         }
1009     }
1010 
1011     return scriptedRotation;
1012 #endif
1013 }
1014 
1015 
1016 // Parse rotation information. Unfortunately, Celestia didn't originally have
1017 // RotationModel objects, so information about the rotation of the object isn't
1018 // grouped into a single subobject--the ssc fields relevant for rotation just
1019 // appear in the top level structure.
1020 RotationModel*
CreateRotationModel(Hash * planetData,const string & path,double syncRotationPeriod)1021 CreateRotationModel(Hash* planetData,
1022                     const string& path,
1023                     double syncRotationPeriod)
1024 {
1025     RotationModel* rotationModel = NULL;
1026 
1027     // If more than one rotation model is specified, the following precedence
1028     // is used to determine which one should be used:
1029     //   CustomRotation
1030     //   SPICE C-Kernel
1031     //   SampledOrientation
1032     //   PrecessingRotation
1033     //   UniformRotation
1034     //   legacy rotation parameters
1035 
1036     string customRotationModelName;
1037     if (planetData->getString("CustomRotation", customRotationModelName))
1038     {
1039         rotationModel = GetCustomRotationModel(customRotationModelName);
1040         if (rotationModel != NULL)
1041         {
1042             return rotationModel;
1043         }
1044         clog << "Could not find custom rotation model named '" <<
1045             customRotationModelName << "'\n";
1046     }
1047 
1048 #ifdef USE_SPICE
1049     Value* spiceRotationDataValue = planetData->getValue("SpiceRotation");
1050     if (spiceRotationDataValue != NULL)
1051     {
1052         if (spiceRotationDataValue->getType() != Value::HashType)
1053         {
1054             clog << "Object has incorrect spice rotation syntax.\n";
1055             return NULL;
1056         }
1057         else
1058         {
1059             rotationModel = CreateSpiceRotation(spiceRotationDataValue->getHash(), path);
1060             if (rotationModel != NULL)
1061             {
1062                 return rotationModel;
1063             }
1064             clog << "Bad spice rotation model\n";
1065             DPRINTF(0, "Could not load SPICE rotation model\n");
1066         }
1067     }
1068 #endif
1069 
1070     Value* scriptedRotationValue = planetData->getValue("ScriptedRotation");
1071     if (scriptedRotationValue != NULL)
1072     {
1073         if (scriptedRotationValue->getType() != Value::HashType)
1074         {
1075             clog << "Object has incorrect scripted rotation syntax.\n";
1076             return NULL;
1077         }
1078         else
1079         {
1080             rotationModel = CreateScriptedRotation(scriptedRotationValue->getHash(), path);
1081             if (rotationModel != NULL)
1082                 return rotationModel;
1083         }
1084     }
1085 
1086     string sampOrientationFile;
1087     if (planetData->getString("SampledOrientation", sampOrientationFile))
1088     {
1089         DPRINTF(1, "Attempting to load orientation file '%s'\n",
1090                 sampOrientationFile.c_str());
1091         ResourceHandle orientationHandle =
1092             GetRotationModelManager()->getHandle(RotationModelInfo(sampOrientationFile, path));
1093         rotationModel = GetRotationModelManager()->find(orientationHandle);
1094         if (rotationModel != NULL)
1095         {
1096             return rotationModel;
1097         }
1098 
1099         clog << "Could not load rotation model file '" <<
1100             sampOrientationFile << "'\n";
1101     }
1102 
1103     Value* precessingRotationValue = planetData->getValue("PrecessingRotation");
1104     if (precessingRotationValue != NULL)
1105     {
1106         if (precessingRotationValue->getType() != Value::HashType)
1107         {
1108             clog << "Object has incorrect syntax for precessing rotation.\n";
1109             return NULL;
1110         }
1111         else
1112         {
1113             return CreatePrecessingRotationModel(precessingRotationValue->getHash(),
1114                                                  syncRotationPeriod);
1115         }
1116     }
1117 
1118     Value* uniformRotationValue = planetData->getValue("UniformRotation");
1119     if (uniformRotationValue != NULL)
1120     {
1121         if (uniformRotationValue->getType() != Value::HashType)
1122         {
1123             clog << "Object has incorrect UniformRotation syntax.\n";
1124             return NULL;
1125         }
1126         else
1127         {
1128             return CreateUniformRotationModel(uniformRotationValue->getHash(),
1129                                               syncRotationPeriod);
1130         }
1131     }
1132 
1133     Value* fixedRotationValue = planetData->getValue("FixedRotation");
1134     if (fixedRotationValue != NULL)
1135     {
1136         if (fixedRotationValue->getType() != Value::HashType)
1137         {
1138             clog << "Object has incorrect FixedRotation syntax.\n";
1139             return NULL;
1140         }
1141         else
1142         {
1143             return CreateFixedRotationModel(fixedRotationValue->getHash());
1144         }
1145     }
1146 
1147     Value* fixedAttitudeValue = planetData->getValue("FixedAttitude");
1148     if (fixedAttitudeValue != NULL)
1149     {
1150         if (fixedAttitudeValue->getType() != Value::HashType)
1151         {
1152             clog << "Object has incorrect FixedAttitude syntax.\n";
1153             return NULL;
1154         }
1155         else
1156         {
1157             return CreateFixedAttitudeRotationModel(fixedAttitudeValue->getHash());
1158         }
1159     }
1160 
1161     // For backward compatibility we need to support rotation parameters
1162     // that appear in the main block of the object definition.
1163     // Default to synchronous rotation
1164     bool specified = false;
1165     double period = syncRotationPeriod;
1166     if (planetData->getNumber("RotationPeriod", period))
1167     {
1168         specified = true;
1169         period = period / 24.0f;
1170     }
1171 
1172     float offset = 0.0f;
1173     if (planetData->getNumber("RotationOffset", offset))
1174     {
1175         specified = true;
1176         offset = degToRad(offset);
1177     }
1178 
1179     double epoch = astro::J2000;
1180     if (ParseDate(planetData, "RotationEpoch", epoch))
1181     {
1182         specified = true;
1183     }
1184 
1185     float inclination = 0.0f;
1186     if (planetData->getNumber("Obliquity", inclination))
1187     {
1188         specified = true;
1189         inclination = degToRad(inclination);
1190     }
1191 
1192     float ascendingNode = 0.0f;
1193     if (planetData->getNumber("EquatorAscendingNode", ascendingNode))
1194     {
1195         specified = true;
1196         ascendingNode = degToRad(ascendingNode);
1197     }
1198 
1199     double precessionRate = 0.0f;
1200     if (planetData->getNumber("PrecessionRate", precessionRate))
1201     {
1202         specified = true;
1203     }
1204 
1205     if (specified)
1206     {
1207         RotationModel* rm = NULL;
1208         if (period == 0.0)
1209         {
1210             // No period was specified, and the default synchronous
1211             // rotation period is zero, indicating that the object
1212             // doesn't have a periodic orbit. Default to a constant
1213             // orientation instead.
1214             rm = CreateFixedRotationModel(offset, inclination, ascendingNode);
1215         }
1216         else if (precessionRate == 0.0)
1217         {
1218             rm = new UniformRotationModel(period,
1219                                           offset,
1220                                           epoch,
1221                                           inclination,
1222                                           ascendingNode);
1223         }
1224         else
1225         {
1226             rm = new PrecessingRotationModel(period,
1227                                              offset,
1228                                              epoch,
1229                                              inclination,
1230                                              ascendingNode,
1231                                              -360.0 / precessionRate);
1232         }
1233 
1234         return rm;
1235     }
1236     else
1237     {
1238         // No rotation fields specified
1239         return NULL;
1240     }
1241 }
1242 
1243 
CreateDefaultRotationModel(double syncRotationPeriod)1244 RotationModel* CreateDefaultRotationModel(double syncRotationPeriod)
1245 {
1246     if (syncRotationPeriod == 0.0)
1247     {
1248         // If syncRotationPeriod is 0, the orbit of the object is
1249         // aperiodic and we'll just return a FixedRotation.
1250         return new ConstantOrientation(Quatd(1.0));
1251     }
1252     else
1253     {
1254         return new UniformRotationModel(syncRotationPeriod,
1255                                         0.0f,
1256                                         astro::J2000,
1257                                         0.0f,
1258                                         0.0f);
1259     }
1260 }
1261 
1262 
1263 // Get the center object of a frame definition. Return an empty selection
1264 // if it's missing or refers to an object that doesn't exist.
1265 static Selection
getFrameCenter(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1266 getFrameCenter(const Universe& universe, Hash* frameData, const Selection& defaultCenter)
1267 {
1268     string centerName;
1269     if (!frameData->getString("Center", centerName))
1270     {
1271         if (defaultCenter.empty())
1272             cerr << "No center specified for reference frame.\n";
1273         return defaultCenter;
1274     }
1275 
1276     Selection centerObject = universe.findPath(centerName, NULL, 0);
1277     if (centerObject.empty())
1278     {
1279         cerr << "Center object '" << centerName << "' of reference frame not found.\n";
1280         return Selection();
1281     }
1282 
1283     // Should verify that center object is a star or planet, and
1284     // that it is a member of the same star system as the body in which
1285     // the frame will be used.
1286 
1287     return centerObject;
1288 }
1289 
1290 
1291 static BodyFixedFrame*
CreateBodyFixedFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1292 CreateBodyFixedFrame(const Universe& universe,
1293                      Hash* frameData,
1294                      const Selection& defaultCenter)
1295 {
1296     Selection center = getFrameCenter(universe, frameData, defaultCenter);
1297     if (center.empty())
1298         return NULL;
1299     else
1300         return new BodyFixedFrame(center, center);
1301 }
1302 
1303 
1304 static BodyMeanEquatorFrame*
CreateMeanEquatorFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1305 CreateMeanEquatorFrame(const Universe& universe,
1306                        Hash* frameData,
1307                        const Selection& defaultCenter)
1308 {
1309     Selection center = getFrameCenter(universe, frameData, defaultCenter);
1310     if (center.empty())
1311         return NULL;
1312 
1313     Selection obj = center;
1314     string objName;
1315     if (frameData->getString("Object", objName))
1316     {
1317         obj = universe.findPath(objName, NULL, 0);
1318         if (obj.empty())
1319         {
1320             clog << "Object '" << objName << "' for mean equator frame not found.\n";
1321             return NULL;
1322         }
1323     }
1324 
1325     clog << "CreateMeanEquatorFrame " << center.getName() << ", " << obj.getName() << "\n";
1326 
1327     double freezeEpoch = 0.0;
1328     if (ParseDate(frameData, "Freeze", freezeEpoch))
1329     {
1330         return new BodyMeanEquatorFrame(center, obj, freezeEpoch);
1331     }
1332     else
1333     {
1334         return new BodyMeanEquatorFrame(center, obj);
1335     }
1336 }
1337 
1338 
1339 // Convert a string to an axis label. Permitted axis labels are
1340 // x, y, z, -x, -y, and -z. +x, +y, and +z are allowed as synonyms for
1341 // x, y, z. Case is ignored.
1342 static int
parseAxisLabel(const std::string & label)1343 parseAxisLabel(const std::string& label)
1344 {
1345     if (compareIgnoringCase(label, "x") == 0 ||
1346         compareIgnoringCase(label, "+x") == 0)
1347     {
1348         return 1;
1349     }
1350 
1351     if (compareIgnoringCase(label, "y") == 0 ||
1352         compareIgnoringCase(label, "+y") == 0)
1353     {
1354         return 2;
1355     }
1356 
1357     if (compareIgnoringCase(label, "z") == 0 ||
1358         compareIgnoringCase(label, "+z") == 0)
1359     {
1360         return 3;
1361     }
1362 
1363     if (compareIgnoringCase(label, "-x") == 0)
1364     {
1365         return -1;
1366     }
1367 
1368     if (compareIgnoringCase(label, "-y") == 0)
1369     {
1370         return -2;
1371     }
1372 
1373     if (compareIgnoringCase(label, "-z") == 0)
1374     {
1375         return -3;
1376     }
1377 
1378     return 0;
1379 }
1380 
1381 
1382 static int
getAxis(Hash * vectorData)1383 getAxis(Hash* vectorData)
1384 {
1385     string axisLabel;
1386     if (!vectorData->getString("Axis", axisLabel))
1387     {
1388         DPRINTF(0, "Bad two-vector frame: missing axis label for vector.\n");
1389         return 0;
1390     }
1391 
1392     int axis = parseAxisLabel(axisLabel);
1393     if (axis == 0)
1394     {
1395         DPRINTF(0, "Bad two-vector frame: vector has invalid axis label.\n");
1396     }
1397 
1398     // Permute axis labels to match non-standard Celestia coordinate
1399     // conventions: y <- z, z <- -y
1400     switch (axis)
1401     {
1402     case 2:
1403         return -3;
1404     case -2:
1405         return 3;
1406     case 3:
1407         return 2;
1408     case -3:
1409         return -2;
1410     default:
1411         return axis;
1412     }
1413 
1414     return axis;
1415 }
1416 
1417 
1418 // Get the target object of a direction vector definition. Return an
1419 // empty selection if it's missing or refers to an object that doesn't exist.
1420 static Selection
getVectorTarget(const Universe & universe,Hash * vectorData)1421 getVectorTarget(const Universe& universe, Hash* vectorData)
1422 {
1423     string targetName;
1424     if (!vectorData->getString("Target", targetName))
1425     {
1426         clog << "Bad two-vector frame: no target specified for vector.\n";
1427         return Selection();
1428     }
1429 
1430     Selection targetObject = universe.findPath(targetName, NULL, 0);
1431     if (targetObject.empty())
1432     {
1433         clog << "Bad two-vector frame: target object '" << targetName << "' of vector not found.\n";
1434         return Selection();
1435     }
1436 
1437     return targetObject;
1438 }
1439 
1440 
1441 // Get the observer object of a direction vector definition. Return an
1442 // empty selection if it's missing or refers to an object that doesn't exist.
1443 static Selection
getVectorObserver(const Universe & universe,Hash * vectorData)1444 getVectorObserver(const Universe& universe, Hash* vectorData)
1445 {
1446     string obsName;
1447     if (!vectorData->getString("Observer", obsName))
1448     {
1449         // Omission of observer is permitted; it will default to the
1450         // frame center.
1451         return Selection();
1452     }
1453 
1454     Selection obsObject = universe.findPath(obsName, NULL, 0);
1455     if (obsObject.empty())
1456     {
1457         clog << "Bad two-vector frame: observer object '" << obsObject.getName() << "' of vector not found.\n";
1458         return Selection();
1459     }
1460 
1461     return obsObject;
1462 }
1463 
1464 
1465 static FrameVector*
CreateFrameVector(const Universe & universe,const Selection & center,Hash * vectorData)1466 CreateFrameVector(const Universe& universe,
1467                   const Selection& center,
1468                   Hash* vectorData)
1469 {
1470     Value* value = NULL;
1471 
1472     value = vectorData->getValue("RelativePosition");
1473     if (value != NULL && value->getHash() != NULL)
1474     {
1475         Hash* relPosData = value->getHash();
1476         Selection observer = getVectorObserver(universe, relPosData);
1477         Selection target = getVectorTarget(universe, relPosData);
1478         // Default observer is the frame center
1479         if (observer.empty())
1480             observer = center;
1481 
1482         if (observer.empty() || target.empty())
1483             return NULL;
1484         else
1485             return new FrameVector(FrameVector::createRelativePositionVector(observer, target));
1486     }
1487 
1488     value = vectorData->getValue("RelativeVelocity");
1489     if (value != NULL && value->getHash() != NULL)
1490     {
1491         Hash* relVData = value->getHash();
1492         Selection observer = getVectorObserver(universe, relVData);
1493         Selection target = getVectorTarget(universe, relVData);
1494         // Default observer is the frame center
1495         if (observer.empty())
1496             observer = center;
1497 
1498         if (observer.empty() || target.empty())
1499             return NULL;
1500         else
1501             return new FrameVector(FrameVector::createRelativeVelocityVector(observer, target));
1502     }
1503 
1504     value = vectorData->getValue("ConstantVector");
1505     if (value != NULL && value->getHash() != NULL)
1506     {
1507         Hash* constVecData = value->getHash();
1508         Vec3d vec(0.0, 0.0, 1.0);
1509         constVecData->getVector("Vector", vec);
1510         if (vec.length() == 0.0)
1511         {
1512             clog << "Bad two-vector frame: constant vector has length zero\n";
1513             return NULL;
1514         }
1515         vec.normalize();
1516         vec = Vec3d(vec.x, vec.z, -vec.y);
1517 
1518         // The frame for the vector is optional; a NULL frame indicates
1519         // J2000 ecliptic.
1520         ReferenceFrame* f = NULL;
1521         Value* frameValue = constVecData->getValue("Frame");
1522         if (frameValue != NULL)
1523         {
1524             f = CreateReferenceFrame(universe, frameValue, center, NULL);
1525             if (f == NULL)
1526                 return NULL;
1527         }
1528 
1529         return new FrameVector(FrameVector::createConstantVector(vec, f));
1530     }
1531     else
1532     {
1533         clog << "Bad two-vector frame: unknown vector type\n";
1534         return NULL;
1535     }
1536 }
1537 
1538 
1539 static TwoVectorFrame*
CreateTwoVectorFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1540 CreateTwoVectorFrame(const Universe& universe,
1541                      Hash* frameData,
1542                      const Selection& defaultCenter)
1543 {
1544     Selection center = getFrameCenter(universe, frameData, defaultCenter);
1545     if (center.empty())
1546         return NULL;
1547 
1548     // Primary and secondary vector definitions are required
1549     Value* primaryValue = frameData->getValue("Primary");
1550     if (!primaryValue)
1551     {
1552         clog << "Primary axis missing from two-vector frame.\n";
1553         return NULL;
1554     }
1555 
1556     Hash* primaryData = primaryValue->getHash();
1557     if (!primaryData)
1558     {
1559         clog << "Bad syntax for primary axis of two-vector frame.\n";
1560         return NULL;
1561     }
1562 
1563     Value* secondaryValue = frameData->getValue("Secondary");
1564     if (!secondaryValue)
1565     {
1566         clog << "Secondary axis missing from two-vector frame.\n";
1567         return NULL;
1568     }
1569 
1570     Hash* secondaryData = secondaryValue->getHash();
1571     if (!secondaryData)
1572     {
1573         clog << "Bad syntax for secondary axis of two-vector frame.\n";
1574         return NULL;
1575     }
1576 
1577     // Get and validate the axes for the direction vectors
1578     int primaryAxis = getAxis(primaryData);
1579     int secondaryAxis = getAxis(secondaryData);
1580 
1581     assert(abs(primaryAxis) <= 3);
1582     assert(abs(secondaryAxis) <= 3);
1583     if (primaryAxis == 0 || secondaryAxis == 0)
1584     {
1585         return NULL;
1586     }
1587 
1588     if (abs(primaryAxis) == abs(secondaryAxis))
1589     {
1590         clog << "Bad two-vector frame: axes for vectors are collinear.\n";
1591         return NULL;
1592     }
1593 
1594     FrameVector* primaryVector = CreateFrameVector(universe,
1595                                                    center,
1596                                                    primaryData);
1597     FrameVector* secondaryVector = CreateFrameVector(universe,
1598                                                      center,
1599                                                      secondaryData);
1600 
1601     TwoVectorFrame* frame = NULL;
1602     if (primaryVector != NULL && secondaryVector != NULL)
1603     {
1604         frame = new TwoVectorFrame(center,
1605                                    *primaryVector, primaryAxis,
1606                                    *secondaryVector, secondaryAxis);
1607     }
1608 
1609     delete primaryVector;
1610     delete secondaryVector;
1611 
1612     return frame;
1613 }
1614 
1615 
1616 static J2000EclipticFrame*
CreateJ2000EclipticFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1617 CreateJ2000EclipticFrame(const Universe& universe,
1618                          Hash* frameData,
1619                          const Selection& defaultCenter)
1620 {
1621     Selection center = getFrameCenter(universe, frameData, defaultCenter);
1622 
1623     if (center.empty())
1624         return NULL;
1625     else
1626         return new J2000EclipticFrame(center);
1627 }
1628 
1629 
1630 static J2000EquatorFrame*
CreateJ2000EquatorFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter)1631 CreateJ2000EquatorFrame(const Universe& universe,
1632                         Hash* frameData,
1633                         const Selection& defaultCenter)
1634 {
1635     Selection center = getFrameCenter(universe, frameData, defaultCenter);
1636 
1637     if (center.empty())
1638         return NULL;
1639     else
1640         return new J2000EquatorFrame(center);
1641 }
1642 
1643 
1644 /* Helper function for CreateTopocentricFrame().
1645  * Creates a two-vector frame with the specified center, target, and observer.
1646  */
1647 TwoVectorFrame*
CreateTopocentricFrame(const Selection & center,const Selection & target,const Selection & observer)1648 CreateTopocentricFrame(const Selection& center,
1649                        const Selection& target,
1650                        const Selection& observer)
1651 {
1652     BodyMeanEquatorFrame* eqFrame = new BodyMeanEquatorFrame(target, target);
1653     FrameVector north = FrameVector::createConstantVector(Vec3d(0.0, 1.0, 0.0), eqFrame);
1654     FrameVector up = FrameVector::createRelativePositionVector(observer, target);
1655 
1656     return new TwoVectorFrame(center, up, -2, north, -3);
1657 }
1658 
1659 
1660 /* Create a new Topocentric frame. The topocentric frame is designed to make it easy
1661  * to place objects on the surface of a planet or moon. The z-axis will point toward
1662  * the observer's zenith (which here is the direction away from the center of the
1663  * planet.) The x-axis will point in the local north direction. The equivalent
1664  * two-vector frame is:
1665  *
1666  * TwoVector
1667  * {
1668  *    Center <center>
1669  *    Primary
1670  *    {
1671  *       Axis "z"
1672  *       RelativePosition { Target <target> Observer <observer> }
1673  *    }
1674  *    Secondary
1675  *    {
1676  *       Axis "x"
1677  *       ConstantVector
1678  *       {
1679  *          Vector [ 0 0 1]
1680  *          Frame { BodyFixed { Center <target> } }
1681  *       }
1682  *    }
1683  * }
1684  *
1685  * Typically, the topocentric frame is used as a BodyFrame to orient an
1686  * object on the surface of a planet. In this situation, the observer is
1687  * object itself and the target object is the planet. In fact, these are
1688  * the defaults: when no target, observer, or center is specified, the
1689  * observer and center are both 'self' and the target is the parent
1690  * object. Thus, for a Mars rover, using a topocentric frame is as simple
1691  * as:
1692  *
1693  * "Rover" "Sol/Mars"
1694  * {
1695  *     BodyFrame { Topocentric { } }
1696  *     ...
1697  * }
1698  */
1699 static TwoVectorFrame*
CreateTopocentricFrame(const Universe & universe,Hash * frameData,const Selection & defaultTarget,const Selection & defaultObserver)1700 CreateTopocentricFrame(const Universe& universe,
1701                        Hash* frameData,
1702                        const Selection& defaultTarget,
1703                        const Selection& defaultObserver)
1704 {
1705     Selection target;
1706     Selection observer;
1707     Selection center;
1708 
1709     string centerName;
1710     if (frameData->getString("Center", centerName))
1711     {
1712         // If a center is provided, the default observer is the center and
1713         // the default target is the center's parent. This gives sensible results
1714         // when a topocentric frame is used as an orbit frame.
1715         center = universe.findPath(centerName, NULL, 0);
1716         if (center.empty())
1717         {
1718             cerr << "Center object '" << centerName << "' for topocentric frame not found.\n";
1719             return NULL;
1720        }
1721 
1722        observer = center;
1723        target = center.parent();
1724     }
1725     else
1726     {
1727         // When no center is provided, use the default observer as the center. This
1728         // is typical when a topocentric frame is the body frame. The default observer
1729         // is usually the object itself.
1730         target = defaultTarget;
1731         observer = defaultObserver;
1732         center = defaultObserver;
1733     }
1734 
1735     string targetName;
1736     if (!frameData->getString("Target", targetName))
1737     {
1738         if (target.empty())
1739         {
1740             cerr << "No target specified for topocentric frame.\n";
1741             return NULL;
1742         }
1743     }
1744     else
1745     {
1746         target = universe.findPath(targetName, NULL, 0);
1747         if (target.empty())
1748         {
1749             cerr << "Target object '" << targetName << "' for topocentric frame not found.\n";
1750             return NULL;
1751         }
1752 
1753         // Should verify that center object is a star or planet, and
1754         // that it is a member of the same star system as the body in which
1755         // the frame will be used.
1756     }
1757 
1758     string observerName;
1759     if (!frameData->getString("Observer", observerName))
1760     {
1761         if (observer.empty())
1762         {
1763             cerr << "No observer specified for topocentric frame.\n";
1764             return NULL;
1765         }
1766     }
1767     else
1768     {
1769         observer = universe.findPath(observerName, NULL, 0);
1770         if (observer.empty())
1771         {
1772             cerr << "Observer object '" << observerName << "' for topocentric frame not found.\n";
1773             return NULL;
1774         }
1775     }
1776 
1777     return CreateTopocentricFrame(center, target, observer);
1778 }
1779 
1780 
1781 static ReferenceFrame*
CreateComplexFrame(const Universe & universe,Hash * frameData,const Selection & defaultCenter,Body * defaultObserver)1782 CreateComplexFrame(const Universe& universe, Hash* frameData, const Selection& defaultCenter, Body* defaultObserver)
1783 {
1784     Value* value = frameData->getValue("BodyFixed");
1785     if (value != NULL)
1786     {
1787         if (value->getType() != Value::HashType)
1788         {
1789             clog << "Object has incorrect body-fixed frame syntax.\n";
1790             return NULL;
1791         }
1792         else
1793         {
1794             return CreateBodyFixedFrame(universe, value->getHash(), defaultCenter);
1795         }
1796     }
1797 
1798     value = frameData->getValue("MeanEquator");
1799     if (value != NULL)
1800     {
1801         if (value->getType() != Value::HashType)
1802         {
1803             clog << "Object has incorrect mean equator frame syntax.\n";
1804             return NULL;
1805         }
1806         else
1807         {
1808             return CreateMeanEquatorFrame(universe, value->getHash(), defaultCenter);
1809         }
1810     }
1811 
1812     value = frameData->getValue("TwoVector");
1813     if (value != NULL)
1814     {
1815         if (value->getType() != Value::HashType)
1816         {
1817             clog << "Object has incorrect two-vector frame syntax.\n";
1818             return NULL;
1819         }
1820         else
1821         {
1822             return CreateTwoVectorFrame(universe, value->getHash(), defaultCenter);
1823         }
1824     }
1825 
1826     value = frameData->getValue("Topocentric");
1827     if (value != NULL)
1828     {
1829         if (value->getType() != Value::HashType)
1830         {
1831             clog << "Object has incorrect topocentric frame syntax.\n";
1832             return NULL;
1833         }
1834         else
1835         {
1836             return CreateTopocentricFrame(universe, value->getHash(), defaultCenter, Selection(defaultObserver));
1837         }
1838     }
1839 
1840     value = frameData->getValue("EclipticJ2000");
1841     if (value != NULL)
1842     {
1843         if (value->getType() != Value::HashType)
1844         {
1845             clog << "Object has incorrect J2000 ecliptic frame syntax.\n";
1846             return NULL;
1847         }
1848         else
1849         {
1850             return CreateJ2000EclipticFrame(universe, value->getHash(), defaultCenter);
1851         }
1852     }
1853 
1854     value = frameData->getValue("EquatorJ2000");
1855     if (value != NULL)
1856     {
1857         if (value->getType() != Value::HashType)
1858         {
1859             clog << "Object has incorrect J2000 equator frame syntax.\n";
1860             return NULL;
1861         }
1862         else
1863         {
1864             return CreateJ2000EquatorFrame(universe, value->getHash(), defaultCenter);
1865         }
1866     }
1867 
1868     clog << "Frame definition does not have a valid frame type.\n";
1869 
1870     return NULL;
1871 }
1872 
1873 
CreateReferenceFrame(const Universe & universe,Value * frameValue,const Selection & defaultCenter,Body * defaultObserver)1874 ReferenceFrame* CreateReferenceFrame(const Universe& universe,
1875                                      Value* frameValue,
1876                                      const Selection& defaultCenter,
1877                                      Body* defaultObserver)
1878 {
1879     if (frameValue->getType() == Value::StringType)
1880     {
1881         // TODO: handle named frames
1882         clog << "Invalid syntax for frame definition.\n";
1883         return NULL;
1884     }
1885     else if (frameValue->getType() == Value::HashType)
1886     {
1887         return CreateComplexFrame(universe, frameValue->getHash(), defaultCenter, defaultObserver);
1888     }
1889     else
1890     {
1891         clog << "Invalid syntax for frame definition.\n";
1892         return NULL;
1893     }
1894 }
1895