Add per-POI travel time filtering and fix heatmap color stops
Replace the single global max travel time filter with per-POI filters. Each POI gets its own travel mode selector and max minutes input in the filter panel. Listings must satisfy ALL active filters (AND logic). Fix Mapbox "Input is not a number" error by ensuring color stops are always strictly monotonic (guard min === max) and always set (even when no valid metric values exist). Also filter Infinity values from the color scale computation. Widen the filter panel from w-64 to w-80.
This commit is contained in:
parent
81d31eaecf
commit
07d4fa5f84
5 changed files with 193 additions and 17 deletions
|
|
@ -15,7 +15,7 @@ import { StreamingProgressBar } from './components/StreamingProgressBar';
|
|||
import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet';
|
||||
import { Button } from './components/ui/button';
|
||||
import { Filter } from 'lucide-react';
|
||||
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI } from '@/types';
|
||||
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types';
|
||||
import { refreshListings, fetchTasksForUser, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
|
||||
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ function App() {
|
|||
poiName: string;
|
||||
travelMode: 'WALK' | 'BICYCLE' | 'TRANSIT';
|
||||
} | null>(null);
|
||||
const [maxTravelMinutes, setMaxTravelMinutes] = useState<number | undefined>(undefined);
|
||||
const [poiTravelFilters, setPoiTravelFilters] = useState<Record<number, POITravelFilter>>({});
|
||||
|
||||
// Ref to track accumulated features during streaming
|
||||
const accumulatedFeaturesRef = useRef<PropertyFeature[]>([]);
|
||||
|
|
@ -151,18 +151,26 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
// Filter by max travel time
|
||||
if (maxTravelMinutes !== undefined && poiMetricSelection) {
|
||||
const maxSeconds = maxTravelMinutes * 60;
|
||||
const propName = poiMetricPropertyName(poiMetricSelection.poiId, poiMetricSelection.travelMode);
|
||||
// Filter by per-POI max travel time (AND logic: listing must satisfy all active filters)
|
||||
const activeFilters = Object.entries(poiTravelFilters)
|
||||
.filter(([, f]) => f.maxMinutes !== undefined)
|
||||
.map(([id, f]) => ({ poiId: Number(id), travelMode: f.travelMode, maxMinutes: f.maxMinutes! }));
|
||||
|
||||
if (activeFilters.length > 0) {
|
||||
features = features.filter((f) => {
|
||||
const value = (f.properties as Record<string, unknown>)[propName] as number | undefined;
|
||||
return value !== undefined && value <= maxSeconds;
|
||||
const distances = f.properties.poi_distances;
|
||||
if (!distances) return false;
|
||||
return activeFilters.every((filter) => {
|
||||
const dist = distances.find(
|
||||
(d) => d.poi_id === filter.poiId && d.travel_mode === filter.travelMode,
|
||||
);
|
||||
return dist !== undefined && dist.duration_seconds <= filter.maxMinutes * 60;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { ...listingData, features };
|
||||
}, [listingData, poiMetricSelection, maxTravelMinutes]);
|
||||
}, [listingData, poiMetricSelection, poiTravelFilters]);
|
||||
|
||||
// Compute the effective metric string for the heatmap
|
||||
const effectiveMetric = useMemo(() => {
|
||||
|
|
@ -342,7 +350,7 @@ function App() {
|
|||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden min-h-0">
|
||||
{/* Filter Panel - Desktop (fixed sidebar) */}
|
||||
<div className="hidden md:block w-64 shrink-0 h-full overflow-hidden">
|
||||
<div className="hidden md:block w-80 shrink-0 h-full overflow-hidden">
|
||||
<FilterPanel
|
||||
onSubmit={onSubmit}
|
||||
onMetricChange={handleMetricChange}
|
||||
|
|
@ -354,8 +362,8 @@ function App() {
|
|||
pickedPoiLocation={pickedPoiLocation}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
maxTravelMinutes={maxTravelMinutes}
|
||||
onMaxTravelMinutesChange={setMaxTravelMinutes}
|
||||
poiTravelFilters={poiTravelFilters}
|
||||
onPoiTravelFiltersChange={setPoiTravelFilters}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -379,8 +387,8 @@ function App() {
|
|||
pickedPoiLocation={pickedPoiLocation}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
maxTravelMinutes={maxTravelMinutes}
|
||||
onMaxTravelMinutesChange={setMaxTravelMinutes}
|
||||
poiTravelFilters={poiTravelFilters}
|
||||
onPoiTravelFiltersChange={setPoiTravelFilters}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue