fix: rewrite Google Maps traffic layer to avoid L-instance constructor error
Replace static import + createLayerComponent approach with dynamic import() inside a useEffect, which ensures leaflet.gridlayer.googlemutant augments the same L instance that's active at runtime. Add loading=async to Maps JS script. Traffic is now toggled via a dedicated button (green when active) rather than LayersControl, bypassing the react-leaflet layer lifecycle that caused the constructor conflict.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
FeatureGroup,
|
FeatureGroup,
|
||||||
LayersControl,
|
LayersControl,
|
||||||
@@ -10,9 +10,7 @@ import {
|
|||||||
TileLayer,
|
TileLayer,
|
||||||
useMap,
|
useMap,
|
||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
import { createLayerComponent } from "@react-leaflet/core";
|
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import "leaflet.gridlayer.googlemutant";
|
|
||||||
import type { CallRecord, IncidentRecord, NodeRecord } from "@/lib/types";
|
import type { CallRecord, IncidentRecord, NodeRecord } from "@/lib/types";
|
||||||
|
|
||||||
// ── Leaflet icon fix ──────────────────────────────────────────────────────────
|
// ── Leaflet icon fix ──────────────────────────────────────────────────────────
|
||||||
@@ -148,17 +146,53 @@ function computeGroups<T extends { id: string; lat: number; lng: number }>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Google Maps traffic layer via leaflet-google-mutant ───────────────────────
|
// ── Google Maps traffic layer via leaflet-google-mutant ───────────────────────
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// Uses dynamic import so the side-effect augments the same L instance at runtime,
|
||||||
const GoogleTrafficOverlay = createLayerComponent<any, object>(
|
// avoiding the "GoogleMutant is not a constructor" error from static top-level imports.
|
||||||
(_props, ctx) => {
|
function GoogleTrafficManager({ enabled }: { enabled: boolean }) {
|
||||||
// leaflet-google-mutant augments L.gridLayer after the side-effect import
|
const map = useMap();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const instance = (L as any).gridLayer.googleMutant({ type: "roadmap" });
|
const layerRef = useRef<any>(null);
|
||||||
instance.addGoogleLayer("TrafficLayer");
|
const scriptLoadedRef = useRef(false);
|
||||||
return { instance, context: ctx };
|
|
||||||
},
|
useEffect(() => {
|
||||||
() => {}
|
if (!enabled) {
|
||||||
);
|
if (layerRef.current) { map.removeLayer(layerRef.current); layerRef.current = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
function mountLayer() {
|
||||||
|
import("leaflet.gridlayer.googlemutant").then(() => {
|
||||||
|
if (layerRef.current) return; // already mounted
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const layer = (L as any).gridLayer.googleMutant({ type: "roadmap" });
|
||||||
|
layer.addGoogleLayer("TrafficLayer");
|
||||||
|
map.addLayer(layer);
|
||||||
|
layerRef.current = layer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
if ((window as any).google?.maps) {
|
||||||
|
mountLayer();
|
||||||
|
} else if (!scriptLoadedRef.current) {
|
||||||
|
scriptLoadedRef.current = true;
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = `https://maps.googleapis.com/maps/api/js?key=${key}&loading=async`;
|
||||||
|
script.async = true;
|
||||||
|
script.onload = mountLayer;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (layerRef.current) { map.removeLayer(layerRef.current); layerRef.current = null; }
|
||||||
|
};
|
||||||
|
}, [enabled, map]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── MapRefCapture — exposes L.Map instance to parent ─────────────────────────
|
// ── MapRefCapture — exposes L.Map instance to parent ─────────────────────────
|
||||||
function MapRefCapture({ onReady }: { onReady: (m: L.Map) => void }) {
|
function MapRefCapture({ onReady }: { onReady: (m: L.Map) => void }) {
|
||||||
@@ -357,20 +391,7 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load Google Maps JS API for leaflet-google-mutant traffic layer
|
const [trafficEnabled, setTrafficEnabled] = useState(false);
|
||||||
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
|
// Live clock for TOC situational awareness
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -468,10 +489,8 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
|
|||||||
</FeatureGroup>
|
</FeatureGroup>
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
|
|
||||||
{/* Overlay: Traffic — Google Maps via leaflet-google-mutant */}
|
{/* Traffic managed outside LayersControl to avoid L-instance conflicts */}
|
||||||
<LayersControl.Overlay name="Traffic">
|
<GoogleTrafficManager enabled={trafficEnabled} />
|
||||||
{googleReady ? <GoogleTrafficOverlay /> : <FeatureGroup />}
|
|
||||||
</LayersControl.Overlay>
|
|
||||||
|
|
||||||
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet; key forces remount on refresh */}
|
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet; key forces remount on refresh */}
|
||||||
<LayersControl.Overlay name="Weather Radar">
|
<LayersControl.Overlay name="Weather Radar">
|
||||||
@@ -504,6 +523,21 @@ export default function MapView({ nodes, activeCalls, incidents = [], lastUpdate
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Traffic toggle ───────────────────────────────────────────────────── */}
|
||||||
|
{process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY && (
|
||||||
|
<button
|
||||||
|
onClick={() => setTrafficEnabled((v) => !v)}
|
||||||
|
title="Toggle traffic layer"
|
||||||
|
className={`absolute bottom-[6.5rem] right-3 z-[1001] w-8 h-8 border rounded text-xs font-bold leading-none transition-colors flex items-center justify-center select-none ${
|
||||||
|
trafficEnabled
|
||||||
|
? "bg-green-700 border-green-500 text-white"
|
||||||
|
: "bg-gray-950/90 border-gray-700 text-gray-400 hover:bg-gray-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🚦
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Auto-fit button ──────────────────────────────────────────────────── */}
|
{/* ── Auto-fit button ──────────────────────────────────────────────────── */}
|
||||||
{mapInstance && allPositions.length > 0 && (
|
{mapInstance && allPositions.length > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user