ruby homepage changes
This commit is contained in:
parent
94ebd0f614
commit
7777d4046e
7 changed files with 312 additions and 223 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters, Bounds, HexagonData, ApiResponse } from '../types';
|
||||
import { apiUrl, logNonAbortError } from '../lib/api';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters } from '../types';
|
||||
|
||||
interface UseFiltersOptions {
|
||||
initialFilters: FeatureFilters;
|
||||
|
|
@ -8,15 +7,10 @@ interface UseFiltersOptions {
|
|||
}
|
||||
|
||||
export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
||||
// Use refs for bounds/resolution so handleDragStart always has latest values
|
||||
const boundsRef = useRef<Bounds | null>(null);
|
||||
const resolutionRef = useRef<number>(8);
|
||||
const [filters, setFilters] = useState<FeatureFilters>(initialFilters);
|
||||
const [activeFeature, setActiveFeature] = useState<string | null>(null);
|
||||
const [dragValue, setDragValue] = useState<[number, number] | null>(null);
|
||||
const [pinnedFeature, setPinnedFeature] = useState<string | null>(null);
|
||||
const [dragData, setDragData] = useState<HexagonData[] | null>(null);
|
||||
const dragAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]);
|
||||
|
||||
|
|
@ -64,40 +58,6 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
setActiveFeature(name);
|
||||
const fval = filters[name];
|
||||
setDragValue(fval && !Array.isArray(fval[0]) ? (fval as [number, number]) : null);
|
||||
|
||||
const currentBounds = boundsRef.current;
|
||||
if (!currentBounds) return;
|
||||
if (dragAbortRef.current) dragAbortRef.current.abort();
|
||||
dragAbortRef.current = new AbortController();
|
||||
|
||||
const otherFilters = Object.entries(filters).filter(([k]) => k !== name);
|
||||
let filtersStr = '';
|
||||
if (otherFilters.length > 0) {
|
||||
filtersStr = otherFilters
|
||||
.map(([n, value]) => {
|
||||
const m = features.find((f) => f.name === n);
|
||||
if (m?.type === 'enum') return `${n}:${(value as string[]).join('|')}`;
|
||||
const [min, max] = value as [number, number];
|
||||
const maxStr = m?.absolute && max === m.max ? 'inf' : String(max);
|
||||
return `${n}:${min}:${maxStr}`;
|
||||
})
|
||||
.join(',');
|
||||
}
|
||||
|
||||
const boundsStr = `${currentBounds.south},${currentBounds.west},${currentBounds.north},${currentBounds.east}`;
|
||||
const params = new URLSearchParams({
|
||||
resolution: resolutionRef.current.toString(),
|
||||
bounds: boundsStr,
|
||||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', name);
|
||||
|
||||
fetch(apiUrl('hexagons', params), {
|
||||
signal: dragAbortRef.current.signal,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json: ApiResponse) => setDragData(json.features))
|
||||
.catch((err) => logNonAbortError('Failed to fetch drag data', err));
|
||||
},
|
||||
[filters, features]
|
||||
);
|
||||
|
|
@ -112,18 +72,12 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
}
|
||||
setActiveFeature(null);
|
||||
setDragValue(null);
|
||||
setDragData(null);
|
||||
if (dragAbortRef.current) {
|
||||
dragAbortRef.current.abort();
|
||||
dragAbortRef.current = null;
|
||||
}
|
||||
}, [activeFeature, dragValue]);
|
||||
|
||||
const handleSetFilters = useCallback((newFilters: FeatureFilters) => {
|
||||
setFilters(newFilters);
|
||||
setActiveFeature(null);
|
||||
setDragValue(null);
|
||||
setDragData(null);
|
||||
setPinnedFeature(null);
|
||||
}, []);
|
||||
|
||||
|
|
@ -139,16 +93,10 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
setPinnedFeature(null);
|
||||
}, []);
|
||||
|
||||
const updateBoundsInfo = useCallback((newBounds: Bounds | null, newResolution: number) => {
|
||||
boundsRef.current = newBounds;
|
||||
resolutionRef.current = newResolution;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
filters,
|
||||
activeFeature,
|
||||
dragValue,
|
||||
dragData,
|
||||
pinnedFeature,
|
||||
enabledFeatures,
|
||||
viewFeature,
|
||||
|
|
@ -164,6 +112,5 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
handleTogglePin,
|
||||
handleSetPin,
|
||||
handleCancelPin,
|
||||
updateBoundsInfo,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ interface UseMapDataOptions {
|
|||
viewFeature: string | null;
|
||||
activeFeature: string | null;
|
||||
dragValue: [number, number] | null;
|
||||
dragData: HexagonData[] | null;
|
||||
travelTimeEntries: TravelTimeEntry[];
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +41,6 @@ export function useMapData({
|
|||
viewFeature,
|
||||
activeFeature,
|
||||
dragValue,
|
||||
dragData,
|
||||
travelTimeEntries,
|
||||
}: UseMapDataOptions) {
|
||||
const [rawData, setRawData] = useState<HexagonData[]>([]);
|
||||
|
|
@ -59,6 +57,13 @@ export function useMapData({
|
|||
const [licenseRequired, setLicenseRequired] = useState(false);
|
||||
const [freeZone, setFreeZone] = useState<Bounds | null>(null);
|
||||
|
||||
// Drag preview state
|
||||
const [dragHexData, setDragHexData] = useState<HexagonData[] | null>(null);
|
||||
const [dragPostcodeData, setDragPostcodeData] = useState<PostcodeFeature[] | null>(null);
|
||||
const dragFeatureRef = useRef<string | null>(null);
|
||||
const dragAbortRef = useRef<AbortController | null>(null);
|
||||
const activeFeatureRef = useRef<string | null>(null);
|
||||
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const prevBoundsRef = useRef<string>('');
|
||||
|
|
@ -85,6 +90,61 @@ export function useMapData({
|
|||
return segments.join('|');
|
||||
}, [travelTimeEntries]);
|
||||
|
||||
// Keep activeFeatureRef in sync
|
||||
useEffect(() => {
|
||||
activeFeatureRef.current = activeFeature;
|
||||
}, [activeFeature]);
|
||||
|
||||
// Drag prefetch: when activeFeature starts, fetch data excluding that filter
|
||||
useEffect(() => {
|
||||
if (!activeFeature || !bounds) return;
|
||||
|
||||
if (dragAbortRef.current) dragAbortRef.current.abort();
|
||||
dragAbortRef.current = new AbortController();
|
||||
|
||||
const filtersStr = buildFilterString(filters, features, activeFeature);
|
||||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
|
||||
if (usePostcodeView) {
|
||||
const params = new URLSearchParams({ bounds: boundsStr });
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
|
||||
fetch(apiUrl('postcodes', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
.then((res) => res.json())
|
||||
.then((json: { features: PostcodeFeature[] }) => {
|
||||
setDragPostcodeData(json.features);
|
||||
setDragHexData(null);
|
||||
dragFeatureRef.current = activeFeature;
|
||||
})
|
||||
.catch((err) => logNonAbortError('Failed to fetch drag postcode data', err));
|
||||
} else {
|
||||
const params = new URLSearchParams({
|
||||
resolution: resolution.toString(),
|
||||
bounds: boundsStr,
|
||||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
if (travelParam) params.set('travel', travelParam);
|
||||
|
||||
fetch(apiUrl('hexagons', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
.then((res) => res.json())
|
||||
.then((json: ApiResponse) => {
|
||||
setDragHexData(json.features);
|
||||
setDragPostcodeData(null);
|
||||
dragFeatureRef.current = activeFeature;
|
||||
})
|
||||
.catch((err) => logNonAbortError('Failed to fetch drag hex data', err));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (dragAbortRef.current) {
|
||||
dragAbortRef.current.abort();
|
||||
dragAbortRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [activeFeature, bounds, resolution, filters, features, usePostcodeView, travelParam]);
|
||||
|
||||
// Fetch hexagons or postcodes when bounds/filters change
|
||||
useEffect(() => {
|
||||
if (!bounds) return;
|
||||
|
|
@ -157,6 +217,13 @@ export function useMapData({
|
|||
setRawData(json.features);
|
||||
setPostcodeData([]);
|
||||
}
|
||||
|
||||
// Clear drag data when committed fetch completes and we're not mid-drag
|
||||
if (!activeFeatureRef.current) {
|
||||
setDragHexData(null);
|
||||
setDragPostcodeData(null);
|
||||
dragFeatureRef.current = null;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isAbortError(err)) logNonAbortError('Failed to fetch data', err);
|
||||
} finally {
|
||||
|
|
@ -171,7 +238,9 @@ export function useMapData({
|
|||
};
|
||||
}, [resolution, bounds, filters, buildFilterParam, viewFeature, usePostcodeView, travelParam]);
|
||||
|
||||
const data = dragData ?? rawData;
|
||||
// Use drag data when it matches the current view feature, otherwise fall back to rawData
|
||||
const data = (viewFeature && dragFeatureRef.current === viewFeature ? dragHexData : null) ?? rawData;
|
||||
const effectivePostcodeData = (viewFeature && dragFeatureRef.current === viewFeature ? dragPostcodeData : null) ?? postcodeData;
|
||||
|
||||
// Compute p5/p95 from visible data for the viewed feature
|
||||
const dataRange = useMemo((): [number, number] | null => {
|
||||
|
|
@ -182,14 +251,14 @@ export function useMapData({
|
|||
if (!isTravelTime) {
|
||||
const meta = features.find((f) => f.name === viewFeature);
|
||||
if (!meta || meta.type === 'enum') return null;
|
||||
if (activeFeature && !dragData) return null;
|
||||
if (activeFeature && !dragHexData && !dragPostcodeData) return null;
|
||||
}
|
||||
|
||||
const vals: number[] = [];
|
||||
|
||||
if (usePostcodeView && !isTravelTime) {
|
||||
if (postcodeData.length === 0) return null;
|
||||
for (const feat of postcodeData) {
|
||||
if (effectivePostcodeData.length === 0) return null;
|
||||
for (const feat of effectivePostcodeData) {
|
||||
if (bounds) {
|
||||
const [lng, lat] = feat.properties.centroid as [number, number];
|
||||
if (lat < bounds.south || lat > bounds.north || lng < bounds.west || lng > bounds.east)
|
||||
|
|
@ -217,7 +286,7 @@ export function useMapData({
|
|||
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
|
||||
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
|
||||
];
|
||||
}, [viewFeature, data, dragData, postcodeData, usePostcodeView, features, activeFeature, bounds]);
|
||||
}, [viewFeature, data, dragHexData, dragPostcodeData, effectivePostcodeData, usePostcodeView, features, activeFeature, bounds]);
|
||||
|
||||
// Color range for the legend and hex coloring
|
||||
const colorRange = useMemo((): [number, number] | null => {
|
||||
|
|
@ -270,7 +339,7 @@ export function useMapData({
|
|||
return {
|
||||
data,
|
||||
rawData,
|
||||
postcodeData,
|
||||
postcodeData: effectivePostcodeData,
|
||||
resolution,
|
||||
bounds,
|
||||
loading,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue