271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { OSMWay } from "@repo/db";
|
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
|
import { useMapStore } from "_store/mapStore";
|
|
import { usePannelStore } from "_store/pannelStore";
|
|
import { MapPin, MapPinned, Search, Car, Ambulance, Siren, Flame } from "lucide-react";
|
|
import { getOsmAddress } from "_querys/osm";
|
|
import { useEffect, useState } from "react";
|
|
import toast from "react-hot-toast";
|
|
import { Popup, useMap } from "react-leaflet";
|
|
import { findClosestPolygon } from "_helpers/findClosestPolygon";
|
|
import { xPlaneObjectsAvailable } from "_helpers/xPlaneObjectsAvailable";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
|
|
|
export const ContextMenu = () => {
|
|
const map = useMap();
|
|
const { data: aircrafts } = useQuery({
|
|
queryKey: ["connectedAircrafts"],
|
|
queryFn: getConnectedAircraftsAPI,
|
|
});
|
|
const {
|
|
contextMenu,
|
|
searchElements,
|
|
setContextMenu,
|
|
setSearchElements,
|
|
setSearchPopup,
|
|
toggleSearchElementSelection,
|
|
} = useMapStore();
|
|
const {
|
|
missionFormValues,
|
|
setMissionFormValues,
|
|
setOpen,
|
|
isOpen: isPannelOpen,
|
|
} = usePannelStore((state) => state);
|
|
const [showObjectOptions, setShowObjectOptions] = useState(false);
|
|
const [rulerHover, setRulerHover] = useState(false);
|
|
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
|
|
|
|
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
|
|
|
useEffect(() => {
|
|
const showObjectOptions = rulerHover || rulerOptionsHover;
|
|
setShowObjectOptions(showObjectOptions);
|
|
}, [isPannelOpen, rulerHover, rulerOptionsHover, setOpen]);
|
|
|
|
useEffect(() => {
|
|
const handleContextMenu = (e: any) => {
|
|
setContextMenu({ lat: e.latlng.lat, lng: e.latlng.lng });
|
|
};
|
|
const handleClick = () => {
|
|
setContextMenu(null);
|
|
setSearchPopup(null);
|
|
};
|
|
|
|
map.on("contextmenu", handleContextMenu);
|
|
map.on("click", handleClick);
|
|
|
|
return () => {
|
|
map.off("contextmenu", handleContextMenu);
|
|
map.off("click", handleClick);
|
|
};
|
|
}, [map, contextMenu, setContextMenu, setSearchPopup]);
|
|
|
|
if (!contextMenu || !dispatcherConnected) return null;
|
|
|
|
const missionBtnText =
|
|
missionFormValues && isPannelOpen ? "Position übernehmen" : "Einsatz erstellen";
|
|
|
|
const addOSMobjects = async (ignorePreviosSelected?: boolean) => {
|
|
const res = await fetch(
|
|
`https://overpass-api.de/api/interpreter?data=${encodeURIComponent(`
|
|
[out:json];
|
|
(
|
|
way["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
|
relation["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
|
);
|
|
out body;
|
|
>;
|
|
out skel qt;
|
|
`)}`,
|
|
);
|
|
|
|
const data = await res.json();
|
|
const parsed: OSMWay[] = data.elements
|
|
.filter((e: any) => e.type === "way")
|
|
.map((e: any) => {
|
|
const elementInMap = searchElements.find((el) => el.wayID === e.id);
|
|
return {
|
|
isSelected: ignorePreviosSelected ? false : (elementInMap?.isSelected ?? false),
|
|
wayID: e.id,
|
|
nodes: e.nodes.map((nodeId: string) => {
|
|
const node = data.elements.find((element: any) => element.id === nodeId);
|
|
return {
|
|
lat: node.lat,
|
|
lon: node.lon,
|
|
};
|
|
}),
|
|
tags: e.tags,
|
|
type: e.type,
|
|
} as OSMWay;
|
|
});
|
|
|
|
setSearchElements(parsed);
|
|
return parsed;
|
|
};
|
|
|
|
return (
|
|
<Popup
|
|
position={[contextMenu.lat, contextMenu.lng]}
|
|
autoClose={false}
|
|
closeOnClick={false}
|
|
autoPan={false}
|
|
>
|
|
<div
|
|
className="pointer-events-none absolute flex items-center justify-center p-3 opacity-100"
|
|
style={{ left: "-13px", top: "-13px" }}
|
|
>
|
|
<div className="w-38 h-38 pointer-events-none relative flex -translate-x-1/2 -translate-y-1/2 items-center justify-center">
|
|
{/* Top Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-top tooltip-accent pointer-events-auto absolute left-1/2 top-0 h-10 w-10 opacity-80"
|
|
data-tip={missionBtnText}
|
|
style={{ transform: "translateX(-50%)" }}
|
|
onClick={async () => {
|
|
const { parsed } = await getOsmAddress(contextMenu.lat, contextMenu.lng);
|
|
setOpen(true);
|
|
setMissionFormValues({
|
|
...missionFormValues,
|
|
...parsed,
|
|
state: "draft",
|
|
addressLat: contextMenu.lat,
|
|
addressLng: contextMenu.lng,
|
|
});
|
|
|
|
const objects = await addOSMobjects(true);
|
|
|
|
const closestObject = findClosestPolygon(objects, {
|
|
lat: contextMenu.lat,
|
|
lon: contextMenu.lng,
|
|
});
|
|
|
|
if (closestObject) {
|
|
toggleSearchElementSelection(closestObject.wayID, true);
|
|
}
|
|
if (isPannelOpen) {
|
|
map.setView([contextMenu.lat, contextMenu.lng], 18, {
|
|
animate: true,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<MapPinned size={20} />
|
|
</button>
|
|
{/* Left Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack pointer-events-auto absolute left-0 top-1/2 h-10 w-10 opacity-80"
|
|
style={{ transform: "translateY(-50%)" }}
|
|
onMouseEnter={() => setRulerHover(true)}
|
|
onMouseLeave={() => setRulerHover(false)}
|
|
disabled={
|
|
!isPannelOpen ||
|
|
!xPlaneObjectsAvailable(missionFormValues?.missionStationIds, aircrafts)
|
|
}
|
|
>
|
|
<Car size={20} />
|
|
</button>
|
|
{/* Bottom Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-bottom tooltip-accent pointer-events-auto absolute bottom-0 left-1/2 h-10 w-10 opacity-80"
|
|
data-tip="Koordinaten kopieren"
|
|
style={{ transform: "translateX(-50%)" }}
|
|
onClick={async () => {
|
|
const coords = `${contextMenu.lat}, ${contextMenu.lng}`;
|
|
await navigator.clipboard.writeText(coords);
|
|
toast.success("Koordinaten in die Zwischenablage kopiert!");
|
|
}}
|
|
>
|
|
<MapPin size={20} />
|
|
</button>
|
|
{/* Right Button (original Search button) */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-right tooltip-accent pointer-events-auto absolute right-0 top-1/2 h-10 w-10 opacity-80"
|
|
data-tip="Gebäude suchen"
|
|
style={{ transform: "translateY(-50%)" }}
|
|
onClick={async () => {
|
|
addOSMobjects();
|
|
}}
|
|
>
|
|
<Search size={20} />
|
|
</button>
|
|
{/* XPlane Object Options - shown when Ruler button is hovered or options are hovered */}
|
|
{showObjectOptions && (
|
|
<div
|
|
className="pointer-events-auto absolute -left-[100px] top-1/2 z-10 flex h-[200px] w-[120px] -translate-y-1/2 flex-col items-center justify-center py-5"
|
|
onMouseEnter={() => setRulerOptionsHover(true)}
|
|
onMouseLeave={() => setRulerOptionsHover(false)}
|
|
>
|
|
<div className="flex w-full flex-col">
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 ml-[30px] h-10 w-10 opacity-80"
|
|
data-tip="Rettungswagen platzieren"
|
|
onClick={() => {
|
|
console.log("Add Ambulance");
|
|
setMissionFormValues({
|
|
...missionFormValues,
|
|
xPlaneObjects: [
|
|
...(missionFormValues?.xPlaneObjects ?? []),
|
|
{
|
|
objectName: "ambulance",
|
|
alt: 0,
|
|
lat: contextMenu.lat,
|
|
lon: contextMenu.lng,
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
>
|
|
<Ambulance size={20} />
|
|
</button>
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 opacity-80"
|
|
data-tip="LF platzieren"
|
|
onClick={() => {
|
|
console.log("Add fire engine");
|
|
setMissionFormValues({
|
|
...missionFormValues,
|
|
xPlaneObjects: [
|
|
...(missionFormValues?.xPlaneObjects ?? []),
|
|
{
|
|
objectName: "fire_engine",
|
|
alt: 0,
|
|
lat: contextMenu.lat,
|
|
lon: contextMenu.lng,
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
>
|
|
<Flame size={20} />
|
|
</button>
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent ml-[30px] h-10 w-10 opacity-80"
|
|
data-tip="Streifenwagen platzieren"
|
|
onClick={() => {
|
|
console.log("Add police");
|
|
setMissionFormValues({
|
|
...missionFormValues,
|
|
xPlaneObjects: [
|
|
...(missionFormValues?.xPlaneObjects ?? []),
|
|
{
|
|
objectName: "police",
|
|
alt: 0,
|
|
lat: contextMenu.lat,
|
|
lon: contextMenu.lng,
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
>
|
|
<Siren size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Popup>
|
|
);
|
|
};
|