From 80d049eebf039abed3e8ed10eea48bb76f6ee2e6 Mon Sep 17 00:00:00 2001 From: hermes-explorigin Date: Mon, 10 Aug 2026 15:52:34 +0000 Subject: [PATCH] feat(ui): context dialog on map click (no auto-save); save-point form; 'Directions from/to here'; remove Directions button + Data sources bar --- src/lib/components/DirectionsPanel.svelte | 55 +++----- src/lib/components/LocationDialog.svelte | 153 ++++++++++++++++++++++ src/lib/components/MapView.svelte | 106 +++++++++++++-- src/routes/+page.svelte | 18 --- 4 files changed, 265 insertions(+), 67 deletions(-) create mode 100644 src/lib/components/LocationDialog.svelte diff --git a/src/lib/components/DirectionsPanel.svelte b/src/lib/components/DirectionsPanel.svelte index 42b609e..6c3d0f9 100644 --- a/src/lib/components/DirectionsPanel.svelte +++ b/src/lib/components/DirectionsPanel.svelte @@ -1,7 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/MapView.svelte b/src/lib/components/MapView.svelte index 683ea90..3f82823 100644 --- a/src/lib/components/MapView.svelte +++ b/src/lib/components/MapView.svelte @@ -5,9 +5,10 @@ import { search, type Searchable, type SearchResult } from '$lib/search'; import { geocode, type GeocodedPlace } from '$lib/geocode'; import DirectionsPanel from '$lib/components/DirectionsPanel.svelte'; + import LocationDialog from '$lib/components/LocationDialog.svelte'; import { prefs } from '$lib/prefs'; import type { RouteGraph, AvoidRule, RouteResult } from '$lib/routing'; - import { savedPoints, addSavedPoint, exportSavedPoints, importSavedPoints, type SavedPoint } from '$lib/savedPoints'; + import { savedPoints, addSavedPoint, removeSavedPoint, exportSavedPoints, importSavedPoints, type SavedPoint } from '$lib/savedPoints'; type FeatureCollection = { type: string; @@ -63,6 +64,13 @@ // Set by a search result or a direct map click; most recent wins. let origin = $state<{ lat: number; lon: number; label?: string } | null>(null); let originLayer: L.Layer | null = null; + let destination = $state<{ lat: number; lon: number; label?: string } | null>(null); + // --- context dialog (shown on map click; no auto-save) --- + let ctx = $state<{ + x: number; y: number; lat: number; lon: number; + feature?: { name: string; rows: [string, string][] } | null; + savedPoint?: SavedPoint | null; + } | null>(null); // --- Saved Points (browser-local) layer --- let savedPointsList = $state([]); let savedPointLayer: L.LayerGroup | null = null; @@ -262,6 +270,10 @@ }).addTo(savedPointLayer); const nm = p.label || `${p.lat.toFixed(4)}, ${p.lon.toFixed(4)}`; mk.bindPopup(`${nm.replace(/
Saved point`); + mk.on('click', (e: L.LeafletMouseEvent) => { + L.DomEvent.stopPropagation(e.originalEvent); + openContextAt(e.latlng, null); + }); } } // Redraw saved points when the underlying list changes while visible. @@ -308,10 +320,60 @@ reader.readAsText(file); } + // Open the context dialog at a lat/lon. `feature` null => treat as a bare + // map spot (offer save). We also check whether this spot is a saved point. + function openContextAt(latlng: L.LatLng, featureInfo: { name: string; rows: [string, string][] } | null) { + const xy = map.latLngToContainerPoint(latlng); + const sp = findSavedPointNear(latlng.lat, latlng.lng); + ctx = { + x: xy.x, y: xy.y, lat: latlng.lat, lon: latlng.lng, + feature: featureInfo, + savedPoint: sp?.id ? sp : null + }; + } + // Return a saved point within ~25m of the click, if any. + function findSavedPointNear(lat: number, lon: number): SavedPoint | null { + const dLat = 25 / 111320; + const dLon = 25 / (111320 * Math.cos((lat * Math.PI) / 180)); + for (const p of savedPointsList) { + if (Math.abs(p.lat - lat) <= dLat && Math.abs(p.lon - lon) <= dLon) return p; + } + return null; + } + function featureInfoFor(f: GeoJSON.Feature): { name: string; rows: [string, string][] } { + const props = (f.properties ?? {}) as Record; + const name = (props.name as string) ?? String(f.id ?? 'Point'); + const rows: [string, string][] = Object.entries(props) + .filter(([k]) => !k.startsWith('_') && k !== 'name') + .map(([k, v]) => [k, String(v)]); + return { name, rows }; + } + function closeContext() { ctx = null; } + function setSavedPointAsOrigin(p: SavedPoint) { setOrigin(p.lat, p.lon, p.label || 'Saved point'); } + // --- context dialog actions --- + function ctxDirectionsFrom(lat: number, lon: number, label?: string) { + setOrigin(lat, lon, label || 'Start'); + ctx = null; + // Ensure the destination picker opens if the road data layer is available. + if (dumpLayers.length) { showDirections = true; void loadRouteGraph(); } + } + function ctxDirectionsTo(lat: number, lon: number, label?: string) { + destination = { lat, lon, label }; + ctx = null; + } + function ctxSavePoint(label?: string) { + const sp = addSavedPoint({ lat: ctx!.lat, lon: ctx!.lon, label }); + ctx = null; + } + function ctxDeleteSavedPoint(id: string) { + removeSavedPoint(id); + ctx = null; + } + function toggle(name: string) { const g = layerGroups[name]; if (!g) return; @@ -376,13 +438,12 @@ attribution: '© OpenStreetMap contributors' }).addTo(map); - // Clicking an empty spot on the map creates a browser-local Saved Point - // and sets it as the origin (start point). POI clicks are handled by the - // individual feature popups, so a bare map click = empty space. + // Map click (empty space, since feature clicks stop propagation) always + // opens a context dialog — whether or not the Directions panel is open. + // This is how the user picks destinations and saves points. No auto-save. map.on('click', (e: L.LeafletMouseEvent) => { - if (showDirections) return; // direction dialog owns clicks while open - const sp = addSavedPoint({ lat: e.latlng.lat, lon: e.latlng.lng }); - setOrigin(sp.lat, sp.lon, sp.label || 'Start'); + if (computingRoute) return; + openContextAt(e.latlng, null); }); try { @@ -433,6 +494,12 @@ totalFeatures++; const props = (f.properties ?? {}) as Record; lay.bindPopup(popupHtml(f)); + // Clicking a feature opens the context dialog with its info and + // stops propagation so the map's empty-click handler doesn't fire. + lay.on('click', (e: L.LeafletMouseEvent) => { + L.DomEvent.stopPropagation(e.originalEvent); + openContextAt(e.latlng, featureInfoFor(f)); + }); featureLayers[f.id as string] = lay; // Build a searchable record. A single malformed feature @@ -542,11 +609,6 @@ {/if} - {#if origin} - - {/if} @@ -573,6 +635,24 @@ + {#if ctx} + + {/if} + {#if showDirections && origin}
setSavedPointAsOrigin(p)} + destination={destination} + onDestinationChange={(d) => (destination = d)} progress={routeProgress} computing={computingRoute} {runRoute} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8b253b2..5624fab 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -19,7 +19,6 @@ loading = false; }); - const totalFeatures = $derived(areas.reduce((n, a) => n + a.featureCount, 0)); @@ -27,14 +26,6 @@
-
- ({areas.length}) data sources - {#if totalFeatures > 0} - {totalFeatures} features - {/if} - Toggle layers on the map (top-right). -
- {#if manifestError}
Could not load data manifest (/data/_index.json).
@@ -58,15 +49,6 @@ height: 100vh; height: 100dvh; overflow: hidden; } - .controls { - display: flex; align-items: center; gap: 1.25rem; - padding: 0.55rem 1.25rem; - background: #1e293b; color: #e2e8f0; - font: 0.85rem system-ui, sans-serif; z-index: 500; - } - .controls strong { color: #fff; } - .stats { color: #94a3b8; } - .hint { color: #64748b; margin-left: auto; } .error { padding: 3rem 2rem; font: 0.95rem/1.5 system-ui, sans-serif; color: #b91c1c; }