From 92e0a336a9873ff993f7c6780e823856c1d7d017 Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Sat, 23 Aug 2025 13:07:30 +0100 Subject: [PATCH 01/10] make cluster ids negative --- index.js | 74 +++-- package-lock.json | 446 ++++++++++---------------- package.json | 4 +- test/fixtures/places-z0-0-0-min5.json | 68 ++-- test/fixtures/places-z0-0-0.json | 100 +++--- test/test.js | 25 +- 6 files changed, 316 insertions(+), 401 deletions(-) diff --git a/index.js b/index.js index acd7d2a5..c6521fb4 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,17 @@ const OFFSET_PARENT = 4; const OFFSET_NUM = 5; const OFFSET_PROP = 6; +function pushClusterData(clusterData, reduce, x, y, zoom, id, parent, numPoints, properties) { + clusterData.push( + x, y, // projected point coordinates + zoom, // the last zoom the point was processed at + id, // index of the source feature in the original input array + parent, // parent cluster id + numPoints // number of points in a cluster + ); + if (reduce) clusterData.push(properties); // noop +} + export default class Supercluster { constructor(options) { this.options = Object.assign(Object.create(defaultOptions), options); @@ -75,14 +86,8 @@ export default class Supercluster { const x = fround(lngX(lng)); const y = fround(latY(lat)); // store internal point/cluster data in flat numeric arrays for performance - currentClusterData.push( - x, y, // projected point coordinates - Infinity, // the last zoom the point was processed at - i, // index of the source feature in the original input array - -1, // parent cluster id - 1 // number of points in a cluster - ); - if (this.options.reduce) currentClusterData.push(0); // noop + pushClusterData(currentClusterData, this.options.reduce, x, y, Infinity, i, -1, 1, 0); + // populate indexData-array because R-Tree needs an array of separate items. // TODO: possible optimization is forking RBush repo and change this to be more like KDBush? @@ -137,8 +142,8 @@ export default class Supercluster { } getChildren(clusterId) { - const originId = this._getOriginId(clusterId); - const originZoom = this._getOriginZoom(clusterId); + const originId = getOriginIdx(clusterId); + const originZoom = getOriginZoom(clusterId); const errorMsg = 'No cluster with the specified id.'; if (!this.trees[originZoom]) throw new Error(errorMsg); @@ -204,7 +209,7 @@ export default class Supercluster { } getClusterExpansionZoom(clusterId) { - let expansionZoom = this._getOriginZoom(clusterId) - 1; + let expansionZoom = getOriginZoom(clusterId) - 1; while (expansionZoom <= this.options.maxZoom) { const children = this.getChildren(clusterId); expansionZoom++; @@ -223,6 +228,27 @@ export default class Supercluster { lodashMerge(this.points[idx], clonedProperties); } + addPoint(point) { + const {maxZoom, reduce} = this.options; + const p = structuredClone(point); + this.points.push(p); + if (!p.geometry) return; + const [lng, lat] = p.geometry.coordinates; + const x = fround(lngX(lng)); + const y = fround(latY(lat)); + this.clusterData[maxZoom + 1].push( + x, y, // projected point coordinates + Infinity, // the last zoom the point was processed at + this.points.length - 1, // index of the source feature in the original input array + -1, // parent cluster id + 1 // number of points in a cluster + ); + if (reduce) this.clusterData[maxZoom + 1].push(0); + this.trees[maxZoom + 1].insert([x, y, this.points.length - 1]); + + // for (let z = maxZoom; z >= minZoom; z--) {} + } + _appendLeaves(result, clusterId, limit, offset, skipped) { const children = this.getChildren(clusterId); @@ -341,8 +367,8 @@ export default class Supercluster { let clusterProperties; let clusterPropIndex = -1; - // encode both zoom and point index on which the cluster originated -- offset by total length of features - const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length; + // encode both zoom and point index on which the cluster originated + const id = -(((i / stride | 0) << 5) + (zoom + 1)); for (const neighborId of neighborIds) { const k = neighborId * stride; @@ -390,16 +416,6 @@ export default class Supercluster { return nextIndexData; } - // get index of the point from which the cluster originated - _getOriginId(clusterId) { - return (clusterId - this.points.length) >> 5; - } - - // get zoom of the point from which the cluster originated - _getOriginZoom(clusterId) { - return (clusterId - this.points.length) % 32; - } - _map(data, i, clone) { if (data[i + OFFSET_NUM] > 1) { const props = this.clusterProps[data[i + OFFSET_PROP]]; @@ -429,6 +445,18 @@ export default class Supercluster { } } +// get index of the point from which the cluster originated +function getOriginIdx(clusterId) { + if (clusterId >= 0) throw new Error('A cluster id should be negative'); + return (-clusterId) >> 5; +} + +// get zoom of the point from which the cluster originated +function getOriginZoom(clusterId) { + if (clusterId >= 0) throw new Error('A cluster id should be negative'); + return (-clusterId) % 32; +} + function getClusterJSON(data, i, clusterProps) { return { type: 'Feature', diff --git a/package-lock.json b/package-lock.json index 1cbfc823..78e5f983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.1.0", "license": "ISC", "dependencies": { "lodash-es": "^4.17.21", @@ -15,7 +15,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", - "eslint": "^9.5.0", + "eslint": "^9.33.0", "eslint-config-mourner": "^4.0.0", "mkdirp": "^3.0.1", "rollup": "^4.18.0" @@ -51,54 +51,54 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", - "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "2.1.2" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -118,25 +118,74 @@ } }, "node_modules/@eslint/js": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", - "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -152,11 +201,10 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -229,44 +277,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", @@ -615,11 +625,10 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -642,7 +651,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -654,16 +662,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -684,22 +682,19 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -730,7 +725,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -783,15 +777,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -802,13 +794,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -850,28 +841,32 @@ } }, "node_modules/eslint": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", - "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/config-array": "^0.16.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.5.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.1", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -881,15 +876,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -899,6 +890,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-mourner": { @@ -913,11 +912,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -930,11 +928,10 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -942,16 +939,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -978,7 +980,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1017,15 +1018,13 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -1034,16 +1033,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1134,7 +1123,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -1166,21 +1154,19 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1261,29 +1247,17 @@ "dev": true, "license": "MIT" }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1302,8 +1276,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -1369,7 +1342,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1394,11 +1366,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -1462,7 +1433,6 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -1485,7 +1455,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1521,36 +1490,14 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", @@ -1597,22 +1544,10 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", @@ -1649,30 +1584,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1709,7 +1620,6 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1722,7 +1632,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1755,25 +1664,11 @@ "source-map": "^0.6.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -1826,13 +1721,6 @@ "node": ">=10" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -1851,7 +1739,6 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -1861,7 +1748,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index 85646d7b..f3c05221 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.1.0", "description": "A library for fast and mutable geospatial point clustering.", "main": "dist/mutable-supercluster.js", "type": "module", @@ -38,7 +38,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", - "eslint": "^9.5.0", + "eslint": "^9.33.0", "eslint-config-mourner": "^4.0.0", "mkdirp": "^3.0.1", "rollup": "^4.18.0" diff --git a/test/fixtures/places-z0-0-0-min5.json b/test/fixtures/places-z0-0-0-min5.json index 8c6ec515..1a5502c8 100644 --- a/test/fixtures/places-z0-0-0-min5.json +++ b/test/fixtures/places-z0-0-0-min5.json @@ -5,55 +5,55 @@ "geometry": [[151, 203]], "tags": { "cluster": true, - "cluster_id": 164, + "cluster_id": -1, "point_count": 15, "point_count_abbreviated": 15 }, - "id": 164 + "id": -1 }, { "type": 1, "geometry": [[165, 241]], "tags": { "cluster": true, - "cluster_id": 196, + "cluster_id": -33, "point_count": 20, "point_count_abbreviated": 20 }, - "id": 196 + "id": -33 }, { "type": 1, "geometry": [[178, 305]], "tags": { "cluster": true, - "cluster_id": 228, + "cluster_id": -65, "point_count": 14, "point_count_abbreviated": 14 }, - "id": 228 + "id": -65 }, { "type": 1, "geometry": [[329, 244]], "tags": { "cluster": true, - "cluster_id": 260, + "cluster_id": -97, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 260 + "id": -97 }, { "type": 1, "geometry": [[296, 291]], "tags": { "cluster": true, - "cluster_id": 356, + "cluster_id": -193, "point_count": 11, "point_count_abbreviated": 11 }, - "id": 356 + "id": -193 }, { "type": 1, @@ -120,11 +120,11 @@ "geometry": [[89, 209]], "tags": { "cluster": true, - "cluster_id": 548, + "cluster_id": -385, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 548 + "id": -385 }, { "type": 1, @@ -191,22 +191,22 @@ "geometry": [[242, 237]], "tags": { "cluster": true, - "cluster_id": 964, + "cluster_id": -801, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 964 + "id": -801 }, { "type": 1, "geometry": [[259, 193]], "tags": { "cluster": true, - "cluster_id": 1092, + "cluster_id": -929, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1092 + "id": -929 }, { "type": 1, @@ -303,11 +303,11 @@ "geometry": [[27, 270]], "tags": { "cluster": true, - "cluster_id": 1444, + "cluster_id": -1281, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1444 + "id": -1281 }, { "type": 1, @@ -389,33 +389,33 @@ "geometry": [[459, 309]], "tags": { "cluster": true, - "cluster_id": 1924, + "cluster_id": -1761, "point_count": 8, "point_count_abbreviated": 8 }, - "id": 1924 + "id": -1761 }, { "type": 1, "geometry": [[483, 272]], "tags": { "cluster": true, - "cluster_id": 2180, + "cluster_id": -2017, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 2180 + "id": -2017 }, { "type": 1, "geometry": [[423, 295]], "tags": { "cluster": true, - "cluster_id": 2340, + "cluster_id": -2177, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 2340 + "id": -2177 }, { "type": 1, @@ -482,22 +482,22 @@ "geometry": [[484, 235]], "tags": { "cluster": true, - "cluster_id": 2692, + "cluster_id": -2529, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 2692 + "id": -2529 }, { "type": 1, "geometry": [[471, 167]], "tags": { "cluster": true, - "cluster_id": 3236, + "cluster_id": -3073, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 3236 + "id": -3073 }, { "type": 1, @@ -579,22 +579,22 @@ "geometry": [[-29, 272]], "tags": { "cluster": true, - "cluster_id": 2180, + "cluster_id": -2017, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 2180 + "id": -2017 }, { "type": 1, "geometry": [[-28, 235]], "tags": { "cluster": true, - "cluster_id": 2692, + "cluster_id": -2529, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 2692 + "id": -2529 }, { "type": 1, @@ -631,11 +631,11 @@ "geometry": [[539, 270]], "tags": { "cluster": true, - "cluster_id": 1444, + "cluster_id": -1281, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1444 + "id": -1281 }, { "type": 1, diff --git a/test/fixtures/places-z0-0-0.json b/test/fixtures/places-z0-0-0.json index c7278d87..6f6a3eb6 100644 --- a/test/fixtures/places-z0-0-0.json +++ b/test/fixtures/places-z0-0-0.json @@ -5,77 +5,77 @@ "geometry": [[150, 205]], "tags": { "cluster": true, - "cluster_id": 164, + "cluster_id": -1, "point_count": 16, "point_count_abbreviated": 16 }, - "id": 164 + "id": -1 }, { "type": 1, "geometry": [[165, 240]], "tags": { "cluster": true, - "cluster_id": 196, + "cluster_id": -33, "point_count": 18, "point_count_abbreviated": 18 }, - "id": 196 + "id": -33 }, { "type": 1, "geometry": [[179, 303]], "tags": { "cluster": true, - "cluster_id": 228, + "cluster_id": -65, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 228 + "id": -65 }, { "type": 1, "geometry": [[336, 234]], "tags": { "cluster": true, - "cluster_id": 260, + "cluster_id": -97, "point_count": 8, "point_count_abbreviated": 8 }, - "id": 260 + "id": -97 }, { "type": 1, "geometry": [[299, 285]], "tags": { "cluster": true, - "cluster_id": 292, + "cluster_id": -129, "point_count": 15, "point_count_abbreviated": 15 }, - "id": 292 + "id": -129 }, { "type": 1, "geometry": [[71, 419]], "tags": { "cluster": true, - "cluster_id": 324, + "cluster_id": -161, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 324 + "id": -161 }, { "type": 1, "geometry": [[92, 212]], "tags": { "cluster": true, - "cluster_id": 420, + "cluster_id": -257, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 420 + "id": -257 }, { "type": 1, @@ -97,33 +97,33 @@ "geometry": [[162, 345]], "tags": { "cluster": true, - "cluster_id": 581, + "cluster_id": -418, "point_count": 3, "point_count_abbreviated": 3 }, - "id": 581 + "id": -418 }, { "type": 1, "geometry": [[236, 232]], "tags": { "cluster": true, - "cluster_id": 580, + "cluster_id": -417, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 580 + "id": -417 }, { "type": 1, "geometry": [[259, 193]], "tags": { "cluster": true, - "cluster_id": 644, + "cluster_id": -481, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 644 + "id": -481 }, { "type": 1, @@ -190,22 +190,22 @@ "geometry": [[220, 147]], "tags": { "cluster": true, - "cluster_id": 836, + "cluster_id": -673, "point_count": 3, "point_count_abbreviated": 3 }, - "id": 836 + "id": -673 }, { "type": 1, "geometry": [[27, 270]], "tags": { "cluster": true, - "cluster_id": 900, + "cluster_id": -737, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 900 + "id": -737 }, { "type": 1, @@ -242,44 +242,44 @@ "geometry": [[26, 115]], "tags": { "cluster": true, - "cluster_id": 1157, + "cluster_id": -994, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1157 + "id": -994 }, { "type": 1, "geometry": [[449, 304]], "tags": { "cluster": true, - "cluster_id": 1124, + "cluster_id": -961, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1124 + "id": -961 }, { "type": 1, "geometry": [[455, 272]], "tags": { "cluster": true, - "cluster_id": 1188, + "cluster_id": -1025, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 1188 + "id": -1025 }, { "type": 1, "geometry": [[227, 121]], "tags": { "cluster": true, - "cluster_id": 1701, + "cluster_id": -1538, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1701 + "id": -1538 }, { "type": 1, @@ -301,22 +301,22 @@ "geometry": [[484, 235]], "tags": { "cluster": true, - "cluster_id": 1380, + "cluster_id": -1217, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1380 + "id": -1217 }, { "type": 1, "geometry": [[503, 260]], "tags": { "cluster": true, - "cluster_id": 1925, + "cluster_id": -1762, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 1925 + "id": -1762 }, { "type": 1, @@ -339,11 +339,11 @@ "geometry": [[475, 165]], "tags": { "cluster": true, - "cluster_id": 1668, + "cluster_id": -1505, "point_count": 7, "point_count_abbreviated": 7 }, - "id": 1668 + "id": -1505 }, { "type": 1, @@ -395,33 +395,33 @@ "geometry": [[202, 262]], "tags": { "cluster": true, - "cluster_id": 4134, + "cluster_id": -3971, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 4134 + "id": -3971 }, { "type": 1, "geometry": [[-28, 235]], "tags": { "cluster": true, - "cluster_id": 1380, + "cluster_id": -1217, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1380 + "id": -1217 }, { "type": 1, "geometry": [[-9, 260]], "tags": { "cluster": true, - "cluster_id": 1925, + "cluster_id": -1762, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 1925 + "id": -1762 }, { "type": 1, @@ -444,11 +444,11 @@ "geometry": [[-37, 165]], "tags": { "cluster": true, - "cluster_id": 1668, + "cluster_id": -1505, "point_count": 7, "point_count_abbreviated": 7 }, - "id": 1668 + "id": -1505 }, { "type": 1, @@ -470,22 +470,22 @@ "geometry": [[539, 270]], "tags": { "cluster": true, - "cluster_id": 900, + "cluster_id": -737, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 900 + "id": -737 }, { "type": 1, "geometry": [[538, 115]], "tags": { "cluster": true, - "cluster_id": 1157, + "cluster_id": -994, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1157 + "id": -994 } ] } diff --git a/test/test.js b/test/test.js index 28eb737d..7361785b 100644 --- a/test/test.js +++ b/test/test.js @@ -34,13 +34,13 @@ test('supports minPoints option', () => { test('returns children of a cluster', () => { const index = new Supercluster({getId}).load(places.features); - const childCounts = index.getChildren(164).map(p => p.properties.point_count || 1); + const childCounts = index.getChildren(-1).map(p => p.properties.point_count || 1); assert.deepEqual(childCounts, [1, 7, 2, 6]); }); test('returns leaves of a cluster', () => { const index = new Supercluster({getId}).load(places.features); - const leafNames = index.getLeaves(164, 10, 5).map(p => p.properties.name); + const leafNames = index.getLeaves(-1, 10, 5).map(p => p.properties.name); assert.deepEqual(leafNames, [ 'I. de Cozumel', 'Cabo Gracias a Dios', @@ -70,17 +70,18 @@ test('getLeaves handles null-property features', () => { coordinates: [-79.04411780507252, 43.08771393436908] } }])); - const leaves = index.getLeaves(165, 1, 12); + const leaves = index.getLeaves(-1, 1, 12); + console.log(leaves); assert.equal(leaves[0].properties, null); }); test('returns cluster expansion zoom', () => { const index = new Supercluster({getId}).load(places.features); - assert.deepEqual(index.getClusterExpansionZoom(164), 1); - assert.deepEqual(index.getClusterExpansionZoom(196), 1); - assert.deepEqual(index.getClusterExpansionZoom(581), 2); - assert.deepEqual(index.getClusterExpansionZoom(1157), 2); - assert.deepEqual(index.getClusterExpansionZoom(4134), 3); + assert.deepEqual(index.getClusterExpansionZoom(-1), 1); + assert.deepEqual(index.getClusterExpansionZoom(-33), 1); + assert.deepEqual(index.getClusterExpansionZoom(-418), 2); + assert.deepEqual(index.getClusterExpansionZoom(-994), 2); + assert.deepEqual(index.getClusterExpansionZoom(-3971), 3); }); test('returns cluster expansion zoom for maxZoom', () => { @@ -91,7 +92,7 @@ test('returns cluster expansion zoom for maxZoom', () => { getId }).load(places.features); - assert.deepEqual(index.getClusterExpansionZoom(2504), 5); + assert.deepEqual(index.getClusterExpansionZoom(-2341), 5); }); test('aggregates cluster properties with reduce', () => { @@ -196,7 +197,7 @@ test('does not throw on zero items', () => { test('update properties succeeds', () => { const index = new Supercluster({getId}).load(places.features); - const leafNames = index.getLeaves(164, 3, 5).map(p => p.properties.name); + const leafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); assert.deepEqual(leafNames, [ 'I. de Cozumel', 'Cabo Gracias a Dios', @@ -204,7 +205,7 @@ test('update properties succeeds', () => { ]); // Update name of point 160, currently named I. de Cozumel. index.updatePointProperties(160, {properties: {name: 'New York'}}); - const newLeafNames = index.getLeaves(164, 3, 5).map(p => p.properties.name); + const newLeafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); assert.deepEqual(newLeafNames, [ 'New York', 'Cabo Gracias a Dios', @@ -217,7 +218,7 @@ test('update properties with different location fails', () => { // Change location of point 160 and try to update. index.updatePointProperties(160, {geometry: {coordinates: [0, 0]}}); // Result should not have changed. - const leafNames = index.getLeaves(164, 3, 5).map(p => p.properties.name); + const leafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); assert.deepEqual(leafNames, [ 'I. de Cozumel', 'Cabo Gracias a Dios', From c704cc1ed7600dd02d3343f366c4e521d05f7c5d Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Sat, 23 Aug 2025 13:43:39 +0100 Subject: [PATCH 02/10] remove separate clusterData function --- index.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 98758ec7..3ba009fc 100644 --- a/index.js +++ b/index.js @@ -42,17 +42,6 @@ const OFFSET_PARENT = 4; const OFFSET_NUM = 5; const OFFSET_PROP = 6; -function pushClusterData(clusterData, reduce, x, y, zoom, id, parent, numPoints, properties) { - clusterData.push( - x, y, // projected point coordinates - zoom, // the last zoom the point was processed at - id, // index of the source feature in the original input array - parent, // parent cluster id - numPoints // number of points in a cluster - ); - if (reduce) clusterData.push(properties); // noop -} - export default class Supercluster { constructor(options) { this.options = Object.assign(Object.create(defaultOptions), options); @@ -87,8 +76,14 @@ export default class Supercluster { const x = fround(lngX(lng)); const y = fround(latY(lat)); // store internal point/cluster data in flat numeric arrays for performance - pushClusterData(currentClusterData, this.options.reduce, x, y, Infinity, i, -1, 1, 0); - + currentClusterData.push( + x, y, // projected point coordinates + Infinity, // the last zoom the point was processed at + i, // index of the source feature in the original input array + -1, // parent cluster id + 1 // number of points in a cluster + ); + if (this.options.reduce) currentClusterData.push(0); // noop // populate indexData-array because R-Tree needs an array of separate items. // TODO: possible optimization is forking RBush repo and change this to be more like KDBush? From 72c9ce0612a948791dfd1c807038036f7c2bb1c5 Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:13:47 +0200 Subject: [PATCH 03/10] chore: run prettier --- index.js | 115 +++++++++++++++++++++---------------------- test/test.js | 135 ++++++++++++++++++++++++++++----------------------- 2 files changed, 132 insertions(+), 118 deletions(-) diff --git a/index.js b/index.js index c765fc0f..efc8f562 100644 --- a/index.js +++ b/index.js @@ -163,10 +163,10 @@ export default class Supercluster { return clusters; } - getChildren(clusterId) { - const originId = getOriginIdx(clusterId); - const originZoom = getOriginZoom(clusterId); - const errorMsg = 'No cluster with the specified id.'; + getChildren(clusterId) { + const originId = getOriginIdx(clusterId); + const originZoom = getOriginZoom(clusterId); + const errorMsg = "No cluster with the specified id."; if (!this.trees[originZoom]) throw new Error(errorMsg); @@ -251,46 +251,47 @@ export default class Supercluster { return tile.features.length ? tile : null; } - getClusterExpansionZoom(clusterId) { - let expansionZoom = getOriginZoom(clusterId) - 1; - while (expansionZoom <= this.options.maxZoom) { - const children = this.getChildren(clusterId); - expansionZoom++; - if (children.length !== 1) break; - clusterId = children[0].properties.cluster_id; - } - return expansionZoom; + getClusterExpansionZoom(clusterId) { + let expansionZoom = getOriginZoom(clusterId) - 1; + while (expansionZoom <= this.options.maxZoom) { + const children = this.getChildren(clusterId); + expansionZoom++; + if (children.length !== 1) break; + clusterId = children[0].properties.cluster_id; } + return expansionZoom; + } - updatePointProperties(id, properties) { - const idx = this._linearSearchInPoints(id); - if (!idx) throw new Error('No point with the given id could be found.'); + updatePointProperties(id, properties) { + const idx = this._linearSearchInPoints(id); + if (!idx) throw new Error("No point with the given id could be found."); - const clonedProperties = structuredClone(properties); - delete clonedProperties.geometry?.coordinates; - lodashMerge(this.points[idx], clonedProperties); - } + const clonedProperties = structuredClone(properties); + delete clonedProperties.geometry?.coordinates; + lodashMerge(this.points[idx], clonedProperties); + } - addPoint(point) { - const {maxZoom, reduce} = this.options; - const p = structuredClone(point); - this.points.push(p); - if (!p.geometry) return; - const [lng, lat] = p.geometry.coordinates; - const x = fround(lngX(lng)); - const y = fround(latY(lat)); - this.clusterData[maxZoom + 1].push( - x, y, // projected point coordinates - Infinity, // the last zoom the point was processed at - this.points.length - 1, // index of the source feature in the original input array - -1, // parent cluster id - 1 // number of points in a cluster - ); - if (reduce) this.clusterData[maxZoom + 1].push(0); - this.trees[maxZoom + 1].insert([x, y, this.points.length - 1]); + addPoint(point) { + const { maxZoom, reduce } = this.options; + const p = structuredClone(point); + this.points.push(p); + if (!p.geometry) return; + const [lng, lat] = p.geometry.coordinates; + const x = fround(lngX(lng)); + const y = fround(latY(lat)); + this.clusterData[maxZoom + 1].push( + x, + y, // projected point coordinates + Infinity, // the last zoom the point was processed at + this.points.length - 1, // index of the source feature in the original input array + -1, // parent cluster id + 1, // number of points in a cluster + ); + if (reduce) this.clusterData[maxZoom + 1].push(0); + this.trees[maxZoom + 1].insert([x, y, this.points.length - 1]); - // for (let z = maxZoom; z >= minZoom; z--) {} - } + // for (let z = maxZoom; z >= minZoom; z--) {} + } _appendLeaves(result, clusterId, limit, offset, skipped) { const children = this.getChildren(clusterId); @@ -421,8 +422,8 @@ export default class Supercluster { let clusterProperties; let clusterPropIndex = -1; - // encode both zoom and point index on which the cluster originated - const id = -(((i / stride | 0) << 5) + (zoom + 1)); + // encode both zoom and point index on which the cluster originated + const id = -((((i / stride) | 0) << 5) + (zoom + 1)); for (const neighborId of neighborIds) { const k = neighborId * stride; @@ -466,20 +467,20 @@ export default class Supercluster { nextIndexData.push([data[i], data[i + 1], nextIndexData.length]); for (let j = 0; j < stride; j++) nextClusterData.push(data[i + j]); - if (numPoints > 1) { - for (const neighborId of neighborIds) { - const k = neighborId * stride; - if (data[k + OFFSET_ZOOM] <= zoom) continue; - data[k + OFFSET_ZOOM] = zoom; - nextIndexData.push([data[k], data[k + 1], nextIndexData.length]); - for (let j = 0; j < stride; j++) nextClusterData.push(data[k + j]); - } - } - } + if (numPoints > 1) { + for (const neighborId of neighborIds) { + const k = neighborId * stride; + if (data[k + OFFSET_ZOOM] <= zoom) continue; + data[k + OFFSET_ZOOM] = zoom; + nextIndexData.push([data[k], data[k + 1], nextIndexData.length]); + for (let j = 0; j < stride; j++) nextClusterData.push(data[k + j]); + } } - this.clusterData[zoom] = nextClusterData; - return nextIndexData; + } } + this.clusterData[zoom] = nextClusterData; + return nextIndexData; + } _map(data, i, clone) { if (data[i + OFFSET_NUM] > 1) { @@ -519,14 +520,14 @@ export default class Supercluster { // get index of the point from which the cluster originated function getOriginIdx(clusterId) { - if (clusterId >= 0) throw new Error('A cluster id should be negative'); - return (-clusterId) >> 5; + if (clusterId >= 0) throw new Error("A cluster id should be negative"); + return -clusterId >> 5; } // get zoom of the point from which the cluster originated function getOriginZoom(clusterId) { - if (clusterId >= 0) throw new Error('A cluster id should be negative'); - return (-clusterId) % 32; + if (clusterId >= 0) throw new Error("A cluster id should be negative"); + return -clusterId % 32; } function getClusterJSON(data, i, clusterProps) { diff --git a/test/test.js b/test/test.js index 42e60cb4..12de5602 100644 --- a/test/test.js +++ b/test/test.js @@ -42,27 +42,33 @@ test("supports minPoints option", () => { assert.deepEqual(tile.features, sortedTileMin5Features); }); -test('returns children of a cluster', () => { - const index = new Supercluster({getId}).load(structuredClone(places.features)); - const childCounts = index.getChildren(-1).map(p => p.properties.point_count || 1); - assert.deepEqual(childCounts, [1, 7, 2, 6]); +test("returns children of a cluster", () => { + const index = new Supercluster({ getId }).load( + structuredClone(places.features), + ); + const childCounts = index + .getChildren(-1) + .map((p) => p.properties.point_count || 1); + assert.deepEqual(childCounts, [1, 7, 2, 6]); }); -test('returns leaves of a cluster', () => { - const index = new Supercluster({getId}).load(structuredClone(places.features)); - const leafNames = index.getLeaves(-1, 10, 5).map(p => p.properties.name); - assert.deepEqual(leafNames, [ - 'I. de Cozumel', - 'Cabo Gracias a Dios', - 'Grand Cayman', - 'Cape Bauld', - 'Miquelon', - 'Cape May', - 'Niagara Falls', - 'Cape Hatteras', - 'Cape Fear', - 'Cape Sable', - ]); +test("returns leaves of a cluster", () => { + const index = new Supercluster({ getId }).load( + structuredClone(places.features), + ); + const leafNames = index.getLeaves(-1, 10, 5).map((p) => p.properties.name); + assert.deepEqual(leafNames, [ + "I. de Cozumel", + "Cabo Gracias a Dios", + "Grand Cayman", + "Cape Bauld", + "Miquelon", + "Cape May", + "Niagara Falls", + "Cape Hatteras", + "Cape Fear", + "Cape Sable", + ]); }); test("generates unique ids with generateId option", () => { @@ -86,22 +92,25 @@ test("getLeaves handles null-property features", () => { type: "Feature", properties: null, geometry: { - type: 'Point', - coordinates: [-79.04411780507252, 43.08771393436908] - } - }])); - const leaves = index.getLeaves(-1, 1, 12); - console.log(leaves); - assert.equal(leaves[0].properties, null); + type: "Point", + coordinates: [-79.04411780507252, 43.08771393436908], + }, + }, + ]), + ); + const leaves = index.getLeaves(-1, 1, 12); + assert.equal(leaves[0].properties, null); }); -test('returns cluster expansion zoom', () => { - const index = new Supercluster({getId}).load(structuredClone(places.features)); - assert.deepEqual(index.getClusterExpansionZoom(-1), 1); - assert.deepEqual(index.getClusterExpansionZoom(-33), 1); - assert.deepEqual(index.getClusterExpansionZoom(-418), 2); - assert.deepEqual(index.getClusterExpansionZoom(-994), 2); - assert.deepEqual(index.getClusterExpansionZoom(-3971), 3); +test("returns cluster expansion zoom", () => { + const index = new Supercluster({ getId }).load( + structuredClone(places.features), + ); + assert.deepEqual(index.getClusterExpansionZoom(-1), 1); + assert.deepEqual(index.getClusterExpansionZoom(-33), 1); + assert.deepEqual(index.getClusterExpansionZoom(-418), 2); + assert.deepEqual(index.getClusterExpansionZoom(-994), 2); + assert.deepEqual(index.getClusterExpansionZoom(-3971), 3); }); test("returns cluster expansion zoom for maxZoom", () => { @@ -112,7 +121,7 @@ test("returns cluster expansion zoom for maxZoom", () => { getId, }).load(structuredClone(places.features)); - assert.deepEqual(index.getClusterExpansionZoom(-2341), 5); + assert.deepEqual(index.getClusterExpansionZoom(-2341), 5); }); test("aggregates cluster properties with reduce", () => { @@ -273,33 +282,37 @@ test("does not throw on zero items", () => { }); }); -test('update properties succeeds', () => { - const index = new Supercluster({getId}).load(structuredClone(places.features)); - const leafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); - assert.deepEqual(leafNames, [ - 'I. de Cozumel', - 'Cabo Gracias a Dios', - 'Grand Cayman', - ]); - // Update name of point 160, currently named I. de Cozumel. - index.updatePointProperties(160, {properties: {name: 'New York'}}); - const newLeafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); - assert.deepEqual(newLeafNames, [ - 'New York', - 'Cabo Gracias a Dios', - 'Grand Cayman', - ]); +test("update properties succeeds", () => { + const index = new Supercluster({ getId }).load( + structuredClone(places.features), + ); + const leafNames = index.getLeaves(-1, 3, 5).map((p) => p.properties.name); + assert.deepEqual(leafNames, [ + "I. de Cozumel", + "Cabo Gracias a Dios", + "Grand Cayman", + ]); + // Update name of point 160, currently named I. de Cozumel. + index.updatePointProperties(160, { properties: { name: "New York" } }); + const newLeafNames = index.getLeaves(-1, 3, 5).map((p) => p.properties.name); + assert.deepEqual(newLeafNames, [ + "New York", + "Cabo Gracias a Dios", + "Grand Cayman", + ]); }); -test('update properties with different location fails', () => { - const index = new Supercluster({getId}).load(structuredClone(places.features)); - // Change location of point 160 and try to update. - index.updatePointProperties(160, {geometry: {coordinates: [0, 0]}}); - // Result should not have changed. - const leafNames = index.getLeaves(-1, 3, 5).map(p => p.properties.name); - assert.deepEqual(leafNames, [ - 'I. de Cozumel', - 'Cabo Gracias a Dios', - 'Grand Cayman', - ]); +test("update properties with different location fails", () => { + const index = new Supercluster({ getId }).load( + structuredClone(places.features), + ); + // Change location of point 160 and try to update. + index.updatePointProperties(160, { geometry: { coordinates: [0, 0] } }); + // Result should not have changed. + const leafNames = index.getLeaves(-1, 3, 5).map((p) => p.properties.name); + assert.deepEqual(leafNames, [ + "I. de Cozumel", + "Cabo Gracias a Dios", + "Grand Cayman", + ]); }); From d4d209a37ee521008f9cb66ad3d90b2929908084 Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:06:16 +0200 Subject: [PATCH 04/10] implement _recluster --- index.js | 253 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 202 insertions(+), 51 deletions(-) diff --git a/index.js b/index.js index efc8f562..180e6291 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import RBush from "rbush"; -import { merge as lodashMerge } from "lodash-es"; +import { merge as lodashMerge, min } from "lodash-es"; class MyRBush extends RBush { toBBox([x, y]) { @@ -54,8 +54,6 @@ export default class Supercluster { this.options = Object.assign(Object.create(defaultOptions), options); if (!this.options.getId) throw new Error("The Id access function (options.getId) can not be null"); - this.trees = new Array(this.options.maxZoom + 1); - this.clusterData = new Array(this.options.maxZoom + 1); this.stride = this.options.reduce ? 7 : 6; this.clusterProps = []; this.getId = this.options.getId; @@ -69,11 +67,14 @@ export default class Supercluster { const timerId = `prepare ${points.length} points`; if (log) console.time(timerId); + this.trees = new Array(maxZoom + 2); + this.clusterData = Array.from({ length: maxZoom + 2 }, () => new Array()); + this.emptyIndices = Array.from({ length: maxZoom + 2 }, () => new Array()); this.points = structuredClone(points); + this.emptyPointIndices = new Array(); points.length = 0; // generate a cluster object for each point and index input points into a R-tree - const currentClusterData = []; const currentIndexData = []; for (let i = 0; i < this.points.length; i++) { @@ -84,22 +85,22 @@ export default class Supercluster { const x = fround(lngX(lng)); const y = fround(latY(lat)); // store internal point/cluster data in flat numeric arrays for performance - currentClusterData.push( + const currentNodeData = [ x, y, // projected point coordinates Infinity, // the last zoom the point was processed at i, // index of the source feature in the original input array -1, // parent cluster id 1, // number of points in a cluster - ); - if (this.options.reduce) currentClusterData.push(0); // noop + ]; + if (this.options.reduce) currentNodeData.push(0); // noop + const idx = this._addNodeToTree(maxZoom + 1, currentNodeData); // populate indexData-array because R-Tree needs an array of separate items. // TODO: possible optimization is forking RBush repo and change this to be more like KDBush? - currentIndexData.push([x, y, i]); + currentIndexData.push([x, y, idx]); } this.trees[maxZoom + 1] = this._createTree(currentIndexData); - this.clusterData[maxZoom + 1] = currentClusterData; if (log) console.timeEnd(timerId); @@ -173,9 +174,7 @@ export default class Supercluster { const data = this.clusterData[originZoom]; if (originId * this.stride >= data.length) throw new Error(errorMsg); - const r = - this.options.radius / - (this.options.extent * Math.pow(this.options.zoomFactor, originZoom - 1)); + const r = this._calculateRadius(originZoom - 1); const x = data[originId * this.stride]; const y = data[originId * this.stride + 1]; const ids = this._rbushWithin(x, y, originZoom, r); @@ -271,27 +270,29 @@ export default class Supercluster { lodashMerge(this.points[idx], clonedProperties); } - addPoint(point) { - const { maxZoom, reduce } = this.options; - const p = structuredClone(point); - this.points.push(p); - if (!p.geometry) return; - const [lng, lat] = p.geometry.coordinates; - const x = fround(lngX(lng)); - const y = fround(latY(lat)); - this.clusterData[maxZoom + 1].push( - x, - y, // projected point coordinates - Infinity, // the last zoom the point was processed at - this.points.length - 1, // index of the source feature in the original input array - -1, // parent cluster id - 1, // number of points in a cluster - ); - if (reduce) this.clusterData[maxZoom + 1].push(0); - this.trees[maxZoom + 1].insert([x, y, this.points.length - 1]); - - // for (let z = maxZoom; z >= minZoom; z--) {} - } + // addPoint(point) { + // const { minZoom, maxZoom, reduce } = this.options; + // const p = structuredClone(point); + // this.points.push(p); + // if (!p.geometry) return; + // const [lng, lat] = p.geometry.coordinates; + // const x = fround(lngX(lng)); + // const y = fround(latY(lat)); + // this.clusterData[maxZoom + 1].push( + // x, + // y, // projected point coordinates + // Infinity, // the last zoom the point was processed at + // this.points.length - 1, // index of the source feature in the original input array + // -1, // parent cluster id + // 1, // number of points in a cluster + // ); + // if (reduce) this.clusterData[maxZoom + 1].push(0); + // this.trees[maxZoom + 1].insert([x, y, this.points.length - 1]); + + // for (let z = maxZoom; z >= minZoom; z--) { + + // } + // } _appendLeaves(result, clusterId, limit, offset, skipped) { const children = this.getChildren(clusterId); @@ -385,16 +386,18 @@ export default class Supercluster { ); } - _cluster(zoom) { - const { radius, extent, reduce, minPoints, zoomFactor } = this.options; - const r = radius / (extent * Math.pow(zoomFactor, zoom)); + _cluster(zoom, childIdxs) { + const { reduce, minPoints } = this.options; + const r = this._calculateRadius(zoom); const data = this.clusterData[zoom + 1]; - const nextClusterData = []; const nextIndexData = []; const stride = this.stride; + childIdxs ??= [...Array(data.length / stride).keys()]; + // loop through each point - for (let i = 0; i < data.length; i += stride) { + for (const childIdx of childIdxs) { + const i = childIdx * stride; // if we've already visited the point at this zoom level, skip it if (data[i + OFFSET_ZOOM] <= zoom) continue; data[i + OFFSET_ZOOM] = zoom; @@ -448,40 +451,77 @@ export default class Supercluster { } data[i + OFFSET_PARENT] = id; - nextIndexData.push([ - wx / numPoints, - wy / numPoints, - nextIndexData.length, - ]); - nextClusterData.push( + const currentNodeData = [ wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints, - ); - if (reduce) nextClusterData.push(clusterPropIndex); + ]; + if (reduce) currentNodeData.push(clusterPropIndex); + const idx = this._addNodeToTree(zoom, currentNodeData); + nextIndexData.push([wx / numPoints, wy / numPoints, idx]); } else { // left points as unclustered - nextIndexData.push([data[i], data[i + 1], nextIndexData.length]); - for (let j = 0; j < stride; j++) nextClusterData.push(data[i + j]); + + nextIndexData.push([ + data[i], + data[i + 1], + this._addNodeToTree(zoom, data.slice(i, i + stride)), + ]); if (numPoints > 1) { for (const neighborId of neighborIds) { const k = neighborId * stride; if (data[k + OFFSET_ZOOM] <= zoom) continue; data[k + OFFSET_ZOOM] = zoom; - nextIndexData.push([data[k], data[k + 1], nextIndexData.length]); - for (let j = 0; j < stride; j++) nextClusterData.push(data[k + j]); + nextIndexData.push([ + data[k], + data[k + 1], + this._addNodeToTree(zoom, data.slice(k, k + stride)), + ]); } } } } - this.clusterData[zoom] = nextClusterData; return nextIndexData; } + _recluster(firstClusteringZoom, childLayerElements, ancestorRemovals) { + const { minZoom, maxZoom } = this.options; + ancestorRemovals ??= Array.from({ length: maxZoom + 1 }, () => new Array()); + + let contiguousChildIdxs = this._visitContiguous( + firstClusteringZoom + 1, + this._calculateRadius(firstClusteringZoom), + childLayerElements, + ); + + for (let zoom = firstClusteringZoom; zoom >= minZoom; zoom--) { + const removed = this._removeParentsOfChildren( + zoom, + contiguousChildIdxs.map((idx) => + this.clusterData[zoom + 1].slice(idx * stride, (idx + 1) * stride), + ), + ); + + this._removeAncestors(zoom - 1, removed); + + const newIndexData = this._cluster(zoom, contiguousChildIdxs); + this.trees[zoom].load(newIndexData); + + if (zoom > minZoom) { + contiguousChildIdxs = this._visitContiguous( + zoom, + this._calculateRadius(zoom - 1), + newIndexData.map((idxData) => idxData[2]), + [...ancestorRemovals[z], ...removed], + ); + } + } + } + _map(data, i, clone) { if (data[i + OFFSET_NUM] > 1) { const props = this.clusterProps[data[i + OFFSET_PROP]]; @@ -492,6 +532,112 @@ export default class Supercluster { return clone && result === original ? Object.assign({}, result) : result; } + _addNodeToTree(zoom, nodeData) { + let idx = -1; + if (this.emptyIndices[zoom].length > 0) { + idx = this.emptyIndices[zoom].pop(); + for (let i = 0; i < this.stride; i++) { + this.clusterData[zoom][idx * this.stride + i] = nodeData[i]; + } + return idx; + } + idx = this.clusterData[zoom].length / this.stride; + this.clusterData[zoom].push(...nodeData.slice(0, this.stride)); + return idx; + } + + _removeNodeFromTree(zoom, idx) { + this.trees[zoom].remove([null, null, idx], (a, b) => a[2] === b[2]); + for (let i = idx * this.stride; i < (idx + 1) * this.stride; i++) { + this.clusterData[zoom][i] = null; + } + this.emptyIndices[zoom].push(idx); + } + + _visitContiguous(zoom, searchRadius, elementIdxs, nonIndexedNodes) { + const { stride } = this.options; + const result = new Set(); + const notVisited = [...elementIdxs]; + + if (nonIndexedNodes) { + for (const node of nonIndexedNodes) { + for (const neighborIdx of this._rbushWithin( + node[0], + node[1], + zoom, + searchRadius, + )) { + if (!result.has(neighborIdx)) { + this.clusterData[zoom][neighborIdx * stride + OFFSET_ZOOM] = max( + zoom, + this.clusterData[zoom][neighborIdx * stride + OFFSET_ZOOM], + ); + result.add(neighborIdx); + notVisited.push(neighborIdx); + } + } + } + } + + while (notVisited.length > 0) { + const nodeIdx = notVisited.pop(); + for (const neighborIdx of this._rbushWithin( + this.clusterData[zoom][nodeIdx * stride], + this.clusterData[zoom][nodeIdx * stride + 1], + zoom, + searchRadius, + )) { + if (!result.has(neighborIdx)) { + this.clusterData[zoom][neighborIdx * stride + OFFSET_ZOOM] = max( + zoom, + this.clusterData[zoom][neighborIdx * stride + OFFSET_ZOOM], + ); + result.add(neighborIdx); + notVisited.push(neighborIdx); + } + } + } + return [...result]; + } + + _removeParentsOfChildren(zoom, childNodes) { + const { stride } = this.options; + const removedParentIds = new Set(); + const removedParentNodes = new Array(); + const r = this._calculateRadius(zoom); + + for (const node of childNodes) { + const parentIdxs = this._rbushWithin(node[0], node[1], zoom, r).filter( + (idx) => + !removedParentIds.has( + this.clusterData[zoom][idx * stride + OFFSET_ID] && + (node[OFFSET_ID] === + this.clusterData[zoom][idx * stride + OFFSET_ID] || + node[OFFSET_PARENT] === + this.clusterData[zoom][idx * stride + OFFSET_ID]), + ), + ); + + for (const idx of parentIdxs) { + removedParentIds.add(this.clusterData[zoom][idx * stride + OFFSET_ID]); + removedParentNodes.push( + this.clusterData[zoom].slice(idx * stride, (idx + 1) * stride), + ); + this._removeNodeFromTree(zoom, idx); + } + } + return removedParentNodes; + } + + _removeAncestors(firstZoom, descendantNodes, removals) { + const { minZoom } = this.options; + for (let zoom = firstZoom; zoom >= minZoom; zoom--) { + if (descendantNodes.length === 0) break; + descendantNodes = this._removeParentsOfChildren(zoom, descendantNodes); + removals[zoom].push(...descendantNodes); + } + } + _rbushWithin(ax, ay, zoom, radius) { const r2 = radius * radius; const pointsInSquare = this.trees[zoom].search({ @@ -516,6 +662,11 @@ export default class Supercluster { const index = this.points.findIndex((p) => this.getId(p) === id); return index !== -1 ? index : null; } + + _calculateRadius(zoom) { + const { radius, extent, zoomFactor } = this.options; + return radius / (extent * Math.pow(zoomFactor, zoom)); + } } // get index of the point from which the cluster originated From 532dd35ddc20f6c15db8008696d0f491006664a1 Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:24:41 +0200 Subject: [PATCH 05/10] add semi works --- add_test.js | 15 ++++++++ demo/index.html | 3 +- demo/index.js | 5 +++ demo/worker.js | 20 +++++++---- index.js | 89 ++++++++++++++++++++++++++++-------------------- rollup.config.js | 2 +- 6 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 add_test.js diff --git a/add_test.js b/add_test.js new file mode 100644 index 00000000..039c3c04 --- /dev/null +++ b/add_test.js @@ -0,0 +1,15 @@ +import { readFileSync } from "fs"; +import Supercluster from "./index.js"; + +const getId = (point) => point.pointId; +const places = JSON.parse( + readFileSync(new URL("./test/fixtures/places.json", import.meta.url)), +); + +const index = new Supercluster({ getId, log: true, maxZoom: 4 }).load( + structuredClone(places.features).slice(10, 14), +); + +index.addPoint(places.features[14]); + +console.log("done"); diff --git a/demo/index.html b/demo/index.html index f8f3908a..9ec6cbfd 100644 --- a/demo/index.html +++ b/demo/index.html @@ -22,13 +22,14 @@ html, body, #map { - height: 100%; + height: 95%; margin: 0; }
+