This commit is contained in:
Andras Schmelczer 2026-02-14 12:53:29 +00:00
parent 3a3f899ea2
commit 128b3191e7
68 changed files with 28060 additions and 1152 deletions

View file

@ -8,9 +8,10 @@ import type {
ViewChangeParams,
ApiResponse,
} from '../types';
import { buildFilterString, apiUrl, logNonAbortError, authHeaders } from '../lib/api';
import { buildFilterString, apiUrl, assertOk, logNonAbortError, authHeaders } from '../lib/api';
import { POSTCODE_ZOOM_THRESHOLD } from '../lib/consts';
import { COLOR_RANGE_LOW_PERCENTILE, COLOR_RANGE_HIGH_PERCENTILE } from '../lib/consts';
import { TRANSPORT_MODES, type TransportMode, type TravelTimeEntries } from './useTravelTime';
/** Return the p-th percentile (0100) from a sorted array via linear interpolation. */
function percentile(sorted: number[], p: number): number {
@ -32,9 +33,7 @@ interface UseMapDataOptions {
activeFeature: string | null;
dragValue: [number, number] | null;
dragData: HexagonData[] | null;
travelTimeEnabled: boolean;
travelTimeDestination: [number, number] | null;
travelTimeMode: string;
travelTimeEntries: TravelTimeEntries;
}
export function useMapData({
@ -44,9 +43,7 @@ export function useMapData({
activeFeature,
dragValue,
dragData,
travelTimeEnabled,
travelTimeDestination,
travelTimeMode,
travelTimeEntries,
}: UseMapDataOptions) {
const [rawData, setRawData] = useState<HexagonData[]>([]);
const [postcodeData, setPostcodeData] = useState<PostcodeFeature[]>([]);
@ -71,6 +68,18 @@ export function useMapData({
[filters, features]
);
// Build the travel param string from entries with destinations
const travelParam = useMemo((): string => {
const segments: string[] = [];
for (const mode of TRANSPORT_MODES) {
const entry = travelTimeEntries[mode];
if (entry?.destination) {
segments.push(`${entry.destination[0]},${entry.destination[1]},${mode}`);
}
}
return segments.join('|');
}, [travelTimeEntries]);
// Fetch hexagons or postcodes when bounds/filters change
useEffect(() => {
if (!bounds) return;
@ -100,6 +109,7 @@ export function useMapData({
signal: abortControllerRef.current.signal,
})
);
assertOk(res, 'postcodes');
const json: { features: PostcodeFeature[] } = await res.json();
setPostcodeData(json.features);
setRawData([]);
@ -110,9 +120,8 @@ export function useMapData({
});
if (filtersStr) params.set('filters', filtersStr);
params.set('fields', viewFeature || '');
if (travelTimeEnabled && travelTimeDestination) {
params.set('destination', `${travelTimeDestination[0]},${travelTimeDestination[1]}`);
params.set('mode', travelTimeMode);
if (travelParam) {
params.set('travel', travelParam);
}
const res = await fetch(
apiUrl('hexagons', params),
@ -120,6 +129,7 @@ export function useMapData({
signal: abortControllerRef.current.signal,
})
);
assertOk(res, 'hexagons');
const json: ApiResponse = await res.json();
setRawData(json.features);
setPostcodeData([]);
@ -136,7 +146,7 @@ export function useMapData({
clearTimeout(debounceRef.current);
}
};
}, [resolution, bounds, filters, buildFilterParam, viewFeature, usePostcodeView, travelTimeEnabled, travelTimeDestination, travelTimeMode]);
}, [resolution, bounds, filters, buildFilterParam, viewFeature, usePostcodeView, travelParam]);
const data = dragData ?? rawData;
@ -159,7 +169,7 @@ export function useMapData({
if (lat < bounds.south || lat > bounds.north || lng < bounds.west || lng > bounds.east)
continue;
}
const val = feat.properties[`avg_${viewFeature}`] ?? feat.properties[`min_${viewFeature}`];
const val = feat.properties[`avg_${viewFeature}`];
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
}
} else {
@ -170,7 +180,7 @@ export function useMapData({
if (lat < bounds.south || lat > bounds.north || lon < bounds.west || lon > bounds.east)
continue;
}
const val = item[`avg_${viewFeature}`] ?? item[`min_${viewFeature}`];
const val = item[`avg_${viewFeature}`];
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
}
}
@ -197,26 +207,32 @@ export function useMapData({
return null;
}, [viewFeature, features, dataRange, activeFeature, dragValue]);
// Color range for travel time (computed from response data)
const travelTimeColorRange = useMemo((): [number, number] | null => {
if (!travelTimeEnabled || !travelTimeDestination) return null;
const vals: number[] = [];
for (const item of data) {
if (bounds) {
const { lat, lon } = item;
if (lat < bounds.south || lat > bounds.north || lon < bounds.west || lon > bounds.east)
continue;
// Color ranges for travel time per mode (computed from response data)
const travelTimeColorRanges = useMemo((): Partial<Record<TransportMode, [number, number]>> => {
const ranges: Partial<Record<TransportMode, [number, number]>> = {};
for (const mode of TRANSPORT_MODES) {
const entry = travelTimeEntries[mode];
if (!entry?.destination) continue;
const fieldName = `travel_time_${mode}`;
const vals: number[] = [];
for (const item of data) {
if (bounds) {
const { lat, lon } = item;
if (lat < bounds.south || lat > bounds.north || lon < bounds.west || lon > bounds.east)
continue;
}
const val = item[fieldName];
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
}
const val = item.travel_time;
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
if (vals.length === 0) continue;
vals.sort((a, b) => a - b);
ranges[mode] = [
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
];
}
if (vals.length === 0) return null;
vals.sort((a, b) => a - b);
return [
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
];
}, [travelTimeEnabled, travelTimeDestination, data, bounds]);
return ranges;
}, [travelTimeEntries, data, bounds]);
const handleViewChange = useCallback(
({
@ -257,7 +273,7 @@ export function useMapData({
currentView,
usePostcodeView,
colorRange,
travelTimeColorRange,
travelTimeColorRanges,
handleViewChange,
setInitialView,
};