Draw a LiveTrack session's planned course alongside the actual track
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 13s

Garmin's own LiveTrack page already shows both when the activity was
started from a saved course — the session payload carries the course's
geoPoints right alongside the track points (LiveTrackClient found it
nested under the same React Query cache the session/points come from).

Rendered as a second, dashed LineString distinct from the actual
track's own line ("kind": "actual" vs "planned" on each GeoJSON
feature) — muted gray rather than matching Garmin's own purple-for-
planned, since this app's actual track is already purple and reusing
it would make the two indistinguishable.
This commit is contained in:
randy committed 2026-09-20 16:54:24 +02:00
1 parent 8eada07869
commit 3106f8c103
4 files changed
+66 -21

No files matched your search

+30 -18
View File
@@ -743,24 +743,36 @@ module Maptool
halt 502, json(error: "track has no positions") if coordinates.empty?
last = points.last
json(
type: "FeatureCollection",
features: [
{ type: "Feature", geometry: { type: "LineString", coordinates: coordinates }, properties: {} },
{
type: "Feature",
geometry: { type: "Point", coordinates: coordinates.last },
# Merges the session summary in too — the point layer's hover
# popup (public/js/livetrack.js) reads it straight off here
# rather than needing a second fetch against /sessions.
properties: summary.merge(
"time" => last["dateTime"],
"speed_meters_per_sec" => last["speedMetersPerSec"],
"heart_rate_beats_per_min" => last["heartRateBeatsPerMin"]
),
},
]
)
features = [
{ type: "Feature", geometry: { type: "LineString", coordinates: coordinates }, properties: { "kind" => "actual" } },
{
type: "Feature",
geometry: { type: "Point", coordinates: coordinates.last },
# Merges the session summary in too — the point layer's hover
# popup (public/js/livetrack.js) reads it straight off here
# rather than needing a second fetch against /sessions.
properties: summary.merge(
"time" => last["dateTime"],
"speed_meters_per_sec" => last["speedMetersPerSec"],
"heart_rate_beats_per_min" => last["heartRateBeatsPerMin"]
),
},
]
# Only present when the activity was started from a saved Garmin
# course (see LiveTrackClient#fetch) — plotted separately from the
# actual track ("kind" distinguishes the two LineStrings) since it's
# the planned route, not where the person has actually been.
course_coordinates = result.course&.[]("geoPoints")&.filter_map { |p| p.dig("position", "lon") && p.dig("position", "lat") && [p["position"]["lon"], p["position"]["lat"]] }
if course_coordinates&.any?
features << {
type: "Feature",
geometry: { type: "LineString", coordinates: course_coordinates },
properties: { "kind" => "planned", "name" => result.course["name"] },
}
end
json(type: "FeatureCollection", features: features)
end
get "/api/gnss/position" do