lmao
This commit is contained in:
parent
03445188ea
commit
524580eb25
102 changed files with 36625 additions and 1295 deletions
|
|
@ -8,10 +8,10 @@ import type {
|
|||
ViewChangeParams,
|
||||
ApiResponse,
|
||||
} from '../types';
|
||||
import { buildFilterString, apiUrl, assertOk, logNonAbortError, authHeaders } from '../lib/api';
|
||||
import { buildFilterString, apiUrl, assertOk, logNonAbortError, authHeaders, isAbortError } 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';
|
||||
import { type TravelTimeEntry, travelFieldKey } from './useTravelTime';
|
||||
|
||||
/** Return the p-th percentile (0–100) from a sorted array via linear interpolation. */
|
||||
function percentile(sorted: number[], p: number): number {
|
||||
|
|
@ -33,7 +33,7 @@ interface UseMapDataOptions {
|
|||
activeFeature: string | null;
|
||||
dragValue: [number, number] | null;
|
||||
dragData: HexagonData[] | null;
|
||||
travelTimeEntries: TravelTimeEntries;
|
||||
travelTimeEntries: TravelTimeEntry[];
|
||||
}
|
||||
|
||||
export function useMapData({
|
||||
|
|
@ -56,6 +56,8 @@ export function useMapData({
|
|||
longitude: number;
|
||||
zoom: number;
|
||||
} | null>(null);
|
||||
const [licenseRequired, setLicenseRequired] = useState(false);
|
||||
const [freeZone, setFreeZone] = useState<Bounds | null>(null);
|
||||
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
|
@ -69,13 +71,16 @@ export function useMapData({
|
|||
);
|
||||
|
||||
// Build the travel param string from entries with destinations
|
||||
// Format: mode:slug|mode:slug or mode:slug:min:max|mode:slug
|
||||
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}`);
|
||||
for (const entry of travelTimeEntries) {
|
||||
if (!entry.slug) continue;
|
||||
let seg = `${entry.mode}:${entry.slug}`;
|
||||
if (entry.timeRange) {
|
||||
seg += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
|
||||
}
|
||||
segments.push(seg);
|
||||
}
|
||||
return segments.join('|');
|
||||
}, [travelTimeEntries]);
|
||||
|
|
@ -109,7 +114,16 @@ export function useMapData({
|
|||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
);
|
||||
if (res.status === 403) {
|
||||
const errBody = await res.json();
|
||||
if (errBody.error === 'license_required' && errBody.free_zone) {
|
||||
setLicenseRequired(true);
|
||||
setFreeZone(errBody.free_zone);
|
||||
return;
|
||||
}
|
||||
}
|
||||
assertOk(res, 'postcodes');
|
||||
setLicenseRequired(false);
|
||||
const json: { features: PostcodeFeature[] } = await res.json();
|
||||
setPostcodeData(json.features);
|
||||
setRawData([]);
|
||||
|
|
@ -129,13 +143,22 @@ export function useMapData({
|
|||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
);
|
||||
if (res.status === 403) {
|
||||
const errBody = await res.json();
|
||||
if (errBody.error === 'license_required' && errBody.free_zone) {
|
||||
setLicenseRequired(true);
|
||||
setFreeZone(errBody.free_zone);
|
||||
return;
|
||||
}
|
||||
}
|
||||
assertOk(res, 'hexagons');
|
||||
setLicenseRequired(false);
|
||||
const json: ApiResponse = await res.json();
|
||||
setRawData(json.features);
|
||||
setPostcodeData([]);
|
||||
}
|
||||
} catch (err) {
|
||||
logNonAbortError('Failed to fetch data', err);
|
||||
if (!isAbortError(err)) logNonAbortError('Failed to fetch data', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -151,7 +174,6 @@ export function useMapData({
|
|||
const data = dragData ?? rawData;
|
||||
|
||||
// Compute p5/p95 from visible data for the viewed feature
|
||||
// Only considers hexagons/postcodes whose center falls within the viewport bounds
|
||||
const dataRange = useMemo((): [number, number] | null => {
|
||||
if (!viewFeature) return null;
|
||||
const meta = features.find((f) => f.name === viewFeature);
|
||||
|
|
@ -207,13 +229,13 @@ export function useMapData({
|
|||
return null;
|
||||
}, [viewFeature, features, dataRange, activeFeature, dragValue]);
|
||||
|
||||
// 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}`;
|
||||
// Color ranges for travel time per entry (computed from response data)
|
||||
const travelTimeColorRanges = useMemo((): Map<number, [number, number]> => {
|
||||
const ranges = new Map<number, [number, number]>();
|
||||
for (let i = 0; i < travelTimeEntries.length; i++) {
|
||||
const entry = travelTimeEntries[i];
|
||||
if (!entry.slug) continue;
|
||||
const fieldName = `avg_${travelFieldKey(entry)}`;
|
||||
const vals: number[] = [];
|
||||
for (const item of data) {
|
||||
if (bounds) {
|
||||
|
|
@ -226,10 +248,10 @@ export function useMapData({
|
|||
}
|
||||
if (vals.length === 0) continue;
|
||||
vals.sort((a, b) => a - b);
|
||||
ranges[mode] = [
|
||||
ranges.set(i, [
|
||||
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
|
||||
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
|
||||
];
|
||||
]);
|
||||
}
|
||||
return ranges;
|
||||
}, [travelTimeEntries, data, bounds]);
|
||||
|
|
@ -276,5 +298,7 @@ export function useMapData({
|
|||
travelTimeColorRanges,
|
||||
handleViewChange,
|
||||
setInitialView,
|
||||
licenseRequired,
|
||||
freeZone,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue