Improve large maps with marker clustering (#24244)

* Improve large maps with marker clustering

* Pin leaflet.markercluster

* Remove custom icon

* Display whether marker are clustered or not
This commit is contained in:
Jan-Philipp Benecke 2025-02-18 14:45:39 +01:00 committed by GitHub
parent 06932d1479
commit 83d4a408f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 39 deletions

View File

@ -90,6 +90,14 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")

View File

@ -120,6 +120,7 @@
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
@ -176,6 +177,7 @@
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",

View File

@ -16,11 +16,30 @@ export const setupLeafletMap = async (
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
await import("leaflet.markercluster");
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
const markerClusterStyle = document.createElement("link");
markerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.css"
);
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
const defaultMarkerClusterStyle = document.createElement("link");
defaultMarkerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.Default.css"
);
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);

View File

@ -0,0 +1,32 @@
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
import { Marker } from "leaflet";
export class DecoratedMarker extends Marker {
decorationLayer: Layer | undefined;
constructor(
latlng: LatLngExpression,
decorationLayer?: Layer,
options?: MarkerOptions
) {
super(latlng, options);
this.decorationLayer = decorationLayer;
}
onAdd(map: Map) {
super.onAdd(map);
// If decoration has been provided, add it to the map as well
this.decorationLayer?.addTo(map);
return this;
}
onRemove(map: Map) {
// If decoration has been provided, remove it from the map as well
this.decorationLayer?.remove();
return super.onRemove(map);
}
}

View File

@ -8,9 +8,10 @@ import type {
Map,
Marker,
Polyline,
MarkerClusterGroup,
} from "leaflet";
import type { PropertyValues } from "lit";
import { ReactiveElement, css } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatDateTime } from "../../common/datetime/format_date_time";
@ -26,6 +27,7 @@ import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
import { DecoratedMarker } from "../../common/map/decorated_marker";
declare global {
// for fire event
@ -84,6 +86,9 @@ export class HaMap extends ReactiveElement {
@property({ type: Number }) public zoom = 14;
@property({ attribute: "cluster-markers", type: Boolean })
public clusterMarkers = true;
@state() private _loaded = false;
public leafletMap?: Map;
@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
private _mapFocusItems: (Marker | Circle)[] = [];
private _mapZones: (Marker | Circle)[] = [];
private _mapZones: DecoratedMarker[] = [];
private _mapFocusZones: (Marker | Circle)[] = [];
private _mapCluster: MarkerClusterGroup | undefined;
private _mapPaths: (Polyline | CircleMarker)[] = [];
private _clickCount = 0;
@ -151,6 +158,10 @@ export class HaMap extends ReactiveElement {
}
}
if (changedProps.has("clusterMarkers")) {
this._drawEntities();
}
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
}
@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
) {
return;
}
this._updateMapStyle();
}
@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
this._mapFocusZones = [];
}
if (this._mapCluster) {
this._mapCluster.remove();
this._mapCluster = undefined;
}
if (!this.entities) {
return;
}
@ -481,26 +498,24 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
}
// create marker with the icon
this._mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
})
);
// create circle around it
const circle = Leaflet.circle([latitude, longitude], {
interactive: false,
color: passive ? passiveZoneColor : zoneColor,
radius,
});
this._mapZones.push(circle);
const marker = new DecoratedMarker([latitude, longitude], circle, {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
});
this._mapZones.push(marker);
if (
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
}
// create marker with the icon
const marker = Leaflet.marker([latitude, longitude], {
const marker = new DecoratedMarker([latitude, longitude], undefined, {
icon: Leaflet.divIcon({
html: entityMarker,
iconSize: [48, 48],
@ -546,24 +561,33 @@ export class HaMap extends ReactiveElement {
}),
title: title,
});
this._mapItems.push(marker);
if (typeof entity === "string" || entity.focus !== false) {
this._mapFocusItems.push(marker);
}
// create circle around if entity has accuracy
if (gpsAccuracy) {
this._mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
})
);
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
});
}
this._mapItems.push(marker);
}
if (this.clusterMarkers) {
this._mapCluster = Leaflet.markerClusterGroup({
showCoverageOnHover: false,
removeOutsideVisibleBounds: false,
});
this._mapCluster.addLayers(this._mapItems);
map.addLayer(this._mapCluster);
} else {
this._mapItems.forEach((marker) => map.addLayer(marker));
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
}

