Added marker clustering when zoomed out
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
import { cn } from "helpers/cn";
|
import { cn } from "helpers/cn";
|
||||||
import { RefAttributes, useEffect, useImperativeHandle } from "react";
|
import {
|
||||||
|
RefAttributes,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
import { createContext, Ref, useContext, useState } from "react";
|
import { createContext, Ref, useContext, useState } from "react";
|
||||||
import { Popup, PopupProps, useMap } from "react-leaflet";
|
import { Popup, PopupProps, useMap } from "react-leaflet";
|
||||||
import { Popup as LPopup, popup } from "leaflet";
|
import { Popup as LPopup } from "leaflet";
|
||||||
|
|
||||||
const PopupContext = createContext({
|
const PopupContext = createContext({
|
||||||
anchor: "topleft",
|
anchor: "topleft",
|
||||||
@@ -16,7 +21,11 @@ export const useSmartPopup = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateAnchor = (id: string, mode: "popup" | "marker") => {
|
export const calculateAnchor = (
|
||||||
|
id: string,
|
||||||
|
mode: "popup" | "marker",
|
||||||
|
ignoreMarker?: boolean,
|
||||||
|
) => {
|
||||||
const otherMarkers = document.querySelectorAll(".map-collision");
|
const otherMarkers = document.querySelectorAll(".map-collision");
|
||||||
// get markers and check if they are overlapping
|
// get markers and check if they are overlapping
|
||||||
const ownMarker =
|
const ownMarker =
|
||||||
@@ -28,6 +37,7 @@ export const calculateAnchor = (id: string, mode: "popup" | "marker") => {
|
|||||||
|
|
||||||
const marksersInCluster = Array.from(otherMarkers).filter((marker) => {
|
const marksersInCluster = Array.from(otherMarkers).filter((marker) => {
|
||||||
if (mode === "popup" && marker.id === `marker-${id}`) return false;
|
if (mode === "popup" && marker.id === `marker-${id}`) return false;
|
||||||
|
if (ignoreMarker && marker.id.startsWith("marker")) return false;
|
||||||
|
|
||||||
const rect1 = (marker as HTMLElement).getBoundingClientRect();
|
const rect1 = (marker as HTMLElement).getBoundingClientRect();
|
||||||
const rect2 = (ownMarker as HTMLElement).getBoundingClientRect();
|
const rect2 = (ownMarker as HTMLElement).getBoundingClientRect();
|
||||||
@@ -90,24 +100,24 @@ export interface SmartPopupRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SmartPopup = (
|
export const SmartPopup = (
|
||||||
props: PopupProps &
|
props: PopupProps & { ignoreMarker?: boolean } & RefAttributes<LPopup> & {
|
||||||
RefAttributes<LPopup> & {
|
|
||||||
smartPopupRef?: Ref<SmartPopupRef>;
|
smartPopupRef?: Ref<SmartPopupRef>;
|
||||||
id: string;
|
id: string;
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
const { smartPopupRef, id, className, wrapperClassName } = props;
|
const { smartPopupRef, id, className, wrapperClassName, ignoreMarker } =
|
||||||
|
props;
|
||||||
|
|
||||||
const [anchor, setAnchor] = useState<
|
const [anchor, setAnchor] = useState<
|
||||||
"topleft" | "topright" | "bottomleft" | "bottomright"
|
"topleft" | "topright" | "bottomleft" | "bottomright"
|
||||||
>("topleft");
|
>("topleft");
|
||||||
|
|
||||||
const handleConflict = () => {
|
const handleConflict = useCallback(() => {
|
||||||
const newAnchor = calculateAnchor(id, "popup");
|
const newAnchor = calculateAnchor(id, "popup", ignoreMarker);
|
||||||
setAnchor(newAnchor);
|
setAnchor(newAnchor);
|
||||||
};
|
}, [id, ignoreMarker]);
|
||||||
|
|
||||||
useImperativeHandle(smartPopupRef, () => ({
|
useImperativeHandle(smartPopupRef, () => ({
|
||||||
handleConflict,
|
handleConflict,
|
||||||
@@ -125,7 +135,7 @@ export const SmartPopup = (
|
|||||||
return () => {
|
return () => {
|
||||||
map.off("zoom", handleConflict);
|
map.off("zoom", handleConflict);
|
||||||
};
|
};
|
||||||
}, [map, anchor]);
|
}, [map, handleConflict]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} className={cn("relative", wrapperClassName)}>
|
<Popup {...props} className={cn("relative", wrapperClassName)}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface Aircraft {
|
|||||||
}[];
|
}[];
|
||||||
location: {
|
location: {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lng: number;
|
||||||
altitude: number;
|
altitude: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ export const useAircraftsStore = create<AircraftStore>((set) => ({
|
|||||||
fmsLog: [],
|
fmsLog: [],
|
||||||
location: {
|
location: {
|
||||||
lat: 52.546781040592776,
|
lat: 52.546781040592776,
|
||||||
lon: 13.369535209542219,
|
lng: 13.369535209542219,
|
||||||
altitude: 0,
|
altitude: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
},
|
},
|
||||||
@@ -58,7 +58,7 @@ export const useAircraftsStore = create<AircraftStore>((set) => ({
|
|||||||
fmsLog: [],
|
fmsLog: [],
|
||||||
location: {
|
location: {
|
||||||
lat: 52.54588546048977,
|
lat: 52.54588546048977,
|
||||||
lon: 13.46470691054384,
|
lng: 13.46470691054384,
|
||||||
altitude: 0,
|
altitude: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
},
|
},
|
||||||
@@ -73,7 +73,7 @@ export const useAircraftsStore = create<AircraftStore>((set) => ({
|
|||||||
fmsLog: [],
|
fmsLog: [],
|
||||||
location: {
|
location: {
|
||||||
lat: 52.497519717230155,
|
lat: 52.497519717230155,
|
||||||
lon: 13.342040806552554,
|
lng: 13.342040806552554,
|
||||||
altitude: 0,
|
altitude: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@ export const useAircraftsStore = create<AircraftStore>((set) => ({
|
|||||||
fmsLog: [],
|
fmsLog: [],
|
||||||
location: {
|
location: {
|
||||||
lat: 52.50175041192073,
|
lat: 52.50175041192073,
|
||||||
lon: 13.478628701227349,
|
lng: 13.478628701227349,
|
||||||
altitude: 0,
|
altitude: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
|
|
||||||
const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||||
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
||||||
|
const [hideMarker, setHideMarker] = useState(false);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const markerRef = useRef<LMarker>(null);
|
const markerRef = useRef<LMarker>(null);
|
||||||
const popupRef = useRef<LPopup>(null);
|
const popupRef = useRef<LPopup>(null);
|
||||||
@@ -269,20 +270,25 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
setAnchor(newAnchor);
|
setAnchor(newAnchor);
|
||||||
}, [aircraft.id]);
|
}, [aircraft.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
/* useEffect(() => {
|
||||||
handleConflict();
|
handleConflict();
|
||||||
}, [aircrafts, openAircraftMarker, handleConflict]);
|
}, [aircrafts, openAircraftMarker, handleConflict]); */
|
||||||
|
|
||||||
useEffect(() => {});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleConflict();
|
handleZoom();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
map.on("zoom", handleConflict);
|
const handleZoom = () => {
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
console.log("zoom", zoom);
|
||||||
|
setHideMarker(zoom < 9);
|
||||||
|
handleConflict();
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on("zoomend", handleZoom);
|
||||||
return () => {
|
return () => {
|
||||||
map.off("zoom", handleConflict);
|
map.off("zoomend", handleZoom);
|
||||||
};
|
};
|
||||||
}, [map, openAircraftMarker, handleConflict]);
|
}, [map, openAircraftMarker, handleConflict]);
|
||||||
|
|
||||||
@@ -296,6 +302,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
"relative w-auto transform inline-flex items-center gap-2 px-2 z-100",
|
"relative w-auto transform inline-flex items-center gap-2 px-2 z-100",
|
||||||
anchor.includes("right") && "-translate-x-full",
|
anchor.includes("right") && "-translate-x-full",
|
||||||
anchor.includes("bottom") && "-translate-y-full",
|
anchor.includes("bottom") && "-translate-y-full",
|
||||||
|
hideMarker ? "opacity-0 pointer-events-none" : "opacity-100",
|
||||||
)}"
|
)}"
|
||||||
style="
|
style="
|
||||||
background-color: ${FMS_STATUS_COLORS[aircraft.fmsStatus]};
|
background-color: ${FMS_STATUS_COLORS[aircraft.fmsStatus]};
|
||||||
@@ -338,7 +345,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
<Fragment key={aircraft.id}>
|
<Fragment key={aircraft.id}>
|
||||||
<Marker
|
<Marker
|
||||||
ref={markerRef}
|
ref={markerRef}
|
||||||
position={[aircraft.location.lat, aircraft.location.lon]}
|
position={[aircraft.location.lat, aircraft.location.lng]}
|
||||||
icon={
|
icon={
|
||||||
new DivIcon({
|
new DivIcon({
|
||||||
iconAnchor: [0, 0],
|
iconAnchor: [0, 0],
|
||||||
@@ -346,11 +353,11 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{openAircraftMarker.some((m) => m.id === aircraft.id) && (
|
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
|
||||||
<SmartPopup
|
<SmartPopup
|
||||||
id={aircraft.id}
|
id={aircraft.id}
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
position={[aircraft.location.lat, aircraft.location.lon]}
|
position={[aircraft.location.lat, aircraft.location.lng]}
|
||||||
autoClose={false}
|
autoClose={false}
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
autoPan={false}
|
autoPan={false}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { ContextMenu } from "dispatch/_components/map/ContextMenu";
|
|||||||
import { MissionLayer } from "dispatch/_components/map/MissionMarkers";
|
import { MissionLayer } from "dispatch/_components/map/MissionMarkers";
|
||||||
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
||||||
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
||||||
|
import { MarkerCluster } from "dispatch/_components/map/_components/MarkerCluster";
|
||||||
|
|
||||||
export default ({}) => {
|
const Map = () => {
|
||||||
const { map } = useMapStore();
|
const { map } = useMapStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,8 +22,11 @@ export default ({}) => {
|
|||||||
<BaseMaps />
|
<BaseMaps />
|
||||||
<SearchElements />
|
<SearchElements />
|
||||||
<ContextMenu />
|
<ContextMenu />
|
||||||
|
<MarkerCluster />
|
||||||
<MissionLayer />
|
<MissionLayer />
|
||||||
<AircraftLayer />
|
<AircraftLayer />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Map;
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ const MissionPopupContent = ({ mission }: { mission: Mission }) => {
|
|||||||
|
|
||||||
const MissionMarker = ({ mission }: { mission: Mission }) => {
|
const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
const [hideMarker, setHideMarker] = useState(false);
|
||||||
const markerRef = useRef<LMarker>(null);
|
const markerRef = useRef<LMarker>(null);
|
||||||
const popupRef = useRef<LPopup>(null);
|
const popupRef = useRef<LPopup>(null);
|
||||||
|
|
||||||
@@ -227,16 +228,20 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
|||||||
handleConflict();
|
handleConflict();
|
||||||
}, [handleConflict]);
|
}, [handleConflict]);
|
||||||
|
|
||||||
useEffect(() => {});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleConflict();
|
handleZoom();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
map.on("zoom", handleConflict);
|
const handleZoom = () => {
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
setHideMarker(zoom < 9);
|
||||||
|
handleConflict();
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on("zoomend", handleZoom);
|
||||||
return () => {
|
return () => {
|
||||||
map.off("zoom", handleConflict);
|
map.off("zoomend", handleZoom);
|
||||||
};
|
};
|
||||||
}, [map, openMissionMarker, handleConflict]);
|
}, [map, openMissionMarker, handleConflict]);
|
||||||
|
|
||||||
@@ -250,6 +255,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
|||||||
"relative w-auto transform inline-flex items-center gap-2 px-2 z-100",
|
"relative w-auto transform inline-flex items-center gap-2 px-2 z-100",
|
||||||
anchor.includes("right") && "-translate-x-full",
|
anchor.includes("right") && "-translate-x-full",
|
||||||
anchor.includes("bottom") && "-translate-y-full",
|
anchor.includes("bottom") && "-translate-y-full",
|
||||||
|
hideMarker ? "opacity-0 pointer-events-none" : "opacity-100",
|
||||||
)}"
|
)}"
|
||||||
style="
|
style="
|
||||||
background-color: ${MISSION_STATUS_COLORS[mission.state]};
|
background-color: ${MISSION_STATUS_COLORS[mission.state]};
|
||||||
@@ -294,7 +300,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{openMissionMarker.some((m) => m.id === mission.id) && (
|
{openMissionMarker.some((m) => m.id === mission.id) && !hideMarker && (
|
||||||
<SmartPopup
|
<SmartPopup
|
||||||
id={mission.id.toString()}
|
id={mission.id.toString()}
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { Mission } from "@repo/db";
|
||||||
|
import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
|
||||||
|
import { Aircraft, useAircraftsStore } from "_store/aircraftsStore";
|
||||||
|
import { useMissionsStore } from "_store/missionsStore";
|
||||||
|
import {
|
||||||
|
FMS_STATUS_COLORS,
|
||||||
|
FMS_STATUS_TEXT_COLORS,
|
||||||
|
} from "dispatch/_components/map/AircraftMarker";
|
||||||
|
import {
|
||||||
|
MISSION_STATUS_COLORS,
|
||||||
|
MISSION_STATUS_TEXT_COLORS,
|
||||||
|
} from "dispatch/_components/map/MissionMarkers";
|
||||||
|
import { cn } from "helpers/cn";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMap } from "react-leaflet";
|
||||||
|
|
||||||
|
const PopupContent = ({
|
||||||
|
aircrafts,
|
||||||
|
missions,
|
||||||
|
}: {
|
||||||
|
aircrafts: Aircraft[];
|
||||||
|
missions: Mission[];
|
||||||
|
}) => {
|
||||||
|
const { anchor } = useSmartPopup();
|
||||||
|
|
||||||
|
let borderColor = "";
|
||||||
|
|
||||||
|
if (anchor.includes("top")) {
|
||||||
|
if (missions.length > 0) {
|
||||||
|
borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state];
|
||||||
|
} else if (aircrafts.length > 0) {
|
||||||
|
borderColor = FMS_STATUS_TEXT_COLORS[aircrafts[0]!.fmsStatus] || "white";
|
||||||
|
}
|
||||||
|
} else if (anchor.includes("bottom")) {
|
||||||
|
if (aircrafts.length > 0) {
|
||||||
|
borderColor = FMS_STATUS_TEXT_COLORS[aircrafts[0]!.fmsStatus] || "white";
|
||||||
|
} else if (missions.length > 0) {
|
||||||
|
borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative flex flex-col text-white min-w-[200px]">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute w-[calc(100%+2px)] h-4 z-99",
|
||||||
|
anchor.includes("left") ? "-left-[2px]" : "-right-[2px]",
|
||||||
|
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
borderLeft: anchor.includes("left")
|
||||||
|
? `3px solid ${borderColor}`
|
||||||
|
: "",
|
||||||
|
borderRight: anchor.includes("right")
|
||||||
|
? `3px solid ${borderColor}`
|
||||||
|
: "",
|
||||||
|
borderTop: anchor.includes("top") ? `3px solid ${borderColor}` : "",
|
||||||
|
borderBottom: anchor.includes("bottom")
|
||||||
|
? `3px solid ${borderColor}`
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{missions.map((mission) => (
|
||||||
|
<div
|
||||||
|
key={mission.id}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex items-center gap-2 text-nowrap w-full",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: MISSION_STATUS_COLORS[mission.state],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mx-2 my-0.5">
|
||||||
|
{mission.missionKeywordAbbreviation}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{aircrafts.map((aircraft) => (
|
||||||
|
<div
|
||||||
|
key={aircraft.id}
|
||||||
|
className="relative w-auto inline-flex items-center gap-2 text-nowrap"
|
||||||
|
style={{
|
||||||
|
backgroundColor: FMS_STATUS_COLORS[aircraft.fmsStatus],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mx-2 my-0.5 text-gt font-bold"
|
||||||
|
style={{
|
||||||
|
color: FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{aircraft.fmsStatus}
|
||||||
|
</span>
|
||||||
|
<span>{aircraft.bosName}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarkerCluster = () => {
|
||||||
|
const map = useMap();
|
||||||
|
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
||||||
|
const missions = useMissionsStore((state) => state.missions);
|
||||||
|
const [cluster, setCluster] = useState<
|
||||||
|
{
|
||||||
|
aircrafts: Aircraft[];
|
||||||
|
missions: Mission[];
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleZoom = () => {
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
let newCluster: typeof cluster = [];
|
||||||
|
aircrafts.forEach((aircraft) => {
|
||||||
|
const lat = aircraft.location.lat;
|
||||||
|
const lng = aircraft.location.lng;
|
||||||
|
|
||||||
|
const existingClusterIndex = newCluster.findIndex(
|
||||||
|
(c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1,
|
||||||
|
);
|
||||||
|
console.log("existingClusterIndex", existingClusterIndex);
|
||||||
|
const existingCluster = newCluster[existingClusterIndex];
|
||||||
|
console.log("existingCluster", existingCluster, lat, lng);
|
||||||
|
if (existingCluster) {
|
||||||
|
newCluster = [...newCluster].map((c, i) => {
|
||||||
|
if (i === existingClusterIndex) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
aircrafts: [...c.aircrafts, aircraft],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newCluster = [
|
||||||
|
...newCluster,
|
||||||
|
{
|
||||||
|
aircrafts: [aircraft],
|
||||||
|
missions: [],
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
missions.forEach((mission) => {
|
||||||
|
const lat = mission.addressLat;
|
||||||
|
const lng = mission.addressLng;
|
||||||
|
const existingClusterIndex = newCluster.findIndex(
|
||||||
|
(c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1,
|
||||||
|
);
|
||||||
|
const existingCluster = newCluster[existingClusterIndex];
|
||||||
|
|
||||||
|
if (existingCluster) {
|
||||||
|
newCluster = [...newCluster].map((c, i) => {
|
||||||
|
if (i === existingClusterIndex) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
missions: [...c.missions, mission],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newCluster = [
|
||||||
|
...newCluster,
|
||||||
|
{
|
||||||
|
aircrafts: [],
|
||||||
|
missions: [mission],
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setCluster((prev) => {
|
||||||
|
return prev.map((c) => {
|
||||||
|
const aircraftPos = c.aircrafts.map((a) => [
|
||||||
|
a.location.lat,
|
||||||
|
a.location.lng,
|
||||||
|
]);
|
||||||
|
const missionPos = c.missions.map((m) => [
|
||||||
|
m.addressLat,
|
||||||
|
m.addressLng,
|
||||||
|
]);
|
||||||
|
const allPos = [...aircraftPos, ...missionPos];
|
||||||
|
|
||||||
|
// Calculate the average position of all markers in the cluster
|
||||||
|
const avgLat =
|
||||||
|
allPos.reduce((sum, pos) => sum + pos[0]!, 0) / allPos.length;
|
||||||
|
const avgLng =
|
||||||
|
allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
lat: avgLat,
|
||||||
|
lng: avgLng,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (zoom >= 9) {
|
||||||
|
setCluster([]);
|
||||||
|
} else {
|
||||||
|
setCluster(newCluster);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleZoom();
|
||||||
|
map.on("zoomend", handleZoom);
|
||||||
|
return () => {
|
||||||
|
map.off("zoomend", handleZoom);
|
||||||
|
};
|
||||||
|
}, [map, aircrafts, missions]);
|
||||||
|
console.log("cluster", cluster);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cluster.map((c, i) => (
|
||||||
|
<SmartPopup
|
||||||
|
ignoreMarker
|
||||||
|
id={`cluster-${i}`}
|
||||||
|
wrapperClassName="relative"
|
||||||
|
key={i}
|
||||||
|
position={[c.lat, c.lng]}
|
||||||
|
autoPan={false}
|
||||||
|
autoClose={false}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<PopupContent aircrafts={c.aircrafts} missions={c.missions} />
|
||||||
|
</div>
|
||||||
|
</SmartPopup>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Binary file not shown.
Reference in New Issue
Block a user