implement marker clusters
This commit is contained in:
9
bun.lock
9
bun.lock
@@ -6,15 +6,18 @@
|
|||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-datepicker": "^9.1.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-leaflet-cluster": "^4.0.0",
|
||||||
"recharts": "^3.6.0",
|
"recharts": "^3.6.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-datepicker": "^7.0.0",
|
"@types/react-datepicker": "^7.0.0",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -64,6 +67,8 @@
|
|||||||
|
|
||||||
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="],
|
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="],
|
||||||
|
|
||||||
|
"@types/leaflet.markercluster": ["@types/leaflet.markercluster@1.5.6", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
||||||
@@ -116,6 +121,8 @@
|
|||||||
|
|
||||||
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
|
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
|
||||||
|
|
||||||
|
"leaflet.markercluster": ["leaflet.markercluster@1.5.3", "", { "peerDependencies": { "leaflet": "^1.3.1" } }, "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
"react-datepicker": ["react-datepicker@9.1.0", "", { "dependencies": { "@floating-ui/react": "^0.27.15", "clsx": "^2.1.1", "date-fns": "^4.1.0" }, "peerDependencies": { "date-fns-tz": "^3.0.0", "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["date-fns-tz"] }, "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA=="],
|
"react-datepicker": ["react-datepicker@9.1.0", "", { "dependencies": { "@floating-ui/react": "^0.27.15", "clsx": "^2.1.1", "date-fns": "^4.1.0" }, "peerDependencies": { "date-fns-tz": "^3.0.0", "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["date-fns-tz"] }, "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA=="],
|
||||||
@@ -126,6 +133,8 @@
|
|||||||
|
|
||||||
"react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw=="],
|
"react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw=="],
|
||||||
|
|
||||||
|
"react-leaflet-cluster": ["react-leaflet-cluster@4.0.0", "", { "dependencies": { "leaflet.markercluster": "^1.5.3" }, "peerDependencies": { "@react-leaflet/core": "^3.0.0", "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-leaflet": "^5.0.0" } }, "sha512-Lu75+KOu2ruGyAx8LoCQvlHuw+3CLLJQGEoSk01ymsDN/YnCiRV6ChkpsvaruVyYBPzUHwiskFw4Jo7WHj5qNw=="],
|
||||||
|
|
||||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||||
|
|
||||||
"recharts": ["recharts@3.6.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg=="],
|
"recharts": ["recharts@3.6.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg=="],
|
||||||
|
|||||||
@@ -10,15 +10,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-datepicker": "^9.1.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-leaflet-cluster": "^4.0.0",
|
||||||
"recharts": "^3.6.0"
|
"recharts": "^3.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-datepicker": "^7.0.0",
|
"@types/react-datepicker": "^7.0.0",
|
||||||
"@types/react-dom": "^19"
|
"@types/react-dom": "^19"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
|
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
|
||||||
|
import MarkerClusterGroup from "react-leaflet-cluster";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||||
import type { Plext } from "../types";
|
import type { Plext } from "../types";
|
||||||
|
|
||||||
// Fix Leaflet marker icons
|
// Fix Leaflet marker icons
|
||||||
@@ -15,6 +18,39 @@ interface PlayerMapProps {
|
|||||||
plexts: Plext[];
|
plexts: Plext[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a custom cluster icon with the number of markers
|
||||||
|
* @param cluster - The cluster object from leaflet.markercluster
|
||||||
|
* @returns A Leaflet DivIcon with the cluster count
|
||||||
|
*/
|
||||||
|
const createClusterIcon = (cluster: any): L.DivIcon => {
|
||||||
|
const count = cluster.getChildCount();
|
||||||
|
let size = 40;
|
||||||
|
if (count > 10) size = 50;
|
||||||
|
if (count > 50) size = 60;
|
||||||
|
if (count > 100) size = 70;
|
||||||
|
|
||||||
|
return new L.DivIcon({
|
||||||
|
html: `<div style="
|
||||||
|
background-color: #666;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: ${size}px;
|
||||||
|
height: ${size}px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: ${size / 2}px;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
">${count}</div>`,
|
||||||
|
className: "custom-cluster-icon",
|
||||||
|
iconSize: [size, size],
|
||||||
|
iconAnchor: [size / 2, size / 2],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function PlayerMap({ plexts }: PlayerMapProps) {
|
export function PlayerMap({ plexts }: PlayerMapProps) {
|
||||||
// Get player locations from plexts
|
// Get player locations from plexts
|
||||||
const playerLocations = plexts
|
const playerLocations = plexts
|
||||||
@@ -45,7 +81,18 @@ export function PlayerMap({ plexts }: PlayerMapProps) {
|
|||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{playerLocations.map((location, index) => {
|
<MarkerClusterGroup
|
||||||
|
chunkedLoading
|
||||||
|
maxClusterRadius={60}
|
||||||
|
spiderfyOnMaxZoom={true}
|
||||||
|
showCoverageOnHover={false}
|
||||||
|
zoomToBoundsOnClick={true}
|
||||||
|
removeOutsideVisibleBounds={true}
|
||||||
|
animate={true}
|
||||||
|
animateAddingMarkers={true}
|
||||||
|
iconCreateFunction={createClusterIcon}
|
||||||
|
>
|
||||||
|
{playerLocations.map((location, index) => {
|
||||||
if (!location) return null;
|
if (!location) return null;
|
||||||
|
|
||||||
const markerColor = location.team === "RESISTANCE" ? "blue" : "green";
|
const markerColor = location.team === "RESISTANCE" ? "blue" : "green";
|
||||||
@@ -76,6 +123,7 @@ export function PlayerMap({ plexts }: PlayerMapProps) {
|
|||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</MarkerClusterGroup>
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user