From d5ca8f9c8e6556398987c8bd32076d0a63deb388 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 22 Dec 2025 21:12:03 -0500 Subject: [PATCH] feat: add map center and zoom state management with URL synchronization --- frontend/src/lib/components/FullMap.svelte | 27 ++++++++++- frontend/src/routes/map/+page.svelte | 52 +++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/FullMap.svelte b/frontend/src/lib/components/FullMap.svelte index 18776f5c..bbd1c149 100644 --- a/frontend/src/lib/components/FullMap.svelte +++ b/frontend/src/lib/components/FullMap.svelte @@ -38,6 +38,7 @@ export let mapClass = 'w-full h-full'; export let standardControls = true; export let zoom = 2; + export let center: [number, number] = [0, 0]; export let mapClickEnabled: boolean = true; // Basemap @@ -128,12 +129,25 @@ mapClick: { lngLat: { lng: number; lat: number } }; markerClick: { feature: unknown; markerProps: Record | null }; clusterClick: LayerClickInfo; + mapMove: { center: { lng: number; lat: number }; zoom: number }; }>(); function handleMapClick(e: CustomEvent<{ lngLat: { lng: number; lat: number } }>) { dispatch('mapClick', e.detail); } + function handleMapMove() { + if (!map) return; + const mapCenter = map.getCenter(); + const mapZoom = map.getZoom(); + if (mapCenter && typeof mapZoom === 'number') { + dispatch('mapMove', { + center: { lng: mapCenter.lng, lat: mapCenter.lat }, + zoom: mapZoom + }); + } + } + function setBasemapType(next: string) { basemapType = next; } @@ -261,7 +275,14 @@ {/if} {/if} - + {#key styleNonce} {#if effectiveGeoJson && Array.isArray(effectiveGeoJson.features) && effectiveGeoJson.features.length > 0} {#if clusterEnabled} @@ -331,7 +352,9 @@ {/key} {#if mapClickEnabled} - + + {:else} + {/if} diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index c8edee82..b907cc2b 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -6,6 +6,7 @@ import type { ClusterOptions } from 'svelte-maplibre'; import { goto } from '$app/navigation'; import { getActivityColor } from '$lib'; + import { page } from '$app/stores'; // Icons import MapIcon from '~icons/mdi/map'; @@ -30,6 +31,11 @@ let basemapType: string = 'default'; + // Map state from URL params + let mapZoom: number = 2; + let mapCenter: [number, number] = [0, 0]; + let updateUrlTimeout: NodeJS.Timeout | null = null; + export let initialLatLng: { lat: number; lng: number } | null = null; let visitedRegions: VisitedRegion[] = data.props.visitedRegions; @@ -350,7 +356,40 @@ newMarker = null; } + function updateUrlParams(lat: number, lng: number, zoom: number) { + if (updateUrlTimeout) clearTimeout(updateUrlTimeout); + updateUrlTimeout = setTimeout(() => { + const params = new URLSearchParams($page.url.searchParams); + params.set('lat', lat.toFixed(6)); + params.set('lng', lng.toFixed(6)); + params.set('zoom', zoom.toFixed(2)); + goto(`?${params.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }); + }, 500); + } + + function handleMapMove(e: CustomEvent<{ center: { lng: number; lat: number }; zoom: number }>) { + const { center, zoom } = e.detail; + updateUrlParams(center.lat, center.lng, zoom); + } + onMount(() => { + // Initialize from URL params + const params = $page.url.searchParams; + const lat = params.get('lat'); + const lng = params.get('lng'); + const zoom = params.get('zoom'); + + if (lat && lng && zoom) { + const parsedLat = parseFloat(lat); + const parsedLng = parseFloat(lng); + const parsedZoom = parseFloat(zoom); + + if (Number.isFinite(parsedLat) && Number.isFinite(parsedLng) && Number.isFinite(parsedZoom)) { + mapCenter = [parsedLng, parsedLat]; + mapZoom = parsedZoom; + } + } + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; const mql = window.matchMedia('(hover: none), (pointer: coarse)'); const update = () => { @@ -359,11 +398,17 @@ update(); if (typeof mql.addEventListener === 'function') { mql.addEventListener('change', update); - return () => mql.removeEventListener('change', update); + return () => { + mql.removeEventListener('change', update); + if (updateUrlTimeout) clearTimeout(updateUrlTimeout); + }; } // Safari < 14 (mql as any).addListener?.(update); - return () => (mql as any).removeListener?.(update); + return () => { + (mql as any).removeListener?.(update); + if (updateUrlTimeout) clearTimeout(updateUrlTimeout); + }; }); // FullMap handles cluster theme styling + cluster expansion on click. @@ -465,7 +510,10 @@ {getMarkerProps} mapClass="w-full h-full min-h-[70vh] rounded-lg" standardControls + zoom={mapZoom} + center={mapCenter} on:mapClick={addMarker} + on:mapMove={handleMapMove} >