mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
392 lines
12 KiB
C++
392 lines
12 KiB
C++
/*
|
|
* Copyright (c) 2019 Eric Christoffersen (impolexg@outlook.com)
|
|
*
|
|
* 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.
|
|
*
|
|
* 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 for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <algorithm>
|
|
#include "LocationInterpolation.h"
|
|
#include "BlinnSolver.h"
|
|
|
|
static double radianstodegrees(double r) { return r * (360.0 / (2 * M_PI)); }
|
|
static double degreestoradians(double d) { return d * (2 * M_PI / 360.0); }
|
|
|
|
xyz geolocation::toxyz() const
|
|
{
|
|
// Approach developed by:
|
|
//
|
|
// (Olson, D.K. (1996).
|
|
// "Converting earth-Centered, Earth-Fixed Coordinates to Geodetic Coordinates,"
|
|
// IEEE Transactions on Aerospace and Electronic Systems, Vol. 32, No. 1, January 1996, pp. 473 - 476).
|
|
//
|
|
//
|
|
// Java implementation:
|
|
//
|
|
// D. Rose (2014).
|
|
// "Converting between Earth-Centered, Earth Fixed and Geodetic Coordinates"
|
|
//
|
|
|
|
//Convert Lat, Lon, Altitude to Earth-Centered-Earth-Fixed (ECEF)
|
|
//Input is a three element array containing lat, lon (rads) and alt (m)
|
|
//Returned array contains x, y, z in meters
|
|
static const double a = 6378137.0; //WGS-84 semi-major axis
|
|
static const double e2 = 6.6943799901377997e-3; //WGS-84 first eccentricity squared
|
|
|
|
double lat = degreestoradians(Lat());
|
|
double lon = degreestoradians(Long());
|
|
double alt = Alt();
|
|
double n = a / sqrt(1 - e2 * sin(lat)*sin(lat));
|
|
double dx = ((n + alt)*cos(lat)*cos(lon)); //ECEF x
|
|
double dy = ((n + alt)*cos(lat)*sin(lon)); //ECEF y
|
|
double dz = ((n*(1 - e2) + alt)*sin(lat)); //ECEF z
|
|
return(xyz(dx, dy, dz)); //Return x, y, z in ECEF
|
|
}
|
|
|
|
// Compute initial bearing from this geoloc to another, in RADIANS.
|
|
// Note that unless bearing is 0 or pi it will vary on the path from one to the other.
|
|
double geolocation::BearingTo(const geolocation& to) const
|
|
{
|
|
if (this->Lat() == to.Lat() && this->Long() == to.Long())
|
|
return 0.;
|
|
|
|
double phi0 = degreestoradians(this->Lat());
|
|
double phi1 = degreestoradians(to.Lat());
|
|
double lamda0 = degreestoradians(this->Long());
|
|
double lamda1 = degreestoradians(to.Long());
|
|
|
|
double y = sin(lamda1 - lamda0) * cos(phi1);
|
|
double x = cos(phi0) * sin(phi1) - sin(phi0) * cos(phi1) * cos(lamda1 - lamda0);
|
|
|
|
double bearing = atan2(y, x);
|
|
|
|
return bearing;
|
|
}
|
|
|
|
|
|
geolocation xyz::togeolocation() const
|
|
{
|
|
// Approach developed by:
|
|
// (Olson, D.K. (1996).
|
|
// "Converting earth-Centered, Earth-Fixed Coordinates to Geodetic Coordinates,"
|
|
// IEEE Transactions on Aerospace and Electronic Systems, Vol. 32, No. 1, January 1996, pp. 473 - 476).
|
|
//
|
|
// Java implementation:
|
|
//
|
|
// D. Rose (2014).
|
|
// "Converting between Earth-Centered, Earth Fixed and Geodetic Coordinates"
|
|
//
|
|
|
|
//Convert Earth-Centered-Earth-Fixed (ECEF) to lat, Lon, Altitude
|
|
//Input is a three element array containing x, y, z in meters
|
|
//Returned array contains lat and lon in radians, and altitude in meters
|
|
|
|
static const double a = 6378137.0; //WGS-84 semi-major axis
|
|
static const double e2 = 6.6943799901377997e-3; //WGS-84 first eccentricity squared
|
|
static const double a1 = 4.2697672707157535e+4; //a1 = a*e2
|
|
static const double a2 = 1.8230912546075455e+9; //a2 = a1*a1
|
|
static const double a3 = 1.4291722289812413e+2; //a3 = a1*e2/2
|
|
static const double a4 = 4.5577281365188637e+9; //a4 = 2.5*a2
|
|
static const double a5 = 4.2840589930055659e+4; //a5 = a1+a3
|
|
static const double a6 = 9.9330562000986220e-1; //a6 = 1-e2
|
|
|
|
double x = xyz::x();
|
|
double y = xyz::y();
|
|
double z = xyz::z();
|
|
double zp = std::abs(z);
|
|
double w2 = x * x + y * y;
|
|
double w = sqrt(w2);
|
|
double r2 = w2 + z * z;
|
|
double r = sqrt(r2);
|
|
|
|
double retLong;
|
|
double retLat;
|
|
double retAlt;
|
|
|
|
retLong = atan2(y, x); // Lon (final)
|
|
double s2 = z * z / r2;
|
|
double c2 = w2 / r2;
|
|
double u = a2 / r;
|
|
double v = a3 - a4 / r;
|
|
double s, ss, c;
|
|
if (c2 > 0.3) {
|
|
s = (zp / r)*(1.0 + c2 * (a1 + u + s2 * v) / r);
|
|
retLat = asin(s); //Lat
|
|
ss = s * s;
|
|
c = sqrt(1.0 - ss);
|
|
}
|
|
else {
|
|
c = (w / r)*(1.0 - s2 * (a5 - u - c2 * v) / r);
|
|
retLat = acos(c); //Lat
|
|
ss = 1.0 - c * c;
|
|
s = sqrt(ss);
|
|
}
|
|
double g = 1.0 - e2 * ss;
|
|
double rg = a / sqrt(g);
|
|
double rf = a6 * rg;
|
|
u = w - rg * c;
|
|
v = zp - rf * s;
|
|
double f = c * u + s * v;
|
|
double m = c * v - s * u;
|
|
double p = m / (rf / g + f);
|
|
retLat = retLat + p; //Lat
|
|
retAlt = f + m * p / 2.0; //Altitude
|
|
if (z < 0.0) {
|
|
retLat = retLat * -1.0; //Lat
|
|
}
|
|
|
|
return geolocation(radianstodegrees(retLat), radianstodegrees(retLong), retAlt);
|
|
}
|
|
|
|
xyz Slerper::Slerp(double frac)
|
|
{
|
|
if (m_sin_angle != 0.0)
|
|
{
|
|
double scale = sin(frac * m_angle) / m_sin_angle;
|
|
return m_x0_norm.scale(sin((1 - frac) * m_angle) / m_sin_angle).add(m_x1_norm.scale(scale));
|
|
}
|
|
return m_x0_norm;
|
|
}
|
|
|
|
// Precompute invariant values needed to geoslerp
|
|
Slerper::Slerper(geolocation g0, geolocation g1) :
|
|
m_g0(g0),
|
|
m_g1(g1),
|
|
m_x0(m_g0.toxyz()),
|
|
m_x1(m_g1.toxyz()),
|
|
m_x0_norm(m_x0.normalize()),
|
|
m_x1_norm(m_x1.normalize()),
|
|
m_x0_magnitude(m_x0.magnitude())
|
|
{
|
|
m_angle = atan2(m_x0_norm.cross(m_x1_norm).magnitude(), m_x0_norm.dot(m_x1_norm));
|
|
m_sin_angle = sin(m_angle);
|
|
m_altitude_delta = (m_g1.Alt() - m_g0.Alt());
|
|
}
|
|
|
|
// Interpolate fractional arc between two wgs84 vectors.
|
|
// This models the earths ellipsoid.
|
|
geolocation Slerper::GeoSlerp(double frac)
|
|
{
|
|
// Generate normalized ECEF vector
|
|
xyz slerp = Slerp(frac);
|
|
|
|
// Scale ecef unit vector so its roughly at earth surface
|
|
double interpaltitudestep = m_altitude_delta * frac;
|
|
slerp = slerp.scale(m_x0_magnitude + interpaltitudestep);
|
|
|
|
// Convert ecef back to wgs84
|
|
geolocation slerpgeo = slerp.togeolocation();
|
|
|
|
// Set linear interpolated altitude on slerped wgs84 vector
|
|
double interpaltitude = m_altitude_delta * frac;
|
|
slerpgeo.Alt() = m_g0.Alt() + interpaltitude;
|
|
|
|
return slerpgeo;
|
|
}
|
|
|
|
xyz LinearTwoPointInterpolator::InterpolateNext(xyz p0, xyz p1)
|
|
{
|
|
xyz delta = p1.subtract(p0);
|
|
xyz retval = p1.add(delta);
|
|
return retval;
|
|
}
|
|
|
|
xyz SphericalTwoPointInterpolator::InterpolateNext(xyz p0, xyz p1)
|
|
{
|
|
Slerper slerper(p0.togeolocation(), p1.togeolocation());
|
|
|
|
geolocation geo = slerper.GeoSlerp(2.0);
|
|
|
|
return geo.toxyz();
|
|
}
|
|
|
|
void UnitCatmullRomInterpolator::Init(double pm1, double p0, double p1, double p2)
|
|
{
|
|
std::get<0>(m_baseCoefs) = pm1;
|
|
std::get<1>(m_baseCoefs) = p0;
|
|
std::get<2>(m_baseCoefs) = p1;
|
|
std::get<3>(m_baseCoefs) = p2;
|
|
|
|
m_locCoefs.Invalidate();
|
|
m_tanCoefs.Invalidate();
|
|
}
|
|
|
|
UnitCatmullRomInterpolator::UnitCatmullRomInterpolator()
|
|
{
|
|
Init(0.0, 0.0, 0.0, 0.0);
|
|
}
|
|
|
|
UnitCatmullRomInterpolator::UnitCatmullRomInterpolator(double pm1, double p0, double p1, double p2)
|
|
{
|
|
Init(pm1, p0, p1, p2);
|
|
}
|
|
|
|
double UnitCatmullRomInterpolator::Location(double u) const
|
|
{
|
|
double A, B, C, D;
|
|
m_locCoefs.Get(m_baseCoefs, A, B, C, D);
|
|
|
|
double retval = (u * u * u) * A +
|
|
(u * u) * B +
|
|
(u) * C +
|
|
D;
|
|
|
|
return retval;
|
|
}
|
|
|
|
double UnitCatmullRomInterpolator::Tangent(double u) const
|
|
{
|
|
double A, B, C;
|
|
m_tanCoefs.Get(m_baseCoefs, A, B, C);
|
|
|
|
double retval = (u * u) * A +
|
|
(u) * B +
|
|
C;
|
|
|
|
return retval;
|
|
}
|
|
|
|
// Given interpolated value r, provides u that would yield r.
|
|
// Only successful if spline is invertable (which is true for
|
|
// the distance spline which is monotonic, etc.)
|
|
bool UnitCatmullRomInterpolator::Inverse(double r, double &u) const
|
|
{
|
|
double a, b, c, d;
|
|
m_locCoefs.Get(m_baseCoefs, a, b, c, d);
|
|
|
|
d -= r; // "- r" because root finder solves expressions that equal zero.
|
|
|
|
Roots roots = BlinnCubicSolver(a, b, c, d);
|
|
|
|
// There are 0, 1, 2 or 3 roots.
|
|
|
|
// In general it is possible that there are multiple roots in range... but should never happen
|
|
// for monotonic distance mapping.
|
|
bool ret = false;
|
|
for (unsigned i = 0; i < roots.resultcount(); i++) {
|
|
double rt = roots.result(i).x / roots.result(i).w;
|
|
// Take the first root we find in range 0..1.
|
|
if (rt >= 0. && rt <= 1.) {
|
|
u = rt;
|
|
ret = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void UnitCatmullRomInterpolator3D::Init(xyz pm1, xyz p0, xyz p1, xyz p2)
|
|
{
|
|
x.Init(pm1.x(), p0.x(), p1.x(), p2.x());
|
|
y.Init(pm1.y(), p0.y(), p1.y(), p2.y());
|
|
z.Init(pm1.z(), p0.z(), p1.z(), p2.z());
|
|
}
|
|
|
|
UnitCatmullRomInterpolator3D::UnitCatmullRomInterpolator3D(xyz pm1, xyz p0, xyz p1, xyz p2)
|
|
{
|
|
Init(pm1, p0, p1, p2);
|
|
}
|
|
|
|
xyz UnitCatmullRomInterpolator3D::Location(double frac) const
|
|
{
|
|
return xyz(x.Location(frac), y.Location(frac), z.Location(frac));
|
|
}
|
|
|
|
xyz UnitCatmullRomInterpolator3D::Tangent(double frac) const
|
|
{
|
|
return xyz(x.Tangent(frac), y.Tangent(frac), z.Tangent(frac));
|
|
}
|
|
|
|
geolocation GeoPointInterpolator::Location(double distance)
|
|
{
|
|
xyz l0xyz = DistancePointInterpolator<SphericalTwoPointInterpolator>::Location(distance);
|
|
geolocation l0 = l0xyz.togeolocation();
|
|
return l0;
|
|
}
|
|
|
|
geolocation GeoPointInterpolator::Location(double distance, double &slope)
|
|
{
|
|
xyz tangentVector;
|
|
xyz l0xyz = DistancePointInterpolator<SphericalTwoPointInterpolator>::Location(distance, tangentVector);
|
|
|
|
// First step, construct 2 geo locations that are separated by a unit-length tangent veocity vector.
|
|
geolocation l1 = xyz(l0xyz.add(tangentVector.normalize())).togeolocation();
|
|
geolocation l0 = l0xyz.togeolocation();
|
|
|
|
// - Route distance is independent of geometric distance.
|
|
// - Route distance is fixed in stone because it must match video
|
|
// synchronization files.
|
|
// - Geometric distance is also fixed since it is a fact of gps location.
|
|
//
|
|
// - Altitude is always in route distance units (so is computable
|
|
// using gradient and route distance.)
|
|
//
|
|
// - Path length on spline is gps-based so uses geometric units.
|
|
//
|
|
// - Catmull-Rom spline is non-uniform, meaning speed of interpolated point can
|
|
// vary across bracket which distorts the tangent vector.
|
|
|
|
// Vertical velocity of unit tangent vector (altitude is always kept in in route distance units.)
|
|
double rise = l1.Alt() - l0.Alt();
|
|
|
|
// Tangent vector's magnitude is velocity in terms of distance spline.
|
|
// Will be unit vector when distance spline has constant rate. Can have
|
|
// zero length when processing points without motion.
|
|
double hyp = tangentVector.magnitude();
|
|
|
|
// Compute adjacent speed.
|
|
double adj = fabs((hyp * hyp) - (rise * rise));
|
|
|
|
// Gradient.
|
|
slope = (adj > 0.) ? rise / sqrt(adj) : 0.;
|
|
|
|
// No matter what we still don't permit slopes above 40%.
|
|
slope = std::min(slope, .4);
|
|
slope = std::max(slope, -.4);
|
|
|
|
// Clear out location if we are an altitude only spline.
|
|
if (!HasLocation()) {
|
|
l0.Lat() = 0;
|
|
l0.Long() = 0;
|
|
}
|
|
|
|
return l0;
|
|
}
|
|
|
|
void GeoPointInterpolator::Push(double distance, geolocation point)
|
|
{
|
|
if (m_locationState == Unset)
|
|
m_locationState = YesLocation;
|
|
else
|
|
assert(m_locationState == YesLocation);
|
|
|
|
DistancePointInterpolator<SphericalTwoPointInterpolator>::Push(distance, point.toxyz());
|
|
}
|
|
|
|
// Special form for case where altitude exists but no location.
|
|
void GeoPointInterpolator::Push(double distance, double altitude)
|
|
{
|
|
if (m_locationState == Unset)
|
|
m_locationState = NoLocation;
|
|
else
|
|
assert(m_locationState == NoLocation);
|
|
|
|
geolocation geo(0, 0, altitude);
|
|
xyz point = geo.toxyz();
|
|
|
|
DistancePointInterpolator<SphericalTwoPointInterpolator>::Push(distance, point);
|
|
}
|