Update map to do filtering
This commit is contained in:
parent
6122ee44da
commit
d4fe881ef4
8 changed files with 349 additions and 372 deletions
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import Map from './components/Map';
|
||||
import Filters from './components/Filters';
|
||||
import { DEFAULT_FILTERS } from './lib/constants';
|
||||
import type {
|
||||
Filters as FiltersType,
|
||||
FeatureMeta,
|
||||
FeatureFilters,
|
||||
Bounds,
|
||||
HexagonData,
|
||||
ViewChangeParams,
|
||||
|
|
@ -11,7 +11,6 @@ import type {
|
|||
POI,
|
||||
POIResponse,
|
||||
POICategoriesMap,
|
||||
ColorMode,
|
||||
} from './types';
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
|
@ -42,8 +41,10 @@ function getApiBaseUrl(): string {
|
|||
}
|
||||
|
||||
export default function App() {
|
||||
const [filters, setFilters] = useState<FiltersType>(DEFAULT_FILTERS);
|
||||
const [data, setData] = useState<HexagonData[]>([]);
|
||||
const [features, setFeatures] = useState<FeatureMeta[]>([]);
|
||||
const [filters, setFilters] = useState<FeatureFilters>({});
|
||||
const [activeFeature, setActiveFeature] = useState<string | null>(null);
|
||||
const [rawData, setRawData] = useState<HexagonData[]>([]);
|
||||
const [resolution, setResolution] = useState<number>(8);
|
||||
const [bounds, setBounds] = useState<Bounds | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
|
@ -51,8 +52,6 @@ export default function App() {
|
|||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const [colorMode, setColorMode] = useState<ColorMode>('price');
|
||||
|
||||
// POI state
|
||||
const [pois, setPois] = useState<POI[]>([]);
|
||||
const [poiCategories, setPOICategories] = useState<POICategoriesMap>({});
|
||||
|
|
@ -60,8 +59,21 @@ export default function App() {
|
|||
const poiDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const poiAbortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Fetch POI category definitions from server on mount
|
||||
// Fetch feature metadata + POI categories on mount
|
||||
useEffect(() => {
|
||||
fetch(`${getApiBaseUrl()}/api/features`)
|
||||
.then((res) => res.json())
|
||||
.then((json: { features: FeatureMeta[] }) => {
|
||||
setFeatures(json.features);
|
||||
// Initialize filters with full range for each feature
|
||||
const initial: FeatureFilters = {};
|
||||
for (const f of json.features) {
|
||||
initial[f.name] = [f.min, f.max];
|
||||
}
|
||||
setFilters(initial);
|
||||
})
|
||||
.catch((err) => console.error('Failed to fetch features:', err));
|
||||
|
||||
fetch(`${getApiBaseUrl()}/api/poi-categories`)
|
||||
.then((res) => res.json())
|
||||
.then((json: { categories: POICategoriesMap }) => {
|
||||
|
|
@ -70,7 +82,7 @@ export default function App() {
|
|||
.catch((err) => console.error('Failed to fetch POI categories:', err));
|
||||
}, []);
|
||||
|
||||
// Debounced fetch when dependencies change
|
||||
// Debounced fetch when resolution/bounds change (no filter params sent)
|
||||
useEffect(() => {
|
||||
if (!bounds) return;
|
||||
|
||||
|
|
@ -89,17 +101,13 @@ export default function App() {
|
|||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
const params = new URLSearchParams({
|
||||
resolution: resolution.toString(),
|
||||
min_year: filters.minYear.toString(),
|
||||
max_year: filters.maxYear.toString(),
|
||||
min_price: filters.minPrice.toString(),
|
||||
max_price: filters.maxPrice.toString(),
|
||||
bounds: boundsStr,
|
||||
});
|
||||
const res = await fetch(`${getApiBaseUrl()}/api/hexagons?${params}`, {
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const json: ApiResponse = await res.json();
|
||||
setData(json.features || []);
|
||||
setRawData(json.features || []);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name !== 'AbortError') {
|
||||
console.error('Failed to fetch data:', err);
|
||||
|
|
@ -114,7 +122,36 @@ export default function App() {
|
|||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, [filters, resolution, bounds]);
|
||||
}, [resolution, bounds]);
|
||||
|
||||
// Client-side filtering
|
||||
const data = useMemo(() => {
|
||||
if (features.length === 0) return rawData;
|
||||
|
||||
return rawData.filter((hex) => {
|
||||
if (activeFeature) {
|
||||
// Only apply the active feature's filter
|
||||
const range = filters[activeFeature];
|
||||
if (!range) return true;
|
||||
const minVal = hex[`min_${activeFeature}`];
|
||||
const maxVal = hex[`max_${activeFeature}`];
|
||||
if (minVal == null || maxVal == null) return true;
|
||||
return (minVal as number) <= range[1] && (maxVal as number) >= range[0];
|
||||
}
|
||||
// Apply ALL filters as intersection
|
||||
for (const f of features) {
|
||||
const range = filters[f.name];
|
||||
if (!range) continue;
|
||||
// Skip features where filter is at full range
|
||||
if (range[0] === f.min && range[1] === f.max) continue;
|
||||
const minVal = hex[`min_${f.name}`];
|
||||
const maxVal = hex[`max_${f.name}`];
|
||||
if (minVal == null || maxVal == null) continue;
|
||||
if ((minVal as number) > range[1] || (maxVal as number) < range[0]) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [rawData, filters, activeFeature, features]);
|
||||
|
||||
// Fetch POIs when bounds or selected categories change
|
||||
useEffect(() => {
|
||||
|
|
@ -171,17 +208,24 @@ export default function App() {
|
|||
return (
|
||||
<div className="h-screen flex">
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
activeFeature={activeFeature}
|
||||
onFiltersChange={setFilters}
|
||||
onActiveFeatureChange={setActiveFeature}
|
||||
zoom={zoom}
|
||||
poiCategories={poiCategories}
|
||||
selectedPOICategories={selectedPOICategories}
|
||||
onPOICategoriesChange={setSelectedPOICategories}
|
||||
colorMode={colorMode}
|
||||
onColorModeChange={setColorMode}
|
||||
/>
|
||||
<div className="flex-1 relative">
|
||||
<Map data={data} pois={pois} onViewChange={handleViewChange} colorMode={colorMode} />
|
||||
<Map
|
||||
data={data}
|
||||
pois={pois}
|
||||
onViewChange={handleViewChange}
|
||||
activeFeature={activeFeature}
|
||||
features={features}
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute top-4 right-4 bg-white px-3 py-1 rounded shadow">Loading...</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue