fixed wrong env var, added base maps

This commit is contained in:
PxlLoewe
2025-06-01 09:00:59 -07:00
parent cded757b9f
commit e73f19d9e8
11 changed files with 113651 additions and 24 deletions

View File

@@ -20,6 +20,10 @@ NEXT_PUBLIC_HUB_SERVER_URL=https://api.hub.premiumag.de
NEXT_PUBLIC_DISPATCH_URL=https://dispatch.premiumag.de NEXT_PUBLIC_DISPATCH_URL=https://dispatch.premiumag.de
NEXT_PUBLIC_DISPATCH_SERVER_URL=https://api.dispatch.premiumag.de NEXT_PUBLIC_DISPATCH_SERVER_URL=https://api.dispatch.premiumag.de
NEXT_PUBLIC_ESRI_ACCESS_TOKEN=
NEXT_PUBLIC_OPENAIP_ACCESS=6e85069940543ef02f8615b737059d98
# ─────────────────────────────────────────────── # ───────────────────────────────────────────────
# 🗄️ Datenbank # 🗄️ Datenbank
# ─────────────────────────────────────────────── # ───────────────────────────────────────────────

View File

@@ -9,3 +9,5 @@ DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880 NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=APIAnsGdtdYp2Ho LIVEKIT_API_KEY=APIAnsGdtdYp2Ho
LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC
NEXT_PUBLIC_ESRI_ACCESS_TOKEN=
NEXT_PUBLIC_OPENAIP_ACCESS=

View File

