added heliport layer
This commit is contained in:
@@ -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<React.ReactNode>(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: '<div style="width: 15px; height: 15px; border-radius: 50%; background-color: white; border: 2px solid #7f7f7f; display: flex; align-items: center; justify-content: center;"><span style="font-size: 12px; color: #7f7f7f;">H</span></div>',
|
||||
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: '<div style="width: 15px; height: 15px; background-color: white; border: 2px solid #7f7f7f; display: flex; align-items: center; justify-content: center;"><span style="font-size: 12px; color: #7f7f7f;">H</span></div>',
|
||||
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: '<div style="width: 15px; height: 15px; border-radius: 50%; background-color: white; border: 2px solid #7f7f7f; display: flex; align-items: center; justify-content: center;"><span style="font-size: 12px; color: #7f7f7f;">M</span></div>',
|
||||
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: '<div style="width: 15px; height: 15px; border-radius: 50%; background-color: white; border: 2px solid #7f7f7f; display: flex; align-items: center; justify-content: center;"><span style="font-size: 12px; color: #7f7f7f;">?</span></div>',
|
||||
iconSize: [15, 15],
|
||||
iconAnchor: [7.5, 15],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeatureGroup attribution="">
|
||||
{isVisible &&
|
||||
heliportsWithIcon.map((heliport) => {
|
||||
const coordinates: LatLngExpression = [heliport.lat, heliport.lng];
|
||||
const designatorLabel = heliport.designator.charAt(0).toUpperCase();
|
||||
const heliportType = heliport.type;
|
||||
return (
|
||||
<Marker
|
||||
key={heliport.id}
|
||||
position={coordinates}
|
||||
icon={createCustomIcon(heliportType)}
|
||||
eventHandlers={{
|
||||
mouseover: (e) => {
|
||||
const tooltipContent = `${heliport.siteNameSub26} (${heliport.designator})`;
|
||||
e.target
|
||||
.bindTooltip(tooltipContent, {
|
||||
direction: "top",
|
||||
offset: [4, -15],
|
||||
})
|
||||
.openTooltip();
|
||||
},
|
||||
mouseout: (e) => {
|
||||
e.target.closeTooltip();
|
||||
},
|
||||
click: () => {
|
||||
setBoxContent(
|
||||
<div>
|
||||
<h4>{heliport.siteNameSub26}</h4>
|
||||
<p>
|
||||
<strong>Designator:</strong> {heliport.designator}
|
||||
</p>
|
||||
{heliport.info?.startsWith("http") ? (
|
||||
<p>
|
||||
<a
|
||||
href={replaceWithYesterdayDate(heliport.info)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{heliport.info}
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<p>{heliport.info}</p>
|
||||
)}
|
||||
<p>
|
||||
{heliport.lat} °N, {heliport.lng} °E
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip direction="top" sticky>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<strong>{heliport.designator}</strong>
|
||||
<small style={{ fontWeight: "bold", fontSize: "0.7em" }}>
|
||||
{` (${designatorLabel})`}
|
||||
</small>
|
||||
<br />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</FeatureGroup>
|
||||
|
||||
{boxContent && <div className="modal-box">{boxContent}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => {
|
||||
const { data: stations } = useQuery({
|
||||
queryKey: ["stations"],
|
||||
@@ -338,6 +533,9 @@ export const BaseMaps = () => {
|
||||
<LayersControl.Overlay name={"LRZs"}>
|
||||
<StationsLayer attribution={map.attributionControl} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name={"Heliports"}>
|
||||
<HeliportsLayer />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name={"OpenAIP"}>
|
||||
<OpenAIP />
|
||||
</LayersControl.Overlay>
|
||||
|
||||
14
apps/dispatch/app/_querys/heliports.ts
Normal file
14
apps/dispatch/app/_querys/heliports.ts
Normal file
@@ -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<Heliport[]>("/api/heliports", {
|
||||
params: {
|
||||
filter: JSON.stringify(filter),
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch heliports");
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
22
apps/dispatch/app/api/heliports/route.ts
Normal file
22
apps/dispatch/app/api/heliports/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user