feat: add map center and zoom state management with URL synchronization

This commit is contained in:
Sean Morley
2025-12-22 21:12:03 -05:00
parent 4198b9e39f
commit d5ca8f9c8e
2 changed files with 75 additions and 4 deletions

View File

@@ -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<string, unknown> | 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}
<MapLibre bind:map style={getBasemapUrl(basemapType)} class={mapClass} {standardControls} {zoom}>
<MapLibre
bind:map
style={getBasemapUrl(basemapType)}
class={mapClass}
{standardControls}
{zoom}
{center}
>
{#key styleNonce}
{#if effectiveGeoJson && Array.isArray(effectiveGeoJson.features) && effectiveGeoJson.features.length > 0}
{#if clusterEnabled}
@@ -331,7 +352,9 @@
{/key}
{#if mapClickEnabled}
<MapEvents on:click={handleMapClick} />
<MapEvents on:click={handleMapClick} on:moveend={handleMapMove} />
{:else}
<MapEvents on:moveend={handleMapMove} />
{/if}
<slot name="overlays" {map} />
</MapLibre>

View File

@@ -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}
>
<svelte:fragment
slot="marker"