More FE changes
This commit is contained in:
parent
f114ada255
commit
a48eb945e0
48 changed files with 4127 additions and 1751 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { useCallback, useRef, useEffect, useState, useMemo, memo } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Map as MapGL, useControl, ScaleControl } 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 {
|
||||
|
|
@ -18,17 +20,12 @@ import type {
|
|||
import {
|
||||
zoomToResolution,
|
||||
getBoundsFromViewState,
|
||||
getBoundsWithBottomScreenInset,
|
||||
getMapStyle,
|
||||
getPoiIconUrl,
|
||||
getMapCenterForTargetScreenPoint,
|
||||
} from '../../lib/map-utils';
|
||||
import {
|
||||
INITIAL_VIEW_STATE,
|
||||
MAP_MIN_ZOOM,
|
||||
MAP_BOUNDS,
|
||||
POI_GROUP_COLORS,
|
||||
POI_DEFAULT_COLOR,
|
||||
} from '../../lib/consts';
|
||||
import { MAP_MIN_ZOOM, MAP_BOUNDS, POI_GROUP_COLORS } from '../../lib/consts';
|
||||
import LocationSearch, { type SearchedLocation } from './LocationSearch';
|
||||
import MapLegend from './MapLegend';
|
||||
import HoverCard from './HoverCard';
|
||||
|
|
@ -57,7 +54,7 @@ interface MapProps {
|
|||
hoveredHexagonId: string | null;
|
||||
onHexagonClick: (id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => void;
|
||||
onHexagonHover: (h3: string | null, x?: number, y?: number) => void;
|
||||
initialViewState?: ViewState;
|
||||
initialViewState: ViewState;
|
||||
flyToRef?: React.MutableRefObject<
|
||||
((lat: number, lng: number, zoom: number, options?: MapFlyToOptions) => void) | null
|
||||
>;
|
||||
|
|
@ -75,6 +72,7 @@ interface MapProps {
|
|||
travelTimeEntries?: TravelTimeEntry[];
|
||||
densityLabel?: string;
|
||||
totalCount?: number;
|
||||
bottomScreenInset?: number;
|
||||
}
|
||||
|
||||
const EMPTY_TRAVEL_ENTRIES: TravelTimeEntry[] = [];
|
||||
|
|
@ -84,6 +82,10 @@ interface Dimensions {
|
|||
height: number;
|
||||
}
|
||||
|
||||
type MapContainerStyle = CSSProperties & {
|
||||
'--map-mobile-bottom-inset'?: string;
|
||||
};
|
||||
|
||||
function resolveInset(
|
||||
pixelValue: number | undefined,
|
||||
ratioValue: number | undefined,
|
||||
|
|
@ -185,6 +187,27 @@ class SafeMapboxOverlay extends MapboxOverlay {
|
|||
}
|
||||
}
|
||||
|
||||
function getPoiGroupColor(group: string): [number, number, number] {
|
||||
const color = POI_GROUP_COLORS[group];
|
||||
if (!color) {
|
||||
throw new Error(`Missing POI group color for '${group}'`);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function getRenderedViewState(map: MapRef | null): ViewState | null {
|
||||
if (!map) return null;
|
||||
|
||||
const center = map.getCenter();
|
||||
return {
|
||||
longitude: center.lng,
|
||||
latitude: center.lat,
|
||||
zoom: map.getZoom(),
|
||||
pitch: map.getPitch(),
|
||||
bearing: map.getBearing(),
|
||||
};
|
||||
}
|
||||
|
||||
function DeckOverlay({
|
||||
layers,
|
||||
getTooltip,
|
||||
|
|
@ -240,18 +263,18 @@ export default memo(function Map({
|
|||
travelTimeEntries = EMPTY_TRAVEL_ENTRIES,
|
||||
densityLabel: densityLabelProp,
|
||||
totalCount: totalCountProp,
|
||||
bottomScreenInset = 0,
|
||||
}: MapProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<MapRef | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const modes = useTranslatedModes();
|
||||
const densityLabel = densityLabelProp ?? t('mapLegend.numberOfProperties');
|
||||
const [internalViewState, setInternalViewState] = useState<ViewState>(
|
||||
initialViewState || INITIAL_VIEW_STATE
|
||||
);
|
||||
const [internalViewState, setInternalViewState] = useState<ViewState>(initialViewState);
|
||||
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;
|
||||
const viewState = screenshotMode ? initialViewState : internalViewState;
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
|
@ -282,17 +305,33 @@ export default memo(function Map({
|
|||
useEffect(() => {
|
||||
if (dimensions.width === 0 || dimensions.height === 0) return;
|
||||
|
||||
const bounds = getBoundsFromViewState(viewState, dimensions.width, dimensions.height);
|
||||
const resolution = zoomToResolution(viewState.zoom);
|
||||
let frame = 0;
|
||||
const emit = () => {
|
||||
const renderedViewState = getRenderedViewState(mapRef.current);
|
||||
// mapRef can be null on the very first effect run if MapLibre hasn't
|
||||
// finished mounting; retry next frame so the initial bounds always reach
|
||||
// the data hook.
|
||||
if (!renderedViewState) {
|
||||
frame = window.requestAnimationFrame(emit);
|
||||
return;
|
||||
}
|
||||
// The bottom sheet can reveal covered map area without a pan/zoom event.
|
||||
const dataBoundsHeight = dimensions.height + Math.max(0, bottomScreenInset);
|
||||
const bounds = getBoundsFromViewState(renderedViewState, dimensions.width, dataBoundsHeight);
|
||||
const resolution = zoomToResolution(renderedViewState.zoom);
|
||||
|
||||
onViewChange({
|
||||
resolution,
|
||||
bounds,
|
||||
zoom: viewState.zoom,
|
||||
latitude: viewState.latitude,
|
||||
longitude: viewState.longitude,
|
||||
});
|
||||
}, [viewState, dimensions, onViewChange]);
|
||||
onViewChange({
|
||||
resolution,
|
||||
bounds,
|
||||
zoom: renderedViewState.zoom,
|
||||
latitude: renderedViewState.latitude,
|
||||
longitude: renderedViewState.longitude,
|
||||
});
|
||||
};
|
||||
frame = window.requestAnimationFrame(emit);
|
||||
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [viewState, dimensions, bottomScreenInset, onViewChange]);
|
||||
|
||||
const handleMove = useCallback((evt: { viewState: ViewState }) => {
|
||||
setInternalViewState((prev) => {
|
||||
|
|
@ -342,6 +381,14 @@ export default memo(function Map({
|
|||
if (flyToRef) flyToRef.current = handleFlyTo;
|
||||
|
||||
const mapStyle = useMemo(() => getMapStyle(theme), [theme]);
|
||||
const maxBounds = useMemo(
|
||||
() => getBoundsWithBottomScreenInset(MAP_BOUNDS, MAP_MIN_ZOOM, bottomScreenInset),
|
||||
[bottomScreenInset]
|
||||
);
|
||||
const mapContainerStyle = useMemo<MapContainerStyle>(
|
||||
() => (bottomScreenInset > 0 ? { '--map-mobile-bottom-inset': `${bottomScreenInset}px` } : {}),
|
||||
[bottomScreenInset]
|
||||
);
|
||||
|
||||
const {
|
||||
layers,
|
||||
|
|
@ -374,8 +421,14 @@ export default memo(function Map({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full relative" ref={containerRef} onMouseLeave={handleMouseLeave}>
|
||||
<div
|
||||
className={`flex-1 h-full relative ${bottomScreenInset > 0 ? 'map-has-mobile-bottom-sheet' : ''}`}
|
||||
ref={containerRef}
|
||||
style={mapContainerStyle}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<MapGL
|
||||
ref={mapRef}
|
||||
{...viewState}
|
||||
onMove={handleMove}
|
||||
onLoad={undefined}
|
||||
|
|
@ -389,7 +442,7 @@ export default memo(function Map({
|
|||
keyboard={true}
|
||||
pitchWithRotate={false}
|
||||
minZoom={MAP_MIN_ZOOM}
|
||||
maxBounds={MAP_BOUNDS}
|
||||
maxBounds={maxBounds}
|
||||
>
|
||||
<DeckOverlay layers={layers} getTooltip={null} />
|
||||
{!screenshotMode && <ScaleControl position="bottom-left" maxWidth={100} unit="metric" />}
|
||||
|
|
@ -486,6 +539,7 @@ export default memo(function Map({
|
|||
}
|
||||
featureName={colorFeatureMeta.name}
|
||||
theme={theme}
|
||||
suffix={colorFeatureMeta.suffix}
|
||||
raw={colorFeatureMeta.raw}
|
||||
/>
|
||||
) : null
|
||||
|
|
@ -553,7 +607,7 @@ export default memo(function Map({
|
|||
<span
|
||||
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: `rgb(${(POI_GROUP_COLORS[popupInfo.group] || POI_DEFAULT_COLOR).join(',')})`,
|
||||
backgroundColor: `rgb(${getPoiGroupColor(popupInfo.group).join(',')})`,
|
||||
}}
|
||||
/>
|
||||
{popupInfo.category}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue