// frame.cpp // // Reference frame base class. // // Copyright (C) 2003-2009, the Celestia Development Team // Original version by Chris Laurel // // 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 #include using namespace Eigen; using namespace std; // Velocity for two-vector frames is computed by differentiation; units // are Julian days. static const double ANGULAR_VELOCITY_DIFF_DELTA = 1.0 / 1440.0; /*** ReferenceFrame ***/ ReferenceFrame::ReferenceFrame(Selection center) : centerObject(center), refCount(0) { } int ReferenceFrame::addRef() const { return ++refCount; } int ReferenceFrame::release() const { --refCount; assert(refCount >= 0); int refCountCopy = refCount; if (refCount <= 0) delete this; return refCountCopy; } // High-precision rotation using 64.64 fixed point path. Rotate uc by // the rotation specified by unit quaternion q. static UniversalCoord rotate(const UniversalCoord& uc, const Quaterniond& q) { Matrix3d r = q.toRotationMatrix(); UniversalCoord uc1; uc1.x = uc.x * BigFix(r(0, 0)) + uc.y * BigFix(r(1, 0)) + uc.z * BigFix(r(2, 0)); uc1.y = uc.x * BigFix(r(0, 1)) + uc.y * BigFix(r(1, 1)) + uc.z * BigFix(r(2, 1)); uc1.z = uc.x * BigFix(r(0, 2)) + uc.y * BigFix(r(1, 2)) + uc.z * BigFix(r(2, 2)); return uc1; } /*! Convert from universal coordinates to frame coordinates. This method * uses 64.64 fixed point arithmetic in conversion, and is thus /much/ slower * than convertFromAstrocentric(), which works with double precision * floating points values. For cases when the bodies are all in the same * solar system, convertFromAstrocentric() should be used. */ UniversalCoord ReferenceFrame::convertFromUniversal(const UniversalCoord& uc, double tjd) const { UniversalCoord uc1 = uc.difference(centerObject.getPosition(tjd)); return rotate(uc1, getOrientation(tjd).conjugate()); } Quaterniond ReferenceFrame::convertFromUniversal(const Quaterniond& q, double tjd) const { return q * getOrientation(tjd).conjugate(); } /*! Convert from local coordinates to universal coordinates. This method * uses 64.64 fixed point arithmetic in conversion, and is thus /much/ slower * than convertFromAstrocentric(), which works with double precision * floating points values. For cases when the bodies are all in the same * solar system, convertFromAstrocentric() should be used. * * To get the position of a solar system object in universal coordinates, * it usually suffices to get the astrocentric position and then add that * to the position of the star in universal coordinates. This avoids any * expensive high-precision multiplication. */ UniversalCoord ReferenceFrame::convertToUniversal(const UniversalCoord& uc, double tjd) const { return centerObject.getPosition(tjd) + rotate(uc, getOrientation(tjd)); } Quaterniond ReferenceFrame::convertToUniversal(const Quaterniond& q, double tjd) const { return q * getOrientation(tjd); } Vector3d ReferenceFrame::convertFromAstrocentric(const Vector3d& p, double tjd) const { if (centerObject.getType() == Selection::Type_Body) { Vector3d center = centerObject.body()->getAstrocentricPosition(tjd); return getOrientation(tjd) * (p - center); } else if (centerObject.getType() == Selection::Type_Star) { return getOrientation(tjd) * p; } else { // TODO: // bad if the center object is a galaxy // what about locations? return Vector3d::Zero(); } } Vector3d ReferenceFrame::convertToAstrocentric(const Vector3d& p, double tjd) const { if (centerObject.getType() == Selection::Type_Body) { Vector3d center = centerObject.body()->getAstrocentricPosition(tjd); return center + getOrientation(tjd).conjugate() * p; } else if (centerObject.getType() == Selection::Type_Star) { return getOrientation(tjd).conjugate() * p; } else { // TODO: // bad if the center object is a galaxy // what about locations? return Vector3d::Zero(); } } /*! Return the object that is the defined origin of the reference frame. */ Selection ReferenceFrame::getCenter() const { return centerObject; } Vector3d ReferenceFrame::getAngularVelocity(double tjd) const { Quaterniond q0 = getOrientation(tjd); Quaterniond q1 = getOrientation(tjd + ANGULAR_VELOCITY_DIFF_DELTA); Quaterniond dq = q0.conjugate() * q1; if (std::abs(dq.w()) > 0.99999999) return Vector3d::Zero(); else return dq.vec().normalized() * (2.0 * acos(dq.w()) / ANGULAR_VELOCITY_DIFF_DELTA); #ifdef CELVEC Vector3d v(dq.x, dq.y, dq.z); v.normalize(); return v * (2.0 * acos(dq.w) / ANGULAR_VELOCITY_DIFF_DELTA); #endif } unsigned int ReferenceFrame::nestingDepth(unsigned int maxDepth, FrameType frameType) const { return this->nestingDepth(0, maxDepth, frameType); } static unsigned int getFrameDepth(const Selection& sel, unsigned int depth, unsigned int maxDepth, ReferenceFrame::FrameType frameType) { if (depth > maxDepth) return depth; Body* body = sel.body(); if (sel.location() != nullptr) body = sel.location()->getParentBody(); if (body == nullptr) { return depth; } unsigned int orbitFrameDepth = depth; unsigned int bodyFrameDepth = depth; // TODO: need to check /all/ orbit frames of body if (body->getOrbitFrame(0.0) != nullptr && frameType == ReferenceFrame::PositionFrame) { orbitFrameDepth = body->getOrbitFrame(0.0)->nestingDepth(depth + 1, maxDepth, frameType); if (orbitFrameDepth > maxDepth) return orbitFrameDepth; } if (body->getBodyFrame(0.0) != nullptr && frameType == ReferenceFrame::OrientationFrame) { bodyFrameDepth = body->getBodyFrame(0.0)->nestingDepth(depth + 1, maxDepth, frameType); } return max(orbitFrameDepth, bodyFrameDepth); } /*** J2000EclipticFrame ***/ J2000EclipticFrame::J2000EclipticFrame(Selection center) : ReferenceFrame(center) { } bool J2000EclipticFrame::isInertial() const { return true; } unsigned int J2000EclipticFrame::nestingDepth(unsigned int depth, unsigned int maxDepth, FrameType) const { return getFrameDepth(getCenter(), depth, maxDepth, PositionFrame); } /*** J2000EquatorFrame ***/ J2000EquatorFrame::J2000EquatorFrame(Selection center) : ReferenceFrame(center) { } Quaterniond J2000EquatorFrame::getOrientation(double /* tjd */) const { return Quaterniond(AngleAxis(astro::J2000Obliquity, Vector3d::UnitX())); } bool J2000EquatorFrame::isInertial() const { return true; } unsigned int J2000EquatorFrame::nestingDepth(unsigned int depth, unsigned int maxDepth, FrameType) const { return getFrameDepth(getCenter(), depth, maxDepth, PositionFrame); } /*** BodyFixedFrame ***/ BodyFixedFrame::BodyFixedFrame(Selection center, Selection obj) : ReferenceFrame(center), fixObject(obj) { } Quaterniond BodyFixedFrame::getOrientation(double tjd) const { // Rotation of 180 degrees about the y axis is required // TODO: this rotation could go in getEclipticalToBodyFixed() Quaterniond yrot180(0.0, 0.0, 1.0, 0.0); switch (fixObject.getType()) { case Selection::Type_Body: return yrot180 * fixObject.body()->getEclipticToBodyFixed(tjd); case Selection::Type_Star: return yrot180 * fixObject.star()->getRotationModel()->orientationAtTime(tjd); case Selection::Type_Location: if (fixObject.location()->getParentBody()) return yrot180 * fixObject.location()->getParentBody()->getEclipticToBodyFixed(tjd); else return yrot180; default: return yrot180; } } Vector3d BodyFixedFrame::getAngularVelocity(double tjd) const { switch (fixObject.getType()) { case Selection::Type_Body: return fixObject.body()->getAngularVelocity(tjd); case Selection::Type_Star: return fixObject.star()->getRotationModel()->angularVelocityAtTime(tjd); case Selection::Type_Location: if (fixObject.location()->getParentBody()) return fixObject.location()->getParentBody()->getAngularVelocity(tjd); else return Vector3d::Zero(); default: return Vector3d::Zero(); } } bool BodyFixedFrame::isInertial() const { return false; } unsigned int BodyFixedFrame::nestingDepth(unsigned int depth, unsigned int maxDepth, FrameType) const { unsigned int n = getFrameDepth(getCenter(), depth, maxDepth, PositionFrame); if (n > maxDepth) { return n; } else { unsigned int m = getFrameDepth(fixObject, depth, maxDepth, OrientationFrame); return max(m, n); } } /*** BodyMeanEquatorFrame ***/ BodyMeanEquatorFrame::BodyMeanEquatorFrame(Selection center, Selection obj) : ReferenceFrame(center), equatorObject(obj), freezeEpoch(astro::J2000), isFrozen(false) { } BodyMeanEquatorFrame::BodyMeanEquatorFrame(Selection center, Selection obj, double freeze) : ReferenceFrame(center), equatorObject(obj), freezeEpoch(freeze), isFrozen(true) { } Quaterniond BodyMeanEquatorFrame::getOrientation(double tjd) const { double t = isFrozen ? freezeEpoch : tjd; switch (equatorObject.getType()) { case Selection::Type_Body: return equatorObject.body()->getEclipticToEquatorial(t); case Selection::Type_Star: return equatorObject.star()->getRotationModel()->equatorOrientationAtTime(t); default: return Quaterniond::Identity(); } } Vector3d BodyMeanEquatorFrame::getAngularVelocity(double tjd) const { if (isFrozen) { return Vector3d::Zero(); } else { if (equatorObject.body() != nullptr) { return equatorObject.body()->getBodyFrame(tjd)->getAngularVelocity(tjd); } else { return Vector3d::Zero(); } } } bool BodyMeanEquatorFrame::isInertial() const { if (isFrozen) { return true; } else { // Although the mean equator of an object may vary slightly due to precession, // treat it as an inertial frame as long as the body frame of the object is // also inertial. if (equatorObject.body() != nullptr) { // TIMELINE-TODO: isInertial must take a time argument. return equatorObject.body()->getBodyFrame(0.0)->isInertial(); } else { return true; } } } unsigned int BodyMeanEquatorFrame::nestingDepth(unsigned int depth, unsigned int maxDepth, FrameType) const { // Test origin and equator object (typically the same) frames unsigned int n = getFrameDepth(getCenter(), depth, maxDepth, PositionFrame); if (n > maxDepth) { return n; } else { unsigned int m = getFrameDepth(equatorObject, depth, maxDepth, OrientationFrame); return max(m, n); } } /*** CachingFrame ***/ CachingFrame::CachingFrame(Selection _center) : ReferenceFrame(_center), lastTime(-1.0e50), lastOrientation(Quaterniond::Identity()), lastAngularVelocity(0.0, 0.0, 0.0), orientationCacheValid(false), angularVelocityCacheValid(false) { } Quaterniond CachingFrame::getOrientation(double tjd) const { if (tjd != lastTime) { lastTime = tjd; lastOrientation = computeOrientation(tjd); orientationCacheValid = true; angularVelocityCacheValid = false; } else if (!orientationCacheValid) { lastOrientation = computeOrientation(tjd); orientationCacheValid = true; } return lastOrientation; } Vector3d CachingFrame::getAngularVelocity(double tjd) const { if (tjd != lastTime) { lastTime = tjd; lastAngularVelocity = computeAngularVelocity(tjd); orientationCacheValid = false; angularVelocityCacheValid = true; } else if (!angularVelocityCacheValid) { lastAngularVelocity = computeAngularVelocity(tjd); angularVelocityCacheValid = true; } return lastAngularVelocity; } /*! Calculate the angular velocity at the specified time (units are * radians / Julian day.) The default implementation just * differentiates the orientation. */ Vector3d CachingFrame::computeAngularVelocity(double tjd) const { Quaterniond q0 = getOrientation(tjd); // Call computeOrientation() instead of getOrientation() so that we // don't affect the cached value. // TODO: check the valid ranges of the frame to make sure that // jd+dt is still in range. Quaterniond q1 = computeOrientation(tjd + ANGULAR_VELOCITY_DIFF_DELTA); Quaterniond dq = q0.conjugate() * q1; if (std::abs(dq.w()) > 0.99999999) { return Vector3d::Zero(); } else { return dq.vec().normalized() * (2.0 * acos(dq.w()) / ANGULAR_VELOCITY_DIFF_DELTA); } } /*** TwoVectorFrame ***/ // Minimum angle permitted between primary and secondary axes of // a two-vector frame. const double TwoVectorFrame::Tolerance = 1.0e-6; TwoVectorFrame::TwoVectorFrame(Selection center, const FrameVector& prim, int primAxis, const FrameVector& sec, int secAxis) : CachingFrame(center), primaryVector(prim), primaryAxis(primAxis), secondaryVector(sec), secondaryAxis(secAxis) { // Verify that primary and secondary axes are valid assert(primaryAxis != 0 && secondaryAxis != 0); assert(abs(primaryAxis) <= 3 && abs(secondaryAxis) <= 3); // Verify that the primary and secondary axes aren't collinear assert(abs(primaryAxis) != abs(secondaryAxis)); if ((abs(primaryAxis) != 1 && abs(secondaryAxis) != 1)) { tertiaryAxis = 1; } else if (abs(primaryAxis) != 2 && abs(secondaryAxis) != 2) { tertiaryAxis = 2; } else { tertiaryAxis = 3; } } Quaterniond TwoVectorFrame::computeOrientation(double tjd) const { Vector3d v0 = primaryVector.direction(tjd); Vector3d v1 = secondaryVector.direction(tjd); // TODO: verify that v0 and v1 aren't zero length v0.normalize(); v1.normalize(); if (primaryAxis < 0) v0 = -v0; if (secondaryAxis < 0) v1 = -v1; Vector3d v2 = v0.cross(v1); // Check for degenerate case when the primary and secondary vectors // are collinear. A well-chosen two vector frame should never have this // problem. double length = v2.norm(); if (length < Tolerance) { // Just return identity . . . return Quaterniond::Identity(); } else { v2 = v2 / length; // Determine whether the primary and secondary axes are in // right hand order. int rhAxis = abs(primaryAxis) + 1; if (rhAxis > 3) rhAxis = 1; bool rhOrder = rhAxis == abs(secondaryAxis); #ifdef CELVEC // Set the rotation matrix axes Vector3d v[3]; v[abs(primaryAxis) - 1] = v0; // Reverse the cross products if the axes are not in right // hand order. if (rhOrder) { v[abs(secondaryAxis) - 1] = v2.cross(v0); v[abs(tertiaryAxis) - 1] = v2; } else { v[abs(secondaryAxis) - 1] = v0.cross(-v2); v[abs(tertiaryAxis) - 1] = -v2; } // The axes are the rows of a rotation matrix. The getOrientation // method must return the quaternion representation of the // orientation, so convert the rotation matrix to a quaternion now. Quatd q = Quatd::matrixToQuaternion(Mat3d(v[0], v[1], v[2])); #endif // Set the rotation matrix axes Matrix3d m; m.row(abs(primaryAxis) - 1) = v0; // Reverse the cross products if the axes are not in right // hand order. if (rhOrder) { m.row(abs(secondaryAxis) - 1) = v2.cross(v0); m.row(abs(tertiaryAxis) - 1) = v2; } else { m.row(abs(secondaryAxis) - 1) = v0.cross(-v2); m.row(abs(tertiaryAxis) - 1) = -v2; } // The axes are the rows of a rotation matrix. The getOrientation // method must return the quaternion representation of the // orientation, so convert the rotation matrix to a quaternion now. Quaterniond q(m); // A rotation matrix will have a determinant of 1; if the matrix also // includes a reflection, the determinant will be -1, indicating that // there's a bug and there's a reversed cross-product or sign error // somewhere. // assert(Mat3d(v[0], v[1], v[2]).determinant() > 0); return q; } } bool TwoVectorFrame::isInertial() const { // Although it's possible to specify an inertial two-vector frame, we won't // bother trying to distinguish these cases: all two-vector frames will be // treated as non-inertial. return true; } unsigned int TwoVectorFrame::nestingDepth(unsigned int depth, unsigned int maxDepth, FrameType) const { // Check nesting of the origin object as well as frames references by // the primary and secondary axes. unsigned int n = getFrameDepth(getCenter(), depth, maxDepth, PositionFrame); if (n > maxDepth) return n; unsigned int m = primaryVector.nestingDepth(depth, maxDepth); n = max(m, n); if (n > maxDepth) return n; m = secondaryVector.nestingDepth(depth, maxDepth); return max(m, n); } // Copy constructor FrameVector::FrameVector(const FrameVector& fv) : vecType(fv.vecType), observer(fv.observer), target(fv.target), vec(fv.vec), frame(fv.frame) { if (frame != nullptr) frame->addRef(); } // Assignment operator (since we have a copy constructor) FrameVector& FrameVector::operator=(const FrameVector& fv) { vecType = fv.vecType; observer = fv.observer; target = fv.target; vec = fv.vec; if (frame != nullptr) frame->release(); frame = fv.frame; if (frame != nullptr) frame->addRef(); return *this; } FrameVector::FrameVector(FrameVectorType t) : vecType(t), observer(), target(), vec(0.0, 0.0, 0.0), frame(nullptr) { } FrameVector::~FrameVector() { if (frame != nullptr) frame->release(); } FrameVector FrameVector::createRelativePositionVector(const Selection& _observer, const Selection& _target) { FrameVector fv(RelativePosition); fv.observer = _observer; fv.target = _target; return fv; } FrameVector FrameVector::createRelativeVelocityVector(const Selection& _observer, const Selection& _target) { FrameVector fv(RelativeVelocity); fv.observer = _observer; fv.target = _target; return fv; } FrameVector FrameVector::createConstantVector(const Vector3d& _vec, const ReferenceFrame* _frame) { FrameVector fv(ConstantVector); fv.vec = _vec; fv.frame = _frame; if (fv.frame != nullptr) fv.frame->addRef(); return fv; } Vector3d FrameVector::direction(double tjd) const { Vector3d v; switch (vecType) { case RelativePosition: v = target.getPosition(tjd).offsetFromKm(observer.getPosition(tjd)); break; case RelativeVelocity: { Vector3d v0 = observer.getVelocity(tjd); Vector3d v1 = target.getVelocity(tjd); v = v1 - v0; } break; case ConstantVector: if (frame == nullptr) v = vec; else v = frame->getOrientation(tjd).conjugate() * vec; break; default: // unhandled vector type v = Vector3d::Zero(); break; } return v; } unsigned int FrameVector::nestingDepth(unsigned int depth, unsigned int maxDepth) const { switch (vecType) { case RelativePosition: case RelativeVelocity: { unsigned int n = getFrameDepth(observer, depth, maxDepth, ReferenceFrame::PositionFrame); if (n > maxDepth) { return n; } else { unsigned int m = getFrameDepth(target, depth, maxDepth, ReferenceFrame::PositionFrame); return max(m, n); } } break; case ConstantVector: if (depth > maxDepth) return depth; else return frame->nestingDepth(depth + 1, maxDepth, ReferenceFrame::OrientationFrame); break; default: return depth; } }