From 0f57e9b9ac88727de40c4f6e1795fd2c8b613b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Thu, 16 Jul 2026 23:45:06 +0200 Subject: [PATCH] fix(planner): clear the route when waypoints drop below two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting waypoints down to <2 left the last computed route rendered — nothing cleared the stale geometry (recompute only runs for >=2). Add a waypoints observer that drops the computed geometry once fewer than two waypoints remain, via a new clearComputedRoute (keeps the routing profile, unlike clearRouteData). Covers all delete paths (sidebar, map, undo). Co-Authored-By: Claude Opus 4.8 --- apps/planner/app/lib/route-data.test.ts | 24 ++++++++++++++++++++++++ apps/planner/app/lib/route-data.ts | 18 +++++++++++++++++- apps/planner/app/lib/use-routing.ts | 13 +++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/planner/app/lib/route-data.test.ts b/apps/planner/app/lib/route-data.test.ts index db3d429..eb4e158 100644 --- a/apps/planner/app/lib/route-data.test.ts +++ b/apps/planner/app/lib/route-data.test.ts @@ -6,6 +6,8 @@ import { PROFILE_KEY, ROAD_METADATA_KEYS, clearRouteData, + clearComputedRoute, + hasComputedRoute, extractNoGoAreas, getBaseLayer, getColorMode, @@ -266,6 +268,28 @@ describe("clearRouteData", () => { }); }); +describe("clearComputedRoute / hasComputedRoute", () => { + it("detects computed geometry and clears it while keeping the profile", () => { + const { doc, routeData } = createDoc(); + expect(hasComputedRoute(routeData)).toBe(false); + + writeComputedRoute(doc, routeData, enrichedFixture()); + setProfile(routeData, "trekking"); + setColorMode(routeData, "surface"); + expect(hasComputedRoute(routeData)).toBe(true); + + clearComputedRoute(doc, routeData); + + expect(hasComputedRoute(routeData)).toBe(false); + expect(getCoordinates(routeData)).toBeNull(); + expect(readGeojson(routeData)).toBeUndefined(); + expect(readRoadMetadata(routeData).surfaces).toEqual([]); + // profile (and other prefs) survive — unlike clearRouteData + expect(getProfile(routeData)).toBe("trekking"); + expect(getColorMode(routeData)).toBe("surface"); + }); +}); + describe("extractNoGoAreas", () => { it("reads areas and drops degenerate ones", () => { const doc = new Y.Doc(); diff --git a/apps/planner/app/lib/route-data.ts b/apps/planner/app/lib/route-data.ts index a586a6f..2b4370a 100644 --- a/apps/planner/app/lib/route-data.ts +++ b/apps/planner/app/lib/route-data.ts @@ -189,7 +189,12 @@ export function setGeojson(routeData: Y.Map, geojson: unknown): void { } /** Removes the computed route and profile (view preferences are kept). */ -export function clearRouteData(doc: Y.Doc, routeData: Y.Map): void { +/** + * Clear the computed route geometry + per-coordinate road metadata, but keep + * the routing profile. Used when the route can no longer exist (fewer than two + * waypoints) so the stale line stops rendering without resetting the profile. + */ +export function clearComputedRoute(doc: Y.Doc, routeData: Y.Map): void { doc.transact(() => { routeData.delete("geojson"); routeData.delete("coordinates"); @@ -198,6 +203,17 @@ export function clearRouteData(doc: Y.Doc, routeData: Y.Map): void { for (const key of ROAD_METADATA_KEYS) { routeData.delete(key); } + }); +} + +/** True if the route data currently holds computed geometry. */ +export function hasComputedRoute(routeData: Y.Map): boolean { + return routeData.get("coordinates") !== undefined || routeData.get("geojson") !== undefined; +} + +export function clearRouteData(doc: Y.Doc, routeData: Y.Map): void { + doc.transact(() => { + clearComputedRoute(doc, routeData); routeData.delete(PROFILE_KEY); }); } diff --git a/apps/planner/app/lib/use-routing.ts b/apps/planner/app/lib/use-routing.ts index 9ffabe1..461887b 100644 --- a/apps/planner/app/lib/use-routing.ts +++ b/apps/planner/app/lib/use-routing.ts @@ -15,6 +15,8 @@ import { extractNoGoAreas, getProfile, writeComputedRoute, + clearComputedRoute, + hasComputedRoute, } from "./route-data.ts"; import { extractWaypoints } from "./waypoint-ymap.ts"; @@ -210,11 +212,22 @@ export function useRouting(yjs: YjsState | null, sessionId: string) { triggerRecompute(); }; + // When waypoints drop below two, no route can exist — drop the stale + // geometry so the line stops rendering. (Recompute for >=2 is triggered by + // the add/move/delete handlers themselves.) + const waypointsObserver = () => { + if (getWaypointsFromYjs(yjs.waypoints).length < 2 && hasComputedRoute(yjs.routeData)) { + clearComputedRoute(yjs.doc, yjs.routeData); + } + }; + yjs.routeData.observe(profileObserver); yjs.noGoAreas.observeDeep(noGoObserver); + yjs.waypoints.observeDeep(waypointsObserver); return () => { yjs.routeData.unobserve(profileObserver); yjs.noGoAreas.unobserveDeep(noGoObserver); + yjs.waypoints.unobserveDeep(waypointsObserver); }; }, [yjs, isHost, requestRoute]);