Add activity type and a click-to-open track profile panel
Track properties gain activity_type (FIT: sport enum from the session/sport message, Garmin's own table; GPX: <trk><type> passed through as-is) alongside the existing distance/elevation/duration. Clicking a track line now opens a bottom panel with a chart per metric the track actually has (elevation always; speed/cadence/heart rate only when present — FIT's record fields or GPX's device extensions). TrackStats.build_profile computes cumulative distance the same way for both formats so they're directly comparable. One pointermove listener drives a synchronized crosshair across every chart, a single tooltip listing all series, and a map marker at the hovered point's position — hovering the chart shows where on the track that moment was.
This commit is contained in:
1 parent
cd4eef342b
commit
105651f005
14 files changed
+655
-42
No files matched your search
+54
-15
@@ -234,21 +234,60 @@ matched URL can never break out of the `href="..."` attribute. Every
|
||||
other value is HTML-escaped before being handed to `Popup#setHTML`.
|
||||
|
||||
Track properties:: Every track feature (both formats) also carries
|
||||
`distance_meters`, `elevation_gain_meters`, `duration_secs` and
|
||||
`point_count`, computed once server-side by `Maptool::TrackStats`
|
||||
(`lib/maptool/track_stats.rb`, shared by `Gpx`/`Fit` so neither
|
||||
duplicates the haversine formula) rather than in the browser — cheap
|
||||
either way, but it means a GPX and a FIT file end up with the same
|
||||
properties shape, so the frontend doesn't need to know which format it
|
||||
got. Distance sums the haversine distance between consecutive points;
|
||||
elevation gain sums only the positive deltas (descents don't
|
||||
subtract); duration is the gap between the first and last known
|
||||
timestamp, skipping unknown ones in between — `nil` for any of these
|
||||
where the source data doesn't have enough to compute it (e.g. a GPX
|
||||
file with no `<time>` elements has `duration_secs: nil`). Hovering a
|
||||
track line (rather than clicking, unlike a waypoint — these properties
|
||||
describe the whole line, not whichever point the cursor happens to be
|
||||
over) shows them in the same popup mechanism.
|
||||
`distance_meters`, `elevation_gain_meters`, `duration_secs`,
|
||||
`point_count` and `activity_type`, computed once server-side by
|
||||
`Maptool::TrackStats` (`lib/maptool/track_stats.rb`, shared by
|
||||
`Gpx`/`Fit` so neither duplicates the haversine formula) rather than
|
||||
in the browser — cheap either way, but it means a GPX and a FIT file
|
||||
end up with the same properties shape, so the frontend doesn't need to
|
||||
know which format it got. Distance sums the haversine distance between
|
||||
consecutive points; elevation gain sums only the positive deltas
|
||||
(descents don't subtract); duration is the gap between the first and
|
||||
last known timestamp, skipping unknown ones in between — `nil` for any
|
||||
of these where the source data doesn't have enough to compute it (e.g.
|
||||
a GPX file with no `<time>` elements has `duration_secs: nil`).
|
||||
`activity_type` comes from wherever each format keeps it: FIT's
|
||||
`session`/`sport` message has a `sport` enum field (`Fit::SPORT_NAMES`
|
||||
— Garmin's own table, github.com/garmin/fit-python-sdk's `profile.py`,
|
||||
not exhaustive of every value Garmin has defined but every mainstream
|
||||
one); GPX's `<trk><type>` is free text, whatever a device/app chose to
|
||||
write, passed through as-is. Hovering a track line (rather than
|
||||
clicking, unlike a waypoint — these properties describe the whole
|
||||
line, not whichever point the cursor happens to be over) shows them in
|
||||
the same popup mechanism as waypoints; the popup also hints that
|
||||
clicking opens the full profile panel below.
|
||||
|
||||
Track profile panel:: Clicking a track line opens a bottom panel
|
||||
(`public/js/track_profile.js`, `TrackProfilePanel`) with a chart per
|
||||
metric the track actually has data for — elevation always (assuming
|
||||
any point carries an `<ele>`/altitude), speed/cadence/heart rate only
|
||||
when present. `Maptool::TrackStats.build_profile` (called from both
|
||||
`Gpx`/`Fit`, alongside `.compute` above) produces this: one entry per
|
||||
point with cumulative distance (computed the same way for every
|
||||
format, not trusted from a device's own odometer field, so a GPX and a
|
||||
FIT track are directly comparable), elevation, and whatever
|
||||
speed/cadence/heart-rate extras that point's format/file carried —
|
||||
`nil` fields simply don't render their chart. For FIT, those extras
|
||||
are the `record` message's own `heart_rate`/`cadence`/`speed` fields
|
||||
(3/4/6); for GPX, a device-specific `<extensions>` block under each
|
||||
`<trkpt>` — usually Garmin's `TrackPointExtension`, matched by local
|
||||
element name only (`hr`/`cad`/`speed`) so it doesn't care which XML
|
||||
namespace prefix a given file happens to declare.
|
||||
+
|
||||
Each chart is its own single-series `<svg>` (elevation/speed/cadence/
|
||||
heart rate are on wildly different scales, so this is small multiples
|
||||
rather than one chart with several y-axes) with a fixed `viewBox` width, stretched to
|
||||
the container by CSS; mouse interaction converts `clientX` through the
|
||||
container's current `getBoundingClientRect()` on every move rather
|
||||
than fighting the SVG's own coordinate transform, so it stays correct
|
||||
across container width without redrawing — though a browser resize
|
||||
while the panel is open won't reflow the already-drawn paths until
|
||||
it's reopened. One `pointermove` listener on the whole chart container
|
||||
(not per-chart) finds the nearest profile point by distance and drives
|
||||
three things at once: a synchronized crosshair line in every chart, a
|
||||
single tooltip listing every active series' value at that point, and a
|
||||
MapLibre `Marker` moved to that point's `lon`/`lat` on the map — so
|
||||
hovering the chart shows where on the track that moment actually was.
|
||||
|
||||
== Live GNSS position
|
||||
|
||||
|
||||
Reference in new issue
Block a user