From 57d1bdbc7f3788ca7e5cc3a88eac2e6e02885f2d Mon Sep 17 00:00:00 2001 From: Trevor Bentley Date: Thu, 27 Mar 2025 19:06:27 +0100 Subject: [PATCH] Use speed data from imported GPX files (#4628) When importing a GPX file, prefer embedded speed data (if present) over GPS coordinates for calculating speed and distance. The GPX file format supports providing the measured speed at each trackpoint as a TrackPointExtension. Some programs use this to report readings from dedicated speed sensors. If no speed data is embedded or if data is missing from any trackpoints, Golden Cheetah calculates speed from the GPS coordinates. When using GPS, weak signal or low-quality receivers can result in irregular speed measurements due to location jitter, whereas dedicated speed sensors should be consistent in any environment. Distance is additionally calculated from speed and time if available, falling back to GPS otherwise. Co-authored-by: Trevor Bentley --- src/FileIO/GpxParser.cpp | 105 +++++++++++++++++++++++++++++-------- src/FileIO/GpxParser.h | 4 ++ src/FileIO/GpxRideFile.cpp | 2 +- 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/FileIO/GpxParser.cpp b/src/FileIO/GpxParser.cpp index 86df5d61d..9b6479a4c 100644 --- a/src/FileIO/GpxParser.cpp +++ b/src/FileIO/GpxParser.cpp @@ -46,6 +46,7 @@ GpxParser::GpxParser (RideFile* rideFile) lon = 0; lat = 0; hr = 0; + speed = std::numeric_limits::infinity(); temp = RideFile::NA; firstTime = true; metadata = false; @@ -86,6 +87,9 @@ bool GpxParser::startElement( const QString&, const QString&, { lon = lastLon; } + + // clear last speed value + speed = std::numeric_limits::infinity(); } return true; } @@ -143,6 +147,11 @@ bool { watts = buffer.toDouble(); } + else if (qName == "gpxtpx:speed" || qName == "ns3:speed" || qName == "speed") + { + // gpx speed is in meters/s. Convert to kph. + speed = buffer.toDouble() * (60.0 * 60.0) / 1000.0; + } else if (qName == "trkpt") @@ -156,42 +165,91 @@ bool last_time = time; lastLon = lon; lastLat = lat; + lastSpeed = speed; // first point rideFile->appendPoint(secs, cad, hr, 0, 0, 0, watts, alt, lon, lat, 0, 0.0, temp, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0); return true; } - // we need to figure out the distance by using the lon,lat - // using the haversine formula - double r = 6371; - double dlat = toRadians(lat -lastLat); // convert to radians - - double dlon = toRadians(lon - lastLon); - double a = sin(dlat /2) * sin(dlat/2) + cos(toRadians(lat)) * cos(toRadians(lastLat)) * sin(dlon/2) * sin(dlon /2); - //double c = 2*asin(sqrt(fabs(a))); // Alternate definition. - double c = 4*atan2(sqrt(a),1+sqrt(1-fabs(a))); - double delta_d = r * c; - if(lastLat != 0) - distance += delta_d; // compute the elapsed time and distance traveled since the // last recorded trackpoint // use msec in case there are msec in QDateTime double delta_t_ms = last_time.msecsTo(time); - if (delta_d<0) - { - delta_d=0; - } - // compute speed for this trackpoint by dividing the distance - // traveled by the elapsed time. The elapsed time will be 0.0 - // for the first trackpoint -- so set speed to 0.0 instead of - // dividing by zero. - double speed = 0.0; - if (delta_t_ms > 0.0) + if (!std::isinf(speed)) { - speed= 1000.0 * delta_d / delta_t_ms * 3600.0; + // If speed is specified in the extensions, use it to calculate + // distance traveled. Note that gaps in speed data could be either + // of two cases: + // + // 1) missing samples during movement -- interpolation appropriate + // 2) paused collection while stopped -- zeroization appropriate + // + // There is no guaranteed way to differentiate between the two, so + // here we use the Garmin Smart Recording threshold to switch from + // (1) to (2). + + // speed adjusted for time gaps + double speed_avg; + + // if Garmin Smart Recording is enabled and time elapsed is less + // than the configured limit, use simple linear interpolation of the + // speed. + if (isGarminSmartRecording.toBool() && + delta_t_ms < GarminHWM.toInt() * 1000.0) { + if (std::isinf(lastSpeed)) { + speed_avg = speed; + } + else { + speed_avg = (lastSpeed + speed) / 2.0; + } + } + // otherwise, for deltas greater than two sampling periods, scale + // the speed by the elapsed time. This is equivalent to treating + // speed as 0m/s for all but the first second, or "zeroing the gap". + else { + if (delta_t_ms >= (2 * GPX_SAMPLE_INTERVAL * 1000.0)) { + speed_avg = speed / (delta_t_ms / 1000.0); + } + else { + speed_avg = speed; + } + } + + // now calculate the distance traveled for this time and speed + double delta_d = speed_avg * delta_t_ms / (60.0 * 60.0) / 1000.0; // speed in kph + distance += delta_d; // distance in km + } + else + { + // we need to figure out the distance by using the lon,lat + // using the haversine formula + double r = 6371; + double dlat = toRadians(lat -lastLat); // convert to radians + + double dlon = toRadians(lon - lastLon); + double a = sin(dlat /2) * sin(dlat/2) + cos(toRadians(lat)) * cos(toRadians(lastLat)) * sin(dlon/2) * sin(dlon /2); + //double c = 2*asin(sqrt(fabs(a))); // Alternate definition. + double c = 4*atan2(sqrt(a),1+sqrt(1-fabs(a))); + double delta_d = r * c; + if(lastLat != 0) + distance += delta_d; + + if (delta_d<0) + { + delta_d=0; + } + + // compute speed for this trackpoint by dividing the distance + // traveled by the elapsed time. The elapsed time will be 0.0 + // for the first trackpoint -- so set speed to 0.0 instead of + // dividing by zero. + if (delta_t_ms > 0.0) + { + speed= 1000.0 * delta_d / delta_t_ms * 3600.0; + } } // Record trackpoint @@ -260,6 +318,7 @@ bool last_time = time; lastLon = lon; lastLat = lat; + lastSpeed = speed; } return true; diff --git a/src/FileIO/GpxParser.h b/src/FileIO/GpxParser.h index 86c42e2d5..14b5c38f7 100644 --- a/src/FileIO/GpxParser.h +++ b/src/FileIO/GpxParser.h @@ -29,6 +29,8 @@ #include #include "Settings.h" +#define GPX_SAMPLE_INTERVAL (1.0) + class GpxParser : public QXmlDefaultHandler { public: @@ -53,6 +55,7 @@ private: QDateTime time; double distance; double lastLat, lastLon; + double lastSpeed; double alt; double lat; @@ -61,6 +64,7 @@ private: double temp; double cad; double watts; + double speed; // set to false after the first time element is seen (not in metadata) bool firstTime; diff --git a/src/FileIO/GpxRideFile.cpp b/src/FileIO/GpxRideFile.cpp index b1f145996..bcabb5df0 100644 --- a/src/FileIO/GpxRideFile.cpp +++ b/src/FileIO/GpxRideFile.cpp @@ -32,7 +32,7 @@ RideFile *GpxFileReader::openRideFile(QFile &file, QStringList &errors, QListsetRecIntSecs(1.0); + rideFile->setRecIntSecs(GPX_SAMPLE_INTERVAL); //rideFile->setDeviceType("GPS Exchange Format"); rideFile->setFileFormat("GPS Exchange Format (gpx)");