Files
var-monorepo/apps/dispatch/app/_components/map/ContextMenu.tsx
2025-07-24 14:31:34 -07:00

248 lines
7.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, Radius, Ruler, Search, RulerDimensionLine, Scan } 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";
export const ContextMenu = () => {
const map = useMap();
const {
contextMenu,
searchElements,
setContextMenu,
setSearchElements,
setSearchPopup,
toggleSearchElementSelection,
} = useMapStore();
const {
missionFormValues,
setMissionFormValues,
setOpen,
isOpen: isPannelOpen,
} = usePannelStore((state) => state);
const [showRulerOptions, setShowRulerOptions] = useState(false);
const [rulerHover, setRulerHover] = useState(false);
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
useEffect(() => {
setShowRulerOptions(rulerHover || rulerOptionsHover);
}, [rulerHover, rulerOptionsHover]);
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
>
<Ruler 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>
{/* Ruler Options - shown when Ruler button is hovered or options are hovered */}
{showRulerOptions && (
<div
className="pointer-events-auto absolute flex flex-col items-center"
style={{
left: "-100px", // position to the right of the left button
top: "50%",
transform: "translateY(-50%)",
zIndex: 10,
width: "120px", // Make the hover area wider
height: "200px", // Make the hover area taller
padding: "20px 0", // Add vertical padding
display: "flex",
justifyContent: "center",
pointerEvents: "auto",
}}
onMouseEnter={() => setRulerOptionsHover(true)}
onMouseLeave={() => setRulerOptionsHover(false)}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<button
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 opacity-80"
data-tip="Strecke Messen"
style={{
transform: "translateX(100%)",
}}
onClick={() => {
/* ... */
}}
>
<RulerDimensionLine 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="Radius Messen"
onClick={() => {
/* ... */
}}
>
<Radius size={20} />
</button>
<button
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent h-10 w-10 opacity-80"
data-tip="Fläche Messen"
style={{
transform: "translateX(100%)",
}}
onClick={() => {
/* ... */
}}
>
<Scan size={20} />
</button>
</div>
</div>
)}
</div>
</div>
</Popup>
);
};