Files
maptool/lib/maptool/track_stats.rb
T
randy a0476f1204
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
Add a Distance/Time toggle to the profile chart's x-axis
TrackStats.build_profile now also computes elapsed_secs per point
(relative to the first known timestamp), alongside the existing
cumulative distance. Point x-positions stay distance-based either
way — only the shared axis strip's 5 tick labels (and the tooltip)
switch between the two, so toggling is a cheap re-render rather than
rebuilding the charts. The toggle only appears when a track actually
has elapsed-time data to switch to.
2026-09-07 13:54:49 +02:00

72 lines
2.7 KiB
Ruby

module Maptool
# Distance/elevation-gain/duration for a track, shared by Gpx and Fit so
# neither has its own copy of the haversine formula.
module TrackStats
EARTH_RADIUS_METERS = 6_371_000
# `coordinates`: array of [lon, lat] or [lon, lat, ele]. `times`: a
# parallel array of Time (or nil where unknown) — only its first and
# last non-nil entries matter, so gaps in the middle are fine.
def self.compute(coordinates, times: [])
distance = 0.0
elevation_gain = 0.0
coordinates.each_cons(2) do |(lon1, lat1, ele1), (lon2, lat2, ele2)|
distance += haversine(lat1, lon1, lat2, lon2)
elevation_gain += ele2 - ele1 if ele1 && ele2 && ele2 > ele1
end
known_times = times.compact
duration = known_times.size >= 2 ? (known_times.last - known_times.first).to_i : nil
{
"distance_meters" => distance.round(1),
"elevation_gain_meters" => elevation_gain.positive? ? elevation_gain.round(1) : nil,
"duration_secs" => duration,
"point_count" => coordinates.size,
}
end
# One entry per coordinate, for the profile chart: cumulative distance
# (computed the same way for every format, rather than trusting a
# device's own odometer field — keeps a GPX and a FIT file directly
# comparable), elapsed time since the first known timestamp, elevation,
# and whatever per-point extras (speed/cadence/heart rate) the caller
# has — nil wherever a format/file doesn't carry it.
# `extras`: array of hashes parallel to `coordinates`, or shorter/empty.
# `times`: array of Time (or nil) parallel to `coordinates`, or [].
def self.build_profile(coordinates, times: [], extras: [])
start_time = times.compact.first
distance = 0.0
previous = nil
coordinates.map.with_index do |(lon, lat, ele), i|
distance += haversine(previous[1], previous[0], lat, lon) if previous
previous = [lon, lat]
extra = extras[i] || {}
point_time = times[i]
{
"distance_meters" => distance.round(1),
"elapsed_secs" => point_time && start_time ? (point_time - start_time).to_i : nil,
"elevation_meters" => ele,
"speed_mps" => extra[:speed_mps],
"cadence_rpm" => extra[:cadence_rpm],
"heart_rate_bpm" => extra[:heart_rate_bpm],
"lon" => lon,
"lat" => lat,
}
end
end
def self.haversine(lat1, lon1, lat2, lon2)
rad = Math::PI / 180
dlat = (lat2 - lat1) * rad
dlon = (lon2 - lon1) * rad
a = (Math.sin(dlat / 2)**2) + (Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * (Math.sin(dlon / 2)**2))
2 * EARTH_RADIUS_METERS * Math.asin(Math.sqrt(a))
end
private_class_method :haversine
end
end