Small changes and fix zooming

This commit is contained in:
Andras Schmelczer 2026-05-05 20:30:04 +01:00
parent c69bb0d614
commit 329685a4ee
16 changed files with 823 additions and 202 deletions

View file

@ -39,6 +39,7 @@ interface UseMapDataOptions {
features: FeatureMeta[];
viewFeature: string | null;
activeFeature: string | null;
pinnedFeature: string | null;
travelTimeEntries: TravelTimeEntry[];
/** Share-link code from the URL; appended to data fetches so the backend
* grants bbox-scoped access for unlicensed recipients. */
@ -50,6 +51,7 @@ export function useMapData({
features,
viewFeature,
activeFeature,
pinnedFeature,
travelTimeEntries,
shareCode,
}: UseMapDataOptions) {
@ -83,6 +85,10 @@ export function useMapData({
() => (viewFeature ? (getSchoolBackendFeatureName(viewFeature) ?? viewFeature) : null),
[viewFeature]
);
const pinnedDataViewFeature = useMemo(
() => (pinnedFeature ? (getSchoolBackendFeatureName(pinnedFeature) ?? pinnedFeature) : null),
[pinnedFeature]
);
// Determine if the current viewFeature is an enum (for enum_dist param)
const viewFeatureIsEnum = useMemo(
@ -95,6 +101,7 @@ export function useMapData({
(): string => buildFilterString(filters, features),
[filters, features]
);
const filtersParam = useMemo(() => buildFilterParam(), [buildFilterParam]);
// Build the travel param string from entries with destinations.
// Format: mode:slug[:best][:min:max] — server filters rows outside [min,max].
@ -122,6 +129,37 @@ export function useMapData({
);
const travelParam = useMemo(() => buildTravelParam(), [buildTravelParam]);
const boundsParam = useMemo(
() => (bounds ? `${bounds.south},${bounds.west},${bounds.north},${bounds.east}` : ''),
[bounds]
);
const dataRequestKey = useMemo(
() =>
bounds
? [
usePostcodeView ? 'postcodes' : 'hexagons',
resolution,
boundsParam,
filtersParam,
dataViewFeature ?? '',
viewFeatureIsEnum && dataViewFeature ? dataViewFeature : '',
travelParam,
shareCode ?? '',
].join('|')
: '',
[
bounds,
boundsParam,
dataViewFeature,
filtersParam,
resolution,
shareCode,
travelParam,
usePostcodeView,
viewFeatureIsEnum,
]
);
const [loadedDataKey, setLoadedDataKey] = useState<string>('');
// Keep activeFeatureRef in sync
useEffect(() => {
@ -219,12 +257,11 @@ export function useMapData({
setLoading(true);
try {
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
const filtersStr = buildFilterParam();
const requestKey = dataRequestKey;
if (usePostcodeView) {
const params = new URLSearchParams({ bounds: boundsStr });
if (filtersStr) params.set('filters', filtersStr);
const params = new URLSearchParams({ bounds: boundsParam });
if (filtersParam) params.set('filters', filtersParam);
params.set(
'fields',
dataViewFeature && !dataViewFeature.startsWith('tt_') ? dataViewFeature : ''
@ -254,12 +291,13 @@ export function useMapData({
const json: { features: PostcodeFeature[] } = await res.json();
setPostcodeData(json.features);
setRawData([]);
setLoadedDataKey(requestKey);
} else {
const params = new URLSearchParams({
resolution: resolution.toString(),
bounds: boundsStr,
bounds: boundsParam,
});
if (filtersStr) params.set('filters', filtersStr);
if (filtersParam) params.set('filters', filtersParam);
params.set(
'fields',
dataViewFeature && !dataViewFeature.startsWith('tt_') ? dataViewFeature : ''
@ -289,6 +327,7 @@ export function useMapData({
const json: ApiResponse = await res.json();
setRawData(json.features);
setPostcodeData([]);
setLoadedDataKey(requestKey);
}
// Clear drag data when committed fetch completes and we're not mid-drag
@ -315,7 +354,9 @@ export function useMapData({
resolution,
bounds,
filters,
buildFilterParam,
filtersParam,
boundsParam,
dataRequestKey,
dataViewFeature,
viewFeatureIsEnum,
usePostcodeView,
@ -377,8 +418,8 @@ export function useMapData({
];
}, [dataViewFeature, rawData, postcodeData, usePostcodeView, features, bounds]);
// Color range for the legend and hex coloring
const colorRange = useMemo((): [number, number] | null => {
// Live color range for the legend and hex coloring.
const liveColorRange = useMemo((): [number, number] | null => {
if (!dataViewFeature) return null;
// Travel time keys: use dataRange directly (no FeatureMeta)
@ -396,6 +437,61 @@ export function useMapData({
return null;
}, [dataViewFeature, features, dataRange]);
const isEyePreviewingPinnedFeature =
!activeFeature && dataViewFeature != null && dataViewFeature === pinnedDataViewFeature;
const [frozenPreviewRange, setFrozenPreviewRange] = useState<{
feature: string;
range: [number, number];
} | null>(null);
useEffect(() => {
setFrozenPreviewRange((prev) => {
if (!pinnedDataViewFeature) return prev ? null : prev;
return prev?.feature === pinnedDataViewFeature ? prev : null;
});
}, [pinnedDataViewFeature]);
useEffect(() => {
if (!isEyePreviewingPinnedFeature || !pinnedDataViewFeature) return;
const meta = pinnedDataViewFeature.startsWith('tt_')
? null
: features.find((f) => f.name === pinnedDataViewFeature);
const rangeToFreeze =
dataRange && loadedDataKey === dataRequestKey
? dataRange
: meta?.type === 'enum' && liveColorRange
? liveColorRange
: null;
if (!rangeToFreeze) return;
setFrozenPreviewRange((prev) =>
prev?.feature === pinnedDataViewFeature
? prev
: { feature: pinnedDataViewFeature, range: rangeToFreeze }
);
}, [
dataRange,
dataRequestKey,
features,
isEyePreviewingPinnedFeature,
loadedDataKey,
liveColorRange,
pinnedDataViewFeature,
]);
const colorRange = useMemo((): [number, number] | null => {
if (
isEyePreviewingPinnedFeature &&
frozenPreviewRange &&
frozenPreviewRange.feature === dataViewFeature
) {
return frozenPreviewRange.range;
}
return liveColorRange;
}, [dataViewFeature, frozenPreviewRange, isEyePreviewingPinnedFeature, liveColorRange]);
const handleViewChange = useCallback(
({
resolution: newRes,

View file

@ -1,9 +1,12 @@
import { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { Step, CallBackProps } from 'react-joyride';
import { ACTIONS, EVENTS, STATUS } from 'react-joyride';
const STORAGE_KEY = 'tutorial_completed';
const JOYRIDE_ACTION_CLOSE = 'close';
const JOYRIDE_EVENT_STEP_AFTER = 'step:after';
const JOYRIDE_STATUS_FINISHED = 'finished';
const JOYRIDE_STATUS_SKIPPED = 'skipped';
export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked = false) {
const { t } = useTranslation();
@ -67,12 +70,12 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
const handleCallback = useCallback((data: CallBackProps) => {
const { status, action, type } = data;
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
if (status === JOYRIDE_STATUS_FINISHED || status === JOYRIDE_STATUS_SKIPPED) {
localStorage.setItem(STORAGE_KEY, '1');
setRun(false);
}
// Also stop if user closes a tooltip via the X button
if (action === ACTIONS.CLOSE && type === EVENTS.STEP_AFTER) {
if (action === JOYRIDE_ACTION_CLOSE && type === JOYRIDE_EVENT_STEP_AFTER) {
localStorage.setItem(STORAGE_KEY, '1');
setRun(false);
}