added heliport layer
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Control, Icon, LatLngExpression } from "leaflet";
|
import { Control, divIcon, Icon, LatLngExpression } from "leaflet";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LayerGroup,
|
LayerGroup,
|
||||||
@@ -20,10 +20,11 @@ import L from "leaflet";
|
|||||||
import LEITSTELLENBERECHE from "./_geojson/Leitstellen.json";
|
import LEITSTELLENBERECHE from "./_geojson/Leitstellen.json";
|
||||||
import WINDFARMS from "./_geojson/Windfarms.json";
|
import WINDFARMS from "./_geojson/Windfarms.json";
|
||||||
import { createCustomMarker } from "_components/map/_components/createCustomMarker";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { getStationsAPI } from "_querys/stations";
|
import { getStationsAPI } from "_querys/stations";
|
||||||
import "./darkMapStyles.css";
|
import "./darkMapStyles.css";
|
||||||
|
import { getHeliportsAPI } from "_querys/heliports";
|
||||||
|
|
||||||
const RadioAreaLayer = () => {
|
const RadioAreaLayer = () => {
|
||||||
const getColor = (randint: number) => {
|
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 StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => {
|
||||||
const { data: stations } = useQuery({
|
const { data: stations } = useQuery({
|
||||||
queryKey: ["stations"],
|
queryKey: ["stations"],
|
||||||
@@ -338,6 +533,9 @@ export const BaseMaps = () => {
|
|||||||
<LayersControl.Overlay name={"LRZs"}>
|
<LayersControl.Overlay name={"LRZs"}>
|
||||||
<StationsLayer attribution={map.attributionControl} />
|
<StationsLayer attribution={map.attributionControl} />
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
|
<LayersControl.Overlay name={"Heliports"}>
|
||||||
|
<HeliportsLayer />
|
||||||
|
</LayersControl.Overlay>
|
||||||
<LayersControl.Overlay name={"OpenAIP"}>
|
<LayersControl.Overlay name={"OpenAIP"}>
|
||||||
<OpenAIP />
|
<OpenAIP />
|
||||||
</LayersControl.Overlay>
|
</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