feat: build MapSift Overpass data explorer

This commit is contained in:
zv
2026-06-23 18:56:59 +02:00
commit b1e1df7e1d
23 changed files with 2909 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import { useEffect, useMemo } from 'react';
import L from 'leaflet';
import {
MapContainer,
Marker,
Popup,
TileLayer,
useMap,
useMapEvents,
} from 'react-leaflet';
import type { BBox, ParsedOsmResult } from '../search/types';
type MapViewProps = {
results: ParsedOsmResult[];
selectedResultId: string | null;
onResultSelect: (id: string) => void;
onViewportChange: (bbox: BBox) => void;
};
const DEFAULT_CENTER: [number, number] = [52.2297, 21.0122];
const DEFAULT_ZOOM = 13;
export function MapView({
results,
selectedResultId,
onResultSelect,
onViewportChange,
}: MapViewProps) {
const markerIcon = useMemo(
() =>
L.divIcon({
className: 'result-marker',
html: '<span></span>',
iconSize: [18, 18],
iconAnchor: [9, 9],
}),
[],
);
const selectedMarkerIcon = useMemo(
() =>
L.divIcon({
className: 'result-marker result-marker-selected',
html: '<span></span>',
iconSize: [24, 24],
iconAnchor: [12, 12],
}),
[],
);
return (
<MapContainer
center={DEFAULT_CENTER}
zoom={DEFAULT_ZOOM}
minZoom={3}
className="map-view"
zoomControl
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<ViewportReporter onViewportChange={onViewportChange} />
<SelectedResultFocus result={results.find((item) => item.id === selectedResultId) ?? null} />
{results.map((result) => (
<Marker
key={result.id}
position={[result.lat, result.lon]}
icon={result.id === selectedResultId ? selectedMarkerIcon : markerIcon}
eventHandlers={{
click: () => onResultSelect(result.id),
}}
>
<Popup>
<strong>{result.name ?? 'Unnamed place'}</strong>
<br />
{result.type} {result.osmId}
</Popup>
</Marker>
))}
</MapContainer>
);
}
function ViewportReporter({
onViewportChange,
}: {
onViewportChange: (bbox: BBox) => void;
}) {
const map = useMapEvents({
moveend: () => onViewportChange(boundsToBBox(map.getBounds())),
zoomend: () => onViewportChange(boundsToBBox(map.getBounds())),
});
useEffect(() => {
onViewportChange(boundsToBBox(map.getBounds()));
}, [map, onViewportChange]);
return null;
}
function SelectedResultFocus({ result }: { result: ParsedOsmResult | null }) {
const map = useMap();
useEffect(() => {
if (result) {
map.setView([result.lat, result.lon], Math.max(map.getZoom(), 15), {
animate: true,
});
}
}, [map, result]);
return null;
}
function boundsToBBox(bounds: L.LatLngBounds): BBox {
const southWest = bounds.getSouthWest();
const northEast = bounds.getNorthEast();
return {
south: southWest.lat,
west: southWest.lng,
north: northEast.lat,
east: northEast.lng,
};
}