"use client"; import "leaflet/dist/leaflet.css"; import { useEffect } from "react"; import { CircleMarker, MapContainer, Pane, TileLayer, useMap } from "react-leaflet"; import type { RadarLayer } from "@/lib/radar/layers"; import type { RadarFrame } from "@/lib/weather/types"; /** * The Leaflet map. Browser-only (Leaflet touches `window`), so RadarPanel * loads it with next/dynamic and `ssr: false`. * * Layers, bottom to top: * 1. dark basemap without labels (CARTO) * 2. the radar/weather overlay * 3. place-name labels (CARTO), so towns stay readable over the radar * 4. a gold dot on the chosen place */ const BASEMAP = "/api/tiles/carto-dark-nolabels/{z}/{x}/{y}?r={r}"; const LABELS = "/api/tiles/carto-dark-labels/{z}/{x}/{y}?r={r}"; const BASEMAP_ATTRIBUTION = '© OpenStreetMap contributors, © CARTO'; // Leaflet writes these straight onto SVG attributes, which accept theme variables. const MARKER_STYLE = { color: "var(--color-gold-bright)", weight: 2, fillColor: "var(--color-black)", fillOpacity: 1 }; /** Moves the map when the chosen place changes, keeping the current zoom. */ function FollowPlace({ lat, lon }: { lat: number; lon: number }) { const map = useMap(); useEffect(() => { map.setView([lat, lon], map.getZoom()); }, [lat, lon, map]); return null; } export default function RadarMap({ lat, lon, layer, frames, frameIndex }: { lat: number; lon: number; layer: RadarLayer; frames: RadarFrame[]; frameIndex: number; }) { // Only the visible frame and the next one are mounted. The next one loads // invisibly, so stepping forward never flashes an empty map. const nextIndex = (frameIndex + 1) % Math.max(frames.length, 1); return ( {frames.map((frame, i) => i === frameIndex || i === nextIndex ? ( ) : null, )} ); }