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
@@ -44,7 +44,7 @@ function JourneyDetailPageDesktop() {
unlinkTrip, setUnlinkTrip, showSettings, setShowSettings,
hideSkeletons, setHideSkeletons,
mapRef, fullMapRef, activeLocationId, handleMarkerClick, handleLocationClick,
mapEntries, sidebarMapItems, tripDates, isMobile,
mapEntries, sidebarMapItems, tripDates, isMobile, tracks,
loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto,
} = useJourneyDetail()
@@ -86,6 +86,7 @@ function JourneyDetailPageDesktop() {
<MobileMapTimeline
entries={timelineEntries}
mapEntries={sidebarMapItems}
tracks={tracks}
dark={document.documentElement.classList.contains('dark')}
readOnly={!canEditEntries}
onEntryClick={(entry) => setViewingEntry(entry)}
@@ -441,6 +442,7 @@ function JourneyDetailPageDesktop() {
ref={mapRef}
checkins={[]}
entries={sidebarMapItems as any}
tracks={tracks}
height={9999}
activeMarkerId={activeEntryId}
onMarkerClick={handleMarkerClick}
@@ -3,6 +3,8 @@ import { useParams, useNavigate, useSearchParams } from 'react-router'
import { useJourneyStore } from '../../store/journeyStore'
import { useTranslation } from '../../i18n'
import { addListener, removeListener } from '../../api/websocket'
import { journeyApi } from '../../api/client'
import type { JourneyTrack } from '@trek/shared'
import { DAY_COLORS } from '../../components/Journey/dayColors'
import type { JourneyMapAutoHandle as JourneyMapHandle } from '../../components/Journey/JourneyMapAuto'
import { useToast } from '../../components/shared/Toast'
@@ -61,6 +63,19 @@ export function useJourneyDetail() {
if (id) loadJourney(Number(id)).catch(() => {})
}, [id])
// GPX tracks of the trips behind this journey (#1260). Loaded separately from the
// journey itself: a journey without tracks is the common case, and a failed lookup
// should cost the map its lines, not the whole page.
const [tracks, setTracks] = useState<JourneyTrack[]>([])
useEffect(() => {
if (!id) return
let cancelled = false
journeyApi.listTracks(Number(id))
.then(res => { if (!cancelled) setTracks(res.tracks ?? []) })
.catch(() => { if (!cancelled) setTracks([]) })
return () => { cancelled = true }
}, [id])
useEffect(() => {
if (current?.hide_skeletons !== undefined) setHideSkeletons(current.hide_skeletons)
}, [current?.hide_skeletons])
@@ -287,7 +302,7 @@ export function useJourneyDetail() {
unlinkTrip, setUnlinkTrip, showSettings, setShowSettings,
hideSkeletons, setHideSkeletons,
mapRef, fullMapRef, activeLocationId, handleMarkerClick, handleLocationClick,
mapEntries, sidebarMapItems, tripDates, isMobile,
mapEntries, sidebarMapItems, tripDates, isMobile, tracks,
loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto,
}
}