From 496ae06bc34431343750f02416fa791276ee1b2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:13:29 +0000 Subject: [PATCH 1/7] chore(deps-dev): bump @babel/preset-env from 7.22.7 to 7.22.9 (#701) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.22.7 to 7.22.9. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.22.9/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb7f4bd0e..303bc619c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,9 +82,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", - "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -160,16 +160,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", - "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", + "@babel/compat-data": "^7.22.9", "@babel/helper-validator-option": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", "browserslist": "^4.21.9", - "lru-cache": "^5.1.1" + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1565,13 +1565,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.7.tgz", - "integrity": "sha512-1whfDtW+CzhETuzYXfcgZAh8/GFMeEbz0V5dVgya8YeJyCU6Y/P2Gnx4Qb3MylK68Zu9UiwUvbPMPTpFAOJ+sQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz", + "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-compilation-targets": "^7.22.6", + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", @@ -1645,11 +1645,11 @@ "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "^0.1.5", "@babel/types": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", "babel-plugin-polyfill-corejs2": "^0.4.4", "babel-plugin-polyfill-corejs3": "^0.8.2", "babel-plugin-polyfill-regenerator": "^0.5.1", - "core-js-compat": "^3.31.0" + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -10336,9 +10336,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" From 68ff918943c04d29aa141d083b51e3bd07c8ef3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:15:45 +0000 Subject: [PATCH 2/7] chore(deps-dev): bump eslint-plugin-jest from 27.2.2 to 27.2.3 (#702) Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.2.2 to 27.2.3. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v27.2.2...v27.2.3) --- updated-dependencies: - dependency-name: eslint-plugin-jest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 303bc619c..bcc2f19a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4760,9 +4760,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.2.tgz", - "integrity": "sha512-euzbp06F934Z7UDl5ZUaRPLAc9MKjh0rMPERrHT7UhlCEwgb25kBj37TvMgWeHZVkR5I9CayswrpoaqZU1RImw==", + "version": "27.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.3.tgz", + "integrity": "sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -4771,7 +4771,7 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", "eslint": "^7.0.0 || ^8.0.0", "jest": "*" }, From 12ac633d343f0e5b5ca0cf26a0f78108381070d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:18:00 +0000 Subject: [PATCH 3/7] chore(deps-dev): bump eslint from 8.44.0 to 8.45.0 (#703) Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.45.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.45.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bcc2f19a0..4a73560fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4692,9 +4692,9 @@ } }, "node_modules/eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -4722,7 +4722,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -4734,7 +4733,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { From 0ba3e66d57803c73a5860bb28809302aea48222c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 04:27:56 +0000 Subject: [PATCH 4/7] chore(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 (#705) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a73560fd..b5006457e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11637,9 +11637,9 @@ "dev": true }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" From d004a3ace52dff37e8fd9fe0c4118d1b63668d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 20:32:36 +0000 Subject: [PATCH 5/7] chore(deps-dev): bump @rollup/plugin-commonjs from 25.0.2 to 25.0.3 (#707) Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins/tree/HEAD/packages/commonjs) from 25.0.2 to 25.0.3. - [Changelog](https://github.com/rollup/plugins/blob/master/packages/commonjs/CHANGELOG.md) - [Commits](https://github.com/rollup/plugins/commits/commonjs-v25.0.3/packages/commonjs) --- updated-dependencies: - dependency-name: "@rollup/plugin-commonjs" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5006457e..63d4e4b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2745,9 +2745,9 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.2.tgz", - "integrity": "sha512-NGTwaJxIO0klMs+WSFFtBP7b9TdTJ3K76HZkewT8/+yHzMiUGVQgaPtLQxNVYIgT5F7lxkEyVID+yS3K7bhCow==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.3.tgz", + "integrity": "sha512-uBdtWr/H3BVcgm97MUdq2oJmqBR23ny1hOrWe2PKo9FTbjsGqg32jfasJUKYAI5ouqacjRnj65mBB/S79F+GQA==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", From fc8e8dd26d4bf24a7d0085b4a7fa484f0a7f98be Mon Sep 17 00:00:00 2001 From: Michael Waddell Date: Thu, 27 Jul 2023 12:07:54 -0500 Subject: [PATCH 6/7] feat: Adding viewport aware version of SuperCluster algorithm for use with legacy markers (#640) --- src/algorithms/index.ts | 1 + src/algorithms/superviewport.test.ts | 206 +++++++++++++++++++++++++++ src/algorithms/superviewport.ts | 149 +++++++++++++++++++ src/algorithms/utils.ts | 15 ++ 4 files changed, 371 insertions(+) create mode 100644 src/algorithms/superviewport.test.ts create mode 100644 src/algorithms/superviewport.ts diff --git a/src/algorithms/index.ts b/src/algorithms/index.ts index fb8cee304..d2dabc1f6 100644 --- a/src/algorithms/index.ts +++ b/src/algorithms/index.ts @@ -18,4 +18,5 @@ export * from "./core"; export * from "./grid"; export * from "./noop"; export * from "./supercluster"; +export * from "./superviewport"; export * from "./utils"; diff --git a/src/algorithms/superviewport.test.ts b/src/algorithms/superviewport.test.ts new file mode 100644 index 000000000..b87a32e25 --- /dev/null +++ b/src/algorithms/superviewport.test.ts @@ -0,0 +1,206 @@ +/** + * 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. + */ + +import { SuperClusterViewportAlgorithm } from "./superviewport"; +import { initialize, MapCanvasProjection } from "@googlemaps/jest-mocks"; +import { Marker } from "../marker-utils"; + +initialize(); +const markerClasses = [ + google.maps.Marker, + google.maps.marker.AdvancedMarkerElement, +]; + +describe.each(markerClasses)( + "SuperCluster works with legacy and Advanced Markers", + (markerClass) => { + let map: google.maps.Map; + + beforeEach(() => { + map = new google.maps.Map(document.createElement("div")); + }); + + test("should only call load if markers change", () => { + const mapCanvasProjection = new MapCanvasProjection(); + const markers: Marker[] = [new markerClass()]; + + const superCluster = new SuperClusterViewportAlgorithm({}); + superCluster["superCluster"].load = jest.fn(); + superCluster.cluster = jest.fn(); + + superCluster.calculate({ + markers, + map, + mapCanvasProjection, + }); + superCluster.calculate({ + markers, + map, + mapCanvasProjection, + }); + expect(superCluster["superCluster"].load).toHaveBeenCalledTimes(1); + expect(superCluster["superCluster"].load).toHaveBeenCalledWith([ + { + type: "Feature", + geometry: { coordinates: [0, 0], type: "Point" }, + properties: { marker: markers[0] }, + }, + ]); + }); + + test("should cluster markers", () => { + const mapCanvasProjection = new MapCanvasProjection(); + const markers: Marker[] = [new markerClass(), new markerClass()]; + + const superCluster = new SuperClusterViewportAlgorithm({}); + map.getZoom = jest.fn().mockReturnValue(0); + map.getBounds = jest.fn().mockReturnValue({ + toJSON: () => ({ + west: -180, + south: -90, + east: 180, + north: 90, + }), + getNorthEast: jest + .fn() + .mockReturnValue({ getLat: () => -3, getLng: () => 34 }), + getSouthWest: jest + .fn() + .mockReturnValue({ getLat: () => 29, getLng: () => 103 }), + }); + const { clusters } = superCluster.calculate({ + markers, + map, + mapCanvasProjection, + }); + + expect(clusters).toHaveLength(1); + }); + + test("should transform to Cluster with single marker if not cluster", () => { + const superCluster = new SuperClusterViewportAlgorithm({}); + const marker: Marker = new markerClass(); + + const cluster = superCluster["transformCluster"]({ + type: "Feature", + geometry: { coordinates: [0, 0], type: "Point" }, + properties: { + marker, + cluster: null, + cluster_id: null, + point_count: 1, + point_count_abbreviated: 1, + }, + }); + expect(cluster.markers.length).toEqual(1); + expect(cluster.markers[0]).toBe(marker); + }); + + test("should not cluster if zoom didn't change", () => { + const mapCanvasProjection = new MapCanvasProjection(); + const markers: Marker[] = [new markerClass(), new markerClass()]; + + const superCluster = new SuperClusterViewportAlgorithm({}); + superCluster["markers"] = markers; + superCluster["state"] = { zoom: 12, view: [1, 2, 3, 4] }; + superCluster.cluster = jest.fn().mockReturnValue([]); + superCluster["clusters"] = []; + + map.getZoom = jest.fn().mockReturnValue(superCluster["state"].zoom); + + const { clusters, changed } = superCluster.calculate({ + markers, + map, + mapCanvasProjection, + }); + + expect(changed).toBeTruthy(); + expect(clusters).toBe(superCluster["clusters"]); + }); + + test("should not cluster if zoom beyond maxZoom", () => { + const mapCanvasProjection = new MapCanvasProjection(); + const markers: Marker[] = [new markerClass(), new markerClass()]; + + const superCluster = new SuperClusterViewportAlgorithm({}); + superCluster["markers"] = markers; + superCluster["state"] = { zoom: 20, view: [1, 2, 3, 4] }; + superCluster.cluster = jest.fn().mockReturnValue([]); + superCluster["clusters"] = []; + + map.getZoom = jest.fn().mockReturnValue(superCluster["state"].zoom + 1); + + const { clusters, changed } = superCluster.calculate({ + markers, + map, + mapCanvasProjection, + }); + + expect(changed).toBeTruthy(); + expect(clusters).toBe(superCluster["clusters"]); + expect(superCluster["state"]).toEqual({ zoom: 21, view: [0, 0, 0, 0] }); + }); + + test("should round fractional zoom", () => { + const mapCanvasProjection = new MapCanvasProjection(); + const markers: Marker[] = [new markerClass(), new markerClass()]; + mapCanvasProjection.fromLatLngToDivPixel = jest + .fn() + .mockImplementation((b: google.maps.LatLng) => ({ + x: b.lat() * 100, + y: b.lng() * 100, + })); + mapCanvasProjection.fromDivPixelToLatLng = jest + .fn() + .mockImplementation( + (p: google.maps.Point) => + new google.maps.LatLng({ lat: p.x / 100, lng: p.y / 100 }) + ); + + map.getBounds = jest.fn().mockReturnValue({ + getNorthEast: jest + .fn() + .mockReturnValue({ lat: () => -3, lng: () => 34 }), + getSouthWest: jest + .fn() + .mockReturnValue({ lat: () => 29, lng: () => 103 }), + }); + + const superCluster = new SuperClusterViewportAlgorithm({}); + superCluster["superCluster"].getClusters = jest.fn().mockReturnValue([]); + superCluster["markers"] = markers; + superCluster["state"] = { zoom: 4, view: [1, 2, 3, 4] }; + superCluster["clusters"] = []; + + map.getZoom = jest.fn().mockReturnValue(1.534); + expect( + superCluster.calculate({ markers, map, mapCanvasProjection }) + ).toEqual({ changed: true, clusters: [] }); + + expect(superCluster["superCluster"].getClusters).toHaveBeenCalledWith( + [0, 0, 0, 0], + 2 + ); + + map.getZoom = jest.fn().mockReturnValue(3.234); + superCluster.calculate({ markers, map, mapCanvasProjection }); + expect(superCluster["superCluster"].getClusters).toHaveBeenCalledWith( + [0, 0, 0, 0], + 3 + ); + }); + } +); diff --git a/src/algorithms/superviewport.ts b/src/algorithms/superviewport.ts new file mode 100644 index 000000000..0f3b6c77b --- /dev/null +++ b/src/algorithms/superviewport.ts @@ -0,0 +1,149 @@ +/** + * 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. + */ + +import { + AbstractViewportAlgorithm, + AlgorithmInput, + AlgorithmOutput, + ViewportAlgorithmOptions, +} from "./core"; +import { SuperClusterOptions } from "./supercluster"; +import SuperCluster, { ClusterFeature } from "supercluster"; +import { MarkerUtils, Marker } from "../marker-utils"; +import { Cluster } from "../cluster"; +import { getPaddedViewport } from "./utils"; +import equal from "fast-deep-equal"; + +export interface SuperClusterViewportOptions + extends SuperClusterOptions, + ViewportAlgorithmOptions {} + +export interface SuperClusterViewportState { + /* The current zoom level */ + zoom: number; + + /* The current viewport as a bbox [westLng, southLat, eastLng, northLat] */ + view: [number, number, number, number]; +} + +/** + * A very fast JavaScript algorithm for geospatial point clustering using KD trees. + * + * @see https://www.npmjs.com/package/supercluster for more information on options. + */ +export class SuperClusterViewportAlgorithm extends AbstractViewportAlgorithm { + protected superCluster: SuperCluster; + protected markers: Marker[]; + protected clusters: Cluster[]; + protected state: SuperClusterViewportState; + + constructor({ + maxZoom, + radius = 60, + viewportPadding = 60, + ...options + }: SuperClusterViewportOptions) { + super({ maxZoom, viewportPadding }); + + this.superCluster = new SuperCluster({ + maxZoom: this.maxZoom, + radius, + ...options, + }); + + this.state = { zoom: -1, view: [0, 0, 0, 0] }; + } + + public calculate(input: AlgorithmInput): AlgorithmOutput { + const state: SuperClusterViewportState = { + 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" as const, + geometry: { + type: "Point" as const, + coordinates, + }, + properties: { marker }, + }; + }); + this.superCluster.load(points); + } + + if (changed) { + this.clusters = this.cluster(input); + this.state = state; + } + + return { clusters: this.clusters, changed }; + } + + public cluster({ map, mapCanvasProjection }: AlgorithmInput): Cluster[] { + /* recalculate new state because we can't use the cached version. */ + const state: SuperClusterViewportState = { + zoom: Math.round(map.getZoom()), + view: getPaddedViewport( + map.getBounds(), + mapCanvasProjection, + this.viewportPadding + ), + }; + + return this.superCluster + .getClusters(state.view, state.zoom) + .map((feature: ClusterFeature<{ marker: Marker }>) => + this.transformCluster(feature) + ); + } + + protected transformCluster({ + geometry: { + coordinates: [lng, lat], + }, + properties, + }: ClusterFeature<{ marker: Marker }>): Cluster { + 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), + }); + } +} diff --git a/src/algorithms/utils.ts b/src/algorithms/utils.ts index 54716430a..4e727c198 100644 --- a/src/algorithms/utils.ts +++ b/src/algorithms/utils.ts @@ -60,6 +60,21 @@ export const extendBoundsToPaddedViewport = ( return pixelBoundsToLatLngBounds(extendedPixelBounds, projection); }; +/** + * Gets the extended bounds as a bbox [westLng, southLat, eastLng, northLat] + */ +export const getPaddedViewport = ( + bounds: google.maps.LatLngBounds, + projection: google.maps.MapCanvasProjection, + pixels: number +): [number, number, number, number] => { + 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. * From e6d696323521279ea147e3f849c1a4dcc1359da4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Jul 2023 17:09:03 +0000 Subject: [PATCH 7/7] chore(release): 2.4.0 [skip ci] ## [2.4.0](https://github.com/googlemaps/js-markerclusterer/compare/v2.3.2...v2.4.0) (2023-07-27) ### Features * Adding viewport aware version of SuperCluster algorithm for use with legacy markers ([#640](https://github.com/googlemaps/js-markerclusterer/issues/640)) ([fc8e8dd](https://github.com/googlemaps/js-markerclusterer/commit/fc8e8dd26d4bf24a7d0085b4a7fa484f0a7f98be)) ### Miscellaneous Chores * **deps-dev:** bump @babel/preset-env from 7.22.7 to 7.22.9 ([#701](https://github.com/googlemaps/js-markerclusterer/issues/701)) ([496ae06](https://github.com/googlemaps/js-markerclusterer/commit/496ae06bc34431343750f02416fa791276ee1b2f)) * **deps-dev:** bump @rollup/plugin-commonjs from 25.0.2 to 25.0.3 ([#707](https://github.com/googlemaps/js-markerclusterer/issues/707)) ([d004a3a](https://github.com/googlemaps/js-markerclusterer/commit/d004a3ace52dff37e8fd9fe0c4118d1b63668d60)) * **deps-dev:** bump eslint from 8.44.0 to 8.45.0 ([#703](https://github.com/googlemaps/js-markerclusterer/issues/703)) ([12ac633](https://github.com/googlemaps/js-markerclusterer/commit/12ac633d343f0e5b5ca0cf26a0f78108381070d9)) * **deps-dev:** bump eslint-plugin-jest from 27.2.2 to 27.2.3 ([#702](https://github.com/googlemaps/js-markerclusterer/issues/702)) ([68ff918](https://github.com/googlemaps/js-markerclusterer/commit/68ff918943c04d29aa141d083b51e3bd07c8ef3d)) * **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 ([#705](https://github.com/googlemaps/js-markerclusterer/issues/705)) ([0ba3e66](https://github.com/googlemaps/js-markerclusterer/commit/0ba3e66d57803c73a5860bb28809302aea48222c)) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63d4e4b44..ea2bb3145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@googlemaps/markerclusterer", - "version": "2.3.2", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@googlemaps/markerclusterer", - "version": "2.3.2", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "fast-deep-equal": "^3.1.3", diff --git a/package.json b/package.json index 154639b19..9c9a4b49a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@googlemaps/markerclusterer", - "version": "2.3.2", + "version": "2.4.0", "description": "Creates and manages per-zoom-level clusters for large amounts of markers.", "keywords": [ "cluster",