336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { OSMWay } from "@repo/db";
|
|
import { useMapStore } from "_store/mapStore";
|
|
import { usePannelStore } from "_store/pannelStore";
|
|
import {
|
|
MapPin,
|
|
MapPinned,
|
|
Radius,
|
|
Ruler,
|
|
Search,
|
|
RulerDimensionLine,
|
|
Scan,
|
|
} from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { Popup, useMap } from "react-leaflet";
|
|
|
|
export const ContextMenu = () => {
|
|
const map = useMap();
|
|
const { contextMenu, setContextMenu, setSearchElements, setSearchPopup } =
|
|
useMapStore();
|
|
const { setMissionFormValues, setOpen } = usePannelStore((state) => state);
|
|
const [showRulerOptions, setShowRulerOptions] = useState(false);
|
|
const [rulerHover, setRulerHover] = useState(false);
|
|
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
|
|
|
|
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) return null;
|
|
|
|
const addOSMobjects = async () => {
|
|
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) => {
|
|
return {
|
|
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}
|
|
>
|
|
{/* // TODO: maske: */}
|
|
<div
|
|
className="absolute opacity-100 pointer-events-none p-3 flex items-center justify-center"
|
|
style={{ left: "-13px", top: "-13px" }}
|
|
>
|
|
<div className="relative w-38 h-38 flex items-center justify-center -translate-x-1/2 -translate-y-1/2 pointer-events-none">
|
|
{/* Top Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 absolute left-1/2 top-0 pointer-events-auto opacity-80 tooltip tooltip-top tooltip-accent"
|
|
data-tip="Nächstes Element übernehmen"
|
|
style={{ transform: "translateX(-50%)" }}
|
|
onClick={async () => {
|
|
const address = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?lat=${contextMenu.lat}&lon=${contextMenu.lng}&format=json`,
|
|
);
|
|
const data = (await address.json()) as {
|
|
address: {
|
|
ISO3166_2_lvl4: string;
|
|
country: string;
|
|
country_code: string;
|
|
county: string;
|
|
house_number: string;
|
|
municipality: string;
|
|
postcode: string;
|
|
road: string;
|
|
state: string;
|
|
city: string;
|
|
town: string;
|
|
};
|
|
display_name: string;
|
|
importance: number;
|
|
lat: string;
|
|
licence: string;
|
|
lon: string;
|
|
name: string;
|
|
osm_id: number;
|
|
osm_type: string;
|
|
place_id: number;
|
|
place_rank: number;
|
|
type: string;
|
|
};
|
|
const objects = await addOSMobjects();
|
|
const exactAddress = objects.find((object) => {
|
|
return (
|
|
object.tags["addr:street"] == data.address.road &&
|
|
object.tags["addr:housenumber"]?.includes(
|
|
data.address.house_number,
|
|
)
|
|
);
|
|
});
|
|
const closestToContext = objects.reduce((prev, curr) => {
|
|
const prevLat = prev.nodes?.[0]?.lat ?? 0;
|
|
const prevLon = prev.nodes?.[0]?.lon ?? 0;
|
|
const currLat = curr.nodes?.[0]?.lat ?? 0;
|
|
const currLon = curr.nodes?.[0]?.lon ?? 0;
|
|
const prevDistance = Math.sqrt(
|
|
Math.pow(prevLat - contextMenu.lat, 2) +
|
|
Math.pow(prevLon - contextMenu.lng, 2),
|
|
);
|
|
const currDistance = Math.sqrt(
|
|
Math.pow(currLat - contextMenu.lat, 2) +
|
|
Math.pow(currLon - contextMenu.lng, 2),
|
|
);
|
|
return prevDistance < currDistance ? prev : curr;
|
|
});
|
|
|
|
setOpen(true);
|
|
console.log(data.address.road);
|
|
setMissionFormValues({
|
|
addressLat: contextMenu.lat,
|
|
addressLng: contextMenu.lng,
|
|
addressCity: data.address.city || data.address.town || "",
|
|
addressStreet: `${data.address.road || "keine Straße"}, ${data.address.house_number || "keine HN"}`,
|
|
addressZip: data.address.postcode || "",
|
|
state: "draft",
|
|
addressOSMways: [(exactAddress || closestToContext) as any],
|
|
});
|
|
}}
|
|
>
|
|
<MapPinned size={20} />
|
|
</button>
|
|
{/* Left Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 absolute top-1/2 left-0 pointer-events-auto opacity-80"
|
|
style={{ transform: "translateY(-50%)" }}
|
|
onMouseEnter={() => setRulerHover(true)}
|
|
onMouseLeave={() => setRulerHover(false)}
|
|
>
|
|
<Ruler size={20} />
|
|
</button>
|
|
{/* Bottom Button */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 absolute left-1/2 bottom-0 pointer-events-auto opacity-80 tooltip tooltip-bottom tooltip-accent"
|
|
data-tip="Koordinaten kopieren"
|
|
style={{ transform: "translateX(-50%)" }}
|
|
onClick={async () => {
|
|
const address = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?lat=${contextMenu.lat}&lon=${contextMenu.lng}&format=json`,
|
|
);
|
|
const data = (await address.json()) as {
|
|
address: {
|
|
ISO3166_2_lvl4: string;
|
|
country: string;
|
|
country_code: string;
|
|
county: string;
|
|
house_number: string;
|
|
municipality: string;
|
|
postcode: string;
|
|
road: string;
|
|
state: string;
|
|
city: string;
|
|
town: string;
|
|
};
|
|
display_name: string;
|
|
importance: number;
|
|
lat: string;
|
|
licence: string;
|
|
lon: string;
|
|
name: string;
|
|
osm_id: number;
|
|
osm_type: string;
|
|
place_id: number;
|
|
place_rank: number;
|
|
type: string;
|
|
};
|
|
const objects = await addOSMobjects();
|
|
const exactAddress = objects.find((object) => {
|
|
return (
|
|
object.tags["addr:street"] == data.address.road &&
|
|
object.tags["addr:housenumber"]?.includes(
|
|
data.address.house_number,
|
|
)
|
|
);
|
|
});
|
|
const closestToContext = objects.reduce((prev, curr) => {
|
|
const prevLat = prev.nodes?.[0]?.lat ?? 0;
|
|
const prevLon = prev.nodes?.[0]?.lon ?? 0;
|
|
const currLat = curr.nodes?.[0]?.lat ?? 0;
|
|
const currLon = curr.nodes?.[0]?.lon ?? 0;
|
|
const prevDistance = Math.sqrt(
|
|
Math.pow(prevLat - contextMenu.lat, 2) +
|
|
Math.pow(prevLon - contextMenu.lng, 2),
|
|
);
|
|
const currDistance = Math.sqrt(
|
|
Math.pow(currLat - contextMenu.lat, 2) +
|
|
Math.pow(currLon - contextMenu.lng, 2),
|
|
);
|
|
return prevDistance < currDistance ? prev : curr;
|
|
}, [] as any);
|
|
|
|
setOpen(true);
|
|
setMissionFormValues({
|
|
addressLat: contextMenu.lat,
|
|
addressLng: contextMenu.lng,
|
|
addressCity: data.address.city || data.address.town || "",
|
|
addressStreet: `${data.address.road || "keine Straße"}, ${data.address.house_number || "keine HN"}`,
|
|
addressZip: data.address.postcode || "",
|
|
state: "draft",
|
|
addressOSMways: [(exactAddress || closestToContext) as any],
|
|
});
|
|
}}
|
|
>
|
|
<MapPin size={20} />
|
|
</button>
|
|
{/* Right Button (original Search button) */}
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 absolute top-1/2 right-0 pointer-events-auto opacity-80 tooltip tooltip-right tooltip-accent"
|
|
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="absolute flex flex-col items-center pointer-events-auto"
|
|
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 w-10 h-10 mb-2 opacity-80 tooltip tooltip-left tooltip-accent"
|
|
data-tip="Strecke Messen"
|
|
style={{
|
|
transform: "translateX(100%)",
|
|
}}
|
|
onClick={() => {
|
|
/* ... */
|
|
}}
|
|
>
|
|
<RulerDimensionLine size={20} />
|
|
</button>
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 mb-2 opacity-80 tooltip tooltip-left tooltip-accent"
|
|
data-tip="Radius Messen"
|
|
onClick={() => {
|
|
/* ... */
|
|
}}
|
|
>
|
|
<Radius size={20} />
|
|
</button>
|
|
<button
|
|
className="btn btn-circle bg-rescuetrack w-10 h-10 opacity-80 tooltip tooltip-left tooltip-accent"
|
|
data-tip="Fläche Messen"
|
|
style={{
|
|
transform: "translateX(100%)",
|
|
}}
|
|
onClick={() => {
|
|
/* ... */
|
|
}}
|
|
>
|
|
<Scan size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Popup>
|
|
);
|
|
};
|