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:
@@ -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 © <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 © <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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user