mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-10-30 19:52:03 +01:00
feat(web): cluster map markers for large node counts
Wrap MapPage node markers in react-leaflet-cluster (leaflet.markercluster) with spiderfy-at-max-zoom, zoom-to-bounds on click, clustering disabled at zoom >= 15, and viewport culling so off-screen markers stay out of the DOM. Cluster bubbles render a plain themed count badge. Marker label HTML is now only built when the show-labels toggle is on, halving per-marker DOM weight; hover-to-reveal no longer applies while labels are toggled off.
This commit is contained in:
5 files changed
+146
-66
No files matched your search
Generated
+58
-60
@@ -14,11 +14,13 @@
|
||||
"i18next": "^26.0.0",
|
||||
"i18next-browser-languagedetector": "^8",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"react": "^19",
|
||||
"react-chartjs-2": "^5",
|
||||
"react-dom": "^19",
|
||||
"react-i18next": "^17.0.0",
|
||||
"react-leaflet": "^5",
|
||||
"react-leaflet-cluster": "^4.1.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-qr-code": "^2.2.0",
|
||||
"react-router": "^8.0.0",
|
||||
@@ -33,6 +35,7 @@
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/leaflet.markercluster": "^1.5.6",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -492,9 +495,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -515,9 +515,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -538,9 +535,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -561,9 +555,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -584,9 +575,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -607,9 +595,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -832,9 +817,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -852,9 +834,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -872,9 +851,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -892,9 +868,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -912,9 +885,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -932,9 +902,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1152,9 +1119,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1171,9 +1135,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1190,9 +1151,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1209,9 +1167,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1527,6 +1482,16 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/leaflet.markercluster": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.6.tgz",
|
||||
"integrity": "sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/leaflet": "^1.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
@@ -1584,6 +1549,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1600,6 +1566,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1616,6 +1583,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1632,6 +1600,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1648,6 +1617,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1664,6 +1634,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1680,6 +1651,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1696,6 +1668,7 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1712,6 +1685,7 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1728,6 +1702,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1744,6 +1719,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1760,6 +1736,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1776,6 +1753,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1792,6 +1770,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1808,6 +1787,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1824,6 +1804,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1840,6 +1821,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1856,6 +1838,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1872,6 +1855,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1888,6 +1872,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2813,6 +2798,15 @@
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leaflet.markercluster": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@@ -4338,6 +4332,22 @@
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-leaflet-cluster": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet-cluster/-/react-leaflet-cluster-4.1.3.tgz",
|
||||
"integrity": "sha512-b3ICS7r9Fs5hBWrFPMVnmkCuDewyG3m1GaGzamXY0eryyAXDfQ+ikSkiR/mVethueIIA5MSyLok2/KsZVotspQ==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
@@ -5207,9 +5217,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5231,9 +5238,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5255,9 +5259,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5279,9 +5280,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/leaflet.markercluster": "^1.5.6",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -37,11 +38,13 @@
|
||||
"i18next": "^26.0.0",
|
||||
"i18next-browser-languagedetector": "^8",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"react": "^19",
|
||||
"react-chartjs-2": "^5",
|
||||
"react-dom": "^19",
|
||||
"react-i18next": "^17.0.0",
|
||||
"react-leaflet": "^5",
|
||||
"react-leaflet-cluster": "^4.1.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-qr-code": "^2.2.0",
|
||||
"react-router": "^8.0.0",
|
||||
|
||||
@@ -383,6 +383,24 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Marker cluster badges (MapPage) */
|
||||
.map-cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-marker-public);
|
||||
border: 2px solid var(--color-marker-public-border);
|
||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6), 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.map-cluster:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
/* Bring hovered marker to front */
|
||||
.leaflet-marker-icon:hover {
|
||||
z-index: 10000 !important;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -7,11 +7,19 @@ vi.mock("react-leaflet", () => ({
|
||||
<div data-testid="mock-map">{children}</div>
|
||||
),
|
||||
TileLayer: () => null,
|
||||
Marker: () => null,
|
||||
Marker: () => <div data-testid="map-marker" />,
|
||||
Popup: () => null,
|
||||
useMap: () => ({ fitBounds: () => {}, latLngToContainerPoint: () => ({ x: 0, y: 0 }) }),
|
||||
}));
|
||||
|
||||
vi.mock("react-leaflet-cluster", () => ({
|
||||
default: ({ children }: { children: ReactNode }) => (
|
||||
<div data-testid="cluster-group">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("leaflet.markercluster", () => ({}));
|
||||
|
||||
vi.mock("leaflet", () => ({
|
||||
divIcon: () => ({}),
|
||||
latLngBounds: () => ({}),
|
||||
@@ -60,6 +68,15 @@ describe("MapPage", () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders node markers inside the cluster group", async () => {
|
||||
vi.spyOn(api, "apiGet").mockResolvedValue(MAP_DATA);
|
||||
renderWithProviders(<MapPage />);
|
||||
const group = await screen.findByTestId("cluster-group");
|
||||
expect(
|
||||
within(group).getAllByTestId("map-marker").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows an error on fetch failure", async () => {
|
||||
vi.spyOn(api, "apiGet").mockRejectedValue(new Error("map error"));
|
||||
const { container } = renderWithProviders(<MapPage />);
|
||||
|
||||
@@ -3,11 +3,15 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet";
|
||||
import MarkerClusterGroup from "react-leaflet-cluster";
|
||||
import "leaflet.markercluster";
|
||||
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||
import {
|
||||
divIcon,
|
||||
latLngBounds,
|
||||
type DivIcon,
|
||||
type Map as LeafletMap,
|
||||
type MarkerCluster,
|
||||
} from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
@@ -91,7 +95,11 @@ function getTypeDisplay(node: MapNode, t: TFunction): string {
|
||||
: t("node_types.unknown");
|
||||
}
|
||||
|
||||
function createNodeIcon(node: MapNode, oidcEnabled: boolean): DivIcon {
|
||||
function createNodeIcon(
|
||||
node: MapNode,
|
||||
oidcEnabled: boolean,
|
||||
showLabels: boolean,
|
||||
): DivIcon {
|
||||
const displayName = node.name || "";
|
||||
const relativeTime = formatRelativeTime(node.last_seen);
|
||||
const timeDisplay = relativeTime ? " (" + relativeTime + ")" : "";
|
||||
@@ -101,6 +109,15 @@ function createNodeIcon(node: MapNode, oidcEnabled: boolean): DivIcon {
|
||||
? '<div style="width: 12px; height: 12px; background: var(--color-marker-infra); border: 2px solid var(--color-marker-infra-border); border-radius: 50%; box-shadow: 0 0 4px rgba(59,130,246,0.6), 0 1px 2px rgba(0,0,0,0.5);"></div>'
|
||||
: '<div style="width: 12px; height: 12px; background: var(--color-marker-public); border: 2px solid var(--color-marker-public-border); border-radius: 50%; box-shadow: 0 0 4px rgba(34,197,94,0.6), 0 1px 2px rgba(0,0,0,0.5);"></div>';
|
||||
|
||||
if (!showLabels) {
|
||||
return divIcon({
|
||||
className: "custom-div-icon",
|
||||
html: '<div class="map-marker">' + iconHtml + "</div>",
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
});
|
||||
}
|
||||
|
||||
return divIcon({
|
||||
className: "custom-div-icon",
|
||||
html:
|
||||
@@ -116,6 +133,24 @@ function createNodeIcon(node: MapNode, oidcEnabled: boolean): DivIcon {
|
||||
});
|
||||
}
|
||||
|
||||
function createClusterIcon(cluster: MarkerCluster): DivIcon {
|
||||
const count = cluster.getChildCount();
|
||||
const size = count >= 1000 ? 48 : count >= 100 ? 42 : count >= 10 ? 36 : 30;
|
||||
return divIcon({
|
||||
className: "custom-div-icon",
|
||||
html:
|
||||
'<div class="map-cluster" style="width: ' +
|
||||
size +
|
||||
"px; height: " +
|
||||
size +
|
||||
'px;"><span>' +
|
||||
formatNumber(count) +
|
||||
"</span></div>",
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
});
|
||||
}
|
||||
|
||||
function NodePopup({
|
||||
node,
|
||||
oidcEnabled,
|
||||
@@ -342,14 +377,14 @@ export function MapPage() {
|
||||
<Marker
|
||||
key={node.public_key}
|
||||
position={[node.lat, node.lon]}
|
||||
icon={createNodeIcon(node, oidcEnabled)}
|
||||
icon={createNodeIcon(node, oidcEnabled, showLabels)}
|
||||
>
|
||||
<Popup>
|
||||
<NodePopup node={node} oidcEnabled={oidcEnabled} />
|
||||
</Popup>
|
||||
</Marker>
|
||||
)),
|
||||
[filteredNodes, oidcEnabled],
|
||||
[filteredNodes, oidcEnabled, showLabels],
|
||||
);
|
||||
|
||||
const clearFilters = () => {
|
||||
@@ -474,7 +509,16 @@ export function MapPage() {
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{markers}
|
||||
<MarkerClusterGroup
|
||||
showCoverageOnHover={false}
|
||||
spiderfyOnMaxZoom
|
||||
zoomToBoundsOnClick
|
||||
disableClusteringAtZoom={15}
|
||||
maxClusterRadius={60}
|
||||
iconCreateFunction={createClusterIcon}
|
||||
>
|
||||
{markers}
|
||||
</MarkerClusterGroup>
|
||||
{mapData && (
|
||||
<MapController
|
||||
mapData={mapData}
|
||||
|
||||
Reference in new issue
Block a user