From dd8d72d7b3f612c667c2fd84205166a7eca9e535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 11 Apr 2026 00:03:56 +0200 Subject: [PATCH] Add map overnight markers, day labels, and context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Overnight waypoints render amber-brown (#8B6D3A) with moon icon - Day boundary labels (white pill "Day N · X km") at overnight waypoints - Right-click middle waypoints toggles overnight (first/last still delete) - Wire days prop through SessionView → PlannerMap Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/components/PlannerMap.tsx | 52 ++++++++++++++++++--- apps/planner/app/components/SessionView.tsx | 2 +- openspec/changes/multi-day-routes/tasks.md | 6 +-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/apps/planner/app/components/PlannerMap.tsx b/apps/planner/app/components/PlannerMap.tsx index f1986d6..b6759bf 100644 --- a/apps/planner/app/components/PlannerMap.tsx +++ b/apps/planner/app/components/PlannerMap.tsx @@ -3,25 +3,43 @@ import { MapContainer, TileLayer, LayersControl, Marker, CircleMarker, useMapEve import L from "leaflet"; import * as Y from "yjs"; import { useTranslation } from "react-i18next"; +import type { DayStage } from "@trails-cool/gpx"; import type { YjsState } from "~/lib/use-yjs"; import { baseLayers } from "@trails-cool/map"; import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx"; +import { isOvernight } from "~/lib/overnight"; +import { setOvernight } from "~/lib/overnight"; import { NoGoAreaLayer } from "./NoGoAreaLayer"; import { ColoredRoute, findSegmentForPoint, type ColorMode } from "./ColoredRoute"; import { RouteInteraction } from "./RouteInteraction"; import "leaflet/dist/leaflet.css"; -function waypointIcon(index: number): L.DivIcon { +function waypointIcon(index: number, overnight?: boolean): L.DivIcon { + const bg = overnight ? "#8B6D3A" : "#2563eb"; return L.divIcon({ className: "", html: `
${index + 1}
`, + ">${overnight ? "☾" : index + 1}`, + iconSize: [0, 0], + }); +} + +function dayLabelIcon(dayNumber: number, distanceKm: string): L.DivIcon { + return L.divIcon({ + className: "", + html: `
Day ${dayNumber} · ${distanceKm} km
`, iconSize: [0, 0], }); } @@ -30,6 +48,7 @@ interface WaypointData { lat: number; lon: number; name?: string; + overnight: boolean; } function getWaypointsFromYjs(waypoints: Y.Array>): WaypointData[] { @@ -37,6 +56,7 @@ function getWaypointsFromYjs(waypoints: Y.Array>): WaypointData[] lat: yMap.get("lat") as number, lon: yMap.get("lon") as number, name: yMap.get("name") as string | undefined, + overnight: isOvernight(yMap), })); } @@ -45,6 +65,7 @@ interface PlannerMapProps { onRouteRequest?: (waypoints: WaypointData[]) => void; onImportError?: (message: string) => void; highlightPosition?: [number, number] | null; + days?: DayStage[]; } function MapExposer() { @@ -206,7 +227,7 @@ function NoGoAreaButton({ active, onClick }: { active: boolean; onClick: () => v ); } -export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportError }: PlannerMapProps) { +export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportError, days }: PlannerMapProps) { const { t } = useTranslation("planner"); const [waypoints, setWaypoints] = useState([]); const [draggingOver, setDraggingOver] = useState(false); @@ -442,7 +463,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr key={i} position={[wp.lat, wp.lon]} draggable - icon={waypointIcon(i)} + icon={waypointIcon(i, wp.overnight)} eventHandlers={{ mouseover: () => { routeInteractionSuspendedRef.current = true; @@ -465,12 +486,31 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, onImportErr }, contextmenu: (e) => { L.DomEvent.preventDefault(e as unknown as Event); - deleteWaypoint(i); + // Middle waypoints: toggle overnight. First/last: delete. + if (i > 0 && i < waypoints.length - 1) { + setOvernight(yjs, i, !wp.overnight); + } else { + deleteWaypoint(i); + } }, }} /> ))} + {/* Day boundary labels on map */} + {days && days.length > 1 && days.map((day) => { + const wp = waypoints[day.endWaypointIndex]; + if (!wp || day.dayNumber === days.length) return null; + return ( + + ); + })} + {routeCoordinates && routeCoordinates.length >= 2 && ( <> } > - addToast(msg, "error")} /> + addToast(msg, "error")} days={days} /> diff --git a/openspec/changes/multi-day-routes/tasks.md b/openspec/changes/multi-day-routes/tasks.md index 5a16b03..13830f8 100644 --- a/openspec/changes/multi-day-routes/tasks.md +++ b/openspec/changes/multi-day-routes/tasks.md @@ -13,9 +13,9 @@ ## 3. Map Integration -- [ ] 3.1 Add overnight marker variant to `PlannerMap`: amber-brown circle (`--stop` token) for overnight waypoints, replacing default olive marker -- [ ] 3.2 Add day label DivIcon markers on route at day boundaries: white pill with "Day N . X km" text, positioned at overnight waypoint coordinates -- [ ] 3.3 Add right-click context menu on waypoint markers with "Mark as overnight stop" / "Remove overnight stop" option +- [x] 3.1 Add overnight marker variant to `PlannerMap`: amber-brown circle (`--stop` token) for overnight waypoints, replacing default olive marker +- [x] 3.2 Add day label DivIcon markers on route at day boundaries: white pill with "Day N . X km" text, positioned at overnight waypoint coordinates +- [x] 3.3 Add right-click context menu on waypoint markers with "Mark as overnight stop" / "Remove overnight stop" option ## 4. Elevation Chart