Changes
This commit is contained in:
parent
3a3f899ea2
commit
128b3191e7
68 changed files with 28060 additions and 1152 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters, POICategoryGroup, ViewState } from '../../types';
|
||||
import type { SearchedPostcode } from './PostcodeSearch';
|
||||
import type { SearchedLocation } from './LocationSearch';
|
||||
import type { Page } from '../ui/Header';
|
||||
import Map from './Map';
|
||||
import Filters from './Filters';
|
||||
|
|
@ -18,8 +18,14 @@ import { usePaneResize } from '../../hooks/usePaneResize';
|
|||
import { useAiFilters } from '../../hooks/useAiFilters';
|
||||
import { useAreaSummary } from '../../hooks/useAreaSummary';
|
||||
import { useUrlSync } from '../../hooks/useUrlSync';
|
||||
import { useTravelTime, type TravelTimeInitial } from '../../hooks/useTravelTime';
|
||||
import { apiUrl, buildFilterString } from '../../lib/api';
|
||||
import {
|
||||
useTravelTime,
|
||||
TRANSPORT_MODES,
|
||||
MODE_LABELS,
|
||||
type TransportMode,
|
||||
type TravelTimeInitial,
|
||||
} from '../../hooks/useTravelTime';
|
||||
import { apiUrl, assertOk, buildFilterString, logNonAbortError } from '../../lib/api';
|
||||
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
|
||||
import { MapPinIcon } from '../ui/icons/MapPinIcon';
|
||||
|
||||
|
|
@ -65,7 +71,6 @@ export default function MapPage({
|
|||
isMobile = false,
|
||||
initialTravelTime,
|
||||
}: MapPageProps) {
|
||||
const [searchedPostcode, setSearchedPostcode] = useState<SearchedPostcode | null>(null);
|
||||
const [selectedPOICategories, setSelectedPOICategories] =
|
||||
useState<Set<string>>(initialPOICategories);
|
||||
|
||||
|
|
@ -109,7 +114,7 @@ export default function MapPage({
|
|||
const handleAiFilterSubmit = useCallback(
|
||||
async (query: string) => {
|
||||
const result = await aiFilters.fetchAiFilters(query);
|
||||
if (result) handleSetFilters(result);
|
||||
if (result) handleSetFilters(result.filters);
|
||||
},
|
||||
[aiFilters.fetchAiFilters, handleSetFilters]
|
||||
);
|
||||
|
|
@ -125,9 +130,7 @@ export default function MapPage({
|
|||
activeFeature,
|
||||
dragValue,
|
||||
dragData,
|
||||
travelTimeEnabled: travelTime.enabled,
|
||||
travelTimeDestination: travelTime.destination,
|
||||
travelTimeMode: travelTime.mode,
|
||||
travelTimeEntries: travelTime.entries,
|
||||
});
|
||||
|
||||
// Keep filter bounds in sync with map data
|
||||
|
|
@ -142,24 +145,42 @@ export default function MapPage({
|
|||
resolution: mapData.resolution,
|
||||
});
|
||||
|
||||
// Location search handler — selects postcode + shows stats
|
||||
const handleLocationSearchResult = useCallback(
|
||||
(result: SearchedLocation | null) => {
|
||||
if (result) {
|
||||
selection.handleLocationSearch(result.postcode, result.geometry);
|
||||
if (isMobile) setMobileDrawerOpen(true);
|
||||
} else {
|
||||
selection.handleCloseSelection();
|
||||
}
|
||||
},
|
||||
[selection.handleLocationSearch, selection.handleCloseSelection, isMobile]
|
||||
);
|
||||
|
||||
// POI data
|
||||
const pois = usePOIData(mapData.bounds, selectedPOICategories);
|
||||
|
||||
// Compute data range for travel time slider
|
||||
const travelTimeDataRange = useMemo((): [number, number] | null => {
|
||||
if (!travelTime.enabled || !travelTime.destination) return null;
|
||||
const vals: number[] = [];
|
||||
for (const item of mapData.data) {
|
||||
const val = item.travel_time;
|
||||
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
|
||||
// Compute data range for travel time slider per mode (full min/max for slider bounds)
|
||||
const travelTimeDataRanges = useMemo((): Partial<Record<TransportMode, [number, number]>> => {
|
||||
const ranges: Partial<Record<TransportMode, [number, number]>> = {};
|
||||
for (const mode of TRANSPORT_MODES) {
|
||||
const entry = travelTime.entries[mode];
|
||||
if (!entry?.destination) continue;
|
||||
const vals: number[] = [];
|
||||
for (const item of mapData.data) {
|
||||
const val = item[`travel_time_${mode}`];
|
||||
if (typeof val === 'number' && !isNaN(val)) vals.push(val);
|
||||
}
|
||||
if (vals.length === 0) continue;
|
||||
vals.sort((a, b) => a - b);
|
||||
ranges[mode] = [vals[0], vals[vals.length - 1]];
|
||||
}
|
||||
if (vals.length === 0) return null;
|
||||
vals.sort((a, b) => a - b);
|
||||
return [vals[0], vals[vals.length - 1]];
|
||||
}, [travelTime.enabled, travelTime.destination, mapData.data]);
|
||||
return ranges;
|
||||
}, [travelTime.entries, mapData.data]);
|
||||
|
||||
// Sync current state to URL
|
||||
useUrlSync(mapData.currentView, filters, features, selectedPOICategories, selection.rightPaneTab, travelTime);
|
||||
useUrlSync(mapData.currentView, filters, features, selectedPOICategories, selection.rightPaneTab, travelTime.entries);
|
||||
|
||||
// Set initial view and tab from URL state
|
||||
useEffect(() => {
|
||||
|
|
@ -238,7 +259,7 @@ export default function MapPage({
|
|||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
})
|
||||
.catch((err) => console.error('Export failed:', err))
|
||||
.catch((err) => logNonAbortError('Export failed', err))
|
||||
.finally(() => setExporting(false));
|
||||
}, [mapData.bounds, filters, features, exporting]);
|
||||
|
||||
|
|
@ -258,10 +279,7 @@ export default function MapPage({
|
|||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const d of items) {
|
||||
const c =
|
||||
'count' in d
|
||||
? (d as { count: number }).count
|
||||
: (d as { properties: { count: number } }).properties.count;
|
||||
const c = 'count' in d ? d.count : d.properties.count;
|
||||
if (c < min) min = c;
|
||||
if (c > max) max = c;
|
||||
}
|
||||
|
|
@ -301,10 +319,8 @@ export default function MapPage({
|
|||
screenshotMode
|
||||
ogMode={ogMode}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEnabled={travelTime.enabled}
|
||||
travelTimeDestination={travelTime.destination}
|
||||
travelTimeColorRange={mapData.travelTimeColorRange}
|
||||
travelTimeRange={travelTime.timeRange}
|
||||
travelTimeEntries={travelTime.entries}
|
||||
travelTimeColorRanges={mapData.travelTimeColorRanges}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -373,19 +389,15 @@ export default function MapPage({
|
|||
onCancelPin={handleCancelPin}
|
||||
openInfoFeature={pendingInfoFeature}
|
||||
onClearOpenInfoFeature={onClearPendingInfoFeature}
|
||||
travelTimeEnabled={travelTime.enabled}
|
||||
travelTimeDestination={travelTime.destination}
|
||||
travelTimeDestinationLabel={travelTime.destinationLabel}
|
||||
travelTimeMode={travelTime.mode}
|
||||
travelTimeRange={travelTime.timeRange}
|
||||
travelTimeDataRange={travelTimeDataRange}
|
||||
onTravelTimeEnable={travelTime.handleEnable}
|
||||
onTravelTimeDisable={travelTime.handleDisable}
|
||||
travelTimeEntries={travelTime.entries}
|
||||
travelTimeDataRanges={travelTimeDataRanges}
|
||||
onTravelTimeEnableMode={travelTime.handleEnableMode}
|
||||
onTravelTimeDisableMode={travelTime.handleDisableMode}
|
||||
onTravelTimeSetDestination={travelTime.handleSetDestination}
|
||||
onTravelTimeModeChange={travelTime.handleModeChange}
|
||||
onTravelTimeRangeChange={travelTime.handleTimeRangeChange}
|
||||
aiFilterLoading={aiFilters.loading}
|
||||
aiFilterError={aiFilters.error}
|
||||
aiFilterNotes={aiFilters.notes}
|
||||
onAiFilterSubmit={handleAiFilterSubmit}
|
||||
/>
|
||||
);
|
||||
|
|
@ -426,14 +438,12 @@ export default function MapPage({
|
|||
initialViewState={initialViewState}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
searchedPostcode={searchedPostcode}
|
||||
onPostcodeSearched={setSearchedPostcode}
|
||||
selectedPostcodeGeometry={selection.selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
bounds={mapData.bounds}
|
||||
hideLegend
|
||||
travelTimeEnabled={travelTime.enabled}
|
||||
travelTimeDestination={travelTime.destination}
|
||||
travelTimeColorRange={mapData.travelTimeColorRange}
|
||||
travelTimeRange={travelTime.timeRange}
|
||||
travelTimeEntries={travelTime.entries}
|
||||
travelTimeColorRanges={mapData.travelTimeColorRanges}
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute bottom-2 left-2 bg-white dark:bg-navy-800 dark:text-warm-200 px-2 py-1 rounded shadow text-xs">
|
||||
|
|
@ -461,43 +471,54 @@ export default function MapPage({
|
|||
style={{ flex: '55 0 0' }}
|
||||
>
|
||||
{/* Legend */}
|
||||
{travelTime.enabled && travelTime.destination && mapData.travelTimeColorRange ? (
|
||||
<MapLegend
|
||||
featureLabel="Travel time"
|
||||
range={mapData.travelTimeColorRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
inline
|
||||
suffix=" min"
|
||||
/>
|
||||
) : viewFeature && mapData.colorRange && mobileLegendMeta ? (
|
||||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? `Previewing \u201c${mobileLegendMeta.name}\u201d`
|
||||
: mobileLegendMeta.name
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
) : (
|
||||
<MapLegend
|
||||
featureLabel="Property density"
|
||||
range={mobileDensityRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="density"
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
const primaryMode = TRANSPORT_MODES.find(
|
||||
(m) => travelTime.entries[m]?.destination && mapData.travelTimeColorRanges[m]
|
||||
);
|
||||
if (primaryMode) {
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel={`Travel time (${MODE_LABELS[primaryMode]})`}
|
||||
range={mapData.travelTimeColorRanges[primaryMode]!}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
inline
|
||||
suffix=" min"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewFeature && mapData.colorRange && mobileLegendMeta) {
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? `Previewing \u201c${mobileLegendMeta.name}\u201d`
|
||||
: mobileLegendMeta.name
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel="Property density"
|
||||
range={mobileDensityRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="density"
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{/* Filters content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{renderFilters()}
|
||||
|
|
@ -565,13 +586,11 @@ export default function MapPage({
|
|||
initialViewState={initialViewState}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
searchedPostcode={searchedPostcode}
|
||||
onPostcodeSearched={setSearchedPostcode}
|
||||
selectedPostcodeGeometry={selection.selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEnabled={travelTime.enabled}
|
||||
travelTimeDestination={travelTime.destination}
|
||||
travelTimeColorRange={mapData.travelTimeColorRange}
|
||||
travelTimeRange={travelTime.timeRange}
|
||||
travelTimeEntries={travelTime.entries}
|
||||
travelTimeColorRanges={mapData.travelTimeColorRanges}
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute bottom-4 left-4 bg-white dark:bg-navy-800 dark:text-warm-200 px-3 py-1 rounded shadow">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue