feat: replace Nominatim geocoding with Google Maps API; add TOC map improvements

Switch geocoding from Nominatim to Google Maps Geocoding API for accurate
local place name resolution (bounds-biased, with 25km distance rejection guard).
Remove the now-unused _get_node_place reverse-geocoder and _node_place_cache.

Map page (TOC improvements):
- Weather radar tiles auto-refresh every 5 minutes via radarEpoch key cycling
- Google Maps traffic overlay added to LayersControl
- Live 24h clock overlay at bottom-left for situational awareness
- Incident sidebar cards now show age (time since dispatch) and unit count
This commit is contained in:
Logan
2026-05-25 13:27:19 -04:00
parent 0db09d6bf7
commit 0279a82b10
3 changed files with 83 additions and 82 deletions
+44 -1
View File
@@ -326,12 +326,31 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
const [mapInstance, setMapInstance] = useState<L.Map | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [agoClock, setAgoClock] = useState(0);
const [radarEpoch, setRadarEpoch] = useState(() => Date.now());
const [clockStr, setClockStr] = useState(() =>
new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })
);
useEffect(() => {
const id = setInterval(() => setAgoClock((t: number) => t + 1), 10_000);
return () => clearInterval(id);
}, []);
// Radar tiles are static once loaded — force remount every 5 min to refresh
useEffect(() => {
const id = setInterval(() => setRadarEpoch(Date.now()), 5 * 60 * 1000);
return () => clearInterval(id);
}, []);
// Live clock for TOC situational awareness
useEffect(() => {
const id = setInterval(() =>
setClockStr(new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })),
1000
);
return () => clearInterval(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const ago = useMemo(() => (lastUpdated ? timeAgo(lastUpdated) : null), [lastUpdated, agoClock]);
@@ -419,9 +438,20 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
</FeatureGroup>
</LayersControl.Overlay>
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet (no API key) */}
{/* Overlay: Traffic — Google Maps traffic layer */}
<LayersControl.Overlay name="Traffic">
<TileLayer
url="https://mt{s}.google.com/vt?lyrs=traffic&x={x}&y={y}&z={z}"
subdomains={["0", "1", "2", "3"]}
attribution='Traffic &copy; <a href="https://maps.google.com/">Google</a>'
opacity={0.8}
/>
</LayersControl.Overlay>
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet; key forces remount on refresh */}
<LayersControl.Overlay name="Weather Radar">
<TileLayer
key={radarEpoch}
url="https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0r-900913/{z}/{x}/{y}.png"
attribution='Radar &copy; <a href="https://mesonet.agron.iastate.edu/">IEM/NWS</a>'
opacity={0.65}
@@ -460,6 +490,11 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
</button>
)}
{/* ── Clock — bottom-left for TOC situational awareness ───────────────── */}
<div className="absolute bottom-8 left-3 z-[1001] bg-gray-950/90 border border-gray-800 rounded-lg px-3 py-2 pointer-events-none">
<span className="text-white text-sm font-mono tabular-nums">{clockStr}</span>
</div>
{/* ── Legend — bottom-right to avoid incident panel on left ────────────── */}
<div className="absolute bottom-8 right-3 z-[1001] bg-gray-950/90 border border-gray-800 rounded-lg px-3 py-2 text-xs font-mono pointer-events-none space-y-1">
<div className="flex items-center gap-2"><span className="text-green-400"></span> Online</div>
@@ -480,6 +515,8 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
<div className="absolute top-3 left-3 bottom-[4.5rem] z-[1001] hidden md:flex flex-col w-56 gap-1.5 overflow-y-auto">
{incidents.map((inc) => {
const color = INCIDENT_COLORS[inc.type ?? "other"] ?? INCIDENT_COLORS.other;
const age = inc.started_at ? timeAgo(new Date(inc.started_at)) : null;
const unitCount = inc.units?.length ?? 0;
return (
<button
key={inc.incident_id}
@@ -505,6 +542,12 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
{inc.location && (
<p className="text-gray-500 truncate mt-0.5">{inc.location}</p>
)}
<div className="flex items-center justify-between mt-0.5">
{age && <span className="text-gray-600">{age}</span>}
{unitCount > 0 && (
<span className="text-gray-600">{unitCount} unit{unitCount !== 1 ? "s" : ""}</span>
)}
</div>
{!inc.location_coords && (
<p className="text-gray-700 italic mt-0.5">no coords</p>
)}