Added marker clustering when zoomed out
This commit is contained in:
@@ -225,6 +225,7 @@ const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
|
||||
const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
||||
const [hideMarker, setHideMarker] = useState(false);
|
||||
const map = useMap();
|
||||
const markerRef = useRef<LMarker>(null);
|
||||
const popupRef = useRef<LPopup>(null);
|
||||
@@ -269,20 +270,25 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
setAnchor(newAnchor);
|
||||
}, [aircraft.id]);
|
||||
|
||||
useEffect(() => {
|
||||
/* useEffect(() => {
|
||||
handleConflict();
|
||||
}, [aircrafts, openAircraftMarker, handleConflict]);
|
||||
|
||||
useEffect(() => {});
|
||||
}, [aircrafts, openAircraftMarker, handleConflict]); */
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
handleConflict();
|
||||
handleZoom();
|
||||
}, 100);
|
||||
|
||||
map.on("zoom", handleConflict);
|
||||
const handleZoom = () => {
|
||||
const zoom = map.getZoom();
|
||||
console.log("zoom", zoom);
|
||||
setHideMarker(zoom < 9);
|
||||
handleConflict();
|
||||
};
|
||||
|
||||
map.on("zoomend", handleZoom);
|
||||
return () => {
|
||||
map.off("zoom", handleConflict);
|
||||
map.off("zoomend", handleZoom);
|
||||
};
|
||||
}, [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",
|
||||
anchor.includes("right") && "-translate-x-full",
|
||||
anchor.includes("bottom") && "-translate-y-full",
|
||||
hideMarker ? "opacity-0 pointer-events-none" : "opacity-100",
|
||||
)}"
|
||||
style="
|
||||
background-color: ${FMS_STATUS_COLORS[aircraft.fmsStatus]};
|
||||
@@ -338,7 +345,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
<Fragment key={aircraft.id}>
|
||||
<Marker
|
||||
ref={markerRef}
|
||||
position={[aircraft.location.lat, aircraft.location.lon]}
|
||||
position={[aircraft.location.lat, aircraft.location.lng]}
|
||||
icon={
|
||||
new DivIcon({
|
||||
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
|
||||
id={aircraft.id}
|
||||
ref={popupRef}
|
||||
position={[aircraft.location.lat, aircraft.location.lon]}
|
||||
position={[aircraft.location.lat, aircraft.location.lng]}
|
||||
autoClose={false}
|
||||
closeOnClick={false}
|
||||
autoPan={false}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { ContextMenu } from "dispatch/_components/map/ContextMenu";
|
||||
import { MissionLayer } from "dispatch/_components/map/MissionMarkers";
|
||||
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
||||
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
||||
import { MarkerCluster } from "dispatch/_components/map/_components/MarkerCluster";
|
||||
|
||||
export default ({}) => {
|
||||
const Map = () => {
|
||||
const { map } = useMapStore();
|
||||
|
||||
return (
|
||||
@@ -21,8 +22,11 @@ export default ({}) => {
|
||||
<BaseMaps />
|
||||
<SearchElements />
|
||||
<ContextMenu />
|
||||
<MarkerCluster />
|
||||
<MissionLayer />
|
||||
<AircraftLayer />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
|
||||
@@ -180,6 +180,7 @@ const MissionPopupContent = ({ mission }: { mission: Mission }) => {
|
||||
|
||||
const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
const map = useMap();
|
||||
const [hideMarker, setHideMarker] = useState(false);
|
||||
const markerRef = useRef<LMarker>(null);
|
||||
const popupRef = useRef<LPopup>(null);
|
||||
|
||||
@@ -227,16 +228,20 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
handleConflict();
|
||||
}, [handleConflict]);
|
||||
|
||||
useEffect(() => {});
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
handleConflict();
|
||||
handleZoom();
|
||||
}, 100);
|
||||
|
||||
map.on("zoom", handleConflict);
|
||||
const handleZoom = () => {
|
||||
const zoom = map.getZoom();
|
||||
setHideMarker(zoom < 9);
|
||||
handleConflict();
|
||||
};
|
||||
|
||||
map.on("zoomend", handleZoom);
|
||||
return () => {
|
||||
map.off("zoom", handleConflict);
|
||||
map.off("zoomend", handleZoom);
|
||||
};
|
||||
}, [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",
|
||||
anchor.includes("right") && "-translate-x-full",
|
||||
anchor.includes("bottom") && "-translate-y-full",
|
||||
hideMarker ? "opacity-0 pointer-events-none" : "opacity-100",
|
||||
)}"
|
||||
style="
|
||||
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
|
||||
id={mission.id.toString()}
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user