diff --git a/apps/dispatch/app/_components/SmartPopup.tsx b/apps/dispatch/app/_components/SmartPopup.tsx new file mode 100644 index 00000000..98f85ebc --- /dev/null +++ b/apps/dispatch/app/_components/SmartPopup.tsx @@ -0,0 +1,153 @@ +import { cn } from "helpers/cn"; +import { RefAttributes, useEffect, useImperativeHandle } from "react"; +import { createContext, Ref, useContext, useState } from "react"; +import { Popup, PopupProps, useMap } from "react-leaflet"; +import { Popup as LPopup } from "leaflet"; + +const PopupContext = createContext({ + anchor: "topleft", +}); + +export const useSmartPopup = () => { + const context = useContext(PopupContext); + if (!context) { + throw new Error("usePopup must be used within a PopupProvider"); + } + return context; +}; + +export const useConflict = (id: string, mode: "popup" | "marker") => { + const otherMarkers = document.querySelectorAll(".map-collision"); + // get markers and check if they are overlapping + const ownMarker = + mode === "popup" + ? document.querySelector(`#popup-${id}`) + : document.querySelector(`#marker-${id}`); + + if (!otherMarkers || !ownMarker) return "topleft"; + + const marksersInCluster = Array.from(otherMarkers).filter((marker) => { + if (mode === "popup" && marker.id === `marker-${id}`) return false; + + const rect1 = (marker as HTMLElement).getBoundingClientRect(); + const rect2 = (ownMarker as HTMLElement).getBoundingClientRect(); + + return !( + rect1.right < rect2.left || + rect1.left > rect2.right || + rect1.bottom < rect2.top || + rect1.top > rect2.bottom + ); + }); + + // get the center of all overlapping markers + const markersPosition = marksersInCluster.map((marker) => { + const rect = (marker as HTMLElement).getBoundingClientRect(); + return { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }; + }); + const ownMarkerBounds = (ownMarker as HTMLElement).getBoundingClientRect(); + const ownMarkerPosition = { + x: ownMarkerBounds.left + ownMarkerBounds.width / 2, + y: ownMarkerBounds.top + ownMarkerBounds.height / 2, + }; + + const centerOfOverlappingMarkers = markersPosition.reduce( + (acc, pos) => { + if (acc.x === 0 && acc.y === 0) return pos; + return { + x: (acc.x + pos.x) / 2, + y: (acc.y + pos.y) / 2, + }; + }, + { + x: 0, + y: 0, + }, + ); + if (marksersInCluster.length > 1) { + if (centerOfOverlappingMarkers.y < ownMarkerPosition.y) { + if (centerOfOverlappingMarkers.x > ownMarkerPosition.x) { + return "topright"; + } else { + return "topleft"; + } + } else { + if (centerOfOverlappingMarkers.x > ownMarkerPosition.x) { + return "bottomright"; + } else { + return "bottomleft"; + } + } + } else { + // Default + return "topleft"; + } +}; + +export interface SmartPopupRef { + handleConflict: () => void; +} + +export const SmartPopup = ( + props: PopupProps & + RefAttributes & { + smartPopupRef?: Ref; + id: string; + wrapperClassName?: string; + }, +) => { + const { smartPopupRef, id, className, wrapperClassName } = props; + + const [anchor, setAnchor] = useState< + "topleft" | "topright" | "bottomleft" | "bottomright" + >("topleft"); + + const handleConflict = () => { + const newAnchor = useConflict(id, "popup"); + setAnchor(newAnchor); + }; + + useImperativeHandle(smartPopupRef, () => ({ + handleConflict, + })); + + const map = useMap(); + + useEffect(() => { + setTimeout(handleConflict, 50); + map.on("zoom", handleConflict); + + return () => { + map.off("zoom", handleConflict); + }; + }, [map, anchor]); + + return ( + +
+
+ + {props.children} + +
+ + ); +}; diff --git a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx index db852d4c..89d9a69c 100644 --- a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx +++ b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx @@ -5,6 +5,7 @@ import { useMapStore } from "_store/mapStore"; import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { cn } from "helpers/cn"; import { Cross, House, Minimize2, Route } from "lucide-react"; +import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup"; export const FMS_STATUS_COLORS: { [key: string]: string } = { "0": "rgb(126,0,5)", @@ -32,7 +33,93 @@ export const FMS_STATUS_TEXT_COLORS: { [key: string]: string } = { "9": "rgb(42,217,42)", }; -const AircraftLabel = ({ aircraft }: { aircraft: Aircraft }) => { +const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => { + const setOpenAircraftMarker = useMapStore( + (state) => state.setOpenAircraftMarker, + ); + const { anchor } = useSmartPopup(); + return ( + <> +
{ + setOpenAircraftMarker({ + open: [], + close: [aircraft.id], + }); + }} + > + +
+ +
+
+
+
+ +
+
+ +
+
+ {aircraft.fmsStatus} +
+
+ Einsatz 250411 +
+
+
+ + ); +}; + +const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => { const aircrafts = useAircraftsStore((state) => state.aircrafts); const map = useMap(); const markerRef = useRef(null); @@ -67,94 +154,25 @@ const AircraftLabel = ({ aircraft }: { aircraft: Aircraft }) => { "topleft" | "topright" | "bottomleft" | "bottomright" >("topleft"); - const handleLabelConflict = useCallback(() => { - const otherMarkers = document.querySelectorAll(".aircraft-collision"); - // get markers and check if they are overlapping - const ownMarker = openAircraftMarker.includes(aircraft.id) - ? document.querySelector(`#aircraft-popup-${aircraft.id}`) - : document.querySelector(`#aircraft-marker-${aircraft.id}`); - - if (!otherMarkers || !ownMarker) return; - - const marksersInCluster = Array.from(otherMarkers).filter((marker) => { - if ( - openAircraftMarker.includes(aircraft.id) && - marker.id === `aircraft-marker-${aircraft.id}` - ) - return false; - - const rect1 = (marker as HTMLElement).getBoundingClientRect(); - const rect2 = (ownMarker as HTMLElement).getBoundingClientRect(); - - return !( - rect1.right < rect2.left || - rect1.left > rect2.right || - rect1.bottom < rect2.top || - rect1.top > rect2.bottom - ); - }); - - // get the center of all overlapping markers - const markersPosition = marksersInCluster.map((marker) => { - const rect = (marker as HTMLElement).getBoundingClientRect(); - return { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2, - }; - }); - const ownMarkerBounds = (ownMarker as HTMLElement).getBoundingClientRect(); - const ownMarkerPosition = { - x: ownMarkerBounds.left + ownMarkerBounds.width / 2, - y: ownMarkerBounds.top + ownMarkerBounds.height / 2, - }; - - const centerOfOverlappingMarkers = markersPosition.reduce( - (acc, pos) => { - if (acc.x === 0 && acc.y === 0) return pos; - return { - x: (acc.x + pos.x) / 2, - y: (acc.y + pos.y) / 2, - }; - }, - { - x: 0, - y: 0, - }, - ); - if (marksersInCluster.length > 1) { - if (centerOfOverlappingMarkers.y < ownMarkerPosition.y) { - if (centerOfOverlappingMarkers.x > ownMarkerPosition.x) { - setAnchor("topright"); - } else { - setAnchor("topleft"); - } - } else { - if (centerOfOverlappingMarkers.x > ownMarkerPosition.x) { - setAnchor("bottomright"); - } else { - setAnchor("bottomleft"); - } - } - } else { - // Default - setAnchor("topleft"); - } - }, [openAircraftMarker, aircraft.id]); + const handleConflict = () => { + const newAnchor = useConflict(aircraft.id, "marker"); + setAnchor(newAnchor); + }; useEffect(() => { - handleLabelConflict(); + handleConflict(); }, [aircrafts, openAircraftMarker]); useEffect(() => {}); useEffect(() => { setTimeout(() => { - handleLabelConflict(); + handleConflict(); }, 100); - map.on("zoom", handleLabelConflict); + map.on("zoom", handleConflict); return () => { - map.off("zoom", handleLabelConflict); + map.off("zoom", handleConflict); }; }, [map, openAircraftMarker]); @@ -194,10 +212,10 @@ const AircraftLabel = ({ aircraft }: { aircraft: Aircraft }) => { ${aircraft.bosName}
{ } /> {openAircraftMarker.includes(aircraft.id) && ( - -
-
-
{ - setOpenAircraftMarker({ - open: [], - close: [aircraft.id], - }); - }} - > - -
- -
-
-
-
- -
-
- -
-
- {aircraft.fmsStatus} -
-
- Einsatz 250411 -
-
-
-
- + + )} ); }; -export const AircraftMarker = (props: any) => { - const { openAircraftMarker, setOpenAircraftMarker } = useMapStore( - (state) => state, - ); - +export const AircraftLayer = () => { const aircrafts = useAircraftsStore((state) => state.aircrafts); - const map = useMap(); - return aircrafts.map((aircraft) => ( - - )); + // IDEA: Add Marker to Map Layer / LayerGroup + return ( + <> + {aircrafts.map((aircraft) => { + return ; + })} + + ); }; diff --git a/apps/dispatch/app/dispatch/_components/map/Map.tsx b/apps/dispatch/app/dispatch/_components/map/Map.tsx index 66a3e513..176cc4f6 100644 --- a/apps/dispatch/app/dispatch/_components/map/Map.tsx +++ b/apps/dispatch/app/dispatch/_components/map/Map.tsx @@ -6,7 +6,7 @@ import { BaseMaps } from "dispatch/_components/map/BaseMaps"; import { ContextMenu } from "dispatch/_components/map/ContextMenu"; import { MissionMarkers } from "dispatch/_components/map/MissionMarkers"; import { SearchElements } from "dispatch/_components/map/SearchElements"; -import { AircraftMarker } from "dispatch/_components/map/AircraftMarker"; +import { AircraftLayer } from "dispatch/_components/map/AircraftMarker"; export default ({}) => { const { map } = useMapStore(); @@ -17,7 +17,7 @@ export default ({}) => { - + ); }; diff --git a/grafana/grafana.db b/grafana/grafana.db index aa974892..b0fd56e4 100644 Binary files a/grafana/grafana.db and b/grafana/grafana.db differ