Refactor map page
This commit is contained in:
parent
29d048ffd4
commit
d4d79f0d99
17 changed files with 1014 additions and 878 deletions
196
frontend/src/hooks/useHexagonSelection.ts
Normal file
196
frontend/src/hooks/useHexagonSelection.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type {
|
||||
FeatureMeta,
|
||||
FeatureFilters,
|
||||
PostcodeFeature,
|
||||
Property,
|
||||
HexagonPropertiesResponse,
|
||||
HexagonStatsResponse,
|
||||
NumericFeatureStats,
|
||||
} from '../types';
|
||||
import { buildFilterString, apiUrl, logNonAbortError } from '../lib/api';
|
||||
|
||||
interface SelectedHexagon {
|
||||
id: string;
|
||||
type: 'hexagon' | 'postcode';
|
||||
resolution: number;
|
||||
}
|
||||
|
||||
interface UseHexagonSelectionOptions {
|
||||
filters: FeatureFilters;
|
||||
features: FeatureMeta[];
|
||||
postcodeData: PostcodeFeature[];
|
||||
resolution: number;
|
||||
}
|
||||
|
||||
export function useHexagonSelection({
|
||||
filters,
|
||||
features,
|
||||
postcodeData,
|
||||
resolution,
|
||||
}: UseHexagonSelectionOptions) {
|
||||
const [selectedHexagon, setSelectedHexagon] = useState<SelectedHexagon | null>(null);
|
||||
const [properties, setProperties] = useState<Property[]>([]);
|
||||
const [propertiesTotal, setPropertiesTotal] = useState(0);
|
||||
const [propertiesOffset, setPropertiesOffset] = useState(0);
|
||||
const [loadingProperties, setLoadingProperties] = useState(false);
|
||||
const [areaStats, setAreaStats] = useState<HexagonStatsResponse | null>(null);
|
||||
const [loadingAreaStats, setLoadingAreaStats] = useState(false);
|
||||
const [hoveredHexagon, setHoveredHexagon] = useState<string | null>(null);
|
||||
const [rightPaneTab, setRightPaneTab] = useState<'pois' | 'properties' | 'area'>('pois');
|
||||
|
||||
const fetchHexagonStats = useCallback(
|
||||
async (h3: string, res: number, signal?: AbortSignal, fields?: string[]) => {
|
||||
const params = new URLSearchParams({
|
||||
h3,
|
||||
resolution: res.toString(),
|
||||
});
|
||||
const filterStr = buildFilterString(filters, features);
|
||||
if (filterStr) params.append('filters', filterStr);
|
||||
if (fields) {
|
||||
params.set('fields', fields.join(','));
|
||||
}
|
||||
const response = await fetch(apiUrl('hexagon-stats', params), { signal });
|
||||
return (await response.json()) as HexagonStatsResponse;
|
||||
},
|
||||
[filters, features]
|
||||
);
|
||||
|
||||
const buildPostcodeStats = useCallback(
|
||||
(postcode: string): HexagonStatsResponse | null => {
|
||||
const feat = postcodeData.find((f) => f.properties.postcode === postcode);
|
||||
if (!feat) return null;
|
||||
const props = feat.properties;
|
||||
|
||||
const numeric_features: NumericFeatureStats[] = [];
|
||||
for (const f of features) {
|
||||
if (f.type !== 'numeric') continue;
|
||||
const minVal = props[`min_${f.name}`];
|
||||
const maxVal = props[`max_${f.name}`];
|
||||
if (typeof minVal !== 'number' || typeof maxVal !== 'number') continue;
|
||||
numeric_features.push({
|
||||
name: f.name,
|
||||
count: props.count,
|
||||
min: minVal,
|
||||
max: maxVal,
|
||||
mean: (minVal + maxVal) / 2,
|
||||
});
|
||||
}
|
||||
|
||||
return { count: props.count, numeric_features, enum_features: [] };
|
||||
},
|
||||
[postcodeData, features]
|
||||
);
|
||||
|
||||
const fetchHexagonProperties = useCallback(
|
||||
async (h3: string, res: number, offset = 0) => {
|
||||
setLoadingProperties(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
h3,
|
||||
resolution: res.toString(),
|
||||
limit: '100',
|
||||
offset: offset.toString(),
|
||||
});
|
||||
|
||||
const filterStr = buildFilterString(filters, features);
|
||||
if (filterStr) params.append('filters', filterStr);
|
||||
|
||||
const response = await fetch(apiUrl('hexagon-properties', params));
|
||||
const data: HexagonPropertiesResponse = await response.json();
|
||||
|
||||
if (offset === 0) {
|
||||
setProperties(data.properties);
|
||||
} else {
|
||||
setProperties((prev) => [...prev, ...data.properties]);
|
||||
}
|
||||
setPropertiesTotal(data.total);
|
||||
setPropertiesOffset(offset + data.properties.length);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch properties:', err);
|
||||
} finally {
|
||||
setLoadingProperties(false);
|
||||
}
|
||||
},
|
||||
[filters, features]
|
||||
);
|
||||
|
||||
const handleHexagonClick = useCallback(
|
||||
(id: string, isPostcode = false) => {
|
||||
if (selectedHexagon?.id === id) {
|
||||
setSelectedHexagon(null);
|
||||
setProperties([]);
|
||||
setAreaStats(null);
|
||||
} else {
|
||||
const type = isPostcode ? 'postcode' : 'hexagon';
|
||||
setSelectedHexagon({ id, type, resolution });
|
||||
setProperties([]);
|
||||
setPropertiesTotal(0);
|
||||
setPropertiesOffset(0);
|
||||
setRightPaneTab('area');
|
||||
|
||||
if (isPostcode) {
|
||||
setAreaStats(buildPostcodeStats(id));
|
||||
setLoadingAreaStats(false);
|
||||
} else {
|
||||
setLoadingAreaStats(true);
|
||||
fetchHexagonStats(id, resolution)
|
||||
.then((stats) => setAreaStats(stats))
|
||||
.catch((error) => logNonAbortError('Failed to fetch area stats', error))
|
||||
.finally(() => setLoadingAreaStats(false));
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedHexagon, resolution, fetchHexagonStats, buildPostcodeStats]
|
||||
);
|
||||
|
||||
const handleHexagonHover = useCallback((h3: string | null) => {
|
||||
setHoveredHexagon(h3);
|
||||
}, []);
|
||||
|
||||
const handleViewPropertiesFromArea = useCallback(() => {
|
||||
if (selectedHexagon && selectedHexagon.type === 'hexagon') {
|
||||
setRightPaneTab('properties');
|
||||
setPropertiesOffset(0);
|
||||
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
|
||||
}
|
||||
}, [selectedHexagon, fetchHexagonProperties]);
|
||||
|
||||
const handlePropertiesTabClick = useCallback(() => {
|
||||
setRightPaneTab('properties');
|
||||
if (selectedHexagon?.type === 'hexagon' && properties.length === 0 && !loadingProperties) {
|
||||
setPropertiesOffset(0);
|
||||
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
|
||||
}
|
||||
}, [selectedHexagon, properties.length, loadingProperties, fetchHexagonProperties]);
|
||||
|
||||
const handleLoadMoreProperties = useCallback(() => {
|
||||
if (selectedHexagon && selectedHexagon.type === 'hexagon') {
|
||||
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, propertiesOffset);
|
||||
}
|
||||
}, [selectedHexagon, propertiesOffset, fetchHexagonProperties]);
|
||||
|
||||
const handleCloseSelection = useCallback(() => {
|
||||
setSelectedHexagon(null);
|
||||
setProperties([]);
|
||||
setAreaStats(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selectedHexagon,
|
||||
properties,
|
||||
propertiesTotal,
|
||||
loadingProperties,
|
||||
areaStats,
|
||||
loadingAreaStats,
|
||||
hoveredHexagon,
|
||||
rightPaneTab,
|
||||
setRightPaneTab,
|
||||
handleHexagonClick,
|
||||
handleHexagonHover,
|
||||
handleViewPropertiesFromArea,
|
||||
handlePropertiesTabClick,
|
||||
handleLoadMoreProperties,
|
||||
handleCloseSelection,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue