This commit is contained in:
Andras Schmelczer 2026-05-06 22:40:46 +01:00
parent 28323f145e
commit 94f9c0d594
76 changed files with 3238 additions and 1230 deletions

View file

@ -12,6 +12,7 @@ import type {
POI,
FeatureMeta,
Bounds,
MapFlyToOptions,
} from '../../types';
import {
@ -19,6 +20,7 @@ import {
getBoundsFromViewState,
getMapStyle,
getPoiIconUrl,
getMapCenterForTargetScreenPoint,
} from '../../lib/map-utils';
import {
INITIAL_VIEW_STATE,
@ -56,7 +58,9 @@ 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>;
flyToRef?: React.MutableRefObject<
((lat: number, lng: number, zoom: number, options?: MapFlyToOptions) => void) | null
>;
theme?: 'light' | 'dark';
screenshotMode?: boolean;
ogMode?: boolean;
@ -80,6 +84,61 @@ interface Dimensions {
height: number;
}
function resolveInset(pixelValue: number | undefined, ratioValue: number | undefined, size: number) {
return Math.max(0, (pixelValue ?? 0) + (ratioValue ?? 0) * size);
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function getMapRelativeVisibleAreaCenter(dimensions: Dimensions, options?: MapFlyToOptions) {
const area = options?.visibleArea;
const leftInset = resolveInset(area?.left, area?.leftRatio, dimensions.width);
const rightInset = resolveInset(area?.right, area?.rightRatio, dimensions.width);
const topInset = resolveInset(area?.top, area?.topRatio, dimensions.height);
const bottomInset = resolveInset(area?.bottom, area?.bottomRatio, dimensions.height);
const left = Math.min(dimensions.width, leftInset);
const right = Math.max(left, dimensions.width - Math.min(dimensions.width, rightInset));
const top = Math.min(dimensions.height, topInset);
const bottom = Math.max(top, dimensions.height - Math.min(dimensions.height, bottomInset));
return {
x: (left + right) / 2,
y: (top + bottom) / 2,
};
}
function getViewportRelativeVisibleAreaCenter(
dimensions: Dimensions,
container: HTMLDivElement | null,
options?: MapFlyToOptions
) {
const area = options?.visibleViewportArea;
if (!area || !container) return null;
const rect = container.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const viewportLeft = resolveInset(area.left, area.leftRatio, viewportWidth);
const viewportRight =
viewportWidth - resolveInset(area.right, area.rightRatio, viewportWidth);
const viewportTop = resolveInset(area.top, area.topRatio, viewportHeight);
const viewportBottom =
viewportHeight - resolveInset(area.bottom, area.bottomRatio, viewportHeight);
const left = clamp(viewportLeft - rect.left, 0, dimensions.width);
const right = clamp(viewportRight - rect.left, left, dimensions.width);
const top = clamp(viewportTop - rect.top, 0, dimensions.height);
const bottom = clamp(viewportBottom - rect.top, top, dimensions.height);
return {
x: (left + right) / 2,
y: (top + bottom) / 2,
};
}
interface DeckWithPrivateDraw {
_drawLayers?: (
redrawReason: string,
@ -255,9 +314,27 @@ export default memo(function Map({
if (screenshotMode) window.__map_idle = true;
}, [screenshotMode]);
const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
setInternalViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
}, []);
const handleFlyTo = useCallback(
(lat: number, lng: number, zoom: number, options?: MapFlyToOptions) => {
setInternalViewState((prev) => {
const targetPoint =
getViewportRelativeVisibleAreaCenter(dimensions, containerRef.current, options) ??
getMapRelativeVisibleAreaCenter(dimensions, options);
const center = getMapCenterForTargetScreenPoint(
lat,
lng,
zoom,
dimensions.width,
dimensions.height,
targetPoint.x,
targetPoint.y
);
return { ...prev, ...center, zoom };
});
},
[dimensions]
);
if (flyToRef) flyToRef.current = handleFlyTo;
@ -361,7 +438,7 @@ export default memo(function Map({
) : null
) : (
<>
<div className="absolute top-3 left-3 right-3 z-[60] flex flex-wrap items-start justify-between gap-2 pointer-events-none">
<div className="absolute top-3 left-3 right-3 z-20 flex flex-wrap items-start justify-between gap-2 pointer-events-none">
{!hideLocationSearch && (
<LocationSearch
onFlyTo={handleFlyTo}