@@ -1,7 +1,272 @@
"use client"; "use client";
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { useEffect } from "react"; import { Control, Icon, LatLngExpression } from "leaflet";
import { TileLayer, useMap } from "react-leaflet"; import { useEffect, useState } from "react";
import {
LayerGroup,
LayersControl,
TileLayer,
useMap,
WMSTileLayer,
GeoJSON,
Circle,
useMapEvent,
FeatureGroup,
Marker,
Tooltip,
} from "react-leaflet";
// @ts-ignore
import type { FeatureCollection, Geometry } from "geojson";
import L from "leaflet";
import LEITSTELLENBERECHE from "./_geojson/Leitstellen_VAR.json";
import { createCustomMarker } from "_components/map/_components/createCustomMarker";
import { Station } from "@repo/db";
import { useQuery } from "@tanstack/react-query";
import { getStationsAPI } from "_querys/stations";
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 [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon?: string })[]>([]); // Zustand für die Stationen mit Icon
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]);
}
};
useEffect(() => {
// Erstelle die Icons für alle Stationen
const fetchIcons = async () => {
if (!stations) return;
const urls = await Promise.all(
stations.map(async (station) => {
return 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 = ({ attribution }: { attribution: Control.Attribution }) => {
const accessToken = process.env.NEXT_PUBLIC_ESRI_ACCESS_TOKEN || "";
const attributionText = "Sources: Esri, TomTom, Garmin, FAO, NOAA, USGS";
return (
<>
{/* Satellite Imagery Layer; API KEY PROVIDED BY VAR0002 */}
<TileLayer
eventHandlers={{
add: () => attribution.addAttribution(attributionText),
remove: () => attribution.removeAttribution(attributionText),
}}
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 = ({ attribution }: { attribution: Control.Attribution }) => {
const accessToken = process.env.NEXT_PUBLIC_OPENAIP_ACCESS;
const attributionText = '© <a href="https://www.openaip.net" target="_blank">OpenAIP</a>';
return (
<TileLayer
eventHandlers={{
add: () => attribution.addAttribution(attributionText),
remove: () => attribution.removeAttribution(attributionText),
}}
url={`https://api.tiles.openaip.net/api/data/openaip/{z}/{x}/{y}.png?apiKey=${accessToken}`}
/>
);
};
const NiederschlagOverlay = ({ attribution }: { attribution: Control.Attribution }) => {
let tileLayer: L.TileLayer | null = null;
useEffect(() => {
if (tileLayer) {
tileLayer.bringToFront();
}
}, [tileLayer]);
return (
<WMSTileLayer
ref={(layer) => {
if (layer) {
tileLayer = layer;
}
}}
eventHandlers={{
add: () => attribution.addAttribution("Quelle: Deutscher Wetterdienst"),
remove: () => attribution.removeAttribution("Quelle: Deutscher Wetterdienst"),
}}
url="https://maps.dwd.de/geoserver/wms?"
format="image/png"
layers="dwd:Niederschlagsradar"
transparent
opacity={0.7}
/>
);
};
export const BaseMaps = () => { export const BaseMaps = () => {
const map = useMap(); const map = useMap();
@@ -14,16 +279,39 @@ export const BaseMaps = () => {
}, [isPannelOpen]); }, [isPannelOpen]);
return ( return (
<> <LayersControl position="topleft">
<TileLayer <LayersControl.Overlay name={"Funknetzbereiche"}>
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' <RadioAreaLayer />
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" </LayersControl.Overlay>
/> <LayersControl.Overlay name={"Niederschlag"}>
<TileLayer <NiederschlagOverlay attribution={map.attributionControl} />
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' </LayersControl.Overlay>
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
className="invert-100 grayscale" <LayersControl.Overlay name={"LRZs"}>
/> <StationsLayer attribution={map.attributionControl} />
</> </LayersControl.Overlay>
<LayersControl.Overlay name={"OpenAIP"}>
<OpenAIP attribution={map.attributionControl} />
</LayersControl.Overlay>
<LayersControl.BaseLayer name="OpenStreetMap" checked>
<TileLayer
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="OpenStreetMap Dark">
<TileLayer
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="ESRI Satellite">
<LayerGroup>
<EsriSatellite attribution={map.attributionControl} />
<StrassentexteEsri />
</LayerGroup>
</LayersControl.BaseLayer>
</LayersControl>
); );
}; };

View File

@@ -0,0 +1,83 @@
const getOperatorColor = (operator: string) => {
switch (operator) {
case "ADAC":
case "ÖAMTC":
case "ANWB":
return "#d5b500";
case "BMI":
return "#FF5900";
case "DRF":
case "ARA":
case "NHC":
return "#eb0000";
case "JLR":
return "#050073";
case "BUND":
return "#334811";
case "Rega":
return "#ED0000";
case "AAA":
return "#164070";
case "Air Zermatt":
return "#0094d4";
case "INAER":
return "#e96b22";
case "Heli Austria":
return "#009de2";
case "Air Glaciers":
return "#004680";
case "Wucher":
return "#F97514";
case "SHS":
return "#c50014";
case "Schenk Air":
return "black";
case "LAR":
return "#ED174F";
case "AAD":
return "#7F1B24";
default:
return "gray";
}
};
export const createCustomMarker = async (operator: string) => {
const canvas = document.createElement("canvas");
canvas.width = 30; // Breite des Markers
canvas.height = 30; // Höhe des Markers
const ctx = canvas.getContext("2d");
const color = getOperatorColor(operator);
if (ctx) {
// Erstelle den farbigen Kreis
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(15, 15, 10, 0, Math.PI * 2);
ctx.fill();
// Füge den weißen Rand hinzu
ctx.strokeStyle = ["ADAC", "ÖAMTC", "ANWB"].includes(operator) ? "black" : "white"; // Randfarbe
ctx.lineWidth = ["ADAC", "ÖAMTC", "ANWB"].includes(operator) ? 1 : 2; // Dicke des Randes
ctx.stroke(); // Zeichne den Rand
const img = new Image();
img.src = ["ADAC", "ÖAMTC", "ANWB"].includes(operator)
? "/icons/Station_Icon_black.svg"
: "/icons/Station_Icon.svg";
await new Promise<void>((resolve, reject) => {
img.onload = () => {
ctx.drawImage(img, 8, 8, 15, 15); // Draw icon on the canvas
resolve(); // Resolve the promise when the image loads
};
img.onerror = () => {
reject(new Error("Image failed to load.")); // Handle image load failure
};
});
} else {
console.error("Canvas context is not initialized.");
}
return canvas.toDataURL(); // Return the image data URL after the marker is created
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,10 @@ import { verify } from "jsonwebtoken";
export const PUT = async (req: Request) => { export const PUT = async (req: Request) => {
const session = await getServerSession(); const session = await getServerSession();
const token = req.headers.get("authorization")?.split(" ")[1]; const token = req.headers.get("authorization")?.split(" ")[1];
if (!token) if (!token) return Response.json({ message: "Missing token" }, { status: 401 });
return Response.json({ message: "Missing token" }, { status: 401 });
const payload = await new Promise<User>((resolve, reject) => { const payload = await new Promise<User>((resolve, reject) => {
verify(token, process.env.NEXTAUTH_HUB_SECRET as string, (err, decoded) => { verify(token, process.env.AUTH_HUB_SECRET as string, (err, decoded) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@@ -18,8 +17,7 @@ export const PUT = async (req: Request) => {
}); });
}); });
if (!session && !payload) if (!session && !payload) return Response.json({ message: "Unauthorized" }, { status: 401 });
return Response.json({ message: "Unauthorized" }, { status: 401 });
const userId = session?.user.id || payload.id; const userId = session?.user.id || payload.id;
const { position, h145 } = (await req.json()) as { const { position, h145 } = (await req.json()) as {
@@ -47,10 +45,7 @@ export const PUT = async (req: Request) => {
}); });
if (!activeAircraft) { if (!activeAircraft) {
return Response.json( return Response.json({ message: "No active aircraft found" }, { status: 400 });
{ message: "No active aircraft found" },
{ status: 400 },
);
} }
const positionLog = await prisma.positionLog.create({ const positionLog = await prisma.positionLog.create({
data: { data: {

View File

@@ -24,6 +24,7 @@
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"axios": "^1.9.0", "axios": "^1.9.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"geojson": "^0.5.0",
"i": "^0.3.7", "i": "^0.3.7",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M360-440v-240q-100 0-170 70t-70 170h240Zm160 160v-400h-80v320H120v80h400Zm80-128 240-24v-48H600v72ZM520-80H120v-80h400v80Zm80-120H120q-33 0-56.5-23.5T40-280v-160q0-134 93-227t227-93h240v200h200l40-80h80v280l-320 32v128Zm160-600H120v-80h640v80ZM600-408v-72 72Zm-80 128Z"/></svg>

After

Width:  |  Height:  |  Size: 393 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000" stroke="#000000" stroke-width="10">
<path d="M360-440v-240q-100 0-170 70t-70 170h240Zm160 160v-400h-80v320H120v80h400Zm80-128 240-24v-48H600v72ZM520-80H120v-80h400v80Zm80-120H120q-33 0-56.5-23.5T40-280v-160q0-134 93-227t227-93h240v200h200l40-80h80v280l-320 32v128Zm160-600H120v-80h640v80ZM600-408v-72 72Zm-80 128Z"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

9
pnpm-lock.yaml generated
View File

@@ -56,6 +56,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
geojson:
specifier: ^0.5.0
version: 0.5.0
i: i:
specifier: ^0.3.7 specifier: ^0.3.7
version: 0.3.7 version: 0.3.7
@@ -2648,6 +2651,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
geojson@0.5.0:
resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==}
engines: {node: '>= 0.10'}
get-caller-file@2.0.5: get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
@@ -6953,6 +6960,8 @@ snapshots:
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
geojson@0.5.0: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:

View File

@@ -9,7 +9,7 @@
"AUTH_DISPATCH_SECRET", "AUTH_DISPATCH_SECRET",
"LIVEKIT_API_KEY", "LIVEKIT_API_KEY",
"LIVEKIT_API_SECRET", "LIVEKIT_API_SECRET",
"NEXTAUTH_HUB_SECRET", "AUTH_HUB_SECRET",
"AUTH_DISPATCH_COOKIE_PREFIX" "AUTH_DISPATCH_COOKIE_PREFIX"
], ],
"ui": "tui", "ui": "tui",