import { useEffect, useMemo, useRef, useState } from 'react' import { MapContainer, TileLayer, useMap } from 'react-leaflet' import { useQuery } from '@tanstack/react-query' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import 'leaflet.markercluster' import 'leaflet.markercluster/dist/MarkerCluster.css' import api from '../lib/api' /** * Clustered map of every place we index, served by `GET /api/browse/place-map`. * * Pins are grouped into independently-toggleable levels (state / county / city / * school_district), each its own marker-cluster group so clusters never mix * levels. Every pin sits on a real census centroid and represents a place with * at least one transcript — no fabricated points. */ interface PlaceMapPin { geoid: string name: string state_code: string | null latitude: number longitude: number place_count: number transcript_count: number } interface PlaceMapLevel { level: string label: string count: number pins: PlaceMapPin[] } interface PlaceMapResponse { levels: PlaceMapLevel[] } /** Per-level pin / cluster color (distinct, accessible hues). */ const LEVEL_COLORS: Record = { state: '#7c3aed', // violet county: '#0d9488', // teal city: '#2563eb', // blue school_district: '#d97706', // amber } const FALLBACK_COLOR = '#475569' function levelColor(level: string): string { return LEVEL_COLORS[level] ?? FALLBACK_COLOR } /** A small colored dot marker for an individual place. */ function pinIcon(color: string): L.DivIcon { return L.divIcon({ className: 'place-cluster-pin', html: ``, iconSize: [14, 14], iconAnchor: [7, 7], popupAnchor: [0, -8], }) } /** A colored cluster bubble whose size scales with the child count. */ function clusterIcon(count: number, color: string): L.DivIcon { const size = count < 10 ? 34 : count < 100 ? 42 : 50 return L.divIcon({ className: 'place-cluster-bubble', html: `
${count}
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2], }) } /** Popup HTML. State pins deep-link to the filtered places list; individual * places link to their place home. Counts are real (from the warehouse). */ function popupHtml(pin: PlaceMapPin, level: string): string { const href = level === 'state' ? `/jurisdictions?state=${encodeURIComponent(pin.state_code ?? '')}` : `/jurisdiction/${encodeURIComponent(pin.geoid)}/meetings` const transcripts = `${pin.transcript_count.toLocaleString()} transcript${pin.transcript_count === 1 ? '' : 's'}` const places = level === 'state' ? `
${pin.place_count.toLocaleString()} indexed place${pin.place_count === 1 ? '' : 's'}
` : '' return ( `
` + `
${pin.name}
` + `
${transcripts}
` + places + `View →` + `
` ) } /** Builds one marker-cluster group per level and toggles each on/off the map. */ function ClusterLayers({ levels, enabled, }: { levels: PlaceMapLevel[] enabled: Set }) { const map = useMap() const groupsRef = useRef>({}) // Rebuild the cluster groups whenever the underlying data changes. useEffect(() => { // Tear down any previous groups first. for (const g of Object.values(groupsRef.current)) { if (map.hasLayer(g)) map.removeLayer(g) g.clearLayers() } groupsRef.current = {} for (const lvl of levels) { const color = levelColor(lvl.level) const group = L.markerClusterGroup({ chunkedLoading: true, showCoverageOnHover: false, maxClusterRadius: 55, iconCreateFunction: (cluster) => clusterIcon(cluster.getChildCount(), color), }) const markers = lvl.pins.map((p) => { const m = L.marker([p.latitude, p.longitude], { icon: pinIcon(color) }) m.bindPopup(popupHtml(p, lvl.level)) return m }) group.addLayers(markers) groupsRef.current[lvl.level] = group } return () => { for (const g of Object.values(groupsRef.current)) { if (map.hasLayer(g)) map.removeLayer(g) } } }, [map, levels]) // Add/remove groups as the enabled set changes. useEffect(() => { for (const [lvl, g] of Object.entries(groupsRef.current)) { if (enabled.has(lvl)) { if (!map.hasLayer(g)) map.addLayer(g) } else if (map.hasLayer(g)) { map.removeLayer(g) } } }, [map, enabled, levels]) return null } /** Keeps the Leaflet canvas correctly sized when its container resizes — e.g. * when the filters panel docks open and the page content reflows narrower. */ function InvalidateOnResize() { const map = useMap() useEffect(() => { const el = map.getContainer() const ro = new ResizeObserver(() => map.invalidateSize()) ro.observe(el) return () => ro.disconnect() }, [map]) return null } /** Fits the viewport to the pins of the currently-selected levels, so the map * zooms into the selection. Re-fires whenever the selected levels (`enabledKey`) * or the page's state focus (`focusKey`) change — including the initial seed — * but never on plain pan/zoom, so a user's manual navigation between selection * changes is preserved. */ function FitToSelection({ levels, enabled, enabledKey, focusKey, }: { levels: PlaceMapLevel[] enabled: Set enabledKey: string focusKey: string }) { const map = useMap() useEffect(() => { const pts: [number, number][] = [] for (const lvl of levels) { if (!enabled.has(lvl.level)) continue for (const p of lvl.pins) pts.push([p.latitude, p.longitude]) } if (pts.length) { map.fitBounds(L.latLngBounds(pts), { padding: [40, 40], maxZoom: 9 }) } // enabledKey / focusKey drive the re-fit; levels/enabled are read fresh. // eslint-disable-next-line react-hooks/exhaustive-deps }, [map, enabledKey, focusKey]) return null } /** Maps a page-level filter id (which includes town / special_district / village) * onto the four map levels. Unmapped ids (special_district, village) have no map * layer and are simply dropped. */ const PAGE_TO_MAP_LEVEL: Record = { city: 'city', town: 'city', county: 'county', state: 'state', school_district: 'school_district', } interface PlaceClusterMapProps { /** 2-letter state code — restrict pins to this state and zoom to it. */ filterState?: string /** City/locality name — focus the map on this single place and zoom to it. */ filterCity?: string /** Page-level jurisdiction filter ids — restrict the visible map layers. */ filterLevels?: string[] } export default function PlaceClusterMap({ filterState, filterCity, filterLevels, }: PlaceClusterMapProps = {}) { const { data, isLoading, error } = useQuery({ queryKey: ['browse-place-map'], queryFn: async () => { const res = await api.get('/browse/place-map') return res.data }, staleTime: 1000 * 60 * 30, // slow-moving index }) const allLevels = useMemo(() => data?.levels ?? [], [data]) // Restrict pins to the active page filters (when they carry one). The map data // is fetched once with every place, so this is an instant client-side narrowing // — no refetch — and counts/labels reflect the filtered set. const stateCode = (filterState ?? '').trim().toUpperCase() const cityName = (filterCity ?? '').trim().toUpperCase() const levels = useMemo(() => { if (!stateCode && !cityName) return allLevels // 1) State narrowing (when a state is selected). const stateScoped = allLevels.map((l) => { const pins = stateCode ? l.pins.filter((p) => (p.state_code ?? '').toUpperCase() === stateCode) : l.pins return { ...l, pins, count: pins.length } }) if (!cityName) return stateScoped // 2) City focus: a selected city should show *that* locality, not every // place in the state. Match pins by name (counties/states have no pin // named for a city, so those layers naturally empty out). If the city // isn't an indexed place, fall back to the state view so the map is // never left blank. const cityScoped = stateScoped.map((l) => { const pins = l.pins.filter( (p) => (p.name ?? '').trim().toUpperCase() === cityName, ) return { ...l, pins, count: pins.length } }) const cityTotal = cityScoped.reduce((sum, l) => sum + l.count, 0) return cityTotal > 0 ? cityScoped : stateScoped }, [allLevels, stateCode, cityName]) // Page-level filter ids mapped onto map levels (empty = no level filter). const filterKey = (filterLevels ?? []).join(',') const mappedLevels = useMemo(() => { const ids = (filterLevels ?? []) .map((id) => PAGE_TO_MAP_LEVEL[id]) .filter(Boolean) as string[] return new Set(ids) // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterKey]) // Visible layers: chip-toggleable, but seeded/overridden by the page's level // filter so picking levels in the flyout updates the map immediately. const [enabled, setEnabled] = useState>(new Set()) useEffect(() => { if (mappedLevels.size > 0) { setEnabled(new Set(mappedLevels)) } else if (allLevels.length) { setEnabled(new Set(allLevels.map((l) => l.level))) } }, [mappedLevels, allLevels]) const toggle = (level: string) => { setEnabled((prev) => { const next = new Set(prev) if (next.has(level)) next.delete(level) else next.add(level) return next }) } // Stable key for the selected level set — drives the map's zoom-to-selection. const enabledKey = useMemo(() => [...enabled].sort().join(','), [enabled]) const totalPins = useMemo( () => levels.reduce((sum, l) => sum + l.count, 0), [levels], ) return (

Explore the places we index

{isLoading ? 'Loading map…' : `${totalPins.toLocaleString()} indexed places — pan, zoom, and click a pin to open it.`}

{/* Level filter chips */}
{levels.map((lvl) => { const on = enabled.has(lvl.level) const color = levelColor(lvl.level) return ( ) })}
{error ? (
Map unavailable right now.
) : (
)}
) }