Files
var-monorepo/apps/dispatch/app/_components/map/BaseMaps.tsx
2025-07-11 23:56:24 -07:00

378 lines
9.6 KiB
TypeScript

"use client";
import { Control, Icon, LatLngExpression } from "leaflet";
import { useEffect, useRef, useState } from "react";
import {
LayerGroup,
LayersControl,
TileLayer,
useMap,
WMSTileLayer,
GeoJSON,
Circle,
useMapEvent,
FeatureGroup,
Marker,
Tooltip,
} from "react-leaflet";
// @ts-expect-error geojson hat keine Typen
import type { FeatureCollection, Geometry } from "geojson";
import L from "leaflet";
import LEITSTELLENBERECHE from "./_geojson/Leitstellen.json";
import WINDFARMS from "./_geojson/Windfarms.json";
import { createCustomMarker } from "_components/map/_components/createCustomMarker";
import { Station } from "@repo/db";
import { useQuery } from "@tanstack/react-query";
import { getStationsAPI } from "_querys/stations";
import "./darkMapStyles.css";
const RadioAreaLayer = () => {
const getColor = (randint: number) => {
switch (randint) {
case 1:
return "#2b6eff";
case 2:
return "#233ee5";
case 3:
return "#7BA5FF";
case 4:
return "#5087FF";
default:
return "#7f7f7f";
}
};
return (
<GeoJSON
data={LEITSTELLENBERECHE as FeatureCollection<Geometry>}
style={(feature) => {
if (!feature || !feature.properties) return {}; // Early return if feature or its properties are undefined
return {
color: getColor(feature.properties.randint),
weight: 1.5,
className: "no-pointer",
};
}}
onEachFeature={(feature, layer) => {
if (feature && feature.properties && feature.properties.name) {
layer.bindTooltip(
new L.Tooltip({
content: feature.properties.name,
direction: "top",
sticky: true,
}),
);
}
}}
/>
);
};
const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => {
const { data: stations } = useQuery({
queryKey: ["stations"],
queryFn: () => getStationsAPI(),
});
const [selectedStations, setSelectedStations] = useState<Station["id"][]>([]);
const attributionText = "";
const resetSelection = () => {
setSelectedStations([]);
};
useMapEvent("click", () => {
resetSelection();
});
const handleMarkerClick = (stationId: number) => {
if (selectedStations.includes(stationId)) {
setSelectedStations((prevStations) => prevStations.filter((s) => s !== stationId));
} else {
setSelectedStations((prevStations) => [...prevStations, stationId]);
}
};
const [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon: string })[]>([]);
useEffect(() => {
if (!stations) {
setStationsWithIcon([]);
return;
}
const fetchIcons = async () => {
const urls = await Promise.all(
stations.map(async (station) => {
return await createCustomMarker(station.operator);
}),
);
setStationsWithIcon(stations.map((station, index) => ({ ...station, icon: urls[index]! })));
};
fetchIcons();
}, [stations]);
return (
<FeatureGroup>
{stationsWithIcon?.map((station) => {
const coordinates: LatLngExpression = [station.latitude, station.longitude];
const typeLabel = station.bosUse?.charAt(0).toUpperCase();
return (
<Marker
key={`marker-${station.id}`}
position={coordinates}
icon={
new Icon({
iconUrl: station.icon,
iconSize: [30, 30],
iconAnchor: [15, 15],
tooltipAnchor: [0, 20],
className: station.hideRangeRings ? "no-pointer" : "pointer",
})
}
eventHandlers={{
click: () => {
if (!station.hideRangeRings) handleMarkerClick(station.id);
},
add: () => attribution.addAttribution(attributionText),
remove: () => attribution.removeAttribution(attributionText),
}}
>
<Tooltip direction="top" sticky>
<div style={{ textAlign: "center" }}>
<strong>{station.bosCallsign}</strong>
<small style={{ fontWeight: "bold", fontSize: "0.7em" }}>{` (${typeLabel})`}</small>
<br />
<small>
{[
station.hasWinch ? "W" : null,
station.is24h ? "24h" : null,
station.hasNvg ? "N" : null,
]
.filter(Boolean)
.join(", ")}
</small>
</div>
</Tooltip>
</Marker>
);
})}
{selectedStations.map((stationId) => {
const station = stations?.find((s) => s.id === stationId);
if (!station) return null;
const center: LatLngExpression = [station.latitude, station.longitude];
return (
<div key={`marker-${stationId}`}>
<Circle
center={center}
radius={(station.aircraftSpeed * 1000) / 6}
color="#0e0ecf"
fillOpacity={0}
weight={2}
/>
<Circle
center={center}
radius={(station.aircraftSpeed * 1000) / 3}
color="navy"
fillOpacity={0}
weight={2}
/>
{(station.bosUse === "SECONDARY" || station.bosUse === "DUAL_USE") && (
<Circle
center={center}
radius={station.aircraftSpeed * 1000}
color="maroon"
fillOpacity={0}
weight={1}
dashArray="40,30"
/>
)}
</div>
);
})}
</FeatureGroup>
);
};
const EsriSatellite = () => {
const accessToken = process.env.NEXT_PUBLIC_ESRI_ACCESS;
return (
<>
{/* Satellite Imagery Layer; API KEY PROVIDED BY VAR0002 */}
<TileLayer
attribution="Sources: Esri, TomTom, Garmin, FAO, NOAA, USGS"
url={`https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}?token=${accessToken}`}
tileSize={256}
/>
</>
);
};
const StrassentexteEsri = () => {
return (
<WMSTileLayer
url="https://tiledbasemaps.arcgis.com/arcgis/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}"
format="image/png"
transparent
/>
);
};
const OpenAIP = () => {
const ref = useRef<L.TileLayer | null>(null);
return (
<TileLayer
eventHandlers={{
add: () => {
if (ref.current) {
ref.current.bringToFront();
}
},
}}
ref={ref}
attribution='© <a href="https://www.openaip.net" target="_blank">OpenAIP</a>'
url={`https://api.tiles.openaip.net/api/data/openaip/{z}/{x}/{y}.png?apiKey=${process.env.NEXT_PUBLIC_OPENAIP_ACCESS}`}
/>
);
};
const NiederschlagOverlay = () => {
const tileLayerRef = useRef<L.TileLayer.WMS | null>(null);
return (
<WMSTileLayer
ref={tileLayerRef}
eventHandlers={{
add: () => {
tileLayerRef.current?.bringToFront();
},
}}
attribution="Quelle: Deutscher Wetterdienst"
url="https://maps.dwd.de/geoserver/wms?"
format="image/png"
layers="dwd:Niederschlagsradar"
transparent
opacity={0.7}
/>
);
};
const SlopesOverlay = () => {
const tileLayerRef = useRef<L.TileLayer.WMS | null>(null);
return (
<WMSTileLayer
ref={tileLayerRef}
eventHandlers={{
add: () => {
tileLayerRef.current?.bringToFront();
},
}}
attribution="Opensnowmap.org (CC-BY-SA)"
url="http://tiles.opensnowmap.org/pistes/{z}/{x}/{y}.png?"
transparent
zIndex={1000}
/>
);
};
const WindfarmOutlineLayer = () => {
const map = useMap();
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleZoom = () => {
setIsVisible(map.getZoom() < 13);
};
// Initial check and event listener
handleZoom();
map.on("zoomend", handleZoom);
// Cleanup on unmount
return () => {
map.off("zoomend", handleZoom);
};
}, [map]);
return isVisible ? (
<GeoJSON
data={WINDFARMS as FeatureCollection<Geometry>}
style={() => ({
color: "#233EE5",
weight: 1.5,
dashArray: "7, 10",
className: "no-pointer",
})}
onEachFeature={(feature, layer) => {
layer.bindTooltip(
new L.Tooltip({
content: feature.properties.Cluster_NR,
direction: "top",
sticky: true,
}),
);
}}
/>
) : null;
};
export const BaseMaps = () => {
const map = useMap();
return (
<LayersControl position="bottomright">
<LayersControl.Overlay name={"Leitstellenbereiche"}>
<RadioAreaLayer />
</LayersControl.Overlay>
<LayersControl.Overlay name={"Niederschlag"}>
<NiederschlagOverlay />
</LayersControl.Overlay>
<LayersControl.Overlay name={"Windkraftanlagen offshore"}>
<WindfarmOutlineLayer />
</LayersControl.Overlay>
<LayersControl.Overlay name={"LRZs"}>
<StationsLayer attribution={map.attributionControl} />
</LayersControl.Overlay>
<LayersControl.Overlay name={"OpenAIP"}>
<OpenAIP />
</LayersControl.Overlay>
<LayersControl.Overlay name={"Skigebiete"}>
<SlopesOverlay />
</LayersControl.Overlay>
<LayersControl.BaseLayer name="OpenStreetMap Dark" checked>
<TileLayer
zIndex={-1}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
className="invert-100 grayscale"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="OpenStreetMap">
<TileLayer
zIndex={-1}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="OpenTopoMap">
<TileLayer
attribution='map data: © <a href="https://opentopomap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="ESRI Satellite">
<LayerGroup>
<EsriSatellite />
<StrassentexteEsri />
</LayerGroup>
</LayersControl.BaseLayer>
</LayersControl>
);
};