Fun changes
This commit is contained in:
parent
cd778dd088
commit
349a6c1d53
60 changed files with 1260 additions and 2600 deletions
|
|
@ -53,7 +53,6 @@ function buildSummary(
|
|||
const parts: string[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(filters)) {
|
||||
if (name === 'Listing status') continue;
|
||||
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'number') {
|
||||
parts.push(ts(name));
|
||||
} else if (Array.isArray(value)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useRef, useState, useMemo, useEffect } from 'react';
|
||||
import { H3HexagonLayer } from '@deck.gl/geo-layers';
|
||||
import { GeoJsonLayer, IconLayer, TextLayer, ScatterplotLayer, PolygonLayer } from '@deck.gl/layers';
|
||||
import { GeoJsonLayer, IconLayer, TextLayer, ScatterplotLayer } from '@deck.gl/layers';
|
||||
import { cellToBoundary } from 'h3-js';
|
||||
import Supercluster from 'supercluster';
|
||||
import type { PickingInfo } from '@deck.gl/core';
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
MINOR_POI_ZOOM_THRESHOLD,
|
||||
POI_CLUSTER_RADIUS,
|
||||
POI_CLUSTER_MAX_ZOOM,
|
||||
getEnumPaletteForFeature,
|
||||
} from '../lib/consts';
|
||||
import { emojiToTwemojiUrl, getFeatureFillColor } from '../lib/map-utils';
|
||||
import type { TravelTimeEntry } from './useTravelTime';
|
||||
|
|
@ -146,6 +147,12 @@ export function useDeckLayers({
|
|||
? colorFeatureMeta.values.length
|
||||
: 0;
|
||||
|
||||
// Per-feature color palette (uses overrides when defined)
|
||||
const enumPaletteRef = useRef(
|
||||
getEnumPaletteForFeature(viewFeature, colorFeatureMeta?.values)
|
||||
);
|
||||
enumPaletteRef.current = getEnumPaletteForFeature(viewFeature, colorFeatureMeta?.values);
|
||||
|
||||
const countRange = useMemo(() => {
|
||||
if (data.length === 0) return { min: 0, max: 1, total: 0 };
|
||||
let min = Infinity;
|
||||
|
|
@ -306,85 +313,42 @@ export function useDeckLayers({
|
|||
const postcodeColorTrigger = `${viewFeature}|${colorRange?.[0]}|${colorRange?.[1]}|${filterRange?.[0]}|${filterRange?.[1]}|${postcodeCountRange.min}|${postcodeCountRange.max}|${hoveredPostcode}|${theme}|${ttTrigger}`;
|
||||
|
||||
// --- Layers ---
|
||||
// For enum features, we bypass H3HexagonLayer and use PolygonLayer directly.
|
||||
// H3HexagonLayer has double CompositeLayer nesting (H3 → PolygonLayer → SolidPolygonLayer)
|
||||
// which prevents custom binary attributes from reaching the fill sublayer.
|
||||
// PolygonLayer has only one level of nesting, so _subLayerProps.fill works reliably.
|
||||
// PieHexExtension uses the canonical deck.gl v9 pattern: defaultProps with
|
||||
// type:'accessor' + stepMode:'dynamic'. LayerExtension.getSubLayerProps()
|
||||
// wraps accessors via getSubLayerAccessor() which unwraps __source.object,
|
||||
// letting accessor functions work through CompositeLayer sublayer chains.
|
||||
const hexLayer = useMemo(() => {
|
||||
const isEnum = enumCountRef.current > 0;
|
||||
const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : '';
|
||||
|
||||
if (isEnum) {
|
||||
const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : '';
|
||||
const n = data.length;
|
||||
|
||||
// Pre-compute hex boundaries and binary attribute buffers
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const polyData: any[] = new Array(n);
|
||||
const centers = new Float32Array(n * 2);
|
||||
const r0 = new Float32Array(n * 4);
|
||||
const r1 = new Float32Array(n * 4);
|
||||
const r2 = new Float32Array(n * 2);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = data[i];
|
||||
polyData[i] = { ...d, polygon: cellToBoundary(d.h3, true) };
|
||||
centers[i * 2] = d.lon as number;
|
||||
centers[i * 2 + 1] = d.lat as number;
|
||||
const r = distToRatios(d[distKey]);
|
||||
r0[i * 4] = r[0];
|
||||
r0[i * 4 + 1] = r[1];
|
||||
r0[i * 4 + 2] = r[2];
|
||||
r0[i * 4 + 3] = r[3];
|
||||
r1[i * 4] = r[4];
|
||||
r1[i * 4 + 1] = r[5];
|
||||
r1[i * 4 + 2] = r[6];
|
||||
r1[i * 4 + 3] = r[7];
|
||||
r2[i * 2] = r[8];
|
||||
r2[i * 2 + 1] = r[9];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return new (PolygonLayer as any)({
|
||||
id: 'h3-hexagons',
|
||||
data: polyData,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getPolygon: (d: any) => d.polygon,
|
||||
getFillColor: [200, 200, 200],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getLineColor: (d: any) => {
|
||||
if (d.h3 === hoveredHexagonIdRef.current)
|
||||
return [29, 228, 195, 200];
|
||||
return [0, 0, 0, 0];
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getLineWidth: (d: any) => {
|
||||
if (d.h3 === hoveredHexagonIdRef.current) return 2;
|
||||
return 0;
|
||||
},
|
||||
lineWidthUnits: 'pixels',
|
||||
updateTriggers: {
|
||||
getLineColor: [colorTrigger],
|
||||
getLineWidth: [colorTrigger],
|
||||
},
|
||||
extensions: [new PieHexExtension()],
|
||||
_subLayerProps: {
|
||||
fill: {
|
||||
instancePieCenter: { value: centers, size: 2 },
|
||||
instanceRatios0: { value: r0, size: 4 },
|
||||
instanceRatios1: { value: r1, size: 4 },
|
||||
instanceRatios2: { value: r2, size: 2 },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pieProps: any = isEnum
|
||||
? {
|
||||
extensions: [new PieHexExtension(enumPaletteRef.current)],
|
||||
getCenter: (d: HexagonData) => [d.lon, d.lat],
|
||||
getRatios0: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[0], r[1], r[2], r[3]];
|
||||
},
|
||||
},
|
||||
extruded: false,
|
||||
pickable: true,
|
||||
onClick: handleHexagonClick,
|
||||
onHover: handleHexagonHover,
|
||||
beforeId: 'landuse_park',
|
||||
});
|
||||
}
|
||||
getRatios1: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[4], r[5], r[6], r[7]];
|
||||
},
|
||||
getRatios2: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[8], r[9]];
|
||||
},
|
||||
updateTriggers: {
|
||||
getCenter: [colorTrigger, data],
|
||||
getRatios0: [colorTrigger, data],
|
||||
getRatios1: [colorTrigger, data],
|
||||
getRatios2: [colorTrigger, data],
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
// Non-enum: use H3HexagonLayer as normal
|
||||
return new H3HexagonLayer<HexagonData>({
|
||||
id: 'h3-hexagons',
|
||||
id: isEnum ? 'h3-hexagons-pie' : 'h3-hexagons',
|
||||
data,
|
||||
getHexagon: (d) => d.h3,
|
||||
getFillColor: (d) => {
|
||||
|
|
@ -434,7 +398,8 @@ export function useDeckLayers({
|
|||
densityGradientRef.current,
|
||||
dark,
|
||||
255,
|
||||
enumCountRef.current
|
||||
enumCountRef.current,
|
||||
enumPaletteRef.current
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -468,6 +433,7 @@ export function useDeckLayers({
|
|||
getFillColor: [colorTrigger],
|
||||
getLineColor: [colorTrigger],
|
||||
getLineWidth: [colorTrigger],
|
||||
...(pieProps.updateTriggers || {}),
|
||||
},
|
||||
extruded: false,
|
||||
pickable: true,
|
||||
|
|
@ -475,59 +441,12 @@ export function useDeckLayers({
|
|||
highPrecision: true,
|
||||
onClick: handleHexagonClick,
|
||||
onHover: handleHexagonHover,
|
||||
// @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps
|
||||
beforeId: 'landuse_park',
|
||||
...pieProps,
|
||||
});
|
||||
}, [data, colorTrigger, handleHexagonClick, handleHexagonHover]);
|
||||
|
||||
const postcodeLayer = useMemo(() => {
|
||||
const isEnum = enumCountRef.current > 0;
|
||||
const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : '';
|
||||
|
||||
// Same binary buffer approach as hexagons, routed via _subLayerProps.
|
||||
// GeoJsonLayer → 'polygons-fill' (PolygonLayer) → 'fill' (SolidPolygonLayer)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let pieProps: any = {};
|
||||
if (isEnum) {
|
||||
const n = postcodeData.length;
|
||||
const centers = new Float32Array(n * 2);
|
||||
const r0 = new Float32Array(n * 4);
|
||||
const r1 = new Float32Array(n * 4);
|
||||
const r2 = new Float32Array(n * 2);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const centroid = postcodeData[i].properties.centroid as [number, number];
|
||||
centers[i * 2] = centroid[0];
|
||||
centers[i * 2 + 1] = centroid[1];
|
||||
const r = distToRatios(postcodeData[i].properties[distKey]);
|
||||
r0[i * 4] = r[0];
|
||||
r0[i * 4 + 1] = r[1];
|
||||
r0[i * 4 + 2] = r[2];
|
||||
r0[i * 4 + 3] = r[3];
|
||||
r1[i * 4] = r[4];
|
||||
r1[i * 4 + 1] = r[5];
|
||||
r1[i * 4 + 2] = r[6];
|
||||
r1[i * 4 + 3] = r[7];
|
||||
r2[i * 2] = r[8];
|
||||
r2[i * 2 + 1] = r[9];
|
||||
}
|
||||
const fillAttrs = {
|
||||
instancePieCenter: { value: centers, size: 2 },
|
||||
instanceRatios0: { value: r0, size: 4 },
|
||||
instanceRatios1: { value: r1, size: 4 },
|
||||
instanceRatios2: { value: r2, size: 2 },
|
||||
};
|
||||
pieProps = {
|
||||
extensions: [new PieHexExtension()],
|
||||
_subLayerProps: {
|
||||
'polygons-fill': {
|
||||
_subLayerProps: {
|
||||
fill: fillAttrs,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return new GeoJsonLayer<PostcodeProperties>({
|
||||
id: 'postcode-polygons',
|
||||
data: postcodeData as PostcodeFeature[],
|
||||
|
|
@ -581,7 +500,8 @@ export function useDeckLayers({
|
|||
densityGradientRef.current,
|
||||
dark,
|
||||
180,
|
||||
enumCountRef.current
|
||||
enumCountRef.current,
|
||||
enumPaletteRef.current
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -627,8 +547,8 @@ export function useDeckLayers({
|
|||
pickable: true,
|
||||
onClick: handlePostcodeClick,
|
||||
onHover: handlePostcodeHoverCallback,
|
||||
// @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps
|
||||
beforeId: 'landuse_park',
|
||||
...pieProps,
|
||||
});
|
||||
}, [postcodeData, postcodeColorTrigger, handlePostcodeClick, handlePostcodeHoverCallback]);
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,14 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
}, [handleUndo]);
|
||||
|
||||
const handleFilterChange = useCallback((name: string, value: [number, number] | string[]) => {
|
||||
setFilters((prev) => ({ ...prev, [name]: value }));
|
||||
setFilters((prev) => {
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
return next;
|
||||
}
|
||||
return { ...prev, [name]: value };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRemoveFilter = useCallback((name: string) => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function usePaneResize(
|
|||
const targetRef = useRef<HTMLElement | null>(null);
|
||||
const containerOffsetRef = useRef(0);
|
||||
const containerSizeRef = useRef(0);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
const isVertical = side === 'top' || side === 'bottom';
|
||||
const styleProp = isVertical ? 'height' : 'width';
|
||||
|
|
@ -80,12 +81,21 @@ export function usePaneResize(
|
|||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!draggingRef.current) return;
|
||||
const newSize = computeSize(e);
|
||||
liveSizeRef.current = newSize;
|
||||
liveSizeRef.current = computeSize(e);
|
||||
if (targetRef.current) {
|
||||
targetRef.current.style[styleProp] = `${newSize}px`;
|
||||
// Batch DOM updates to once per animation frame — on mobile, pointermove
|
||||
// can fire multiple times per frame, and each direct style.height write
|
||||
// forces a synchronous reflow that desynchronises MapLibre and deck.gl.
|
||||
if (rafRef.current == null) {
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
rafRef.current = null;
|
||||
if (targetRef.current) {
|
||||
targetRef.current.style[styleProp] = `${liveSizeRef.current}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setSize(newSize);
|
||||
setSize(liveSizeRef.current);
|
||||
}
|
||||
},
|
||||
[computeSize, styleProp]
|
||||
|
|
@ -93,8 +103,16 @@ export function usePaneResize(
|
|||
|
||||
const handlePointerUp = useCallback(() => {
|
||||
draggingRef.current = false;
|
||||
if (rafRef.current != null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
// Apply final size synchronously so the commit is immediate
|
||||
if (targetRef.current) {
|
||||
targetRef.current.style[styleProp] = `${liveSizeRef.current}px`;
|
||||
}
|
||||
setSize(liveSizeRef.current);
|
||||
}, []);
|
||||
}, [styleProp]);
|
||||
|
||||
return [
|
||||
size,
|
||||
|
|
|
|||
|
|
@ -12,11 +12,7 @@ export interface SavedPropertyData {
|
|||
energyRating?: string;
|
||||
price?: number;
|
||||
estimatedPrice?: number;
|
||||
askingPrice?: number;
|
||||
askingRent?: number;
|
||||
bedrooms?: number;
|
||||
floorArea?: number;
|
||||
listingUrl?: string;
|
||||
}
|
||||
|
||||
export interface SavedProperty {
|
||||
|
|
@ -84,11 +80,7 @@ export function useSavedProperties(userId: string | null) {
|
|||
energyRating: property.current_energy_rating,
|
||||
price: getNum(property, 'Last known price'),
|
||||
estimatedPrice: getNum(property, 'Estimated current price'),
|
||||
askingPrice: getNum(property, 'Asking price'),
|
||||
askingRent: getNum(property, 'Asking rent (monthly)'),
|
||||
bedrooms: getNum(property, 'Bedrooms'),
|
||||
floorArea: getNum(property, 'Total floor area (sqm)'),
|
||||
listingUrl: property.listing_url || undefined,
|
||||
};
|
||||
|
||||
await pb.collection('saved_properties').create({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue