957 lines
34 KiB
JavaScript
957 lines
34 KiB
JavaScript
import equal from 'fast-deep-equal';
|
|
import SuperCluster from 'supercluster';
|
|
|
|
/*! *****************************************************************************
|
|
Copyright (c) Microsoft Corporation.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
PERFORMANCE OF THIS SOFTWARE.
|
|
***************************************************************************** */
|
|
|
|
function __rest(s, e) {
|
|
var t = {};
|
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
t[p] = s[p];
|
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
t[p[i]] = s[p[i]];
|
|
}
|
|
return t;
|
|
}
|
|
|
|
/**
|
|
* Copyright 2023 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* util class that creates a common set of convenience functions to wrap
|
|
* shared behavior of Advanced Markers and Markers.
|
|
*/
|
|
class MarkerUtils {
|
|
static isAdvancedMarkerAvailable(map) {
|
|
return (google.maps.marker &&
|
|
map.getMapCapabilities().isAdvancedMarkersAvailable === true);
|
|
}
|
|
static isAdvancedMarker(marker) {
|
|
return (google.maps.marker &&
|
|
marker instanceof google.maps.marker.AdvancedMarkerElement);
|
|
}
|
|
static setMap(marker, map) {
|
|
if (this.isAdvancedMarker(marker)) {
|
|
marker.map = map;
|
|
}
|
|
else {
|
|
marker.setMap(map);
|
|
}
|
|
}
|
|
static getPosition(marker) {
|
|
// SuperClusterAlgorithm.calculate expects a LatLng instance so we fake it for Adv Markers
|
|
if (this.isAdvancedMarker(marker)) {
|
|
if (marker.position) {
|
|
if (marker.position instanceof google.maps.LatLng) {
|
|
return marker.position;
|
|
}
|
|
// since we can't cast to LatLngLiteral for reasons =(
|
|
if (marker.position.lat && marker.position.lng) {
|
|
return new google.maps.LatLng(marker.position.lat, marker.position.lng);
|
|
}
|
|
}
|
|
return new google.maps.LatLng(null);
|
|
}
|
|
return marker.getPosition();
|
|
}
|
|
static getVisible(marker) {
|
|
if (this.isAdvancedMarker(marker)) {
|
|
/**
|
|
* Always return true for Advanced Markers because the clusterer
|
|
* uses getVisible as a way to count legacy markers not as an actual
|
|
* indicator of visibility for some reason. Even when markers are hidden
|
|
* Marker.getVisible returns `true` and this is used to set the marker count
|
|
* on the cluster. See the behavior of Cluster.count
|
|
*/
|
|
return true;
|
|
}
|
|
return marker.getVisible();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
class Cluster {
|
|
constructor({ markers, position }) {
|
|
this.markers = markers;
|
|
if (position) {
|
|
if (position instanceof google.maps.LatLng) {
|
|
this._position = position;
|
|
}
|
|
else {
|
|
this._position = new google.maps.LatLng(position);
|
|
}
|
|
}
|
|
}
|
|
get bounds() {
|
|
if (this.markers.length === 0 && !this._position) {
|
|
return;
|
|
}
|
|
const bounds = new google.maps.LatLngBounds(this._position, this._position);
|
|
for (const marker of this.markers) {
|
|
bounds.extend(MarkerUtils.getPosition(marker));
|
|
}
|
|
return bounds;
|
|
}
|
|
get position() {
|
|
return this._position || this.bounds.getCenter();
|
|
}
|
|
/**
|
|
* Get the count of **visible** markers.
|
|
*/
|
|
get count() {
|
|
return this.markers.filter((m) => MarkerUtils.getVisible(m)).length;
|
|
}
|
|
/**
|
|
* Add a marker to the cluster.
|
|
*/
|
|
push(marker) {
|
|
this.markers.push(marker);
|
|
}
|
|
/**
|
|
* Cleanup references and remove marker from map.
|
|
*/
|
|
delete() {
|
|
if (this.marker) {
|
|
MarkerUtils.setMap(this.marker, null);
|
|
this.marker = undefined;
|
|
}
|
|
this.markers.length = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* Returns the markers visible in a padded map viewport
|
|
*
|
|
* @param map
|
|
* @param mapCanvasProjection
|
|
* @param markers The list of marker to filter
|
|
* @param viewportPaddingPixels The padding in pixel
|
|
* @returns The list of markers in the padded viewport
|
|
*/
|
|
const filterMarkersToPaddedViewport = (map, mapCanvasProjection, markers, viewportPaddingPixels) => {
|
|
const extendedMapBounds = extendBoundsToPaddedViewport(map.getBounds(), mapCanvasProjection, viewportPaddingPixels);
|
|
return markers.filter((marker) => extendedMapBounds.contains(MarkerUtils.getPosition(marker)));
|
|
};
|
|
/**
|
|
* Extends a bounds by a number of pixels in each direction
|
|
*/
|
|
const extendBoundsToPaddedViewport = (bounds, projection, numPixels) => {
|
|
const { northEast, southWest } = latLngBoundsToPixelBounds(bounds, projection);
|
|
const extendedPixelBounds = extendPixelBounds({ northEast, southWest }, numPixels);
|
|
return pixelBoundsToLatLngBounds(extendedPixelBounds, projection);
|
|
};
|
|
/**
|
|
* Gets the extended bounds as a bbox [westLng, southLat, eastLng, northLat]
|
|
*/
|
|
const getPaddedViewport = (bounds, projection, pixels) => {
|
|
const extended = extendBoundsToPaddedViewport(bounds, projection, pixels);
|
|
const ne = extended.getNorthEast();
|
|
const sw = extended.getSouthWest();
|
|
return [sw.lng(), sw.lat(), ne.lng(), ne.lat()];
|
|
};
|
|
/**
|
|
* Returns the distance between 2 positions.
|
|
*
|
|
* @hidden
|
|
*/
|
|
const distanceBetweenPoints = (p1, p2) => {
|
|
const R = 6371; // Radius of the Earth in km
|
|
const dLat = ((p2.lat - p1.lat) * Math.PI) / 180;
|
|
const dLon = ((p2.lng - p1.lng) * Math.PI) / 180;
|
|
const sinDLat = Math.sin(dLat / 2);
|
|
const sinDLon = Math.sin(dLon / 2);
|
|
const a = sinDLat * sinDLat +
|
|
Math.cos((p1.lat * Math.PI) / 180) *
|
|
Math.cos((p2.lat * Math.PI) / 180) *
|
|
sinDLon *
|
|
sinDLon;
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
};
|
|
/**
|
|
* Converts a LatLng bound to pixels.
|
|
*
|
|
* @hidden
|
|
*/
|
|
const latLngBoundsToPixelBounds = (bounds, projection) => {
|
|
return {
|
|
northEast: projection.fromLatLngToDivPixel(bounds.getNorthEast()),
|
|
southWest: projection.fromLatLngToDivPixel(bounds.getSouthWest()),
|
|
};
|
|
};
|
|
/**
|
|
* Extends a pixel bounds by numPixels in all directions.
|
|
*
|
|
* @hidden
|
|
*/
|
|
const extendPixelBounds = ({ northEast, southWest }, numPixels) => {
|
|
northEast.x += numPixels;
|
|
northEast.y -= numPixels;
|
|
southWest.x -= numPixels;
|
|
southWest.y += numPixels;
|
|
return { northEast, southWest };
|
|
};
|
|
/**
|
|
* @hidden
|
|
*/
|
|
const pixelBoundsToLatLngBounds = ({ northEast, southWest }, projection) => {
|
|
const sw = projection.fromDivPixelToLatLng(southWest);
|
|
const ne = projection.fromDivPixelToLatLng(northEast);
|
|
return new google.maps.LatLngBounds(sw, ne);
|
|
};
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* @hidden
|
|
*/
|
|
class AbstractAlgorithm {
|
|
constructor({ maxZoom = 16 }) {
|
|
this.maxZoom = maxZoom;
|
|
}
|
|
/**
|
|
* Helper function to bypass clustering based upon some map state such as
|
|
* zoom, number of markers, etc.
|
|
*
|
|
* ```typescript
|
|
* cluster({markers, map}: AlgorithmInput): Cluster[] {
|
|
* if (shouldBypassClustering(map)) {
|
|
* return this.noop({markers})
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
noop({ markers, }) {
|
|
return noop(markers);
|
|
}
|
|
}
|
|
/**
|
|
* Abstract viewport algorithm proves a class to filter markers by a padded
|
|
* viewport. This is a common optimization.
|
|
*
|
|
* @hidden
|
|
*/
|
|
class AbstractViewportAlgorithm extends AbstractAlgorithm {
|
|
constructor(_a) {
|
|
var { viewportPadding = 60 } = _a, options = __rest(_a, ["viewportPadding"]);
|
|
super(options);
|
|
this.viewportPadding = 60;
|
|
this.viewportPadding = viewportPadding;
|
|
}
|
|
calculate({ markers, map, mapCanvasProjection, }) {
|
|
if (map.getZoom() >= this.maxZoom) {
|
|
return {
|
|
clusters: this.noop({
|
|
markers,
|
|
}),
|
|
changed: false,
|
|
};
|
|
}
|
|
return {
|
|
clusters: this.cluster({
|
|
markers: filterMarkersToPaddedViewport(map, mapCanvasProjection, markers, this.viewportPadding),
|
|
map,
|
|
mapCanvasProjection,
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* @hidden
|
|
*/
|
|
const noop = (markers) => {
|
|
const clusters = markers.map((marker) => new Cluster({
|
|
position: MarkerUtils.getPosition(marker),
|
|
markers: [marker],
|
|
}));
|
|
return clusters;
|
|
};
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* The default Grid algorithm historically used in Google Maps marker
|
|
* clustering.
|
|
*
|
|
* The Grid algorithm does not implement caching and markers may flash as the
|
|
* viewport changes. Instead use {@link SuperClusterAlgorithm}.
|
|
*/
|
|
class GridAlgorithm extends AbstractViewportAlgorithm {
|
|
constructor(_a) {
|
|
var { maxDistance = 40000, gridSize = 40 } = _a, options = __rest(_a, ["maxDistance", "gridSize"]);
|
|
super(options);
|
|
this.clusters = [];
|
|
this.state = { zoom: -1 };
|
|
this.maxDistance = maxDistance;
|
|
this.gridSize = gridSize;
|
|
}
|
|
calculate({ markers, map, mapCanvasProjection, }) {
|
|
const state = { zoom: map.getZoom() };
|
|
let changed = false;
|
|
if (this.state.zoom >= this.maxZoom && state.zoom >= this.maxZoom) ;
|
|
else {
|
|
changed = !equal(this.state, state);
|
|
}
|
|
this.state = state;
|
|
if (map.getZoom() >= this.maxZoom) {
|
|
return {
|
|
clusters: this.noop({
|
|
markers,
|
|
}),
|
|
changed,
|
|
};
|
|
}
|
|
return {
|
|
clusters: this.cluster({
|
|
markers: filterMarkersToPaddedViewport(map, mapCanvasProjection, markers, this.viewportPadding),
|
|
map,
|
|
mapCanvasProjection,
|
|
}),
|
|
};
|
|
}
|
|
cluster({ markers, map, mapCanvasProjection, }) {
|
|
this.clusters = [];
|
|
markers.forEach((marker) => {
|
|
this.addToClosestCluster(marker, map, mapCanvasProjection);
|
|
});
|
|
return this.clusters;
|
|
}
|
|
addToClosestCluster(marker, map, projection) {
|
|
let maxDistance = this.maxDistance; // Some large number
|
|
let cluster = null;
|
|
for (let i = 0; i < this.clusters.length; i++) {
|
|
const candidate = this.clusters[i];
|
|
const distance = distanceBetweenPoints(candidate.bounds.getCenter().toJSON(), MarkerUtils.getPosition(marker).toJSON());
|
|
if (distance < maxDistance) {
|
|
maxDistance = distance;
|
|
cluster = candidate;
|
|
}
|
|
}
|
|
if (cluster &&
|
|
extendBoundsToPaddedViewport(cluster.bounds, projection, this.gridSize).contains(MarkerUtils.getPosition(marker))) {
|
|
cluster.push(marker);
|
|
}
|
|
else {
|
|
const cluster = new Cluster({ markers: [marker] });
|
|
this.clusters.push(cluster);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* Noop algorithm does not generate any clusters or filter markers by the an extended viewport.
|
|
*/
|
|
class NoopAlgorithm extends AbstractAlgorithm {
|
|
constructor(_a) {
|
|
var options = __rest(_a, []);
|
|
super(options);
|
|
}
|
|
calculate({ markers, map, mapCanvasProjection, }) {
|
|
return {
|
|
clusters: this.cluster({ markers, map, mapCanvasProjection }),
|
|
changed: false,
|
|
};
|
|
}
|
|
cluster(input) {
|
|
return this.noop(input);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* A very fast JavaScript algorithm for geospatial point clustering using KD trees.
|
|
*
|
|
* @see https://www.npmjs.com/package/supercluster for more information on options.
|
|
*/
|
|
class SuperClusterAlgorithm extends AbstractAlgorithm {
|
|
constructor(_a) {
|
|
var { maxZoom, radius = 60 } = _a, options = __rest(_a, ["maxZoom", "radius"]);
|
|
super({ maxZoom });
|
|
this.state = { zoom: -1 };
|
|
this.superCluster = new SuperCluster(Object.assign({ maxZoom: this.maxZoom, radius }, options));
|
|
}
|
|
calculate(input) {
|
|
let changed = false;
|
|
const state = { zoom: input.map.getZoom() };
|
|
if (!equal(input.markers, this.markers)) {
|
|
changed = true;
|
|
// TODO use proxy to avoid copy?
|
|
this.markers = [...input.markers];
|
|
const points = this.markers.map((marker) => {
|
|
const position = MarkerUtils.getPosition(marker);
|
|
const coordinates = [position.lng(), position.lat()];
|
|
return {
|
|
type: "Feature",
|
|
geometry: {
|
|
type: "Point",
|
|
coordinates,
|
|
},
|
|
properties: { marker },
|
|
};
|
|
});
|
|
this.superCluster.load(points);
|
|
}
|
|
if (!changed) {
|
|
if (this.state.zoom <= this.maxZoom || state.zoom <= this.maxZoom) {
|
|
changed = !equal(this.state, state);
|
|
}
|
|
}
|
|
this.state = state;
|
|
if (changed) {
|
|
this.clusters = this.cluster(input);
|
|
}
|
|
return { clusters: this.clusters, changed };
|
|
}
|
|
cluster({ map }) {
|
|
return this.superCluster
|
|
.getClusters([-180, -90, 180, 90], Math.round(map.getZoom()))
|
|
.map((feature) => this.transformCluster(feature));
|
|
}
|
|
transformCluster({ geometry: { coordinates: [lng, lat], }, properties, }) {
|
|
if (properties.cluster) {
|
|
return new Cluster({
|
|
markers: this.superCluster
|
|
.getLeaves(properties.cluster_id, Infinity)
|
|
.map((leaf) => leaf.properties.marker),
|
|
position: { lat, lng },
|
|
});
|
|
}
|
|
const marker = properties.marker;
|
|
return new Cluster({
|
|
markers: [marker],
|
|
position: MarkerUtils.getPosition(marker),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* A very fast JavaScript algorithm for geospatial point clustering using KD trees.
|
|
*
|
|
* @see https://www.npmjs.com/package/supercluster for more information on options.
|
|
*/
|
|
class SuperClusterViewportAlgorithm extends AbstractViewportAlgorithm {
|
|
constructor(_a) {
|
|
var { maxZoom, radius = 60, viewportPadding = 60 } = _a, options = __rest(_a, ["maxZoom", "radius", "viewportPadding"]);
|
|
super({ maxZoom, viewportPadding });
|
|
this.superCluster = new SuperCluster(Object.assign({ maxZoom: this.maxZoom, radius }, options));
|
|
this.state = { zoom: -1, view: [0, 0, 0, 0] };
|
|
}
|
|
calculate(input) {
|
|
const state = {
|
|
zoom: Math.round(input.map.getZoom()),
|
|
view: getPaddedViewport(input.map.getBounds(), input.mapCanvasProjection, this.viewportPadding),
|
|
};
|
|
let changed = !equal(this.state, state);
|
|
if (!equal(input.markers, this.markers)) {
|
|
changed = true;
|
|
// TODO use proxy to avoid copy?
|
|
this.markers = [...input.markers];
|
|
const points = this.markers.map((marker) => {
|
|
const position = MarkerUtils.getPosition(marker);
|
|
const coordinates = [position.lng(), position.lat()];
|
|
return {
|
|
type: "Feature",
|
|
geometry: {
|
|
type: "Point",
|
|
coordinates,
|
|
},
|
|
properties: { marker },
|
|
};
|
|
});
|
|
this.superCluster.load(points);
|
|
}
|
|
if (changed) {
|
|
this.clusters = this.cluster(input);
|
|
this.state = state;
|
|
}
|
|
return { clusters: this.clusters, changed };
|
|
}
|
|
cluster({ map, mapCanvasProjection }) {
|
|
/* recalculate new state because we can't use the cached version. */
|
|
const state = {
|
|
zoom: Math.round(map.getZoom()),
|
|
view: getPaddedViewport(map.getBounds(), mapCanvasProjection, this.viewportPadding),
|
|
};
|
|
return this.superCluster
|
|
.getClusters(state.view, state.zoom)
|
|
.map((feature) => this.transformCluster(feature));
|
|
}
|
|
transformCluster({ geometry: { coordinates: [lng, lat], }, properties, }) {
|
|
if (properties.cluster) {
|
|
return new Cluster({
|
|
markers: this.superCluster
|
|
.getLeaves(properties.cluster_id, Infinity)
|
|
.map((leaf) => leaf.properties.marker),
|
|
position: { lat, lng },
|
|
});
|
|
}
|
|
const marker = properties.marker;
|
|
return new Cluster({
|
|
markers: [marker],
|
|
position: MarkerUtils.getPosition(marker),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* Provides statistics on all clusters in the current render cycle for use in {@link Renderer.render}.
|
|
*/
|
|
class ClusterStats {
|
|
constructor(markers, clusters) {
|
|
this.markers = { sum: markers.length };
|
|
const clusterMarkerCounts = clusters.map((a) => a.count);
|
|
const clusterMarkerSum = clusterMarkerCounts.reduce((a, b) => a + b, 0);
|
|
this.clusters = {
|
|
count: clusters.length,
|
|
markers: {
|
|
mean: clusterMarkerSum / clusters.length,
|
|
sum: clusterMarkerSum,
|
|
min: Math.min(...clusterMarkerCounts),
|
|
max: Math.max(...clusterMarkerCounts),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
class DefaultRenderer {
|
|
/**
|
|
* The default render function for the library used by {@link MarkerClusterer}.
|
|
*
|
|
* Currently set to use the following:
|
|
*
|
|
* ```typescript
|
|
* // change color if this cluster has more markers than the mean cluster
|
|
* const color =
|
|
* count > Math.max(10, stats.clusters.markers.mean)
|
|
* ? "#ff0000"
|
|
* : "#0000ff";
|
|
*
|
|
* // create svg url with fill color
|
|
* const svg = window.btoa(`
|
|
* <svg fill="${color}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
|
* <circle cx="120" cy="120" opacity=".6" r="70" />
|
|
* <circle cx="120" cy="120" opacity=".3" r="90" />
|
|
* <circle cx="120" cy="120" opacity=".2" r="110" />
|
|
* <circle cx="120" cy="120" opacity=".1" r="130" />
|
|
* </svg>`);
|
|
*
|
|
* // create marker using svg icon
|
|
* return new google.maps.Marker({
|
|
* position,
|
|
* icon: {
|
|
* url: `data:image/svg+xml;base64,${svg}`,
|
|
* scaledSize: new google.maps.Size(45, 45),
|
|
* },
|
|
* label: {
|
|
* text: String(count),
|
|
* color: "rgba(255,255,255,0.9)",
|
|
* fontSize: "12px",
|
|
* },
|
|
* // adjust zIndex to be above other markers
|
|
* zIndex: 1000 + count,
|
|
* });
|
|
* ```
|
|
*/
|
|
render({ count, position }, stats, map) {
|
|
// change color if this cluster has more markers than the mean cluster
|
|
const color = count > Math.max(10, stats.clusters.markers.mean) ? "#ff0000" : "#0000ff";
|
|
// create svg literal with fill color
|
|
const svg = `<svg fill="${color}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="50" height="50">
|
|
<circle cx="120" cy="120" opacity=".6" r="70" />
|
|
<circle cx="120" cy="120" opacity=".3" r="90" />
|
|
<circle cx="120" cy="120" opacity=".2" r="110" />
|
|
<text x="50%" y="50%" style="fill:#fff" text-anchor="middle" font-size="50" dominant-baseline="middle" font-family="roboto,arial,sans-serif">${count}</text>
|
|
</svg>`;
|
|
const title = `Cluster of ${count} markers`,
|
|
// adjust zIndex to be above other markers
|
|
zIndex = Number(google.maps.Marker.MAX_ZINDEX) + count;
|
|
if (MarkerUtils.isAdvancedMarkerAvailable(map)) {
|
|
// create cluster SVG element
|
|
const parser = new DOMParser();
|
|
const svgEl = parser.parseFromString(svg, "image/svg+xml").documentElement;
|
|
svgEl.setAttribute("transform", "translate(0 25)");
|
|
const clusterOptions = {
|
|
map,
|
|
position,
|
|
zIndex,
|
|
title,
|
|
content: svgEl,
|
|
};
|
|
return new google.maps.marker.AdvancedMarkerElement(clusterOptions);
|
|
}
|
|
const clusterOptions = {
|
|
position,
|
|
zIndex,
|
|
title,
|
|
icon: {
|
|
url: `data:image/svg+xml;base64,${btoa(svg)}`,
|
|
anchor: new google.maps.Point(25, 25),
|
|
},
|
|
};
|
|
return new google.maps.Marker(clusterOptions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2019 Google LLC. All Rights Reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/**
|
|
* Extends an object's prototype by another's.
|
|
*
|
|
* @param type1 The Type to be extended.
|
|
* @param type2 The Type to extend with.
|
|
* @ignore
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function extend(type1, type2) {
|
|
/* istanbul ignore next */
|
|
// eslint-disable-next-line prefer-const
|
|
for (let property in type2.prototype) {
|
|
type1.prototype[property] = type2.prototype[property];
|
|
}
|
|
}
|
|
/**
|
|
* @ignore
|
|
*/
|
|
class OverlayViewSafe {
|
|
constructor() {
|
|
// MarkerClusterer implements google.maps.OverlayView interface. We use the
|
|
// extend function to extend MarkerClusterer with google.maps.OverlayView
|
|
// because it might not always be available when the code is defined so we
|
|
// look for it at the last possible moment. If it doesn't exist now then
|
|
// there is no point going ahead :)
|
|
extend(OverlayViewSafe, google.maps.OverlayView);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
var MarkerClustererEvents;
|
|
(function (MarkerClustererEvents) {
|
|
MarkerClustererEvents["CLUSTERING_BEGIN"] = "clusteringbegin";
|
|
MarkerClustererEvents["CLUSTERING_END"] = "clusteringend";
|
|
MarkerClustererEvents["CLUSTER_CLICK"] = "click";
|
|
})(MarkerClustererEvents || (MarkerClustererEvents = {}));
|
|
const defaultOnClusterClickHandler = (_, cluster, map) => {
|
|
map.fitBounds(cluster.bounds);
|
|
};
|
|
/**
|
|
* MarkerClusterer creates and manages per-zoom-level clusters for large amounts
|
|
* of markers. See {@link MarkerClustererOptions} for more details.
|
|
*
|
|
*/
|
|
class MarkerClusterer extends OverlayViewSafe {
|
|
constructor({ map, markers = [], algorithmOptions = {}, algorithm = new SuperClusterAlgorithm(algorithmOptions), renderer = new DefaultRenderer(), onClusterClick = defaultOnClusterClickHandler, }) {
|
|
super();
|
|
this.markers = [...markers];
|
|
this.clusters = [];
|
|
this.algorithm = algorithm;
|
|
this.renderer = renderer;
|
|
this.onClusterClick = onClusterClick;
|
|
if (map) {
|
|
this.setMap(map);
|
|
}
|
|
}
|
|
addMarker(marker, noDraw) {
|
|
if (this.markers.includes(marker)) {
|
|
return;
|
|
}
|
|
this.markers.push(marker);
|
|
if (!noDraw) {
|
|
this.render();
|
|
}
|
|
}
|
|
addMarkers(markers, noDraw) {
|
|
markers.forEach((marker) => {
|
|
this.addMarker(marker, true);
|
|
});
|
|
if (!noDraw) {
|
|
this.render();
|
|
}
|
|
}
|
|
removeMarker(marker, noDraw) {
|
|
const index = this.markers.indexOf(marker);
|
|
if (index === -1) {
|
|
// Marker is not in our list of markers, so do nothing:
|
|
return false;
|
|
}
|
|
MarkerUtils.setMap(marker, null);
|
|
this.markers.splice(index, 1); // Remove the marker from the list of managed markers
|
|
if (!noDraw) {
|
|
this.render();
|
|
}
|
|
return true;
|
|
}
|
|
removeMarkers(markers, noDraw) {
|
|
let removed = false;
|
|
markers.forEach((marker) => {
|
|
removed = this.removeMarker(marker, true) || removed;
|
|
});
|
|
if (removed && !noDraw) {
|
|
this.render();
|
|
}
|
|
return removed;
|
|
}
|
|
clearMarkers(noDraw) {
|
|
this.markers.length = 0;
|
|
if (!noDraw) {
|
|
this.render();
|
|
}
|
|
}
|
|
/**
|
|
* Recalculates and draws all the marker clusters.
|
|
*/
|
|
render() {
|
|
const map = this.getMap();
|
|
if (map instanceof google.maps.Map && map.getProjection()) {
|
|
google.maps.event.trigger(this, MarkerClustererEvents.CLUSTERING_BEGIN, this);
|
|
const { clusters, changed } = this.algorithm.calculate({
|
|
markers: this.markers,
|
|
map,
|
|
mapCanvasProjection: this.getProjection(),
|
|
});
|
|
// Allow algorithms to return flag on whether the clusters/markers have changed.
|
|
if (changed || changed == undefined) {
|
|
// Accumulate the markers of the clusters composed of a single marker.
|
|
// Those clusters directly use the marker.
|
|
// Clusters with more than one markers use a group marker generated by a renderer.
|
|
const singleMarker = new Set();
|
|
for (const cluster of clusters) {
|
|
if (cluster.markers.length == 1) {
|
|
singleMarker.add(cluster.markers[0]);
|
|
}
|
|
}
|
|
const groupMarkers = [];
|
|
// Iterate the clusters that are currently rendered.
|
|
for (const cluster of this.clusters) {
|
|
if (cluster.marker == null) {
|
|
continue;
|
|
}
|
|
if (cluster.markers.length == 1) {
|
|
if (!singleMarker.has(cluster.marker)) {
|
|
// The marker:
|
|
// - was previously rendered because it is from a cluster with 1 marker,
|
|
// - should no more be rendered as it is not in singleMarker.
|
|
MarkerUtils.setMap(cluster.marker, null);
|
|
}
|
|
}
|
|
else {
|
|
// Delay the removal of old group markers to avoid flickering.
|
|
groupMarkers.push(cluster.marker);
|
|
}
|
|
}
|
|
this.clusters = clusters;
|
|
this.renderClusters();
|
|
// Delayed removal of the markers of the former groups.
|
|
requestAnimationFrame(() => groupMarkers.forEach((marker) => MarkerUtils.setMap(marker, null)));
|
|
}
|
|
google.maps.event.trigger(this, MarkerClustererEvents.CLUSTERING_END, this);
|
|
}
|
|
}
|
|
onAdd() {
|
|
this.idleListener = this.getMap().addListener("idle", this.render.bind(this));
|
|
this.render();
|
|
}
|
|
onRemove() {
|
|
google.maps.event.removeListener(this.idleListener);
|
|
this.reset();
|
|
}
|
|
reset() {
|
|
this.markers.forEach((marker) => MarkerUtils.setMap(marker, null));
|
|
this.clusters.forEach((cluster) => cluster.delete());
|
|
this.clusters = [];
|
|
}
|
|
renderClusters() {
|
|
// Generate stats to pass to renderers.
|
|
const stats = new ClusterStats(this.markers, this.clusters);
|
|
const map = this.getMap();
|
|
this.clusters.forEach((cluster) => {
|
|
if (cluster.markers.length === 1) {
|
|
cluster.marker = cluster.markers[0];
|
|
}
|
|
else {
|
|
// Generate the marker to represent the group.
|
|
cluster.marker = this.renderer.render(cluster, stats, map);
|
|
// Make sure all individual markers are removed from the map.
|
|
cluster.markers.forEach((marker) => MarkerUtils.setMap(marker, null));
|
|
if (this.onClusterClick) {
|
|
cluster.marker.addListener("click",
|
|
/* istanbul ignore next */
|
|
(event) => {
|
|
google.maps.event.trigger(this, MarkerClustererEvents.CLUSTER_CLICK, cluster);
|
|
this.onClusterClick(event, cluster, map);
|
|
});
|
|
}
|
|
}
|
|
MarkerUtils.setMap(cluster.marker, map);
|
|
});
|
|
}
|
|
}
|
|
|
|
export { AbstractAlgorithm, AbstractViewportAlgorithm, Cluster, ClusterStats, DefaultRenderer, GridAlgorithm, MarkerClusterer, MarkerClustererEvents, MarkerUtils, NoopAlgorithm, SuperClusterAlgorithm, SuperClusterViewportAlgorithm, defaultOnClusterClickHandler, distanceBetweenPoints, extendBoundsToPaddedViewport, extendPixelBounds, filterMarkersToPaddedViewport, getPaddedViewport, noop, pixelBoundsToLatLngBounds };
|
|
//# sourceMappingURL=index.esm.js.map
|