feat: replace Google tile URL hack with leaflet-google-mutant for traffic layer

Add leaflet-google-mutant@0.16.0 (exact/locked version) as a proper bridge
between the Google Maps JavaScript API and Leaflet. The old mt{s}.google.com
tile URL approach was unofficial and produced empty tiles.

Traffic layer now renders via createLayerComponent + googleMutant, loaded only
after the Maps JS API script is injected and ready (keyed off NEXT_PUBLIC_GOOGLE_MAPS_API_KEY).
Added leaflet-google-mutant to transpilePackages in next.config.mjs.
This commit is contained in:
Logan
2026-05-25 13:41:10 -04:00
parent 0279a82b10
commit 6a9fe5d26f
3 changed files with 34 additions and 8 deletions
+32 -7
View File
@@ -10,7 +10,9 @@ import {
TileLayer,
useMap,
} from "react-leaflet";
import { createLayerComponent } from "@react-leaflet/core";
import L from "leaflet";
import "leaflet-google-mutant";
import type { CallRecord, IncidentRecord, NodeRecord } from "@/lib/types";
// ── Leaflet icon fix ──────────────────────────────────────────────────────────
@@ -145,6 +147,19 @@ function computeGroups<T extends { id: string; lat: number; lng: number }>(
return result;
}
// ── Google Maps traffic layer via leaflet-google-mutant ───────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const GoogleTrafficOverlay = createLayerComponent<any, object>(
(_props, ctx) => {
// leaflet-google-mutant augments L.gridLayer after the side-effect import
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (L as any).gridLayer.googleMutant({ type: "roadmap" });
instance.addGoogleLayer("TrafficLayer");
return { instance, context: ctx };
},
() => {}
);
// ── MapRefCapture — exposes L.Map instance to parent ─────────────────────────
function MapRefCapture({ onReady }: { onReady: (m: L.Map) => void }) {
const map = useMap();
@@ -342,6 +357,21 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
return () => clearInterval(id);
}, []);
// Load Google Maps JS API for leaflet-google-mutant traffic layer
const [googleReady, setGoogleReady] = useState(false);
useEffect(() => {
const key = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!key) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((window as any).google?.maps) { setGoogleReady(true); return; }
if (document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]')) return;
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${key}`;
script.async = true;
script.onload = () => setGoogleReady(true);
document.head.appendChild(script);
}, []);
// Live clock for TOC situational awareness
useEffect(() => {
const id = setInterval(() =>
@@ -438,14 +468,9 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
</FeatureGroup>
</LayersControl.Overlay>
{/* Overlay: Traffic — Google Maps traffic layer */}
{/* Overlay: Traffic — Google Maps via leaflet-google-mutant */}
<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}
/>
{googleReady ? <GoogleTrafficOverlay /> : <FeatureGroup />}
</LayersControl.Overlay>
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet; key forces remount on refresh */}