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