2022-03-01 17:17:56 +00:00
///////////////////////////////////////////////////////////////////////////////////
2023-11-19 06:43:20 +01:00
// Copyright (C) 2022 Jon Beniston, M7RCE <jon@beniston.com> //
2022-03-01 17:17:56 +00:00
// Copyright (C) 2011-2020 Cesium Contributors //
// //
// 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 as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "coordinates.h"
#include "units.h"
// Scale cartesian position on to surface of ellipsoid
QVector3D Coordinates :: scaleToGeodeticSurface ( QVector3D cartesian , QVector3D oneOverRadii , QVector3D oneOverRadiiSquared )
{
float centerToleranceSquared = 0.1 ;
double x2 = cartesian . x () * cartesian . x () * oneOverRadii . x () * oneOverRadii . x ();
double y2 = cartesian . y () * cartesian . y () * oneOverRadii . y () * oneOverRadii . y ();
double z2 = cartesian . z () * cartesian . z () * oneOverRadii . z () * oneOverRadii . z ();
double squaredNorm = x2 + y2 + z2 ;
double ratio = sqrt ( 1.0 / squaredNorm );
QVector3D intersection = cartesian * ratio ;
if ( squaredNorm < centerToleranceSquared ) {
return intersection ;
}
QVector3D gradient (
intersection . x () * oneOverRadiiSquared . x () * 2.0 ,
intersection . y () * oneOverRadiiSquared . y () * 2.0 ,
intersection . z () * oneOverRadiiSquared . z () * 2.0
);
double lambda = (( 1.0 - ratio ) * cartesian . length ()) / ( 0.5 * gradient . length ());
double correction = 0.0 ;
double func ;
double denominator ;
double xMultiplier ;
double yMultiplier ;
double zMultiplier ;
double xMultiplier2 ;
double yMultiplier2 ;
double zMultiplier2 ;
double xMultiplier3 ;
double yMultiplier3 ;
double zMultiplier3 ;
do
{
lambda -= correction ;
xMultiplier = 1.0 / ( 1.0 + lambda * oneOverRadiiSquared . x ());
yMultiplier = 1.0 / ( 1.0 + lambda * oneOverRadiiSquared . y ());
zMultiplier = 1.0 / ( 1.0 + lambda * oneOverRadiiSquared . z ());
xMultiplier2 = xMultiplier * xMultiplier ;
yMultiplier2 = yMultiplier * yMultiplier ;
zMultiplier2 = zMultiplier * zMultiplier ;
xMultiplier3 = xMultiplier2 * xMultiplier ;
yMultiplier3 = yMultiplier2 * yMultiplier ;
zMultiplier3 = zMultiplier2 * zMultiplier ;
func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0 ;
denominator =
x2 * xMultiplier3 * oneOverRadiiSquared . x () +
y2 * yMultiplier3 * oneOverRadiiSquared . y () +
z2 * zMultiplier3 * oneOverRadiiSquared . z ();
double derivative = - 2.0 * denominator ;
correction = func / derivative ;
}
while ( abs ( func ) > 0.000000000001 );
QVector3D result (
cartesian . x () * xMultiplier ,
cartesian . y () * yMultiplier ,
cartesian . z () * zMultiplier
);
return result ;
}
// QVector3D.normalized doesn't work with small numbers
QVector3D Coordinates :: normalized ( QVector3D vec )
{
QVector3D result ;
float magnitude = vec . length ();
result . setX ( vec . x () / magnitude );
result . setY ( vec . y () / magnitude );
result . setZ ( vec . z () / magnitude );
return result ;
}
// Convert ECEF position to geodetic coordinates
void Coordinates :: ecefToGeodetic ( double x , double y , double z , double & latitude , double & longitude , double & height )
{
QVector3D wgs84OneOverRadix ( 1.0 / 6378137.0 ,
1.0 / 6378137.0 ,
1.0 / 6356752.3142451793 );
QVector3D wgs84OneOverRadiiSquared ( 1.0 / ( 6378137.0 * 6378137.0 ),
1.0 / ( 6378137.0 * 6378137.0 ),
1.0 / ( 6356752.3142451793 * 6356752.3142451793 ));
QVector3D cartesian ( x , y , z );
QVector3D p = scaleToGeodeticSurface ( cartesian , wgs84OneOverRadix , wgs84OneOverRadiiSquared );
QVector3D n = p * wgs84OneOverRadiiSquared ;
n = normalized ( n );
QVector3D h = cartesian - p ;
longitude = atan2 ( n . y (), n . x ());
latitude = asin ( n . z ());
longitude = Units :: radiansToDegrees ( longitude );
latitude = Units :: radiansToDegrees ( latitude );
double t = QVector3D :: dotProduct ( h , cartesian );
double sign = t >= 0.0 ? 1.0 : 0.0 ;
height = sign * h . length ();
}
// Convert ECEF velocity to speed and heading
void Coordinates :: ecefVelToSpeedHeading ( double latitude , double longitude ,
double velX , double velY , double velZ ,
double & speed , double & verticalRate , double & heading )
{
if (( velX == 0.0 ) && ( velY == 0.0 ) && ( velZ == 0.0 ))
{
speed = 0.0 ;
heading = 0.0 ;
verticalRate = 0.0 ;
return ;
}
double latRad = Units :: degreesToRadians ( latitude );
double lonRad = Units :: degreesToRadians ( longitude );
double sinLat = sin ( latRad );
double cosLat = cos ( latRad );
double sinLon = sin ( lonRad );
double cosLon = cos ( lonRad );
double velEast = - velX * sinLon + velY * cosLon ;
double velNorth = - velX * sinLat * cosLon - velY * sinLat * sinLon + velZ * cosLat ;
double velUp = velX * cosLat * cosLon + velY * cosLat * sinLon + velZ * sinLat ;
speed = sqrt ( velNorth * velNorth + velEast * velEast );
verticalRate = velUp ;
double headingRad = atan2 ( velEast , velNorth );
heading = Units :: radiansToDegrees ( headingRad );
if ( heading < 0.0 ) {
heading += 360.0 ;
} else if ( heading >= 360.0 ) {
heading -= 360.0 ;
}
}
// Convert a position specified in longitude, latitude in degrees and height in metres above WGS84 ellipsoid in to
// Earth Centered Earth Fixed frame cartesian coordinates
// See Cesium.Cartesian3.fromDegrees
QVector3D Coordinates :: geodeticToECEF ( double longitude , double latitude , double height )
{
return geodeticRadiansToECEF ( Units :: degreesToRadians ( longitude ), Units :: degreesToRadians ( latitude ), height );
}
// FIXME: QVector3D is only float!
// See Cesium.Cartesian3.fromRadians
QVector3D Coordinates :: geodeticRadiansToECEF ( double longitude , double latitude , double height )
{
QVector3D wgs84RadiiSquared ( 6378137.0 * 6378137.0 , 6378137.0 * 6378137.0 , 6356752.3142451793 * 6356752.3142451793 );
double cosLatitude = cos ( latitude );
QVector3D n ;
n . setX ( cosLatitude * cos ( longitude ));
n . setY ( cosLatitude * sin ( longitude ));
n . setZ ( sin ( latitude ));
n . normalize ();
QVector3D k ;
k = wgs84RadiiSquared * n ;
double gamma = sqrt ( QVector3D :: dotProduct ( n , k ));
k = k / gamma ;
n = n * height ;
return k + n ;
}
// Convert heading, pitch and roll in degrees to a quaternoin
// See: Cesium.Quaternion.fromHeadingPitchRoll
QQuaternion Coordinates :: fromHeadingPitchRoll ( double heading , double pitch , double roll )
{
QVector3D xAxis ( 1 , 0 , 0 );
QVector3D yAxis ( 0 , 1 , 0 );
QVector3D zAxis ( 0 , 0 , 1 );
QQuaternion rollQ = QQuaternion :: fromAxisAndAngle ( xAxis , roll );
QQuaternion pitchQ = QQuaternion :: fromAxisAndAngle ( yAxis , - pitch );
QQuaternion headingQ = QQuaternion :: fromAxisAndAngle ( zAxis , - heading );
QQuaternion temp = rollQ * pitchQ ;
return headingQ * temp ;
}
// Calculate a transformation matrix from a East, North, Up frame at the given position to Earth Centered Earth Fixed frame
// See: Cesium.Transforms.eastNorthUpToFixedFrame
QMatrix4x4 Coordinates :: eastNorthUpToECEF ( QVector3D origin )
{
// TODO: Handle special case at centre of earth and poles
QVector3D up = origin . normalized ();
QVector3D east ( - origin . y (), origin . x (), 0.0 );
east . normalize ();
QVector3D north = QVector3D :: crossProduct ( up , east );
QMatrix4x4 result (
east . x (), north . x (), up . x (), origin . x (),
east . y (), north . y (), up . y (), origin . y (),
east . z (), north . z (), up . z (), origin . z (),
0.0 , 0.0 , 0.0 , 1.0
);
return result ;
}
// Convert 3x3 rotation matrix to a quaternoin
// Although there is a method for this in Qt: QQuaternion::fromRotationMatrix, it seems to
// result in different signs, so the following is based on Cesium code
QQuaternion Coordinates :: fromRotation ( QMatrix3x3 mat )
{
QQuaternion q ;
double trace = mat ( 0 , 0 ) + mat ( 1 , 1 ) + mat ( 2 , 2 );
if ( trace > 0.0 )
{
double root = sqrt ( trace + 1.0 );
q . setScalar ( 0.5 * root );
root = 0.5 / root ;
q . setX (( mat ( 2 , 1 ) - mat ( 1 , 2 )) * root );
q . setY (( mat ( 0 , 2 ) - mat ( 2 , 0 )) * root );
q . setZ (( mat ( 1 , 0 ) - mat ( 0 , 1 )) * root );
}
else
{
double next [] = { 1 , 2 , 0 };
int i = 0 ;
if ( mat ( 1 , 1 ) > mat ( 0 , 0 )) {
i = 1 ;
}
if ( mat ( 2 , 2 ) > mat ( 0 , 0 ) && mat ( 2 , 2 ) > mat ( 1 , 1 )) {
i = 2 ;
}
int j = next [ i ];
int k = next [ j ];
double root = sqrt ( mat ( i , i ) - mat ( j , j ) - mat ( k , k ) + 1 );
double quat [] = { 0.0 , 0.0 , 0.0 };
quat [ i ] = 0.5 * root ;
root = 0.5 / root ;
q . setScalar (( mat ( j , k ) - mat ( k , j )) * root );
quat [ j ] = ( mat ( i , j ) + mat ( j , i )) * root ;
quat [ k ] = ( mat ( i , k ) + mat ( k , i )) * root ;
q . setX ( - quat [ 0 ]);
q . setY ( - quat [ 1 ]);
q . setZ ( - quat [ 2 ]);
}
return q ;
}
// Calculate orientation quaternion for a model (such as an aircraft) based on position and (HPR) heading, pitch and roll (in degrees)
// While Cesium supports specifying orientation as HPR, CZML doesn't currently. See https://github.com/CesiumGS/cesium/issues/5184
// CZML requires the orientation to be in the Earth Centered Earth Fixed (geocentric) reference frame (https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates)
// The orientation therefore depends not only on HPR but also on position
//
// glTF uses a right-handed axis convention; that is, the cross product of right and forward yields up. glTF defines +Y as up, +Z as forward, and -X as right.
// Cesium.Quaternion.fromHeadingPitchRoll Heading is the rotation about the negative z axis. Pitch is the rotation about the negative y axis. Roll is the rotation about the positive x axis.
QQuaternion Coordinates :: orientation ( double longitude , double latitude , double altitude , double heading , double pitch , double roll )
{
// Forward direction for gltf models in Cesium seems to be Eastward, rather than Northward, so we adjust heading by -90 degrees
heading = - 90 + heading ;
// Convert position to Earth Centered Earth Fixed (ECEF) frame
QVector3D positionECEF = geodeticToECEF ( longitude , latitude , altitude );
// Calculate matrix to transform from East, North, Up (ENU) frame to ECEF frame
QMatrix4x4 enuToECEFTransform = eastNorthUpToECEF ( positionECEF );
// Calculate rotation based on HPR in ENU frame
QQuaternion hprENU = fromHeadingPitchRoll ( heading , pitch , roll );
// Transform rotation from ENU to ECEF
QMatrix3x3 hprENU3 = hprENU . toRotationMatrix ();
QMatrix4x4 hprENU4 ( hprENU3 );
QMatrix4x4 transform = enuToECEFTransform * hprENU4 ;
// Convert from 4x4 matrix to 3x3 matrix then to a quaternion
QQuaternion oq = fromRotation ( transform . toGenericMatrix < 3 , 3 > ());
return oq ;
}