View File

@ -1,4 +1,8 @@
import { mdiImageFilterCenterFocus } from "@mdi/js";
import {
mdiDotsHexagon,
mdiGoogleCirclesCommunities,
mdiImageFilterCenterFocus,
} from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { LatLngTuple } from "leaflet";
import type { PropertyValues } from "lit";
@ -72,6 +76,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
@state() private _error?: { code: string; message: string };
@state() private _clusterMarkers = true;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
public setConfig(config: MapCardConfig): void {
@ -170,18 +176,32 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones}
.themeMode=${themeMode}
.clusterMarkers=${this._clusterMarkers}
interactive-zones
render-passive
></ha-map>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
<div id="buttons">
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.toggle_grouping"
)}
.path=${this._clusterMarkers
? mdiGoogleCirclesCommunities
: mdiDotsHexagon}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._toggleClusterMarkers}
tabindex="0"
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
</div>
</div>
</ha-card>
`;
@ -320,6 +340,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._map?.fitMap();
}
private _toggleClusterMarkers() {
this._clusterMarkers = !this._clusterMarkers;
}
private _getColor(entityId: string): string {
let color = this._colorDict[entityId];
if (color) {
@ -464,11 +488,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
overflow: hidden;
}
ha-icon-button {
#buttons {
position: absolute;
top: 75px;
left: 3px;
outline: none;
display: flex;
flex-direction: column;
}
#root {

View File

@ -6310,7 +6310,8 @@
"description": "Home Assistant is starting, please wait…"
},
"map": {
"reset_focus": "Reset focus"
"reset_focus": "Reset focus",
"toggle_grouping": "Toggle grouping"
},
"energy": {
"loading": "Loading…",

View File

@ -4737,6 +4737,15 @@ __metadata:
languageName: node
linkType: hard
"@types/leaflet.markercluster@npm:1.5.5":
version: 1.5.5
resolution: "@types/leaflet.markercluster@npm:1.5.5"
dependencies:
"@types/leaflet": "npm:*"
checksum: 10/17647d187ed8c9c38124005c3c45c0c7998c6359d8783e2ea162f9649b151862750c813eba2373054e90156a11a37af2b220429f937b302889b9d6e2105bf2ca
languageName: node
linkType: hard
"@types/leaflet@npm:*, @types/leaflet@npm:1.9.16":
version: 1.9.16
resolution: "@types/leaflet@npm:1.9.16"
@ -9380,6 +9389,7 @@ __metadata:
"@types/js-yaml": "npm:4.0.9"
"@types/leaflet": "npm:1.9.16"
"@types/leaflet-draw": "npm:1.0.11"
"@types/leaflet.markercluster": "npm:1.5.5"
"@types/lodash.merge": "npm:4.6.9"
"@types/luxon": "npm:3.4.2"
"@types/mocha": "npm:10.0.10"
@ -9444,6 +9454,7 @@ __metadata:
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
lint-staged: "npm:15.4.3"
lit: "npm:2.8.0"
lit-analyzer: "npm:2.0.3"
@ -10748,6 +10759,15 @@ __metadata:
languageName: node
linkType: hard
"leaflet.markercluster@npm:1.5.3":
version: 1.5.3
resolution: "leaflet.markercluster@npm:1.5.3"
peerDependencies:
leaflet: ^1.3.1
checksum: 10/28dc441de7012b19628144407bde89576f758dbea31ecb86e3412d1fb3a46723fb47d2ba45d9b858c2e65592479a127474fb1cbf7ff33b3023c4d14f851e5fe4
languageName: node
linkType: hard
"leaflet@npm:1.9.4":
version: 1.9.4
resolution: "leaflet@npm:1.9.4"