implement marker clusters

This commit is contained in:
Matteo Rosati
2026-01-18 00:09:18 +01:00
parent 6df84ac67e
commit c351da70b2
3 changed files with 61 additions and 1 deletions

View File

@@ -6,15 +6,18 @@
"name": "bun-react-template",
"dependencies": {
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"react": "^19",
"react-datepicker": "^9.1.0",
"react-dom": "^19",
"react-leaflet": "^5.0.0",
"react-leaflet-cluster": "^4.0.0",
"recharts": "^3.6.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/leaflet": "^1.9.21",
"@types/leaflet.markercluster": "^1.5.6",
"@types/react": "^19",
"@types/react-datepicker": "^7.0.0",
"@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.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/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.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-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-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=="],
"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=="],

View File

@@ -10,15 +10,18 @@
},
"dependencies": {
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"react": "^19",
"react-datepicker": "^9.1.0",
"react-dom": "^19",
"react-leaflet": "^5.0.0",
"react-leaflet-cluster": "^4.0.0",
"recharts": "^3.6.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/leaflet": "^1.9.21",
"@types/leaflet.markercluster": "^1.5.6",
"@types/react": "^19",
"@types/react-datepicker": "^7.0.0",
"@types/react-dom": "^19"

View File

@@ -1,6 +1,9 @@
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import MarkerClusterGroup from "react-leaflet-cluster";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
import type { Plext } from "../types";
// Fix Leaflet marker icons
@@ -15,6 +18,39 @@ interface PlayerMapProps {
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) {
// Get player locations from plexts
const playerLocations = plexts
@@ -45,7 +81,18 @@ export function PlayerMap({ plexts }: PlayerMapProps) {
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
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;
const markerColor = location.team === "RESISTANCE" ? "blue" : "green";
@@ -76,6 +123,7 @@ export function PlayerMap({ plexts }: PlayerMapProps) {
</Marker>
);
})}
</MarkerClusterGroup>
</MapContainer>
</div>
);