From c351da70b20dcfbfeb6205418c3de3e72f962ed7 Mon Sep 17 00:00:00 2001 From: Matteo Rosati Date: Sun, 18 Jan 2026 00:09:18 +0100 Subject: [PATCH] implement marker clusters --- bun.lock | 9 +++++++ package.json | 3 +++ src/components/PlayerMap.tsx | 50 +++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 02dffeb..de86290 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 7d22d1a..d33bbc5 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/PlayerMap.tsx b/src/components/PlayerMap.tsx index eccef2a..b62abcc 100644 --- a/src/components/PlayerMap.tsx +++ b/src/components/PlayerMap.tsx @@ -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: `
${count}
`, + 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='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - {playerLocations.map((location, index) => { + + {playerLocations.map((location, index) => { if (!location) return null; const markerColor = location.team === "RESISTANCE" ? "blue" : "green"; @@ -76,6 +123,7 @@ export function PlayerMap({ plexts }: PlayerMapProps) { ); })} + );