172 lines
3.9 KiB
TypeScript
172 lines
3.9 KiB
TypeScript
import { useEffect, useMemo } from 'react';
|
|
import L from 'leaflet';
|
|
import {
|
|
MapContainer,
|
|
Marker,
|
|
Popup,
|
|
TileLayer,
|
|
Tooltip,
|
|
useMap,
|
|
useMapEvents,
|
|
} from 'react-leaflet';
|
|
import type { MapTileStyle } from './mapStyles';
|
|
import type { BBox, ParsedOsmResult } from '../search/types';
|
|
|
|
type MapViewProps = {
|
|
results: ParsedOsmResult[];
|
|
selectedResultId: string | null;
|
|
tileStyle: MapTileStyle;
|
|
showResultLabels: boolean;
|
|
fitMapToResults: boolean;
|
|
dimMap: boolean;
|
|
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,
|
|
tileStyle,
|
|
showResultLabels,
|
|
fitMapToResults,
|
|
dimMap,
|
|
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={dimMap ? 'map-view map-view-dimmed' : 'map-view'}
|
|
zoomControl
|
|
>
|
|
<TileLayer
|
|
key={tileStyle.id}
|
|
attribution={tileStyle.attribution}
|
|
url={tileStyle.url}
|
|
maxZoom={tileStyle.maxZoom}
|
|
/>
|
|
<ViewportReporter onViewportChange={onViewportChange} />
|
|
<FitResults results={results} enabled={fitMapToResults} />
|
|
<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>
|
|
{showResultLabels ? (
|
|
<Tooltip permanent direction="top" offset={[0, -10]} opacity={0.94}>
|
|
{result.name ?? `${result.type} ${result.osmId}`}
|
|
</Tooltip>
|
|
) : null}
|
|
</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 FitResults({
|
|
results,
|
|
enabled,
|
|
}: {
|
|
results: ParsedOsmResult[];
|
|
enabled: boolean;
|
|
}) {
|
|
const map = useMap();
|
|
|
|
useEffect(() => {
|
|
if (!enabled || results.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const bounds = L.latLngBounds(results.map((result) => [result.lat, result.lon]));
|
|
|
|
if (bounds.isValid()) {
|
|
map.fitBounds(bounds.pad(0.2), {
|
|
animate: true,
|
|
maxZoom: 16,
|
|
});
|
|
}
|
|
}, [enabled, map, results]);
|
|
|
|
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,
|
|
};
|
|
}
|