feat(maps): draw every travel day's route at once, with the trip total

Extract the day route builder out of useRouteCalculation into a pure module and
aggregate it over the whole trip: one map with every day's route, each day in its
own colour, the mode per stage and the trip's total distance. Straight lines first,
roads as they answer; a refused leg keeps its line and adds no distance.

Give the floating map controls one shared drop shadow while adding the toggle, so
the new pill and the base-layer switcher stand off the tiles the way the locate
button always has. Document the control in wiki/Map-Features.md.

Addresses #1736
This commit is contained in:
Konstantinos Thermos
2026-09-10 15:57:48 +03:00
parent 8acfaea3c4
commit bf1900c3a0
43 changed files with 1095 additions and 192 deletions
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { MapViewAuto } from '../../../../components/Map/MapViewAuto'
import { MapCompassPill, type CompassMap } from '../../../../components/Map/MapCompassPill'
import { TripRouteOverviewPill, TripRouteOverviewPanel } from '../../../../components/Map/TripRouteOverview'
import PoiCategoryPill from '../../../../components/Map/PoiCategoryPill'
import { usePoiExplore } from '../../../../components/Map/usePoiExplore'
import { useSettingsStore } from '../../../../store/settingsStore'
@@ -29,6 +30,7 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
const poi = usePoiExplore()
const [glMap, setGlMap] = useState<CompassMap | null>(null)
const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
const mapActive = shell.view === 'map'
@@ -44,14 +46,16 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
tripId={planner.tripId}
places={planner.mapPlaces}
dayPlaces={planner.dayPlaces}
route={planner.route}
route={planner.overviewActive ? planner.tripOverview.lines : planner.route}
routeColors={planner.overviewActive ? planner.tripOverview.lineColors : undefined}
focusPoints={planner.overviewActive ? planner.tripOverview.focusPoints : undefined}
routeVias={planner.routeVias}
showTransitRoutes={planner.transitRoutesShown}
// The route toggle belongs to one day, so the map needs that day to know
// which automated transports may ride it (#2019).
days={planner.days}
selectedDayId={planner.selectedDayId}
routeSegments={planner.routeSegments}
routeSegments={planner.overviewActive ? planner.tripOverview.segments : planner.routeSegments}
selectedPlaceId={planner.selectedPlaceId}
onMarkerClick={planner.handleMarkerClick}
// Tap on empty map = deselect, same contract as desktop.
@@ -104,6 +108,29 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
<MapCompassPill map={glMap} />
</div>
)}
{/* Whole-trip overview (#1736): the stages stack above their toggle on the right,
clear of the round-controls band below it and of the compass and Leaflet's
base-layer switcher, which both sit in the bottom-LEFT corner. The offset is
Tailwind rather than inline because the compass band is identified by being
the one element with an inline --bottom-nav-h, and a second would make that
ambiguous. */}
{mapActive && !planner.roadtripActive && (
<div className="pointer-events-none absolute left-3 right-3 z-[25] flex flex-col items-end gap-2 bottom-[calc(var(--bottom-nav-h,84px)+58px)]">
{planner.overviewActive && (
<TripRouteOverviewPanel
overview={planner.tripOverview}
unit={distanceUnit}
selectedDayId={planner.selectedDayId}
onSelectDay={planner.handleSelectDay}
// Tighter than the desktop card: the map is the whole screen here, so a
// long day name ellipsizes rather than eating another 80px of it.
maxWidth={240}
/>
)}
<TripRouteOverviewPill active={planner.overviewShown} onToggle={planner.toggleOverview} />
</div>
)}
</div>
)
}