fixed wrong env var, added base maps
This commit is contained in:
@@ -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_SERVER_URL=https://api.dispatch.premiumag.de
|
||||
|
||||
|
||||
NEXT_PUBLIC_ESRI_ACCESS_TOKEN=
|
||||
NEXT_PUBLIC_OPENAIP_ACCESS=6e85069940543ef02f8615b737059d98
|
||||
|
||||
# ───────────────────────────────────────────────
|
||||
# 🗄️ Datenbank
|
||||
# ───────────────────────────────────────────────
|
||||
|
||||
@@ -8,4 +8,6 @@ NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
|
||||
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
|
||||
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
|
||||
LIVEKIT_API_KEY=APIAnsGdtdYp2Ho
|
||||
LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC
|
||||
LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC
|
||||
NEXT_PUBLIC_ESRI_ACCESS_TOKEN=
|
||||
NEXT_PUBLIC_OPENAIP_ACCESS=
|
||||
@@ -1,7 +1,272 @@
|
||||
"use client";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { useEffect } from "react";
|
||||
import { TileLayer, useMap } from "react-leaflet";
|
||||
import { Control, Icon, LatLngExpression } from "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 = () => {
|
||||
const map = useMap();
|
||||
@@ -14,16 +279,39 @@ export const BaseMaps = () => {
|
||||
}, [isPannelOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<TileLayer
|
||||
attribution='© <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 position="topleft">
|
||||
<LayersControl.Overlay name={"Funknetzbereiche"}>
|
||||
<RadioAreaLayer />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name={"Niederschlag"}>
|
||||
<NiederschlagOverlay attribution={map.attributionControl} />
|
||||
</LayersControl.Overlay>
|
||||
|
||||
<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='© <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='© <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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
113241
apps/dispatch/app/_components/map/_geojson/Leitstellen_VAR.json
Normal file
113241
apps/dispatch/app/_components/map/_geojson/Leitstellen_VAR.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,10 @@ import { verify } from "jsonwebtoken";
|
||||
export const PUT = async (req: Request) => {
|
||||
const session = await getServerSession();
|
||||
const token = req.headers.get("authorization")?.split(" ")[1];
|
||||
if (!token)
|
||||
return Response.json({ message: "Missing token" }, { status: 401 });
|
||||
if (!token) return Response.json({ message: "Missing token" }, { status: 401 });
|
||||
|
||||
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) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -18,8 +17,7 @@ export const PUT = async (req: Request) => {
|
||||
});
|
||||
});
|
||||
|
||||
if (!session && !payload)
|
||||
return Response.json({ message: "Unauthorized" }, { status: 401 });
|
||||
if (!session && !payload) return Response.json({ message: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const userId = session?.user.id || payload.id;
|
||||
const { position, h145 } = (await req.json()) as {
|
||||
@@ -47,10 +45,7 @@ export const PUT = async (req: Request) => {
|
||||
});
|
||||
|
||||
if (!activeAircraft) {
|
||||
return Response.json(
|
||||
{ message: "No active aircraft found" },
|
||||
{ status: 400 },
|
||||
);
|
||||
return Response.json({ message: "No active aircraft found" }, { status: 400 });
|
||||
}
|
||||
const positionLog = await prisma.positionLog.create({
|
||||
data: {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"geojson": "^0.5.0",
|
||||
"i": "^0.3.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
|
||||
1
apps/dispatch/public/icons/Station_Icon.svg
Normal file
1
apps/dispatch/public/icons/Station_Icon.svg
Normal 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 |
3
apps/dispatch/public/icons/Station_Icon_black.svg
Normal file
3
apps/dispatch/public/icons/Station_Icon_black.svg
Normal 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
9
pnpm-lock.yaml
generated
@@ -56,6 +56,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
geojson:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
i:
|
||||
specifier: ^0.3.7
|
||||
version: 0.3.7
|
||||
@@ -2648,6 +2651,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
@@ -6953,6 +6960,8 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
geojson@0.5.0: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"AUTH_DISPATCH_SECRET",
|
||||
"LIVEKIT_API_KEY",
|
||||
"LIVEKIT_API_SECRET",
|
||||
"NEXTAUTH_HUB_SECRET",
|
||||
"AUTH_HUB_SECRET",
|
||||
"AUTH_DISPATCH_COOKIE_PREFIX"
|
||||
],
|
||||
"ui": "tui",
|
||||
|
||||
Reference in New Issue
Block a user