import { useEffect, useRef, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' type Basemap = 'streets' | 'satellite' interface LocalViewProps { center: { lat: number; lng: number } zoom?: number /** Label shown in a tooltip on the marker. */ label?: string /** "Back to county" or similar button slot — rendered top-left. */ topLeftSlot?: React.ReactNode /** Basemap to show first. Defaults to 'satellite'. */ initialBasemap?: Basemap /** Fires when the user clicks the dropped pin — page handles details lookup. */ onMarkerClick?: () => void /** Drilled-from county boundary (lng/lat GeoJSON) — toggled via the overlay control. */ countyOutline?: GeoJSON.Feature | null /** ZCTA (ZIP) boundaries in/around the county (lng/lat GeoJSON) — toggled via the control. */ zctaOutlines?: GeoJSON.Feature[] | null } /** * Tile providers picked for universal accessibility (no API key, no signed * referrer, no rate-limit auth): * - OSM for streets: the most widely-cached tile server on the public web. * - Esri World Imagery for satellite: hosted on the ArcGIS Online CDN, broad * coverage, attribution-only license. The previous USGS endpoints sometimes * get blocked by network policies or browser extensions, leaving the map * gray with no error. */ const STREETS_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' const SATELLITE_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' export default function CensusDrilldownLocalView({ center, zoom = 17, label, topLeftSlot, initialBasemap = 'satellite', onMarkerClick, countyOutline = null, zctaOutlines = null, }: LocalViewProps) { const containerRef = useRef(null) const mapRef = useRef(null) const markerRef = useRef(null) const layersRef = useRef<{ streets?: L.TileLayer; satellite?: L.TileLayer; active?: L.TileLayer }>({}) const countyLayerRef = useRef(null) const zctaLayerRef = useRef(null) const [basemap, setBasemap] = useState(initialBasemap) // Outline overlays are opt-in (off by default — keeps the aerial clean). const [showCounty, setShowCounty] = useState(false) const [showZip, setShowZip] = useState(false) const [tileStatus, setTileStatus] = useState<'loading' | 'ok' | 'partial' | 'failed'>('loading') const tileErrCountRef = useRef(0) const tileLoadCountRef = useRef(0) useEffect(() => { if (!containerRef.current || mapRef.current) return const map = L.map(containerRef.current, { center: [center.lat, center.lng], zoom, zoomControl: true, attributionControl: true, zoomSnap: 0.5, }) mapRef.current = map const wireDiagnostics = (layer: L.TileLayer, name: string) => { layer.on('tileerror', (e: any) => { tileErrCountRef.current += 1 const url = e?.tile?.src ?? '(no URL)' if (tileErrCountRef.current <= 3) { console.warn(`[Local map] tile error on "${name}":`, url) } if (tileErrCountRef.current > 6 && tileLoadCountRef.current === 0) { setTileStatus('failed') } else if (tileErrCountRef.current > 3) { setTileStatus('partial') } }) layer.on('tileload', () => { tileLoadCountRef.current += 1 if (tileStatus !== 'ok' && tileLoadCountRef.current >= 1) { setTileStatus(tileErrCountRef.current > 3 ? 'partial' : 'ok') } }) } const streets = L.tileLayer(STREETS_URL, { maxZoom: 19, attribution: '© OpenStreetMap contributors', }) const satellite = L.tileLayer(SATELLITE_URL, { maxZoom: 22, maxNativeZoom: 19, attribution: 'Imagery © Esri, Maxar, Earthstar Geographics', }) wireDiagnostics(streets, 'streets') wireDiagnostics(satellite, 'satellite') layersRef.current.streets = streets layersRef.current.satellite = satellite const initial = initialBasemap === 'streets' ? streets : satellite initial.addTo(map) layersRef.current.active = initial // Leaflet needs the container measured to start fetching tiles. The // post-mount layout race used to leave the map blank — these calls poke // it after the parent's height transitions in. map.invalidateSize() const t1 = window.setTimeout(() => map.invalidateSize(), 120) const t2 = window.setTimeout(() => map.invalidateSize(), 500) return () => { window.clearTimeout(t1) window.clearTimeout(t2) map.remove() mapRef.current = null markerRef.current = null layersRef.current = {} } }, []) // Reset diagnostics + switch active layer when basemap toggles. useEffect(() => { const map = mapRef.current if (!map) return const next = basemap === 'streets' ? layersRef.current.streets : layersRef.current.satellite const active = layersRef.current.active if (next && next !== active) { if (active) map.removeLayer(active) next.addTo(map) layersRef.current.active = next tileErrCountRef.current = 0 tileLoadCountRef.current = 0 setTileStatus('loading') } }, [basemap]) // Fly to new center / zoom when props change after mount. useEffect(() => { const map = mapRef.current if (!map) return map.flyTo([center.lat, center.lng], zoom, { duration: 0.9 }) }, [center.lat, center.lng, zoom]) // Update marker + label. useEffect(() => { const map = mapRef.current if (!map) return if (markerRef.current) { markerRef.current.setLatLng([center.lat, center.lng]) } else { markerRef.current = L.circleMarker([center.lat, center.lng], { // Small enough to not dominate a house-scale aerial, big enough for // the click target (Leaflet's hit detection has a few px slop too). radius: 4, color: '#ffffff', weight: 1, fillColor: '#b8442c', fillOpacity: 0.95, className: 'cursor-pointer', }).addTo(map) } if (label) { markerRef.current.bindTooltip(label, { direction: 'top', offset: [0, -8] }) } else { markerRef.current.unbindTooltip() } // Rebind click on every update so the latest onMarkerClick closure is used. markerRef.current.off('click') if (onMarkerClick) { markerRef.current.on('click', () => onMarkerClick()) } }, [center.lat, center.lng, label, onMarkerClick]) // County outline overlay — add/remove with the toggle. Non-interactive so it // never steals the pin click; amber dashed to match the SVG map's county accent. useEffect(() => { const map = mapRef.current if (!map) return if (countyLayerRef.current) { map.removeLayer(countyLayerRef.current) countyLayerRef.current = null } if (showCounty && countyOutline) { countyLayerRef.current = L.geoJSON(countyOutline as never, { interactive: false, style: { color: '#b45309', weight: 2.5, dashArray: '6 4', fill: false }, }).addTo(map) } }, [showCounty, countyOutline]) // ZCTA (ZIP) outlines overlay — cyan thin strokes, labels at each centroid. useEffect(() => { const map = mapRef.current if (!map) return if (zctaLayerRef.current) { map.removeLayer(zctaLayerRef.current) zctaLayerRef.current = null } if (showZip && zctaOutlines && zctaOutlines.length) { zctaLayerRef.current = L.geoJSON( { type: 'FeatureCollection', features: zctaOutlines } as never, { interactive: false, style: { color: '#0e7490', weight: 1.25, fillColor: '#06b6d4', fillOpacity: 0.05 }, onEachFeature: (feat, layer) => { const zid = String((feat as GeoJSON.Feature).id ?? '') if (zid) { layer.bindTooltip(zid, { permanent: true, direction: 'center', className: 'zcta-outline-label', }) } }, }, ).addTo(map) } }, [showZip, zctaOutlines]) return (
{topLeftSlot ?
{topLeftSlot}
: null}
{/* Boundary overlay toggles — show county and/or ZIP outlines over the aerial/streets basemap. Each disabled until its geometry is available. */}
Outlines
{tileStatus === 'failed' ? (
Tiles failed to load
Your browser couldn't reach the {basemap === 'satellite' ? 'Esri World Imagery' : 'OpenStreetMap'} tile server. Likely causes: an ad-blocker / privacy extension blocking arcgisonline.com or{' '} openstreetmap.org, a corporate proxy, or no internet.
Check DevTools → Network for the actual error.
) : null} {tileStatus === 'loading' ? (
Loading tiles…
) : null}
) }