diff --git a/apps/dispatch/app/_components/map/BaseMaps.tsx b/apps/dispatch/app/_components/map/BaseMaps.tsx index e33d0d91..ba27c113 100644 --- a/apps/dispatch/app/_components/map/BaseMaps.tsx +++ b/apps/dispatch/app/_components/map/BaseMaps.tsx @@ -1,5 +1,5 @@ "use client"; -import { Control, Icon, LatLngExpression } from "leaflet"; +import { Control, divIcon, Icon, LatLngExpression } from "leaflet"; import { useEffect, useRef, useState } from "react"; import { LayerGroup, @@ -20,10 +20,11 @@ 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 { Heliport, Station } from "@repo/db"; import { useQuery } from "@tanstack/react-query"; import { getStationsAPI } from "_querys/stations"; import "./darkMapStyles.css"; +import { getHeliportsAPI } from "_querys/heliports"; const RadioAreaLayer = () => { const getColor = (randint: number) => { @@ -67,6 +68,200 @@ const RadioAreaLayer = () => { ); }; +const HeliportsLayer = () => { + const { data: heliports } = useQuery({ + queryKey: ["heliports"], + queryFn: () => getHeliportsAPI(), + }); + console.log("Heliports Layer", heliports); + const [heliportsWithIcon, setHeliportsWithIcon] = useState<(Heliport & { icon?: string })[]>([]); + const map = useMap(); + const [isVisible, setIsVisible] = useState(true); + const [boxContent, setBoxContent] = useState(null); + // Übergangslösung + const formatDate = (date: Date): string => { + const year = date.getFullYear().toString().slice(-2); // Letzte 2 Stellen des Jahres + const month = (date.getMonth() + 1).toString().padStart(2, "0"); // Monat (mit führender Null, falls notwendig) + const day = date.getDate().toString().padStart(2, "0"); // Tag (mit führender Null, falls notwendig) + return `${year}${month}${day}`; + }; + const replaceWithYesterdayDate = (url: string): string => { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 2); // Einen Tag zurücksetzen + const formattedDate = formatDate(yesterday); + + return url.replace(/\.at\/lo\/\d{6}/, `.at/lo/${formattedDate}`); + }; + + const resetSelection = () => { + setBoxContent(null); + }; + + useMapEvent("click", () => { + resetSelection(); + }); + + useEffect(() => { + const handleZoom = () => { + setIsVisible(map.getZoom() > 9); + }; + + handleZoom(); + + map.on("zoomend", handleZoom); + + const fetchIcons = async () => { + if (!heliports) return; + const urls = await Promise.all( + heliports.map(async (heliport) => { + return createCustomMarker(heliport.type); + }), + ); + setHeliportsWithIcon( + heliports.map((heliport, index) => ({ ...heliport, icon: urls[index] })), + ); + }; + + const filterVisibleHeliports = () => { + const bounds = map.getBounds(); + if (!heliports?.length) return; + // Filtere die Heliports, die innerhalb der Kartenansicht liegen + const visibleHeliports = heliports.filter((heliport) => { + const coordinates: LatLngExpression = [heliport.lat, heliport.lng]; + return bounds.contains(coordinates); // Überprüft, ob der Heliport innerhalb der aktuellen Bounds liegt + }); + + setHeliportsWithIcon(visibleHeliports); + }; + + if (heliports?.length) { + fetchIcons(); + filterVisibleHeliports(); + } + + handleZoom(); + + map.on("zoomend", handleZoom); + map.on("moveend", filterVisibleHeliports); + + return () => { + map.off("zoomend", handleZoom); + map.off("moveend", filterVisibleHeliports); + }; + }, [map, heliports]); + + const createCustomIcon = (heliportType: string) => { + if (heliportType === "POI") { + return divIcon({ + className: "custom-marker no-pointer", // CSS-Klasse für Styling + html: '
H
', + iconSize: [15, 15], // Größe des Icons + iconAnchor: [7.5, 15], // Ankerpunkt des Icons + }); + } + + // Heliport Typ: H-Icon + if (heliportType === "HELIPAD") { + return divIcon({ + className: "custom-marker no-pointer", // CSS-Klasse für Styling + html: '
H
', + iconSize: [15, 15], // Größe des Icons (15x15 px Viereck) + iconAnchor: [7.5, 15], // Ankerpunkt des Icons + }); + } + + // Mountain Typ: Kreis mit "M" + if (heliportType === "MOUNTAIN") { + return divIcon({ + className: "custom-marker no-pointer", + html: '
M
', + iconSize: [15, 15], // Größe des Icons + iconAnchor: [7.5, 15], // Ankerpunkt des Icons + }); + } + + // Falls kein Typ übereinstimmt, standardmäßig das POI-Icon mit Fragezeichen verwenden + return divIcon({ + className: "custom-marker no-pointer", + html: '
?
', + iconSize: [15, 15], + iconAnchor: [7.5, 15], + }); + }; + + return ( + <> + + {isVisible && + heliportsWithIcon.map((heliport) => { + const coordinates: LatLngExpression = [heliport.lat, heliport.lng]; + const designatorLabel = heliport.designator.charAt(0).toUpperCase(); + const heliportType = heliport.type; + return ( + { + const tooltipContent = `${heliport.siteNameSub26} (${heliport.designator})`; + e.target + .bindTooltip(tooltipContent, { + direction: "top", + offset: [4, -15], + }) + .openTooltip(); + }, + mouseout: (e) => { + e.target.closeTooltip(); + }, + click: () => { + setBoxContent( +
+

{heliport.siteNameSub26}

+

+ Designator: {heliport.designator} +

+ {heliport.info?.startsWith("http") ? ( +

+ + {heliport.info} + +

+ ) : ( +

{heliport.info}

+ )} +

+ {heliport.lat} °N, {heliport.lng} °E +

+
, + ); + }, + }} + > + +
+ {heliport.designator} + + {` (${designatorLabel})`} + +
+
+
+
+ ); + })} +
+ + {boxContent &&
{boxContent}
} + + ); +}; + const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => { const { data: stations } = useQuery({ queryKey: ["stations"], @@ -338,6 +533,9 @@ export const BaseMaps = () => { + + + diff --git a/apps/dispatch/app/_querys/heliports.ts b/apps/dispatch/app/_querys/heliports.ts new file mode 100644 index 00000000..6e1def86 --- /dev/null +++ b/apps/dispatch/app/_querys/heliports.ts @@ -0,0 +1,14 @@ +import { Heliport, Prisma } from "@repo/db"; +import axios from "axios"; + +export const getHeliportsAPI = async (filter?: Prisma.HeliportWhereInput) => { + const res = await axios.get("/api/heliports", { + params: { + filter: JSON.stringify(filter), + }, + }); + if (res.status !== 200) { + throw new Error("Failed to fetch heliports"); + } + return res.data; +}; diff --git a/apps/dispatch/app/api/heliports/route.ts b/apps/dispatch/app/api/heliports/route.ts new file mode 100644 index 00000000..509bcb93 --- /dev/null +++ b/apps/dispatch/app/api/heliports/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@repo/db"; + +export async function GET(req: NextRequest): Promise { + const { searchParams } = new URL(req.url); + const id = searchParams.get("id"); + const filter = searchParams.get("filter"); + + try { + const data = await prisma.heliport.findMany({ + where: { + id: id ? Number(id) : undefined, + ...(filter ? JSON.parse(filter) : {}), + }, + }); + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: "Failed to fetch heliport" }, { status: 500 }); + } +}