feat(journey): draw the trips' GPX tracks on the journey map (#1911)

* feat(journey): serve the GPX tracks behind a journey

A journey has no trip of its own, but every entry records the trip it came from,
and a trip's routed geometries live on its places. So the tracks are derivable
without any new storage: GET /journeys/:id/tracks walks that link and returns the
geometries, dropping elevation nothing on the map reads and skipping anything
that is not a usable line.

* feat(journey): draw those tracks on the journey map

Both renderers, Leaflet and GL, plus the mobile map-timeline that shares them.
Solid and in the track's own colour with a white casing, so it reads as a
recorded route rather than as the dashed line that merely connects entries in
time order, and stays legible on satellite tiles.

* feat(journey): load the tracks alongside the journey

Fetched separately from the journey itself: most journeys have no tracks, and a
failed lookup should cost the map its lines rather than the whole page.

* docs(journey): document the GPX tracks on the map

* fix(journey): label tracks with the house map tooltip

bindTooltip's default box ignores the appearance tokens, so it sat there in
Leaflet grey next to markers already using .map-tooltip. Same class now, and an
unnamed track gets no tooltip at all instead of an empty one.
This commit is contained in:
Maurice
2026-08-12 15:14:50 +02:00
committed by GitHub
parent 767dc662b7
commit 870bd6131f
15 changed files with 334 additions and 11 deletions
+3 -1
View File
@@ -11,7 +11,7 @@ import {
mapsPlaceEnrichmentResultSchema,
type NotificationRespondRequest,
type SettingUpsertRequest, type SettingsBulkRequest,
type JourneyCreateRequest, type JourneyAddTripRequest,
type JourneyCreateRequest, type JourneyAddTripRequest, type JourneyTracksResponse,
type JourneyReorderEntriesRequest, type JourneyProviderPhotosRequest,
type JourneyShareLinkRequest,
type RegisterRequest, type LoginRequest, type ForgotPasswordRequest,
@@ -887,6 +887,8 @@ export const journeyApi = {
// Entries
listEntries: (id: number) => apiClient.get(`/journeys/${id}/entries`).then(r => r.data),
// GPX tracks of the trips this journey's entries came from (#1260).
listTracks: (id: number): Promise<JourneyTracksResponse> => apiClient.get(`/journeys/${id}/tracks`).then(r => r.data),
createEntry: (id: number, data: Record<string, unknown>) => apiClient.post(`/journeys/${id}/entries`, data).then(r => r.data),
updateEntry: (entryId: number, data: Record<string, unknown>) => apiClient.patch(`/journeys/entries/${entryId}`, data).then(r => r.data),
deleteEntry: (entryId: number) => apiClient.delete(`/journeys/entries/${entryId}`).then(r => r.data),