UI changes

This commit is contained in:
Andras Schmelczer 2026-03-24 20:50:24 +00:00
parent 0aba73a2a3
commit 8616837c01
13 changed files with 126 additions and 100 deletions

View file

@ -24,7 +24,7 @@ import {
POI_CLUSTER_MAX_ZOOM,
} from '../lib/consts';
import { emojiToTwemojiUrl, getFeatureFillColor } from '../lib/map-utils';
import { type TravelTimeEntry, travelFieldKey } from './useTravelTime';
import type { TravelTimeEntry } from './useTravelTime';
import { MarchingAntsExtension } from '../lib/MarchingAntsExtension';
interface UseDeckLayersProps {
@ -120,9 +120,6 @@ export function useDeckLayers({
const hoveredPostcodeRef = useRef(hoveredPostcode);
hoveredPostcodeRef.current = hoveredPostcode;
const travelTimeEntriesRef = useRef(travelTimeEntries);
travelTimeEntriesRef.current = travelTimeEntries;
const colorFeatureMeta = useMemo(
() => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
[viewFeature, features]
@ -302,28 +299,6 @@ export function useDeckLayers({
getHexagon: (d) => d.h3,
getFillColor: (d) => {
const dark = isDarkRef.current;
const entries = travelTimeEntriesRef.current;
// Dim-filter: all travel entries with timeRange dim hexagons outside range
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry.timeRange || !entry.slug) continue;
const fk = travelFieldKey(entry);
const modeVal = d[`avg_${fk}`];
if (
modeVal == null ||
(modeVal as number) < entry.timeRange[0] ||
(modeVal as number) > entry.timeRange[1]
) {
return (dark ? [60, 55, 50, 60] : [180, 180, 180, 60]) as [
number,
number,
number,
number,
];
}
}
const vf = viewFeatureRef.current;
const clr = colorRangeRef.current;
const fr = filterRangeRef.current;
@ -425,28 +400,6 @@ export function useDeckLayers({
getFillColor: (f) => {
const d = f.properties;
const dark = isDarkRef.current;
const entries = travelTimeEntriesRef.current;
// Dim-filter: all travel entries with timeRange dim postcodes outside range
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry.timeRange || !entry.slug) continue;
const fk = travelFieldKey(entry);
const modeVal = d[`avg_${fk}`];
if (
modeVal == null ||
(modeVal as number) < entry.timeRange[0] ||
(modeVal as number) > entry.timeRange[1]
) {
return (dark ? [60, 55, 50, 60] : [180, 180, 180, 60]) as [
number,
number,
number,
number,
];
}
}
const vf = viewFeatureRef.current;
const clr = colorRangeRef.current;
const fr = filterRangeRef.current;

View file

@ -120,6 +120,20 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
dragValueRef.current = null;
}, []);
/** End drag without committing to filters — caller handles the commit (e.g. travel time). */
const handleDragEndNoCommit = useCallback((): [number, number] | null => {
if (pendingDragRef.current) {
pendingDragRef.current = null;
return null;
}
const dv = dragValueRef.current;
setActiveFeature(null);
setDragValue(null);
dragActiveRef.current = null;
dragValueRef.current = null;
return dv;
}, []);
const handleSetFilters = useCallback((newFilters: FeatureFilters) => {
setFilters(newFilters);
setActiveFeature(null);
@ -159,6 +173,7 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
handleDragStart,
handleDragChange,
handleDragEnd,
handleDragEndNoCommit,
handleTogglePin,
handleSetPin,
handleCancelPin,

View file

@ -81,25 +81,34 @@ export function useMapData({
);
// Build the travel param string from entries with destinations.
// timeRange is NOT included — range filtering is handled purely client-side
// (dimming in useDeckLayers) so slider changes never trigger server refetches.
const travelParam = useMemo((): string => {
const segments: string[] = [];
for (const entry of travelTimeEntries) {
if (!entry.slug) continue;
let seg = `${entry.mode}:${entry.slug}`;
if (entry.useBest) seg += ':best';
segments.push(seg);
}
return segments.join('|');
}, [travelTimeEntries]);
// Format: mode:slug[:best][:min:max] — server filters rows outside [min,max].
// When excludeFieldKey is set, that entry's time range is omitted (for drag preview).
const buildTravelParam = useCallback(
(excludeFieldKey?: string): string => {
const segments: string[] = [];
for (const entry of travelTimeEntries) {
if (!entry.slug) continue;
let seg = `${entry.mode}:${entry.slug}`;
if (entry.useBest) seg += ':best';
const isExcluded = excludeFieldKey === `tt_${entry.mode}_${entry.slug}`;
if (entry.timeRange && !isExcluded) seg += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
segments.push(seg);
}
return segments.join('|');
},
[travelTimeEntries]
);
const travelParam = useMemo(() => buildTravelParam(), [buildTravelParam]);
// Keep activeFeatureRef in sync
useEffect(() => {
activeFeatureRef.current = activeFeature;
}, [activeFeature]);
// Drag prefetch: when activeFeature starts, fetch data excluding that filter
// Drag prefetch: when activeFeature starts, fetch data excluding that filter.
// For regular filters: excludes the filter from the filter string.
// For travel time: excludes the time range from that entry's travel param segment.
useEffect(() => {
if (!activeFeature || !bounds) return;
@ -108,11 +117,14 @@ export function useMapData({
const filtersStr = buildFilterString(filters, features, activeFeature);
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
const isTravelTimeDrag = activeFeature.startsWith('tt_');
const dragTravelParam = isTravelTimeDrag ? buildTravelParam(activeFeature) : travelParam;
if (usePostcodeView) {
const params = new URLSearchParams({ bounds: boundsStr });
if (filtersStr) params.set('filters', filtersStr);
params.set('fields', activeFeature);
if (dragTravelParam) params.set('travel', dragTravelParam);
fetch(apiUrl('postcodes', params), authHeaders({ signal: dragAbortRef.current.signal }))
.then((res) => res.json())
@ -129,7 +141,7 @@ export function useMapData({
});
if (filtersStr) params.set('filters', filtersStr);
params.set('fields', activeFeature);
if (travelParam) params.set('travel', travelParam);
if (dragTravelParam) params.set('travel', dragTravelParam);
fetch(apiUrl('hexagons', params), authHeaders({ signal: dragAbortRef.current.signal }))
.then((res) => res.json())
@ -147,7 +159,7 @@ export function useMapData({
dragAbortRef.current = null;
}
};
}, [activeFeature, bounds, resolution, filters, features, usePostcodeView, travelParam]);
}, [activeFeature, bounds, resolution, filters, features, usePostcodeView, travelParam, buildTravelParam]);
// Fetch hexagons or postcodes when bounds/filters change
useEffect(() => {

View file

@ -24,10 +24,11 @@ export function usePaneResize(
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!draggingRef.current) return;
const resolvedMax = maxWidth <= 1 ? window.innerWidth * maxWidth : maxWidth;
const newWidth =
side === 'left'
? Math.min(maxWidth, Math.max(minWidth, e.clientX))
: Math.min(maxWidth, Math.max(minWidth, window.innerWidth - e.clientX));
? Math.min(resolvedMax, Math.max(minWidth, e.clientX))
: Math.min(resolvedMax, Math.max(minWidth, window.innerWidth - e.clientX));
setWidth(newWidth);
},
[side, minWidth, maxWidth]