1916 lines
57 KiB
C++
1916 lines
57 KiB
C++
// parseobject.cpp
|
|
//
|
|
// Copyright (C) 2004-2009, the Celestia Development Team
|
|
// Original version by Chris Laurel <claurel@gmail.com>
|
|
//
|
|
// Functions for parsing objects common to star, solar system, and
|
|
// deep sky catalogs.
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
|
|
#include "parseobject.h"
|
|
#include "frame.h"
|
|
#include "trajmanager.h"
|
|
#include "rotationmanager.h"
|
|
#include "universe.h"
|
|
#include "eigenport.h"
|
|
#include <celephem/customorbit.h>
|
|
#include <celephem/customrotation.h>
|
|
#include <celephem/spiceorbit.h>
|
|
#include <celephem/spicerotation.h>
|
|
#include <celephem/scriptorbit.h>
|
|
#include <celephem/scriptrotation.h>
|
|
#include <celmath/geomutil.h>
|
|
#include <celutil/debug.h>
|
|
#include <cassert>
|
|
|
|
using namespace Eigen;
|
|
using namespace std;
|
|
|
|
|
|
/**
|
|
* Returns the default units scale for orbits.
|
|
*
|
|
* If the usePlanetUnits flag is set, this returns a distance scale of AU and a
|
|
* time scale of years. Otherwise the distace scale is kilometers and the time
|
|
* scale is days.
|
|
*
|
|
* @param[in] usePlanetUnits Controls whether to return planet units or satellite units.
|
|
* @param[out] distanceScale The default distance scale in kilometers.
|
|
* @param[out] timeScale The default time scale in days.
|
|
*/
|
|
static void
|
|
GetDefaultUnits(bool usePlanetUnits, double& distanceScale, double& timeScale)
|
|
{
|
|
if(usePlanetUnits)
|
|
{
|
|
distanceScale = KM_PER_AU;
|
|
timeScale = DAYS_PER_YEAR;
|
|
}
|
|
else
|
|
{
|
|
distanceScale = 1.0;
|
|
timeScale = 1.0;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the default distance scale for orbits.
|
|
*
|
|
* If the usePlanetUnits flag is set, this returns AU, otherwise it returns
|
|
* kilometers.
|
|
*
|
|
* @param[in] usePlanetUnits Controls whether to return planet units or satellite units.
|
|
* @param[out] distanceScale The default distance scale in kilometers.
|
|
*/
|
|
static void
|
|
GetDefaultUnits(bool usePlanetUnits, double& distanceScale)
|
|
{
|
|
distanceScale = (usePlanetUnits) ? KM_PER_AU : 1.0;
|
|
}
|
|
|
|
|
|
bool
|
|
ParseDate(Hash* hash, const string& name, double& jd)
|
|
{
|
|
// Check first for a number value representing a Julian date
|
|
if (hash->getNumber(name, jd))
|
|
return true;
|
|
|
|
string dateString;
|
|
if (hash->getString(name, dateString))
|
|
{
|
|
astro::Date date(1, 1, 1);
|
|
if (astro::parseDate(dateString, date))
|
|
{
|
|
jd = (double) date;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/*!
|
|
* Create a new Keplerian orbit from an ssc property table:
|
|
*
|
|
* \code EllipticalOrbit
|
|
* {
|
|
* # One of the following is required to specify orbit size:
|
|
* SemiMajorAxis <number>
|
|
* PericenterDistance <number>
|
|
*
|
|
* # Required
|
|
* Period <number>
|
|
*
|
|
* Eccentricity <number> (default: 0.0)
|
|
* Inclination <degrees> (default: 0.0)
|
|
* AscendingNode <degrees> (default: 0.0)
|
|
*
|
|
* # One or none of the following:
|
|
* ArgOfPericenter <degrees> (default: 0.0)
|
|
* LongOfPericenter <degrees> (default: 0.0)
|
|
*
|
|
* Epoch <date> (default J2000.0)
|
|
*
|
|
* # One or none of the following:
|
|
* MeanAnomaly <degrees> (default: 0.0)
|
|
* MeanLongitude <degrees> (default: 0.0)
|
|
* } \endcode
|
|
*
|
|
* If usePlanetUnits is true:
|
|
* Period is in Julian years
|
|
* SemiMajorAxis or PericenterDistance is in AU
|
|
* Otherwise:
|
|
* Period is in Julian days
|
|
* SemiMajorAxis or PericenterDistance is in kilometers.
|
|
*/
|
|
static EllipticalOrbit*
|
|
CreateEllipticalOrbit(Hash* orbitData,
|
|
bool usePlanetUnits)
|
|
{
|
|
|
|
// default units for planets are AU and years, otherwise km and days
|
|
|
|
double distanceScale;
|
|
double timeScale;
|
|
GetDefaultUnits(usePlanetUnits, distanceScale, timeScale);
|
|
|
|
// SemiMajorAxis and Period are absolutely required; everything
|
|
// else has a reasonable default.
|
|
double pericenterDistance = 0.0;
|
|
double semiMajorAxis = 0.0;
|
|
if (!orbitData->getLength("SemiMajorAxis", semiMajorAxis, 1.0, distanceScale))
|
|
{
|
|
if (!orbitData->getLength("PericenterDistance", pericenterDistance, 1.0, distanceScale))
|
|
{
|
|
clog << "SemiMajorAxis/PericenterDistance missing! Skipping planet . . .\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
double period = 0.0;
|
|
if (!orbitData->getTime("Period", period, 1.0, timeScale))
|
|
{
|
|
clog << "Period missing! Skipping planet . . .\n";
|
|
return nullptr;
|
|
}
|
|
|
|
double eccentricity = 0.0;
|
|
orbitData->getNumber("Eccentricity", eccentricity);
|
|
|
|
double inclination = 0.0;
|
|
orbitData->getAngle("Inclination", inclination);
|
|
|
|
double ascendingNode = 0.0;
|
|
orbitData->getAngle("AscendingNode", ascendingNode);
|
|
|
|
double argOfPericenter = 0.0;
|
|
if (!orbitData->getAngle("ArgOfPericenter", argOfPericenter))
|
|
{
|
|
double longOfPericenter = 0.0;
|
|
if (orbitData->getAngle("LongOfPericenter", longOfPericenter))
|
|
{
|
|
argOfPericenter = longOfPericenter - ascendingNode;
|
|
}
|
|
}
|
|
|
|
double epoch = astro::J2000;
|
|
ParseDate(orbitData, "Epoch", epoch);
|
|
|
|
// Accept either the mean anomaly or mean longitude--use mean anomaly
|
|
// if both are specified.
|
|
double anomalyAtEpoch = 0.0;
|
|
if (!orbitData->getAngle("MeanAnomaly", anomalyAtEpoch))
|
|
{
|
|
double longAtEpoch = 0.0;
|
|
if (orbitData->getAngle("MeanLongitude", longAtEpoch))
|
|
{
|
|
anomalyAtEpoch = longAtEpoch - (argOfPericenter + ascendingNode);
|
|
}
|
|
}
|
|
|
|
// If we read the semi-major axis, use it to compute the pericenter
|
|
// distance.
|
|
if (semiMajorAxis != 0.0)
|
|
pericenterDistance = semiMajorAxis * (1.0 - eccentricity);
|
|
|
|
return new EllipticalOrbit(pericenterDistance,
|
|
eccentricity,
|
|
degToRad(inclination),
|
|
degToRad(ascendingNode),
|
|
degToRad(argOfPericenter),
|
|
degToRad(anomalyAtEpoch),
|
|
period,
|
|
epoch);
|
|
}
|
|
|
|
|
|
/*!
|
|
* Create a new sampled orbit from an ssc property table:
|
|
*
|
|
* \code SampledTrajectory
|
|
* {
|
|
* Source <string>
|
|
* Interpolation "Cubic" | "Linear"
|
|
* DoublePrecision <boolean>
|
|
* } \endcode
|
|
*
|
|
* Source is the only required field. Interpolation defaults to cubic, and
|
|
* DoublePrecision defaults to true.
|
|
*/
|
|
static Orbit*
|
|
CreateSampledTrajectory(Hash* trajData, const string& path)
|
|
{
|
|
string sourceName;
|
|
if (!trajData->getString("Source", sourceName))
|
|
{
|
|
clog << "SampledTrajectory is missing a source.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Read interpolation type; string value must be either "Linear" or "Cubic"
|
|
// Default interpolation type is cubic.
|
|
string interpolationString;
|
|
TrajectoryInterpolation interpolation = TrajectoryInterpolationCubic;
|
|
if (trajData->getString("Interpolation", interpolationString))
|
|
{
|
|
if (!compareIgnoringCase(interpolationString, "linear"))
|
|
interpolation = TrajectoryInterpolationLinear;
|
|
else if (!compareIgnoringCase(interpolationString, "cubic"))
|
|
interpolation = TrajectoryInterpolationCubic;
|
|
else
|
|
clog << "Unknown interpolation type " << interpolationString << endl; // non-fatal error
|
|
}
|
|
|
|
// Double precision is true by default
|
|
bool useDoublePrecision = true;
|
|
trajData->getBoolean("DoublePrecision", useDoublePrecision);
|
|
TrajectoryPrecision precision = useDoublePrecision ? TrajectoryPrecisionDouble : TrajectoryPrecisionSingle;
|
|
|
|
DPRINTF(1, "Attempting to load sampled trajectory from source '%s'\n", sourceName.c_str());
|
|
ResourceHandle orbitHandle = GetTrajectoryManager()->getHandle(TrajectoryInfo(sourceName, path, interpolation, precision));
|
|
Orbit* orbit = GetTrajectoryManager()->find(orbitHandle);
|
|
if (orbit == nullptr)
|
|
{
|
|
clog << "Could not load sampled trajectory from '" << sourceName << "'\n";
|
|
}
|
|
|
|
return orbit;
|
|
}
|
|
|
|
|
|
/** Create a new FixedPosition trajectory.
|
|
*
|
|
* A FixedPosition is a property list with one of the following 3-vector properties:
|
|
*
|
|
* - \c Rectangular
|
|
* - \c Planetographic
|
|
* - \c Planetocentric
|
|
*
|
|
* Planetographic and planetocentric coordinates are given in the order longitude,
|
|
* latitude, altitude. Units of altitude are kilometers. Planetographic and
|
|
* and planetocentric coordinates are only practical when the coordinate system
|
|
* is BodyFixed.
|
|
*/
|
|
static Orbit*
|
|
CreateFixedPosition(Hash* trajData, const Selection& centralObject, bool usePlanetUnits)
|
|
{
|
|
double distanceScale;
|
|
GetDefaultUnits(usePlanetUnits, distanceScale);
|
|
|
|
Vector3d position = Vector3d::Zero();
|
|
|
|
Vector3d v = Vector3d::Zero();
|
|
if (trajData->getLengthVector("Rectangular", v, 1.0, distanceScale))
|
|
{
|
|
// Convert to Celestia's coordinate system
|
|
position = Vector3d(v.x(), v.z(), -v.y());
|
|
}
|
|
else if (trajData->getSphericalTuple("Planetographic", v))
|
|
{
|
|
if (centralObject.getType() != Selection::Type_Body)
|
|
{
|
|
clog << "FixedPosition planetographic coordinates aren't valid for stars.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// TODO: Need function to calculate planetographic coordinates
|
|
// TODO: Change planetocentricToCartesian so that 180 degree offset isn't required
|
|
position = centralObject.body()->planetocentricToCartesian(180.0 + v.x(), v.y(), v.z());
|
|
}
|
|
else if (trajData->getSphericalTuple("Planetocentric", v))
|
|
{
|
|
if (centralObject.getType() != Selection::Type_Body)
|
|
{
|
|
clog << "FixedPosition planetocentric coordinates aren't valid for stars.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// TODO: Change planetocentricToCartesian so that 180 degree offset isn't required
|
|
position = centralObject.body()->planetocentricToCartesian(180.0 + v.x(), v.y(), v.z());
|
|
}
|
|
else
|
|
{
|
|
clog << "Missing coordinates for FixedPosition\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return new FixedOrbit(position);
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse a string list--either a single string or an array of strings is permitted.
|
|
*/
|
|
static bool
|
|
ParseStringList(Hash* table,
|
|
const string& propertyName,
|
|
list<string>& stringList)
|
|
{
|
|
Value* v = table->getValue(propertyName);
|
|
if (v == nullptr)
|
|
return false;
|
|
|
|
// Check for a single string first.
|
|
if (v->getType() == Value::StringType)
|
|
{
|
|
stringList.push_back(v->getString());
|
|
return true;
|
|
}
|
|
if (v->getType() == Value::ArrayType)
|
|
{
|
|
ValueArray* array = v->getArray();
|
|
ValueArray::const_iterator iter;
|
|
|
|
// Verify that all array entries are strings
|
|
for (iter = array->begin(); iter != array->end(); iter++)
|
|
{
|
|
if ((*iter)->getType() != Value::StringType)
|
|
return false;
|
|
}
|
|
|
|
// Add strings to stringList
|
|
for (iter = array->begin(); iter != array->end(); iter++)
|
|
stringList.push_back((*iter)->getString());
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef USE_SPICE
|
|
|
|
/*! Create a new SPICE orbit. This is just a Celestia wrapper for a trajectory specified
|
|
* in a SPICE SPK file.
|
|
*
|
|
* \code SpiceOrbit
|
|
* {
|
|
* Kernel <string|string array> # optional
|
|
* Target <string>
|
|
* Origin <string>
|
|
* BoundingRadius <number>
|
|
* Period <number> # optional
|
|
* Beginning <number> # optional
|
|
* Ending <number> # optional
|
|
* } \endcode
|
|
*
|
|
* The Kernel property specifies one or more SPK files that must be loaded. Any
|
|
* already loaded kernels will also be used if they contain trajectories for
|
|
* the target or origin.
|
|
* Target and origin are strings that give NAIF IDs for the target and origin
|
|
* objects. Either names or integer IDs are valid, but integer IDs still must
|
|
* be quoted.
|
|
* BoundingRadius gives a conservative estimate of the maximum distance between
|
|
* the target and origin objects. It is required by Celestia for visibility
|
|
* culling when rendering.
|
|
* Beginning and Ending specify the valid time range of the SPICE orbit. It is
|
|
* an error to specify Beginning without Ending, and vice versa. If neither is
|
|
* specified, the valid range is computed from the coverage window in the SPICE
|
|
* kernel pool. If the coverage window is noncontiguous, the first interval is
|
|
* used.
|
|
*/
|
|
static SpiceOrbit*
|
|
CreateSpiceOrbit(Hash* orbitData,
|
|
const string& path,
|
|
bool usePlanetUnits)
|
|
{
|
|
string targetBodyName;
|
|
string originName;
|
|
list<string> kernelList;
|
|
double distanceScale;
|
|
double timeScale;
|
|
|
|
GetDefaultUnits(usePlanetUnits, distanceScale, timeScale);
|
|
|
|
if (orbitData->getValue("Kernel") != nullptr)
|
|
{
|
|
// Kernel list is optional; a SPICE orbit may rely on kernels already loaded into
|
|
// the kernel pool.
|
|
if (!ParseStringList(orbitData, "Kernel", kernelList))
|
|
{
|
|
clog << "Kernel list for SPICE orbit is neither a string nor array of strings\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
if (!orbitData->getString("Target", targetBodyName))
|
|
{
|
|
clog << "Target name missing from SPICE orbit\n";
|
|
return nullptr;
|
|
}
|
|
|
|
if (!orbitData->getString("Origin", originName))
|
|
{
|
|
clog << "Origin name missing from SPICE orbit\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// A bounding radius for culling is required for SPICE orbits
|
|
double boundingRadius = 0.0;
|
|
if (!orbitData->getLength("BoundingRadius", boundingRadius, 1.0, distanceScale))
|
|
{
|
|
clog << "Bounding Radius missing from SPICE orbit\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// The period of the orbit may be specified if appropriate; a value
|
|
// of zero for the period (the default), means that the orbit will
|
|
// be considered aperiodic.
|
|
double period = 0.0;
|
|
orbitData->getTime("Period", period, 1.0, timeScale);
|
|
|
|
// Either a complete time interval must be specified with Beginning/Ending, or
|
|
// else neither field can be present.
|
|
Value* beginningDate = orbitData->getValue("Beginning");
|
|
Value* endingDate = orbitData->getValue("Ending");
|
|
if (beginningDate != nullptr && endingDate == nullptr)
|
|
{
|
|
clog << "Beginning specified for SPICE orbit, but ending is missing.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
if (endingDate != nullptr && beginningDate == nullptr)
|
|
{
|
|
clog << "Ending specified for SPICE orbit, but beginning is missing.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
SpiceOrbit* orbit = nullptr;
|
|
if (beginningDate != nullptr && endingDate != nullptr)
|
|
{
|
|
double beginningTDBJD = 0.0;
|
|
if (!ParseDate(orbitData, "Beginning", beginningTDBJD))
|
|
{
|
|
clog << "Invalid beginning date specified for SPICE orbit.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
double endingTDBJD = 0.0;
|
|
if (!ParseDate(orbitData, "Ending", endingTDBJD))
|
|
{
|
|
clog << "Invalid ending date specified for SPICE orbit.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
orbit = new SpiceOrbit(targetBodyName,
|
|
originName,
|
|
period,
|
|
boundingRadius,
|
|
beginningTDBJD,
|
|
endingTDBJD);
|
|
}
|
|
else
|
|
{
|
|
// No time interval given; we'll use whatever coverage window is given
|
|
// in the SPICE kernel.
|
|
orbit = new SpiceOrbit(targetBodyName,
|
|
originName,
|
|
period,
|
|
boundingRadius);
|
|
}
|
|
|
|
if (!orbit->init(path, &kernelList))
|
|
{
|
|
// Error using SPICE library; destroy the orbit; hopefully a
|
|
// fallback is defined in the SSC file.
|
|
delete orbit;
|
|
orbit = nullptr;
|
|
}
|
|
|
|
return orbit;
|
|
}
|
|
|
|
|
|
/*! Create a new rotation model based on a SPICE frame.
|
|
*
|
|
* \code SpiceRotation
|
|
* {
|
|
* Kernel <string|string array> # optional
|
|
* Frame <string>
|
|
* BaseFrame <string> # optional (defaults to ecliptic)
|
|
* Period <number> # optional (units are hours)
|
|
* Beginning <number> # optional
|
|
* Ending <number> # optional
|
|
* } \endcode
|
|
*
|
|
* The Kernel property specifies one or more SPICE kernel files that must be
|
|
* loaded in order for the frame to be defined over the required range. Any
|
|
* already loaded kernels will be used if they contain information relevant
|
|
* for defining the frame.
|
|
* Frame and base name are strings that give SPICE names for the frames. The
|
|
* orientation of the SpiceRotation is the orientation of the frame relative to
|
|
* the base frame. By default, the base frame is eclipj2000.
|
|
* Beginning and Ending specify the valid time range of the SPICE rotation.
|
|
* If the Beginning and Ending are omitted, the rotation model is assumed to
|
|
* be valid at any time. It is an error to specify Beginning without Ending,
|
|
* and vice versa.
|
|
* Period specifies the principal rotation period; it defaults to 0 indicating
|
|
* that the rotation is aperiodic. It is not essential to provide the rotation
|
|
* period; it is only used by Celestia for displaying object information such
|
|
* as sidereal day length.
|
|
*/
|
|
static SpiceRotation*
|
|
CreateSpiceRotation(Hash* rotationData,
|
|
const string& path)
|
|
{
|
|
string frameName;
|
|
string baseFrameName = "eclipj2000";
|
|
list<string> kernelList;
|
|
|
|
if (rotationData->getValue("Kernel") != nullptr)
|
|
{
|
|
// Kernel list is optional; a SPICE rotation may rely on kernels already loaded into
|
|
// the kernel pool.
|
|
if (!ParseStringList(rotationData, "Kernel", kernelList))
|
|
{
|
|
clog << "Kernel list for SPICE rotation is neither a string nor array of strings\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
if (!rotationData->getString("Frame", frameName))
|
|
{
|
|
clog << "Frame name missing from SPICE rotation\n";
|
|
return nullptr;
|
|
}
|
|
|
|
rotationData->getString("BaseFrame", baseFrameName);
|
|
|
|
// The period of the rotation may be specified if appropriate; a value
|
|
// of zero for the period (the default), means that the rotation will
|
|
// be considered aperiodic.
|
|
double period = 0.0;
|
|
rotationData->getTime("Period", period, 1.0, 1.0 / HOURS_PER_DAY);
|
|
|
|
// Either a complete time interval must be specified with Beginning/Ending, or
|
|
// else neither field can be present.
|
|
Value* beginningDate = rotationData->getValue("Beginning");
|
|
Value* endingDate = rotationData->getValue("Ending");
|
|
if (beginningDate != nullptr && endingDate == nullptr)
|
|
{
|
|
clog << "Beginning specified for SPICE rotation, but ending is missing.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
if (endingDate != nullptr && beginningDate == nullptr)
|
|
{
|
|
clog << "Ending specified for SPICE rotation, but beginning is missing.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
SpiceRotation* rotation = nullptr;
|
|
if (beginningDate != nullptr && endingDate != nullptr)
|
|
{
|
|
double beginningTDBJD = 0.0;
|
|
if (!ParseDate(rotationData, "Beginning", beginningTDBJD))
|
|
{
|
|
clog << "Invalid beginning date specified for SPICE rotation.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
double endingTDBJD = 0.0;
|
|
if (!ParseDate(rotationData, "Ending", endingTDBJD))
|
|
{
|
|
clog << "Invalid ending date specified for SPICE rotation.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
rotation = new SpiceRotation(frameName,
|
|
baseFrameName,
|
|
period,
|
|
beginningTDBJD,
|
|
endingTDBJD);
|
|
}
|
|
else
|
|
{
|
|
// No time interval given; rotation is valid at any time.
|
|
rotation = new SpiceRotation(frameName,
|
|
baseFrameName,
|
|
period);
|
|
}
|
|
|
|
if (!rotation->init(path, &kernelList))
|
|
{
|
|
// Error using SPICE library; destroy the rotation.
|
|
delete rotation;
|
|
rotation = nullptr;
|
|
}
|
|
|
|
return rotation;
|
|
}
|
|
#endif
|
|
|
|
|
|
static ScriptedOrbit*
|
|
CreateScriptedOrbit(Hash* orbitData,
|
|
const string& path)
|
|
{
|
|
#if !defined(CELX)
|
|
clog << "ScriptedOrbit not usable without scripting support.\n";
|
|
return nullptr;
|
|
#else
|
|
|
|
// Function name is required
|
|
string funcName;
|
|
if (!orbitData->getString("Function", funcName))
|
|
{
|
|
clog << "Function name missing from script orbit definition.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Module name is optional
|
|
string moduleName;
|
|
orbitData->getString("Module", moduleName);
|
|
|
|
string* pathCopy = new string(path);
|
|
Value* pathValue = new Value(*pathCopy);
|
|
orbitData->addValue("AddonPath", *pathValue);
|
|
|
|
ScriptedOrbit* scriptedOrbit = new ScriptedOrbit();
|
|
if (scriptedOrbit != nullptr)
|
|
{
|
|
if (!scriptedOrbit->initialize(moduleName, funcName, orbitData))
|
|
{
|
|
delete scriptedOrbit;
|
|
scriptedOrbit = nullptr;
|
|
}
|
|
}
|
|
|
|
return scriptedOrbit;
|
|
#endif
|
|
}
|
|
|
|
|
|
Orbit*
|
|
CreateOrbit(const Selection& centralObject,
|
|
Hash* planetData,
|
|
const string& path,
|
|
bool usePlanetUnits)
|
|
{
|
|
Orbit* orbit = nullptr;
|
|
|
|
string customOrbitName;
|
|
if (planetData->getString("CustomOrbit", customOrbitName))
|
|
{
|
|
orbit = GetCustomOrbit(customOrbitName);
|
|
if (orbit != nullptr)
|
|
{
|
|
return orbit;
|
|
}
|
|
clog << "Could not find custom orbit named '" << customOrbitName <<
|
|
"'\n";
|
|
}
|
|
|
|
#ifdef USE_SPICE
|
|
Value* spiceOrbitDataValue = planetData->getValue("SpiceOrbit");
|
|
if (spiceOrbitDataValue != nullptr)
|
|
{
|
|
if (spiceOrbitDataValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect spice orbit syntax.\n";
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
orbit = CreateSpiceOrbit(spiceOrbitDataValue->getHash(), path, usePlanetUnits);
|
|
if (orbit != nullptr)
|
|
{
|
|
return orbit;
|
|
}
|
|
clog << "Bad spice orbit\n";
|
|
DPRINTF(0, "Could not load SPICE orbit\n");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Trajectory calculated by Lua script
|
|
Value* scriptedOrbitValue = planetData->getValue("ScriptedOrbit");
|
|
if (scriptedOrbitValue != nullptr)
|
|
{
|
|
if (scriptedOrbitValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect scripted orbit syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
orbit = CreateScriptedOrbit(scriptedOrbitValue->getHash(), path);
|
|
if (orbit != nullptr)
|
|
return orbit;
|
|
}
|
|
|
|
// New 1.5.0 style for sampled trajectories. Permits specification of
|
|
// precision and interpolation type.
|
|
Value* sampledTrajDataValue = planetData->getValue("SampledTrajectory");
|
|
if (sampledTrajDataValue != nullptr)
|
|
{
|
|
if (sampledTrajDataValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect syntax for SampledTrajectory.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateSampledTrajectory(sampledTrajDataValue->getHash(), path);
|
|
}
|
|
|
|
// Old style for sampled trajectories. Assumes cubic interpolation and
|
|
// single precision.
|
|
string sampOrbitFile;
|
|
if (planetData->getString("SampledOrbit", sampOrbitFile))
|
|
{
|
|
DPRINTF(1, "Attempting to load sampled orbit file '%s'\n",
|
|
sampOrbitFile.c_str());
|
|
ResourceHandle orbitHandle =
|
|
GetTrajectoryManager()->getHandle(TrajectoryInfo(sampOrbitFile,
|
|
path,
|
|
TrajectoryInterpolationCubic,
|
|
TrajectoryPrecisionSingle));
|
|
orbit = GetTrajectoryManager()->find(orbitHandle);
|
|
if (orbit != nullptr)
|
|
{
|
|
return orbit;
|
|
}
|
|
clog << "Could not load sampled orbit file '" << sampOrbitFile << "'\n";
|
|
}
|
|
|
|
Value* orbitDataValue = planetData->getValue("EllipticalOrbit");
|
|
if (orbitDataValue != nullptr)
|
|
{
|
|
if (orbitDataValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect elliptical orbit syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateEllipticalOrbit(orbitDataValue->getHash(),
|
|
usePlanetUnits);
|
|
}
|
|
|
|
// Create an 'orbit' that places the object at a fixed point in its
|
|
// reference frame. There are two forms for FixedPosition: a simple
|
|
// form with an 3-vector value, and complex form with a properlist
|
|
// value. The simple form:
|
|
//
|
|
// FixedPosition [ x y z ]
|
|
//
|
|
// is a shorthand for:
|
|
//
|
|
// FixedPosition { Rectangular [ x y z ] }
|
|
//
|
|
// In addition to Rectangular, other coordinate types for fixed position are
|
|
// Planetographic and Planetocentric.
|
|
Value* fixedPositionValue = planetData->getValue("FixedPosition");
|
|
if (fixedPositionValue != nullptr)
|
|
{
|
|
Vector3d fixedPosition = Vector3d::Zero();
|
|
double distanceScale;
|
|
GetDefaultUnits(usePlanetUnits, distanceScale);
|
|
|
|
if (planetData->getLengthVector("FixedPosition", fixedPosition, 1.0, distanceScale))
|
|
{
|
|
// Convert to Celestia's coordinate system
|
|
fixedPosition = Vector3d(fixedPosition.x(),
|
|
fixedPosition.z(),
|
|
-fixedPosition.y());
|
|
|
|
return new FixedOrbit(fixedPosition);
|
|
}
|
|
if (fixedPositionValue->getType() == Value::HashType)
|
|
{
|
|
return CreateFixedPosition(fixedPositionValue->getHash(), centralObject, usePlanetUnits);
|
|
}
|
|
|
|
clog << "Object has incorrect FixedPosition syntax.\n";
|
|
}
|
|
|
|
// LongLat will make an object fixed relative to the surface of its center
|
|
// object. This is done by creating an orbit with a period equal to the
|
|
// rotation rate of the parent object. A body-fixed reference frame is a
|
|
// much better way to accomplish this.
|
|
Vector3d longlat = Vector3d::Zero();
|
|
if (planetData->getSphericalTuple("LongLat", longlat))
|
|
{
|
|
Body* centralBody = centralObject.body();
|
|
if (centralBody != nullptr)
|
|
{
|
|
Vector3d pos = centralBody->planetocentricToCartesian(longlat.x(), longlat.y(), longlat.z());
|
|
return new SynchronousOrbit(*centralBody, pos);
|
|
}
|
|
// TODO: Allow fixing objects to the surface of stars.
|
|
return nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
static ConstantOrientation*
|
|
CreateFixedRotationModel(double offset,
|
|
double inclination,
|
|
double ascendingNode)
|
|
{
|
|
Quaterniond q = YRotation(-PI - offset) *
|
|
XRotation(-inclination) *
|
|
YRotation(-ascendingNode);
|
|
|
|
return new ConstantOrientation(q);
|
|
}
|
|
|
|
|
|
static RotationModel*
|
|
CreateUniformRotationModel(Hash* rotationData,
|
|
double syncRotationPeriod)
|
|
{
|
|
// Default to synchronous rotation
|
|
double period = syncRotationPeriod;
|
|
rotationData->getTime("Period", period, 1.0, 1.0 / HOURS_PER_DAY);
|
|
|
|
float offset = 0.0f;
|
|
if (rotationData->getAngle("MeridianAngle", offset))
|
|
{
|
|
offset = degToRad(offset);
|
|
}
|
|
|
|
double epoch = astro::J2000;
|
|
ParseDate(rotationData, "Epoch", epoch);
|
|
|
|
float inclination = 0.0f;
|
|
if (rotationData->getAngle("Inclination", inclination))
|
|
{
|
|
inclination = degToRad(inclination);
|
|
}
|
|
|
|
float ascendingNode = 0.0f;
|
|
if (rotationData->getAngle("AscendingNode", ascendingNode))
|
|
{
|
|
ascendingNode = degToRad(ascendingNode);
|
|
}
|
|
|
|
// No period was specified, and the default synchronous
|
|
// rotation period is zero, indicating that the object
|
|
// doesn't have a periodic orbit. Default to a constant
|
|
// orientation instead.
|
|
if (period == 0.0)
|
|
{
|
|
return CreateFixedRotationModel(offset, inclination, ascendingNode);
|
|
}
|
|
else
|
|
{
|
|
return new UniformRotationModel(period,
|
|
offset,
|
|
epoch,
|
|
inclination,
|
|
ascendingNode);
|
|
}
|
|
}
|
|
|
|
|
|
static ConstantOrientation*
|
|
CreateFixedRotationModel(Hash* rotationData)
|
|
{
|
|
double offset = 0.0;
|
|
if (rotationData->getAngle("MeridianAngle", offset))
|
|
{
|
|
offset = degToRad(offset);
|
|
}
|
|
|
|
double inclination = 0.0;
|
|
if (rotationData->getAngle("Inclination", inclination))
|
|
{
|
|
inclination = degToRad(inclination);
|
|
}
|
|
|
|
double ascendingNode = 0.0;
|
|
if (rotationData->getAngle("AscendingNode", ascendingNode))
|
|
{
|
|
ascendingNode = degToRad(ascendingNode);
|
|
}
|
|
|
|
Quaterniond q = YRotation(-PI - offset) *
|
|
XRotation(-inclination) *
|
|
YRotation(-ascendingNode);
|
|
|
|
return new ConstantOrientation(q);
|
|
}
|
|
|
|
|
|
static ConstantOrientation*
|
|
CreateFixedAttitudeRotationModel(Hash* rotationData)
|
|
{
|
|
double heading = 0.0;
|
|
if (rotationData->getAngle("Heading", heading))
|
|
{
|
|
heading = degToRad(heading);
|
|
}
|
|
|
|
double tilt = 0.0;
|
|
if (rotationData->getAngle("Tilt", tilt))
|
|
{
|
|
tilt = degToRad(tilt);
|
|
}
|
|
|
|
double roll = 0.0;
|
|
if (rotationData->getAngle("Roll", roll))
|
|
{
|
|
roll = degToRad(roll);
|
|
}
|
|
|
|
Quaterniond q = YRotation(-PI - heading) *
|
|
XRotation(-tilt) *
|
|
ZRotation(-roll);
|
|
|
|
return new ConstantOrientation(q);
|
|
}
|
|
|
|
|
|
static RotationModel*
|
|
CreatePrecessingRotationModel(Hash* rotationData,
|
|
double syncRotationPeriod)
|
|
{
|
|
// Default to synchronous rotation
|
|
double period = syncRotationPeriod;
|
|
rotationData->getTime("Period", period, 1.0, 1.0 / HOURS_PER_DAY);
|
|
|
|
float offset = 0.0f;
|
|
if (rotationData->getAngle("MeridianAngle", offset))
|
|
{
|
|
offset = degToRad(offset);
|
|
}
|
|
|
|
double epoch = astro::J2000;
|
|
ParseDate(rotationData, "Epoch", epoch);
|
|
|
|
float inclination = 0.0f;
|
|
if (rotationData->getAngle("Inclination", inclination))
|
|
{
|
|
inclination = degToRad(inclination);
|
|
}
|
|
|
|
float ascendingNode = 0.0f;
|
|
if (rotationData->getAngle("AscendingNode", ascendingNode))
|
|
{
|
|
ascendingNode = degToRad(ascendingNode);
|
|
}
|
|
|
|
// The default value of 0 is handled specially, interpreted to indicate
|
|
// that there's no precession.
|
|
double precessionPeriod = 0.0;
|
|
rotationData->getTime("PrecessionPeriod", precessionPeriod, 1.0, DAYS_PER_YEAR);
|
|
|
|
// No period was specified, and the default synchronous
|
|
// rotation period is zero, indicating that the object
|
|
// doesn't have a periodic orbit. Default to a constant
|
|
// orientation instead.
|
|
if (period == 0.0)
|
|
{
|
|
return CreateFixedRotationModel(offset, inclination, ascendingNode);
|
|
}
|
|
else
|
|
{
|
|
return new PrecessingRotationModel(period,
|
|
offset,
|
|
epoch,
|
|
inclination,
|
|
ascendingNode,
|
|
precessionPeriod);
|
|
}
|
|
}
|
|
|
|
|
|
static ScriptedRotation*
|
|
CreateScriptedRotation(Hash* rotationData,
|
|
const string& path)
|
|
{
|
|
#if !defined(CELX)
|
|
clog << "ScriptedRotation not usable without scripting support.\n";
|
|
return nullptr;
|
|
#else
|
|
|
|
// Function name is required
|
|
string funcName;
|
|
if (!rotationData->getString("Function", funcName))
|
|
{
|
|
clog << "Function name missing from scripted rotation definition.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Module name is optional
|
|
string moduleName;
|
|
rotationData->getString("Module", moduleName);
|
|
|
|
string* pathCopy = new string(path);
|
|
Value* pathValue = new Value(*pathCopy);
|
|
rotationData->addValue("AddonPath", *pathValue);
|
|
|
|
ScriptedRotation* scriptedRotation = new ScriptedRotation();
|
|
if (scriptedRotation != nullptr)
|
|
{
|
|
if (!scriptedRotation->initialize(moduleName, funcName, rotationData))
|
|
{
|
|
delete scriptedRotation;
|
|
scriptedRotation = nullptr;
|
|
}
|
|
}
|
|
|
|
return scriptedRotation;
|
|
#endif
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse rotation information. Unfortunately, Celestia didn't originally have
|
|
* RotationModel objects, so information about the rotation of the object isn't
|
|
* grouped into a single subobject--the ssc fields relevant for rotation just
|
|
* appear in the top level structure.
|
|
*/
|
|
RotationModel*
|
|
CreateRotationModel(Hash* planetData,
|
|
const string& path,
|
|
double syncRotationPeriod)
|
|
{
|
|
RotationModel* rotationModel = nullptr;
|
|
|
|
// If more than one rotation model is specified, the following precedence
|
|
// is used to determine which one should be used:
|
|
// CustomRotation
|
|
// SPICE C-Kernel
|
|
// SampledOrientation
|
|
// PrecessingRotation
|
|
// UniformRotation
|
|
// legacy rotation parameters
|
|
|
|
string customRotationModelName;
|
|
if (planetData->getString("CustomRotation", customRotationModelName))
|
|
{
|
|
rotationModel = GetCustomRotationModel(customRotationModelName);
|
|
if (rotationModel != nullptr)
|
|
{
|
|
return rotationModel;
|
|
}
|
|
clog << "Could not find custom rotation model named '" <<
|
|
customRotationModelName << "'\n";
|
|
}
|
|
|
|
#ifdef USE_SPICE
|
|
Value* spiceRotationDataValue = planetData->getValue("SpiceRotation");
|
|
if (spiceRotationDataValue != nullptr)
|
|
{
|
|
if (spiceRotationDataValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect spice rotation syntax.\n";
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
rotationModel = CreateSpiceRotation(spiceRotationDataValue->getHash(), path);
|
|
if (rotationModel != nullptr)
|
|
{
|
|
return rotationModel;
|
|
}
|
|
clog << "Bad spice rotation model\n";
|
|
DPRINTF(0, "Could not load SPICE rotation model\n");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
Value* scriptedRotationValue = planetData->getValue("ScriptedRotation");
|
|
if (scriptedRotationValue != nullptr)
|
|
{
|
|
if (scriptedRotationValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect scripted rotation syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
rotationModel = CreateScriptedRotation(scriptedRotationValue->getHash(), path);
|
|
if (rotationModel != nullptr)
|
|
return rotationModel;
|
|
|
|
}
|
|
|
|
string sampOrientationFile;
|
|
if (planetData->getString("SampledOrientation", sampOrientationFile))
|
|
{
|
|
DPRINTF(1, "Attempting to load orientation file '%s'\n",
|
|
sampOrientationFile.c_str());
|
|
ResourceHandle orientationHandle =
|
|
GetRotationModelManager()->getHandle(RotationModelInfo(sampOrientationFile, path));
|
|
rotationModel = GetRotationModelManager()->find(orientationHandle);
|
|
if (rotationModel != nullptr)
|
|
{
|
|
return rotationModel;
|
|
}
|
|
|
|
clog << "Could not load rotation model file '" <<
|
|
sampOrientationFile << "'\n";
|
|
}
|
|
|
|
Value* precessingRotationValue = planetData->getValue("PrecessingRotation");
|
|
if (precessingRotationValue != nullptr)
|
|
{
|
|
if (precessingRotationValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect syntax for precessing rotation.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreatePrecessingRotationModel(precessingRotationValue->getHash(),
|
|
syncRotationPeriod);
|
|
}
|
|
|
|
Value* uniformRotationValue = planetData->getValue("UniformRotation");
|
|
if (uniformRotationValue != nullptr)
|
|
{
|
|
if (uniformRotationValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect UniformRotation syntax.\n";
|
|
return nullptr;
|
|
}
|
|
return CreateUniformRotationModel(uniformRotationValue->getHash(),
|
|
syncRotationPeriod);
|
|
}
|
|
|
|
Value* fixedRotationValue = planetData->getValue("FixedRotation");
|
|
if (fixedRotationValue != nullptr)
|
|
{
|
|
if (fixedRotationValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect FixedRotation syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateFixedRotationModel(fixedRotationValue->getHash());
|
|
}
|
|
|
|
Value* fixedAttitudeValue = planetData->getValue("FixedAttitude");
|
|
if (fixedAttitudeValue != nullptr)
|
|
{
|
|
if (fixedAttitudeValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect FixedAttitude syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateFixedAttitudeRotationModel(fixedAttitudeValue->getHash());
|
|
}
|
|
|
|
// For backward compatibility we need to support rotation parameters
|
|
// that appear in the main block of the object definition.
|
|
// Default to synchronous rotation
|
|
bool specified = false;
|
|
double period = syncRotationPeriod;
|
|
if (planetData->getNumber("RotationPeriod", period))
|
|
{
|
|
specified = true;
|
|
period = period / 24.0f;
|
|
}
|
|
|
|
float offset = 0.0f;
|
|
if (planetData->getNumber("RotationOffset", offset))
|
|
{
|
|
specified = true;
|
|
offset = degToRad(offset);
|
|
}
|
|
|
|
double epoch = astro::J2000;
|
|
if (ParseDate(planetData, "RotationEpoch", epoch))
|
|
{
|
|
specified = true;
|
|
}
|
|
|
|
float inclination = 0.0f;
|
|
if (planetData->getNumber("Obliquity", inclination))
|
|
{
|
|
specified = true;
|
|
inclination = degToRad(inclination);
|
|
}
|
|
|
|
float ascendingNode = 0.0f;
|
|
if (planetData->getNumber("EquatorAscendingNode", ascendingNode))
|
|
{
|
|
specified = true;
|
|
ascendingNode = degToRad(ascendingNode);
|
|
}
|
|
|
|
double precessionRate = 0.0f;
|
|
if (planetData->getNumber("PrecessionRate", precessionRate))
|
|
{
|
|
specified = true;
|
|
}
|
|
|
|
if (specified)
|
|
{
|
|
RotationModel* rm = nullptr;
|
|
if (period == 0.0)
|
|
{
|
|
// No period was specified, and the default synchronous
|
|
// rotation period is zero, indicating that the object
|
|
// doesn't have a periodic orbit. Default to a constant
|
|
// orientation instead.
|
|
rm = CreateFixedRotationModel(offset, inclination, ascendingNode);
|
|
}
|
|
else if (precessionRate == 0.0)
|
|
{
|
|
rm = new UniformRotationModel(period,
|
|
offset,
|
|
epoch,
|
|
inclination,
|
|
ascendingNode);
|
|
}
|
|
else
|
|
{
|
|
rm = new PrecessingRotationModel(period,
|
|
offset,
|
|
epoch,
|
|
inclination,
|
|
ascendingNode,
|
|
-360.0 / precessionRate);
|
|
}
|
|
|
|
return rm;
|
|
}
|
|
else
|
|
{
|
|
// No rotation fields specified
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
RotationModel* CreateDefaultRotationModel(double syncRotationPeriod)
|
|
{
|
|
if (syncRotationPeriod == 0.0)
|
|
{
|
|
// If syncRotationPeriod is 0, the orbit of the object is
|
|
// aperiodic and we'll just return a FixedRotation.
|
|
return new ConstantOrientation(Quaterniond::Identity());
|
|
}
|
|
else
|
|
{
|
|
return new UniformRotationModel(syncRotationPeriod,
|
|
0.0f,
|
|
astro::J2000,
|
|
0.0f,
|
|
0.0f);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the center object of a frame definition. Return an empty selection
|
|
* if it's missing or refers to an object that doesn't exist.
|
|
*/
|
|
static Selection
|
|
getFrameCenter(const Universe& universe, Hash* frameData, const Selection& defaultCenter)
|
|
{
|
|
string centerName;
|
|
if (!frameData->getString("Center", centerName))
|
|
{
|
|
if (defaultCenter.empty())
|
|
cerr << "No center specified for reference frame.\n";
|
|
return defaultCenter;
|
|
}
|
|
|
|
Selection centerObject = universe.findPath(centerName, nullptr, 0);
|
|
if (centerObject.empty())
|
|
{
|
|
cerr << "Center object '" << centerName << "' of reference frame not found.\n";
|
|
return Selection();
|
|
}
|
|
|
|
// Should verify that center object is a star or planet, and
|
|
// that it is a member of the same star system as the body in which
|
|
// the frame will be used.
|
|
|
|
return centerObject;
|
|
}
|
|
|
|
|
|
static BodyFixedFrame*
|
|
CreateBodyFixedFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultCenter)
|
|
{
|
|
Selection center = getFrameCenter(universe, frameData, defaultCenter);
|
|
if (center.empty())
|
|
return nullptr;
|
|
|
|
return new BodyFixedFrame(center, center);
|
|
}
|
|
|
|
|
|
static BodyMeanEquatorFrame*
|
|
CreateMeanEquatorFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultCenter)
|
|
{
|
|
Selection center = getFrameCenter(universe, frameData, defaultCenter);
|
|
if (center.empty())
|
|
return nullptr;
|
|
|
|
Selection obj = center;
|
|
string objName;
|
|
if (frameData->getString("Object", objName))
|
|
{
|
|
obj = universe.findPath(objName, nullptr, 0);
|
|
if (obj.empty())
|
|
{
|
|
clog << "Object '" << objName << "' for mean equator frame not found.\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
clog << "CreateMeanEquatorFrame " << center.getName() << ", " << obj.getName() << "\n";
|
|
|
|
double freezeEpoch = 0.0;
|
|
if (ParseDate(frameData, "Freeze", freezeEpoch))
|
|
{
|
|
return new BodyMeanEquatorFrame(center, obj, freezeEpoch);
|
|
}
|
|
else
|
|
{
|
|
return new BodyMeanEquatorFrame(center, obj);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert a string to an axis label. Permitted axis labels are
|
|
* x, y, z, -x, -y, and -z. +x, +y, and +z are allowed as synonyms for
|
|
* x, y, z. Case is ignored.
|
|
*/
|
|
static int
|
|
parseAxisLabel(const std::string& label)
|
|
{
|
|
if (compareIgnoringCase(label, "x") == 0 ||
|
|
compareIgnoringCase(label, "+x") == 0)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (compareIgnoringCase(label, "y") == 0 ||
|
|
compareIgnoringCase(label, "+y") == 0)
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
if (compareIgnoringCase(label, "z") == 0 ||
|
|
compareIgnoringCase(label, "+z") == 0)
|
|
{
|
|
return 3;
|
|
}
|
|
|
|
if (compareIgnoringCase(label, "-x") == 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (compareIgnoringCase(label, "-y") == 0)
|
|
{
|
|
return -2;
|
|
}
|
|
|
|
if (compareIgnoringCase(label, "-z") == 0)
|
|
{
|
|
return -3;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
getAxis(Hash* vectorData)
|
|
{
|
|
string axisLabel;
|
|
if (!vectorData->getString("Axis", axisLabel))
|
|
{
|
|
DPRINTF(0, "Bad two-vector frame: missing axis label for vector.\n");
|
|
return 0;
|
|
}
|
|
|
|
int axis = parseAxisLabel(axisLabel);
|
|
if (axis == 0)
|
|
{
|
|
DPRINTF(0, "Bad two-vector frame: vector has invalid axis label.\n");
|
|
}
|
|
|
|
// Permute axis labels to match non-standard Celestia coordinate
|
|
// conventions: y <- z, z <- -y
|
|
switch (axis)
|
|
{
|
|
case 2:
|
|
return -3;
|
|
case -2:
|
|
return 3;
|
|
case 3:
|
|
return 2;
|
|
case -3:
|
|
return -2;
|
|
default:
|
|
return axis;
|
|
}
|
|
|
|
return axis;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the target object of a direction vector definition. Return an
|
|
* empty selection if it's missing or refers to an object that doesn't exist.
|
|
*/
|
|
static Selection
|
|
getVectorTarget(const Universe& universe, Hash* vectorData)
|
|
{
|
|
string targetName;
|
|
if (!vectorData->getString("Target", targetName))
|
|
{
|
|
clog << "Bad two-vector frame: no target specified for vector.\n";
|
|
return Selection();
|
|
}
|
|
|
|
Selection targetObject = universe.findPath(targetName, nullptr, 0);
|
|
if (targetObject.empty())
|
|
{
|
|
clog << "Bad two-vector frame: target object '" << targetName << "' of vector not found.\n";
|
|
return Selection();
|
|
}
|
|
|
|
return targetObject;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the observer object of a direction vector definition. Return an
|
|
* empty selection if it's missing or refers to an object that doesn't exist.
|
|
*/
|
|
static Selection
|
|
getVectorObserver(const Universe& universe, Hash* vectorData)
|
|
{
|
|
string obsName;
|
|
if (!vectorData->getString("Observer", obsName))
|
|
{
|
|
// Omission of observer is permitted; it will default to the
|
|
// frame center.
|
|
return Selection();
|
|
}
|
|
|
|
Selection obsObject = universe.findPath(obsName, nullptr, 0);
|
|
if (obsObject.empty())
|
|
{
|
|
clog << "Bad two-vector frame: observer object '" << obsObject.getName() << "' of vector not found.\n";
|
|
return Selection();
|
|
}
|
|
|
|
return obsObject;
|
|
}
|
|
|
|
|
|
static FrameVector*
|
|
CreateFrameVector(const Universe& universe,
|
|
const Selection& center,
|
|
Hash* vectorData)
|
|
{
|
|
Value* value = nullptr;
|
|
|
|
value = vectorData->getValue("RelativePosition");
|
|
if (value != nullptr && value->getHash() != nullptr)
|
|
{
|
|
Hash* relPosData = value->getHash();
|
|
Selection observer = getVectorObserver(universe, relPosData);
|
|
Selection target = getVectorTarget(universe, relPosData);
|
|
// Default observer is the frame center
|
|
if (observer.empty())
|
|
observer = center;
|
|
|
|
if (observer.empty() || target.empty())
|
|
return nullptr;
|
|
|
|
return new FrameVector(FrameVector::createRelativePositionVector(observer, target));
|
|
}
|
|
|
|
value = vectorData->getValue("RelativeVelocity");
|
|
if (value != nullptr && value->getHash() != nullptr)
|
|
{
|
|
Hash* relVData = value->getHash();
|
|
Selection observer = getVectorObserver(universe, relVData);
|
|
Selection target = getVectorTarget(universe, relVData);
|
|
// Default observer is the frame center
|
|
if (observer.empty())
|
|
observer = center;
|
|
|
|
if (observer.empty() || target.empty())
|
|
return nullptr;
|
|
|
|
return new FrameVector(FrameVector::createRelativeVelocityVector(observer, target));
|
|
}
|
|
|
|
value = vectorData->getValue("ConstantVector");
|
|
if (value != nullptr && value->getHash() != nullptr)
|
|
{
|
|
Hash* constVecData = value->getHash();
|
|
Vector3d vec = Vector3d::UnitZ();
|
|
constVecData->getVector("Vector", vec);
|
|
if (vec.norm() == 0.0)
|
|
{
|
|
clog << "Bad two-vector frame: constant vector has length zero\n";
|
|
return nullptr;
|
|
}
|
|
vec.normalize();
|
|
vec = Vector3d(vec.x(), vec.z(), -vec.y());
|
|
|
|
// The frame for the vector is optional; a nullptr frame indicates
|
|
// J2000 ecliptic.
|
|
ReferenceFrame* f = nullptr;
|
|
Value* frameValue = constVecData->getValue("Frame");
|
|
if (frameValue != nullptr)
|
|
{
|
|
f = CreateReferenceFrame(universe, frameValue, center, nullptr);
|
|
if (f == nullptr)
|
|
return nullptr;
|
|
}
|
|
|
|
return new FrameVector(FrameVector::createConstantVector(vec, f));
|
|
}
|
|
else
|
|
{
|
|
clog << "Bad two-vector frame: unknown vector type\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
static TwoVectorFrame*
|
|
CreateTwoVectorFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultCenter)
|
|
{
|
|
Selection center = getFrameCenter(universe, frameData, defaultCenter);
|
|
if (center.empty())
|
|
return nullptr;
|
|
|
|
// Primary and secondary vector definitions are required
|
|
Value* primaryValue = frameData->getValue("Primary");
|
|
if (primaryValue == nullptr)
|
|
{
|
|
clog << "Primary axis missing from two-vector frame.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
Hash* primaryData = primaryValue->getHash();
|
|
if (primaryData == nullptr)
|
|
{
|
|
clog << "Bad syntax for primary axis of two-vector frame.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
Value* secondaryValue = frameData->getValue("Secondary");
|
|
if (secondaryValue == nullptr)
|
|
{
|
|
clog << "Secondary axis missing from two-vector frame.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
Hash* secondaryData = secondaryValue->getHash();
|
|
if (secondaryData == nullptr)
|
|
{
|
|
clog << "Bad syntax for secondary axis of two-vector frame.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Get and validate the axes for the direction vectors
|
|
int primaryAxis = getAxis(primaryData);
|
|
int secondaryAxis = getAxis(secondaryData);
|
|
|
|
assert(abs(primaryAxis) <= 3);
|
|
assert(abs(secondaryAxis) <= 3);
|
|
if (primaryAxis == 0 || secondaryAxis == 0)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (abs(primaryAxis) == abs(secondaryAxis))
|
|
{
|
|
clog << "Bad two-vector frame: axes for vectors are collinear.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
FrameVector* primaryVector = CreateFrameVector(universe,
|
|
center,
|
|
primaryData);
|
|
FrameVector* secondaryVector = CreateFrameVector(universe,
|
|
center,
|
|
secondaryData);
|
|
|
|
TwoVectorFrame* frame = nullptr;
|
|
if (primaryVector != nullptr && secondaryVector != nullptr)
|
|
{
|
|
frame = new TwoVectorFrame(center,
|
|
*primaryVector, primaryAxis,
|
|
*secondaryVector, secondaryAxis);
|
|
}
|
|
|
|
delete primaryVector;
|
|
delete secondaryVector;
|
|
|
|
return frame;
|
|
}
|
|
|
|
|
|
static J2000EclipticFrame*
|
|
CreateJ2000EclipticFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultCenter)
|
|
{
|
|
Selection center = getFrameCenter(universe, frameData, defaultCenter);
|
|
|
|
if (center.empty())
|
|
return nullptr;
|
|
|
|
return new J2000EclipticFrame(center);
|
|
}
|
|
|
|
|
|
static J2000EquatorFrame*
|
|
CreateJ2000EquatorFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultCenter)
|
|
{
|
|
Selection center = getFrameCenter(universe, frameData, defaultCenter);
|
|
|
|
if (center.empty())
|
|
return nullptr;
|
|
|
|
return new J2000EquatorFrame(center);
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper function for CreateTopocentricFrame().
|
|
* Creates a two-vector frame with the specified center, target, and observer.
|
|
*/
|
|
TwoVectorFrame*
|
|
CreateTopocentricFrame(const Selection& center,
|
|
const Selection& target,
|
|
const Selection& observer)
|
|
{
|
|
BodyMeanEquatorFrame* eqFrame = new BodyMeanEquatorFrame(target, target);
|
|
FrameVector north = FrameVector::createConstantVector(Vector3d::UnitY(), eqFrame);
|
|
FrameVector up = FrameVector::createRelativePositionVector(observer, target);
|
|
|
|
return new TwoVectorFrame(center, up, -2, north, -3);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new Topocentric frame. The topocentric frame is designed to make it easy
|
|
* to place objects on the surface of a planet or moon. The z-axis will point toward
|
|
* the observer's zenith (which here is the direction away from the center of the
|
|
* planet.) The x-axis will point in the local north direction. The equivalent
|
|
* two-vector frame is:
|
|
*
|
|
* \code TwoVector
|
|
* {
|
|
* Center <center>
|
|
* Primary
|
|
* {
|
|
* Axis "z"
|
|
* RelativePosition { Target <target> Observer <observer> }
|
|
* }
|
|
* Secondary
|
|
* {
|
|
* Axis "x"
|
|
* ConstantVector
|
|
* {
|
|
* Vector [ 0 0 1]
|
|
* Frame { BodyFixed { Center <target> } }
|
|
* }
|
|
* }
|
|
* } \endcode
|
|
*
|
|
* Typically, the topocentric frame is used as a BodyFrame to orient an
|
|
* object on the surface of a planet. In this situation, the observer is
|
|
* object itself and the target object is the planet. In fact, these are
|
|
* the defaults: when no target, observer, or center is specified, the
|
|
* observer and center are both 'self' and the target is the parent
|
|
* object. Thus, for a Mars rover, using a topocentric frame is as simple
|
|
* as:
|
|
*
|
|
* <pre> "Rover" "Sol/Mars"
|
|
* {
|
|
* BodyFrame { Topocentric { } }
|
|
* ...
|
|
* } </pre>
|
|
*/
|
|
static TwoVectorFrame*
|
|
CreateTopocentricFrame(const Universe& universe,
|
|
Hash* frameData,
|
|
const Selection& defaultTarget,
|
|
const Selection& defaultObserver)
|
|
{
|
|
Selection target;
|
|
Selection observer;
|
|
Selection center;
|
|
|
|
string centerName;
|
|
if (frameData->getString("Center", centerName))
|
|
{
|
|
// If a center is provided, the default observer is the center and
|
|
// the default target is the center's parent. This gives sensible results
|
|
// when a topocentric frame is used as an orbit frame.
|
|
center = universe.findPath(centerName, nullptr, 0);
|
|
if (center.empty())
|
|
{
|
|
cerr << "Center object '" << centerName << "' for topocentric frame not found.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
observer = center;
|
|
target = center.parent();
|
|
}
|
|
else
|
|
{
|
|
// When no center is provided, use the default observer as the center. This
|
|
// is typical when a topocentric frame is the body frame. The default observer
|
|
// is usually the object itself.
|
|
target = defaultTarget;
|
|
observer = defaultObserver;
|
|
center = defaultObserver;
|
|
}
|
|
|
|
string targetName;
|
|
if (!frameData->getString("Target", targetName))
|
|
{
|
|
if (target.empty())
|
|
{
|
|
cerr << "No target specified for topocentric frame.\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target = universe.findPath(targetName, nullptr, 0);
|
|
if (target.empty())
|
|
{
|
|
cerr << "Target object '" << targetName << "' for topocentric frame not found.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
// Should verify that center object is a star or planet, and
|
|
// that it is a member of the same star system as the body in which
|
|
// the frame will be used.
|
|
}
|
|
|
|
string observerName;
|
|
if (!frameData->getString("Observer", observerName))
|
|
{
|
|
if (observer.empty())
|
|
{
|
|
cerr << "No observer specified for topocentric frame.\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
observer = universe.findPath(observerName, nullptr, 0);
|
|
if (observer.empty())
|
|
{
|
|
cerr << "Observer object '" << observerName << "' for topocentric frame not found.\n";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return CreateTopocentricFrame(center, target, observer);
|
|
}
|
|
|
|
|
|
static ReferenceFrame*
|
|
CreateComplexFrame(const Universe& universe, Hash* frameData, const Selection& defaultCenter, Body* defaultObserver)
|
|
{
|
|
Value* value = frameData->getValue("BodyFixed");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect body-fixed frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateBodyFixedFrame(universe, value->getHash(), defaultCenter);
|
|
}
|
|
|
|
value = frameData->getValue("MeanEquator");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect mean equator frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateMeanEquatorFrame(universe, value->getHash(), defaultCenter);
|
|
}
|
|
|
|
value = frameData->getValue("TwoVector");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect two-vector frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateTwoVectorFrame(universe, value->getHash(), defaultCenter);
|
|
}
|
|
|
|
value = frameData->getValue("Topocentric");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect topocentric frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateTopocentricFrame(universe, value->getHash(), defaultCenter, Selection(defaultObserver));
|
|
}
|
|
|
|
value = frameData->getValue("EclipticJ2000");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect J2000 ecliptic frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateJ2000EclipticFrame(universe, value->getHash(), defaultCenter);
|
|
}
|
|
|
|
value = frameData->getValue("EquatorJ2000");
|
|
if (value != nullptr)
|
|
{
|
|
if (value->getType() != Value::HashType)
|
|
{
|
|
clog << "Object has incorrect J2000 equator frame syntax.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateJ2000EquatorFrame(universe, value->getHash(), defaultCenter);
|
|
}
|
|
|
|
clog << "Frame definition does not have a valid frame type.\n";
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
ReferenceFrame* CreateReferenceFrame(const Universe& universe,
|
|
Value* frameValue,
|
|
const Selection& defaultCenter,
|
|
Body* defaultObserver)
|
|
{
|
|
if (frameValue->getType() == Value::StringType)
|
|
{
|
|
// TODO: handle named frames
|
|
clog << "Invalid syntax for frame definition.\n";
|
|
return nullptr;
|
|
}
|
|
if (frameValue->getType() != Value::HashType)
|
|
{
|
|
clog << "Invalid syntax for frame definition.\n";
|
|
return nullptr;
|
|
}
|
|
|
|
return CreateComplexFrame(universe, frameValue->getHash(), defaultCenter, defaultObserver);
|
|
}
|