File indexing completed on 2024-04-28 15:09:05

0001 /*
0002     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "rotations.h"
0008 
0009 #include <cmath>
0010 
0011 // In the coordinate system used in this file:
0012 // x points forward
0013 // y points to the left
0014 // z points up.
0015 // In this system, theta is the angle between x & y and is related
0016 // to our azimuth: theta = 90-azimuth.
0017 // Phi is the angle away from z, and is related to our altitude as 90-altitude.
0018 
0019 namespace Rotations
0020 {
0021 
0022 double d2r(double degrees)
0023 {
0024     return 2 * M_PI * degrees / 360.0;
0025 }
0026 
0027 double r2d(double radians)
0028 {
0029     return 360.0 * radians / (2 * M_PI);
0030 }
0031 
0032 V3 V3::normal(const V3 &v1, const V3 &v2, const V3 &v3)
0033 {
0034     // First subtract d21 = V2-V1; d32 = V3-V2
0035     const V3 d21 = V3(v2.x() - v1.x(), v2.y() - v1.y(), v2.z() - v1.z());
0036     const V3 d32 = V3(v3.x() - v2.x(), v3.y() - v2.y(), v3.z() - v2.z());
0037     // Now take the cross-product of d21 and d31
0038     const V3 cross = V3(d21.y() * d32.z() - d21.z() * d32.y(),
0039                         d21.z() * d32.x() - d21.x() * d32.z(),
0040                         d21.x() * d32.y() - d21.y() * d32.x());
0041     // Finally normalize cross so that it is a unit vector.
0042     const double lenSq = cross.x() * cross.x() + cross.y() * cross.y() + cross.z() * cross.z();
0043     if (lenSq == 0.0) return V3();
0044     const double len = sqrt(lenSq);
0045     // Should we also fail if len < e.g. 5e-8 ??
0046     return V3(cross.x() / len, cross.y() / len, cross.z() / len);
0047 }
0048 
0049 double V3::length()
0050 {
0051     return sqrt(X * X + Y * Y + Z * Z);
0052 }
0053 
0054 V3 azAlt2xyz(const QPointF &azAlt)
0055 {
0056     // Convert the new point to xyz
0057     // See https://mathworld.wolfram.com/SphericalCoordinates.html
0058     const double azRadians = d2r(azAlt.x());
0059     const double altRadians = d2r(azAlt.y());
0060 
0061     const double theta = -azRadians;
0062     const double phi = (M_PI / 2.0) - altRadians;
0063     const double x = cos(theta) * sin(phi);
0064     const double y = sin(theta) * sin (phi);
0065     const double z = cos(phi);
0066     return V3(x, y, z);
0067 
0068 }
0069 
0070 QPointF xyz2azAlt(const V3 &xyz)
0071 {
0072     // Deal with degenerate values for the atan along the meridian (y == 0).
0073     if (xyz.y() == 0.0 && xyz.x() == 0.0)
0074     {
0075         // Straight overhead
0076         return QPointF(0.0, 90.0);
0077     }
0078 
0079     const double azRadians = (xyz.y() == 0) ? 0.0 : -atan2(xyz.y(), xyz.x());
0080     const double altRadians = (M_PI / 2.0) - acos(xyz.z());
0081 
0082     return QPointF(r2d(azRadians), r2d(altRadians));
0083 }
0084 
0085 V3 haDec2xyz(const QPointF &haDec, double latitude)
0086 {
0087     const double haRadians = d2r(haDec.x());
0088     // Since dec=90 points at the pole.
0089     const double decRadians = d2r(haDec.y() - 90);
0090 
0091     const double x = cos(decRadians);
0092     const double y = sin(haRadians) * sin(decRadians);
0093     const double z = cos(haRadians) * sin(decRadians);
0094     return rotateAroundY(V3(x, y, z), -fabs(latitude));
0095 }
0096 
0097 QPointF xyz2haDec(const V3 &xyz, double latitude)
0098 {
0099 
0100     const V3 pt = rotateAroundY(xyz, latitude);
0101     const double ha = r2d(atan2(pt.y(), pt.z()));
0102 
0103     V3 pole = Rotations::azAlt2xyz(QPointF(0, fabs(latitude)));
0104     const double dec = 90 - getAngle(xyz, pole);
0105     return QPointF(ha, dec);
0106 }
0107 
0108 V3 getAxis(const V3 &p1, const V3 &p2, const V3 &p3)
0109 {
0110     return V3::normal(p1, p2, p3);
0111 }
0112 
0113 // Returns the angle between two points on a sphere using the spherical law of
0114 // cosines: https://en.wikipedia.org/wiki/Great-circle_distance
0115 double getAngle(const V3 &p1, const V3 &p2)
0116 {
0117     QPointF a1 = xyz2azAlt(p1);
0118     QPointF a2 = xyz2azAlt(p2);
0119     return r2d(acos(sin(d2r(a1.y())) * sin(d2r(a2.y())) +
0120                     cos(d2r(a1.y())) * cos(d2r(a2.y())) * cos(d2r(a1.x()) - d2r(a2.x()))));
0121 }
0122 
0123 // Using the equations in
0124 // https://sites.google.com/site/glennmurray/Home/rotation-matrices-and-formulas/rotation-about-an-arbitrary-axis-in-3-dimensions
0125 // This rotates point around axis (which should be a unit vector) by degrees.
0126 V3 rotateAroundAxis(const V3 &point, const V3 &axis, double degrees)
0127 {
0128     const double cosAngle = cos(d2r(degrees));
0129     const double sinAngle = sin(d2r(degrees));
0130     const double pointDotAxis = (point.x() * axis.x() + point.y() * axis.y() + point.z() * axis.z());
0131     const double x = axis.x() * pointDotAxis * (1.0 - cosAngle) + point.x() * cosAngle + (-axis.z() * point.y() + axis.y() *
0132                      point.z()) * sinAngle;
0133     const double y = axis.y() * pointDotAxis * (1.0 - cosAngle) + point.y() * cosAngle + (axis.z() * point.x() + -axis.x() *
0134                      point.z()) * sinAngle;
0135     const double z = axis.z() * pointDotAxis * (1.0 - cosAngle) + point.z() * cosAngle + (-axis.y() * point.x() + axis.x() *
0136                      point.y()) * sinAngle;
0137     return V3(x, y, z);
0138 }
0139 
0140 // Simpler version of above for altitude rotations, our main case.
0141 // Multiply [x,y,z] by the rotate-Y by "angle" rotation matrix
0142 // as in https://en.wikipedia.org/wiki/Rotation_matrix
0143 //   cos(angle)  0   sin(angle)
0144 //       0       1       0
0145 //  -sin(angle)  0   cos(angle)
0146 V3 rotateAroundY(const V3 &point, double degrees)
0147 {
0148     const double radians = d2r(degrees);
0149     const double cosAngle = cos(radians);
0150     const double sinAngle = sin(radians);
0151     return V3( point.x() * cosAngle + point.z() * sinAngle,
0152                point.y(),
0153                -point.x() * sinAngle + point.z() * cosAngle);
0154 }
0155 
0156 V3 rotateAroundZ(const V3 &point, double degrees)
0157 {
0158     const double radians = d2r(degrees);
0159     const double cosAngle = cos(radians);
0160     const double sinAngle = sin(radians);
0161     return V3( point.x() * cosAngle - point.y() * sinAngle,
0162                point.x() * sinAngle + point.y() * cosAngle,
0163                point.z());
0164 }
0165 
0166 // Rotates in altitude then azimuth, as is done to correct for polar alignment.
0167 // Note, NOT a single rotation along a great circle, but rather two separate
0168 // rotations.
0169 QPointF rotateRaAxis(const QPointF &azAltPoint, const QPointF &azAltRotation)
0170 {
0171     const V3 point = azAlt2xyz(azAltPoint);
0172     const V3 altRotatedPoint = rotateAroundY(point, -azAltRotation.y());
0173 
0174     // Az rotation is simply adding in the az angle.
0175     const QPointF altAz = xyz2azAlt(altRotatedPoint);
0176     return QPointF(altAz.x() + azAltRotation.x(), altAz.y());
0177 }
0178 
0179 }  // namespace rotations