This commit is contained in:
Andras Schmelczer 2026-02-18 21:22:15 +00:00
parent 524580eb25
commit ffe080adef
82 changed files with 2652 additions and 2956 deletions

View file

@ -1,6 +1,5 @@
import { useCallback, useRef, useEffect, useState, useMemo, memo } from 'react';
import { Map as MapGL, useControl } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
import { MapboxOverlay } from '@deck.gl/mapbox';
import 'maplibre-gl/dist/maplibre-gl.css';
import type {
@ -21,7 +20,7 @@ import MapLegend from './MapLegend';
import HoverCard from './HoverCard';
import type { FeatureFilters } from '../../types';
import { useDeckLayers, osmIdToUrl } from '../../hooks/useDeckLayers';
import { MODE_LABELS, type TravelTimeEntry, travelFieldKey } from '../../hooks/useTravelTime';
import { MODE_LABELS, type TravelTimeEntry } from '../../hooks/useTravelTime';
interface MapProps {
data: HexagonData[];
@ -40,6 +39,7 @@ interface MapProps {
onHexagonClick: (id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => void;
onHexagonHover: (h3: string | null, x?: number, y?: number) => void;
initialViewState?: ViewState;
flyToRef?: React.MutableRefObject<((lat: number, lng: number, zoom: number) => void) | null>;
theme?: 'light' | 'dark';
screenshotMode?: boolean;
ogMode?: boolean;
@ -49,11 +49,9 @@ interface MapProps {
bounds?: Bounds | null;
hideLegend?: boolean;
travelTimeEntries?: TravelTimeEntry[];
travelTimeColorRanges?: Map<number, [number, number]>;
}
const EMPTY_TRAVEL_ENTRIES: TravelTimeEntry[] = [];
const EMPTY_TRAVEL_RANGES = new globalThis.Map<number, [number, number]>();
interface Dimensions {
width: number;
@ -97,6 +95,7 @@ export default memo(function Map({
onHexagonClick,
onHexagonHover,
initialViewState,
flyToRef,
theme = 'light',
screenshotMode = false,
ogMode = false,
@ -106,12 +105,17 @@ export default memo(function Map({
bounds: viewportBounds,
hideLegend = false,
travelTimeEntries = EMPTY_TRAVEL_ENTRIES,
travelTimeColorRanges = EMPTY_TRAVEL_RANGES,
}: MapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [viewState, setViewState] = useState<ViewState>(initialViewState || INITIAL_VIEW_STATE);
const [internalViewState, setInternalViewState] = useState<ViewState>(
initialViewState || INITIAL_VIEW_STATE
);
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
// In screenshot mode, use the prop directly for instant updates (no async lag)
const viewState =
screenshotMode && initialViewState ? initialViewState : internalViewState;
useEffect(() => {
const container = containerRef.current;
if (!container) return;
@ -130,8 +134,6 @@ export default memo(function Map({
useEffect(() => {
if (dimensions.width === 0 || dimensions.height === 0) return;
// Send exact viewport bounds - server will filter to only return
// hexagons/postcodes that intersect this precise AABB
const bounds = getBoundsFromViewState(viewState, dimensions.width, dimensions.height);
const resolution = zoomToResolution(viewState.zoom);
@ -145,19 +147,14 @@ export default memo(function Map({
}, [viewState, dimensions, onViewChange]);
const handleMove = useCallback((evt: { viewState: ViewState }) => {
setViewState(evt.viewState);
setInternalViewState(evt.viewState);
}, []);
const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
setViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
setInternalViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
}, []);
const handleMapLoad = useCallback(
(_evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
// Road opacity is set in getMapStyle
},
[]
);
if (flyToRef) flyToRef.current = handleFlyTo;
const mapStyle = useMemo(() => getMapStyle(theme), [theme]);
@ -169,7 +166,6 @@ export default memo(function Map({
postcodeCountRange,
colorFeatureMeta,
handleMouseLeave,
primaryTravelIndex,
} = useDeckLayers({
data,
postcodeData,
@ -187,7 +183,6 @@ export default memo(function Map({
selectedPostcodeGeometry,
bounds: viewportBounds,
travelTimeEntries,
travelTimeColorRanges,
});
return (
@ -195,7 +190,7 @@ export default memo(function Map({
<MapGL
{...viewState}
onMove={handleMove}
onLoad={handleMapLoad as never}
onLoad={undefined}
mapStyle={mapStyle}
style={{ width: '100%', height: '100%' }}
attributionControl={false}
@ -222,35 +217,37 @@ export default memo(function Map({
) : null
) : (
<>
<LocationSearch onFlyTo={handleFlyTo} onLocationSearched={onLocationSearched} />
<LocationSearch onFlyTo={handleFlyTo} onLocationSearched={onLocationSearched} onMouseEnter={handleMouseLeave} />
{!hideLegend &&
(primaryTravelIndex >= 0 && travelTimeColorRanges.get(primaryTravelIndex) ? (
<MapLegend
featureLabel={`Travel time (${MODE_LABELS[travelTimeEntries[primaryTravelIndex].mode]})`}
range={travelTimeColorRanges.get(primaryTravelIndex)!}
showCancel={false}
onCancel={onCancelPin}
mode="feature"
theme={theme}
suffix=" min"
/>
) : viewFeature && colorRange && colorFeatureMeta ? (
<MapLegend
featureLabel={
viewSource === 'eye'
? `Previewing \u201c${colorFeatureMeta.name}\u201d`
: colorFeatureMeta.name
}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
mode="feature"
enumValues={colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined}
theme={theme}
/>
(viewFeature && colorRange ? (
viewFeature.startsWith('tt_') ? (
<MapLegend
featureLabel={`Travel time (${MODE_LABELS[viewFeature.split('_')[1] as keyof typeof MODE_LABELS]})`}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
mode="feature"
theme={theme}
suffix=" min"
/>
) : colorFeatureMeta ? (
<MapLegend
featureLabel={
viewSource === 'eye'
? `Previewing \u201c${colorFeatureMeta.name}\u201d`
: colorFeatureMeta.name
}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
mode="feature"
enumValues={colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined}
theme={theme}
/>
) : null
) : (
<MapLegend
featureLabel="Property density"
featureLabel="Number of properties"
range={
usePostcodeView
? [postcodeCountRange.min, postcodeCountRange.max]