wayfarer-weather/components/radar/RadarMap.tsx
Cayden Schwechten 6250d23297 init
2026-10-31 21:20:16 -05:00

73 lines
2.7 KiB
TypeScript

"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 =
'&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>';
// 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 (
<MapContainer center={[lat, lon]} zoom={7} minZoom={3} maxZoom={12} className="h-full w-full" scrollWheelZoom={false}>
<FollowPlace lat={lat} lon={lon} />
<TileLayer url={BASEMAP} attribution={BASEMAP_ATTRIBUTION} />
{frames.map((frame, i) =>
i === frameIndex || i === nextIndex ? (
<TileLayer
key={frame.url}
url={frame.url}
opacity={i === frameIndex ? 0.75 : 0}
maxNativeZoom={layer.maxNativeZoom}
attribution={layer.attribution}
zIndex={10}
/>
) : null,
)}
<Pane name="labels" style={{ zIndex: 450, pointerEvents: "none" }}>
<TileLayer url={LABELS} />
</Pane>
<CircleMarker center={[lat, lon]} radius={6} pathOptions={MARKER_STYLE} />
</MapContainer>
);
}