From 818003d24487b44eb72ffb58a3869c764732e8ea Mon Sep 17 00:00:00 2001 From: dariovillar Date: Tue, 11 Apr 2023 19:57:32 +0200 Subject: [PATCH 1/2] Support Decimal Zoom Steps and Update API Add support for requesting clusters on decimal zoom steps in the Supercluster JavaScript library. Previously, only integer zoom levels were supported. Introduce a new 'stepSize' parameter and incorporate it into the API. This update includes internal modifications to the handling of zoom levels, allowing for accommodation of decimal zoom steps. As an API-breaking change, the 'clusterId' structure has been altered. Previously, the 'clusterId' encoded the zoom level within the ID, supporting up to 32 levels. Now, it encodes the index of the 'trees' array. --- bench.js | 2 +- index.js | 65 ++-- package.json | 1 + test/fixtures/places-z0-0-0-min5.json | 409 ++++++++++++++++++++------ test/fixtures/places-z0-0-0.json | 372 +++++++++++++++++------ test/test.js | 45 ++- 6 files changed, 694 insertions(+), 200 deletions(-) diff --git a/bench.js b/bench.js index 4e734bcf..ef1b7b99 100644 --- a/bench.js +++ b/bench.js @@ -22,7 +22,7 @@ for (let i = 0; i < 1000000; i++) { global.gc(); const size = v8.getHeapStatistics().used_heap_size; -const index = new Supercluster({log: true, maxZoom: 6}).load(points); +const index = new Supercluster({log: true, maxZoom: 6, zoomStep: 0.5}).load(points); global.gc(); console.log(`memory used: ${ Math.round((v8.getHeapStatistics().used_heap_size - size) / 1024) } KB`); diff --git a/index.js b/index.js index dc2601ba..bd05eabd 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,9 @@ import KDBush from 'kdbush'; const defaultOptions = { - minZoom: 0, // min zoom to generate clusters on - maxZoom: 16, // max zoom level to cluster the points on + minZoom: 0.0, // min zoom to generate clusters on + maxZoom: 16.0, // max zoom level to cluster the points on + zoomStep: 1, // Indicate whether to compute clusters for each 0.1 zoom level. It may impact on the performance. minPoints: 2, // minimum points to form a cluster radius: 40, // cluster radius in pixels extent: 512, // tile extent (radius is calculated relative to it) @@ -25,13 +26,19 @@ const fround = Math.fround || (tmp => ((x) => { tmp[0] = +x; return tmp[0]; }))( export default class Supercluster { constructor(options) { this.options = extend(Object.create(defaultOptions), options); - this.trees = new Array(this.options.maxZoom + 1); + this.zoomStep = +(this.options.zoomStep).toFixed(1); + this.numTreesByZoomLevel = Math.ceil(1 / this.zoomStep); + this.zoomRange = this.options.maxZoom - this.options.minZoom; + this.numtrees = (this.zoomRange * this.numTreesByZoomLevel) + 2; + //Trees needed are, the number of trees on each zoom level times + // the range in zoom levels + this.trees = new Array(this.numtrees); } load(points) { - const {log, minZoom, maxZoom, nodeSize} = this.options; + const {log, minZoom, maxZoom, nodeSize, zoomStep} = this.options; - if (log) console.time('total time'); + if (log) console.time('total time', zoomStep); const timerId = `prepare ${ points.length } points`; if (log) console.time(timerId); @@ -44,20 +51,19 @@ export default class Supercluster { if (!points[i].geometry) continue; clusters.push(createPointCluster(points[i], i)); } - this.trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); + this.trees[this._zoomToIndex(this.options.maxZoom) + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); if (log) console.timeEnd(timerId); // cluster points on max zoom, then cluster the results on previous zoom, etc.; // results in a cluster hierarchy across zoom levels - for (let z = maxZoom; z >= minZoom; z--) { + for (let z = maxZoom; z >= minZoom; z -= this.zoomStep) { const now = +Date.now(); // create a new set of clusters for the zoom and index them with a KD-tree clusters = this._cluster(clusters, z); - this.trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); - - if (log) console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now); + this.trees[this._zoomToIndex(z)] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); + if (log) console.log('z%d: %d clusters in %dms', +z.toFixed(1), clusters.length, +Date.now() - now); } if (log) console.timeEnd('total time'); @@ -80,7 +86,7 @@ export default class Supercluster { return easternHem.concat(westernHem); } - const tree = this.trees[this._limitZoom(zoom)]; + const tree = this.trees[this._zoomToIndex(zoom)]; const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat)); const clusters = []; for (const id of ids) { @@ -94,12 +100,12 @@ export default class Supercluster { const originId = this._getOriginId(clusterId); const originZoom = this._getOriginZoom(clusterId); const errorMsg = 'No cluster with the specified id.'; - - const index = this.trees[originZoom]; + const errorMsg2 = 'No point with the specified id.'; + const index = this.trees[this._zoomToIndex(originZoom)]; if (!index) throw new Error(errorMsg); const origin = index.points[originId]; - if (!origin) throw new Error(errorMsg); + if (!origin) throw new Error(errorMsg2); const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)); const ids = index.within(origin.x, origin.y, r); @@ -127,7 +133,7 @@ export default class Supercluster { } getTile(z, x, y) { - const tree = this.trees[this._limitZoom(z)]; + const tree = this.trees[this._zoomToIndex(z)]; const z2 = Math.pow(2, z); const {extent, radius} = this.options; const p = radius / extent; @@ -160,7 +166,7 @@ export default class Supercluster { let expansionZoom = this._getOriginZoom(clusterId) - 1; while (expansionZoom <= this.options.maxZoom) { const children = this.getChildren(clusterId); - expansionZoom++; + expansionZoom += this.options.zoomStep; if (children.length !== 1) break; clusterId = children[0].properties.cluster_id; } @@ -239,14 +245,11 @@ export default class Supercluster { } } - _limitZoom(z) { - return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1)); - } - _cluster(points, zoom) { const clusters = []; const {radius, extent, reduce, minPoints} = this.options; const r = radius / (extent * Math.pow(2, zoom)); + const tree = this.trees[this._zoomToIndex(zoom) + 1]; // loop through each point for (let i = 0; i < points.length; i++) { @@ -256,7 +259,6 @@ export default class Supercluster { p.zoom = zoom; // find all nearby points - const tree = this.trees[zoom + 1]; const neighborIds = tree.within(p.x, p.y, r); const numPointsOrigin = p.numPoints || 1; @@ -277,7 +279,7 @@ export default class Supercluster { let clusterProperties = reduce && numPointsOrigin > 1 ? this._map(p, true) : null; // encode both zoom and point index on which the cluster originated -- offset by total length of features - const id = (i << 5) + (zoom + 1) + this.points.length; + const id = (i << 8) + (this._zoomToIndex(zoom) + 1) + this.points.length; for (const neighborId of neighborIds) { const b = tree.points[neighborId]; @@ -317,16 +319,31 @@ export default class Supercluster { return clusters; } + _zoomToIndex(zoom) { + const clampedZoom = Math.max(this.options.minZoom, Math.min(zoom.toFixed(1), this.options.maxZoom + this.zoomStep)); + // Get the index of the tree that better suits the zoom level + const adjustedZoom = clampedZoom - this.options.minZoom; + const stepIndex = Math.round(adjustedZoom * this.numTreesByZoomLevel); + return stepIndex; + } + // get index of the point from which the cluster originated _getOriginId(clusterId) { - return (clusterId - this.points.length) >> 5; + return (clusterId - this.points.length) >> 8; } // get zoom of the point from which the cluster originated + _getOriginIndex(clusterId) { + return (clusterId - this.points.length) % 256; + } + _getOriginZoom(clusterId) { - return (clusterId - this.points.length) % 32; + const originIndex = this._getOriginIndex(clusterId); + const originZoom = (originIndex / this.numTreesByZoomLevel) + this.options.minZoom; + return originZoom; } + _map(point, clone) { if (point.numPoints) { return clone ? extend({}, point.properties) : point.properties; diff --git a/package.json b/package.json index e091abcf..f79f47bd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "cov": "c8 npm run test", "bench": "node --expose-gc -r esm bench.js", "build": "mkdirp dist && rollup -c", + "fix": "eslint --fix index.js bench.js test/test.js demo/index.js demo/worker.js", "prepublishOnly": "npm run test && npm run build" }, "files": [ diff --git a/test/fixtures/places-z0-0-0-min5.json b/test/fixtures/places-z0-0-0-min5.json index 8c6ec515..1bd50d34 100644 --- a/test/fixtures/places-z0-0-0-min5.json +++ b/test/fixtures/places-z0-0-0-min5.json @@ -2,7 +2,12 @@ "features": [ { "type": 1, - "geometry": [[151, 203]], + "geometry": [ + [ + 151, + 203 + ] + ], "tags": { "cluster": true, "cluster_id": 164, @@ -13,51 +18,76 @@ }, { "type": 1, - "geometry": [[165, 241]], + "geometry": [ + [ + 165, + 241 + ] + ], "tags": { "cluster": true, - "cluster_id": 196, + "cluster_id": 420, "point_count": 20, "point_count_abbreviated": 20 }, - "id": 196 + "id": 420 }, { "type": 1, - "geometry": [[178, 305]], + "geometry": [ + [ + 178, + 305 + ] + ], "tags": { "cluster": true, - "cluster_id": 228, + "cluster_id": 676, "point_count": 14, "point_count_abbreviated": 14 }, - "id": 228 + "id": 676 }, { "type": 1, - "geometry": [[329, 244]], + "geometry": [ + [ + 329, + 244 + ] + ], "tags": { "cluster": true, - "cluster_id": 260, + "cluster_id": 932, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 260 + "id": 932 }, { "type": 1, - "geometry": [[296, 291]], + "geometry": [ + [ + 296, + 291 + ] + ], "tags": { "cluster": true, - "cluster_id": 356, + "cluster_id": 1700, "point_count": 11, "point_count_abbreviated": 11 }, - "id": 356 + "id": 1700 }, { "type": 1, - "geometry": [[90, 416]], + "geometry": [ + [ + 90, + 416 + ] + ], "tags": { "scalerank": 3, "name": "Wright I.", @@ -72,7 +102,12 @@ }, { "type": 1, - "geometry": [[74, 419]], + "geometry": [ + [ + 74, + 419 + ] + ], "tags": { "scalerank": 3, "name": "Dean I.", @@ -87,7 +122,12 @@ }, { "type": 1, - "geometry": [[69, 418]], + "geometry": [ + [ + 69, + 418 + ] + ], "tags": { "scalerank": 3, "name": "Grant I.", @@ -102,7 +142,12 @@ }, { "type": 1, - "geometry": [[49, 425]], + "geometry": [ + [ + 49, + 425 + ] + ], "tags": { "scalerank": 3, "name": "Newman I.", @@ -117,18 +162,28 @@ }, { "type": 1, - "geometry": [[89, 209]], + "geometry": [ + [ + 89, + 209 + ] + ], "tags": { "cluster": true, - "cluster_id": 548, + "cluster_id": 3236, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 548 + "id": 3236 }, { "type": 1, - "geometry": [[106, 226]], + "geometry": [ + [ + 106, + 226 + ] + ], "tags": { "scalerank": 4, "name": "Cabo Corrientes", @@ -143,7 +198,12 @@ }, { "type": 1, - "geometry": [[123, 152]], + "geometry": [ + [ + 123, + 152 + ] + ], "tags": { "scalerank": 3, "name": "Cape Churchill", @@ -158,7 +218,12 @@ }, { "type": 1, - "geometry": [[160, 352]], + "geometry": [ + [ + 160, + 352 + ] + ], "tags": { "scalerank": 3, "name": "Cabo de Hornos", @@ -173,7 +238,12 @@ }, { "type": 1, - "geometry": [[163, 349]], + "geometry": [ + [ + 163, + 349 + ] + ], "tags": { "scalerank": 5, "name": "Cabo San Diego", @@ -188,29 +258,44 @@ }, { "type": 1, - "geometry": [[242, 237]], + "geometry": [ + [ + 242, + 237 + ] + ], "tags": { "cluster": true, - "cluster_id": 964, + "cluster_id": 6564, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 964 + "id": 6564 }, { "type": 1, - "geometry": [[259, 193]], + "geometry": [ + [ + 259, + 193 + ] + ], "tags": { "cluster": true, - "cluster_id": 1092, + "cluster_id": 7588, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1092 + "id": 7588 }, { "type": 1, - "geometry": [[80, 336]], + "geometry": [ + [ + 80, + 336 + ] + ], "tags": { "scalerank": 3, "name": "Oceanic pole of inaccessibility", @@ -225,7 +310,12 @@ }, { "type": 1, - "geometry": [[452, 377]], + "geometry": [ + [ + 452, + 377 + ] + ], "tags": { "scalerank": 3, "name": "South Magnetic Pole 2005 (est)", @@ -240,7 +330,12 @@ }, { "type": 1, - "geometry": [[93, 32]], + "geometry": [ + [ + 93, + 32 + ] + ], "tags": { "scalerank": 3, "name": "North Magnetic Pole 2005 (est)", @@ -255,7 +350,12 @@ }, { "type": 1, - "geometry": [[159, 84]], + "geometry": [ + [ + 159, + 84 + ] + ], "tags": { "scalerank": 4, "name": "Cape York", @@ -270,7 +370,12 @@ }, { "type": 1, - "geometry": [[194, 149]], + "geometry": [ + [ + 194, + 149 + ] + ], "tags": { "scalerank": 4, "name": "Nunap Isua", @@ -285,7 +390,12 @@ }, { "type": 1, - "geometry": [[227, 139]], + "geometry": [ + [ + 227, + 139 + ] + ], "tags": { "scalerank": 5, "name": "Surtsey", @@ -300,18 +410,28 @@ }, { "type": 1, - "geometry": [[27, 270]], + "geometry": [ + [ + 27, + 270 + ] + ], "tags": { "cluster": true, - "cluster_id": 1444, + "cluster_id": 10404, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1444 + "id": 10404 }, { "type": 1, - "geometry": [[100, 296]], + "geometry": [ + [ + 100, + 296 + ] + ], "tags": { "scalerank": 4, "name": "I. de Pascua", @@ -326,7 +446,12 @@ }, { "type": 1, - "geometry": [[401, 226]], + "geometry": [ + [ + 401, + 226 + ] + ], "tags": { "scalerank": 4, "name": "Plain of Jars", @@ -341,7 +466,12 @@ }, { "type": 1, - "geometry": [[371, 248]], + "geometry": [ + [ + 371, + 248 + ] + ], "tags": { "scalerank": 5, "name": "Dondra Head", @@ -356,7 +486,12 @@ }, { "type": 1, - "geometry": [[19, 121]], + "geometry": [ + [ + 19, + 121 + ] + ], "tags": { "scalerank": 4, "name": "Cape Hope", @@ -371,7 +506,12 @@ }, { "type": 1, - "geometry": [[33, 109]], + "geometry": [ + [ + 33, + 109 + ] + ], "tags": { "scalerank": 4, "name": "Point Barrow", @@ -386,40 +526,60 @@ }, { "type": 1, - "geometry": [[459, 309]], + "geometry": [ + [ + 459, + 309 + ] + ], "tags": { "cluster": true, - "cluster_id": 1924, + "cluster_id": 14244, "point_count": 8, "point_count_abbreviated": 8 }, - "id": 1924 + "id": 14244 }, { "type": 1, - "geometry": [[483, 272]], + "geometry": [ + [ + 483, + 272 + ] + ], "tags": { "cluster": true, - "cluster_id": 2180, + "cluster_id": 16292, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 2180 + "id": 16292 }, { "type": 1, - "geometry": [[423, 295]], + "geometry": [ + [ + 423, + 295 + ] + ], "tags": { "cluster": true, - "cluster_id": 2340, + "cluster_id": 17572, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 2340 + "id": 17572 }, { "type": 1, - "geometry": [[225, 114]], + "geometry": [ + [ + 225, + 114 + ] + ], "tags": { "scalerank": 5, "name": "Cape Brewster", @@ -434,7 +594,12 @@ }, { "type": 1, - "geometry": [[230, 127]], + "geometry": [ + [ + 230, + 127 + ] + ], "tags": { "scalerank": 5, "name": "Grmsey", @@ -449,7 +614,12 @@ }, { "type": 1, - "geometry": [[210, 21]], + "geometry": [ + [ + 210, + 21 + ] + ], "tags": { "scalerank": 5, "name": "Cape Morris Jesup", @@ -464,7 +634,12 @@ }, { "type": 1, - "geometry": [[238, 154]], + "geometry": [ + [ + 238, + 154 + ] + ], "tags": { "scalerank": 5, "name": "Rockall", @@ -479,29 +654,44 @@ }, { "type": 1, - "geometry": [[484, 235]], + "geometry": [ + [ + 484, + 235 + ] + ], "tags": { "cluster": true, - "cluster_id": 2692, + "cluster_id": 20388, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 2692 + "id": 20388 }, { "type": 1, - "geometry": [[471, 167]], + "geometry": [ + [ + 471, + 167 + ] + ], "tags": { "cluster": true, - "cluster_id": 3236, + "cluster_id": 24740, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 3236 + "id": 24740 }, { "type": 1, - "geometry": [[498, 149]], + "geometry": [ + [ + 498, + 149 + ] + ], "tags": { "scalerank": 5, "name": "Cape Olyutorskiy", @@ -516,7 +706,12 @@ }, { "type": 1, - "geometry": [[511, 142]], + "geometry": [ + [ + 511, + 142 + ] + ], "tags": { "scalerank": 5, "name": "Cape Navarin", @@ -531,7 +726,12 @@ }, { "type": 1, - "geometry": [[469, 106]], + "geometry": [ + [ + 469, + 106 + ] + ], "tags": { "scalerank": 5, "name": "Cape Lopatka", @@ -546,7 +746,12 @@ }, { "type": 1, - "geometry": [[292, 110]], + "geometry": [ + [ + 292, + 110 + ] + ], "tags": { "scalerank": 5, "name": "Nordkapp", @@ -561,7 +766,12 @@ }, { "type": 1, - "geometry": [[205, 263]], + "geometry": [ + [ + 205, + 263 + ] + ], "tags": { "scalerank": 5, "name": "Cabo de São Roque", @@ -576,29 +786,44 @@ }, { "type": 1, - "geometry": [[-29, 272]], + "geometry": [ + [ + -29, + 272 + ] + ], "tags": { "cluster": true, - "cluster_id": 2180, + "cluster_id": 16292, "point_count": 10, "point_count_abbreviated": 10 }, - "id": 2180 + "id": 16292 }, { "type": 1, - "geometry": [[-28, 235]], + "geometry": [ + [ + -28, + 235 + ] + ], "tags": { "cluster": true, - "cluster_id": 2692, + "cluster_id": 20388, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 2692 + "id": 20388 }, { "type": 1, - "geometry": [[-14, 149]], + "geometry": [ + [ + -14, + 149 + ] + ], "tags": { "scalerank": 5, "name": "Cape Olyutorskiy", @@ -613,7 +838,12 @@ }, { "type": 1, - "geometry": [[-1, 142]], + "geometry": [ + [ + -1, + 142 + ] + ], "tags": { "scalerank": 5, "name": "Cape Navarin", @@ -628,18 +858,28 @@ }, { "type": 1, - "geometry": [[539, 270]], + "geometry": [ + [ + 539, + 270 + ] + ], "tags": { "cluster": true, - "cluster_id": 1444, + "cluster_id": 10404, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 1444 + "id": 10404 }, { "type": 1, - "geometry": [[531, 121]], + "geometry": [ + [ + 531, + 121 + ] + ], "tags": { "scalerank": 4, "name": "Cape Hope", @@ -654,7 +894,12 @@ }, { "type": 1, - "geometry": [[545, 109]], + "geometry": [ + [ + 545, + 109 + ] + ], "tags": { "scalerank": 4, "name": "Point Barrow", @@ -668,4 +913,4 @@ } } ] -} +} \ No newline at end of file diff --git a/test/fixtures/places-z0-0-0.json b/test/fixtures/places-z0-0-0.json index c7278d87..0b939af7 100644 --- a/test/fixtures/places-z0-0-0.json +++ b/test/fixtures/places-z0-0-0.json @@ -1,8 +1,14 @@ { + "type": "FeatureCollection", "features": [ { "type": 1, - "geometry": [[150, 205]], + "geometry": [ + [ + 150, + 205 + ] + ], "tags": { "cluster": true, "cluster_id": 164, @@ -13,73 +19,108 @@ }, { "type": 1, - "geometry": [[165, 240]], + "geometry": [ + [ + 165, + 240 + ] + ], "tags": { "cluster": true, - "cluster_id": 196, + "cluster_id": 420, "point_count": 18, "point_count_abbreviated": 18 }, - "id": 196 + "id": 420 }, { "type": 1, - "geometry": [[179, 303]], + "geometry": [ + [ + 179, + 303 + ] + ], "tags": { "cluster": true, - "cluster_id": 228, + "cluster_id": 676, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 228 + "id": 676 }, { "type": 1, - "geometry": [[336, 234]], + "geometry": [ + [ + 336, + 234 + ] + ], "tags": { "cluster": true, - "cluster_id": 260, + "cluster_id": 932, "point_count": 8, "point_count_abbreviated": 8 }, - "id": 260 + "id": 932 }, { "type": 1, - "geometry": [[299, 285]], + "geometry": [ + [ + 299, + 285 + ] + ], "tags": { "cluster": true, - "cluster_id": 292, + "cluster_id": 1188, "point_count": 15, "point_count_abbreviated": 15 }, - "id": 292 + "id": 1188 }, { "type": 1, - "geometry": [[71, 419]], + "geometry": [ + [ + 71, + 419 + ] + ], "tags": { "cluster": true, - "cluster_id": 324, + "cluster_id": 1444, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 324 + "id": 1444 }, { "type": 1, - "geometry": [[92, 212]], + "geometry": [ + [ + 92, + 212 + ] + ], "tags": { "cluster": true, - "cluster_id": 420, + "cluster_id": 2212, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 420 + "id": 2212 }, { "type": 1, - "geometry": [[123, 152]], + "geometry": [ + [ + 123, + 152 + ] + ], "tags": { "scalerank": 3, "name": "Cape Churchill", @@ -94,40 +135,60 @@ }, { "type": 1, - "geometry": [[162, 345]], + "geometry": [ + [ + 162, + 345 + ] + ], "tags": { "cluster": true, - "cluster_id": 581, + "cluster_id": 3493, "point_count": 3, "point_count_abbreviated": 3 }, - "id": 581 + "id": 3493 }, { "type": 1, - "geometry": [[236, 232]], + "geometry": [ + [ + 236, + 232 + ] + ], "tags": { "cluster": true, - "cluster_id": 580, + "cluster_id": 3492, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 580 + "id": 3492 }, { "type": 1, - "geometry": [[259, 193]], + "geometry": [ + [ + 259, + 193 + ] + ], "tags": { "cluster": true, - "cluster_id": 644, + "cluster_id": 4004, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 644 + "id": 4004 }, { "type": 1, - "geometry": [[80, 336]], + "geometry": [ + [ + 80, + 336 + ] + ], "tags": { "scalerank": 3, "name": "Oceanic pole of inaccessibility", @@ -142,7 +203,12 @@ }, { "type": 1, - "geometry": [[452, 377]], + "geometry": [ + [ + 452, + 377 + ] + ], "tags": { "scalerank": 3, "name": "South Magnetic Pole 2005 (est)", @@ -157,7 +223,12 @@ }, { "type": 1, - "geometry": [[93, 32]], + "geometry": [ + [ + 93, + 32 + ] + ], "tags": { "scalerank": 3, "name": "North Magnetic Pole 2005 (est)", @@ -172,7 +243,12 @@ }, { "type": 1, - "geometry": [[159, 84]], + "geometry": [ + [ + 159, + 84 + ] + ], "tags": { "scalerank": 4, "name": "Cape York", @@ -187,29 +263,44 @@ }, { "type": 1, - "geometry": [[220, 147]], + "geometry": [ + [ + 220, + 147 + ] + ], "tags": { "cluster": true, - "cluster_id": 836, + "cluster_id": 5540, "point_count": 3, "point_count_abbreviated": 3 }, - "id": 836 + "id": 5540 }, { "type": 1, - "geometry": [[27, 270]], + "geometry": [ + [ + 27, + 270 + ] + ], "tags": { "cluster": true, - "cluster_id": 900, + "cluster_id": 6052, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 900 + "id": 6052 }, { "type": 1, - "geometry": [[100, 296]], + "geometry": [ + [ + 100, + 296 + ] + ], "tags": { "scalerank": 4, "name": "I. de Pascua", @@ -224,7 +315,12 @@ }, { "type": 1, - "geometry": [[401, 226]], + "geometry": [ + [ + 401, + 226 + ] + ], "tags": { "scalerank": 4, "name": "Plain of Jars", @@ -239,51 +335,76 @@ }, { "type": 1, - "geometry": [[26, 115]], + "geometry": [ + [ + 26, + 115 + ] + ], "tags": { "cluster": true, - "cluster_id": 1157, + "cluster_id": 8101, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1157 + "id": 8101 }, { "type": 1, - "geometry": [[449, 304]], + "geometry": [ + [ + 449, + 304 + ] + ], "tags": { "cluster": true, - "cluster_id": 1124, + "cluster_id": 7844, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1124 + "id": 7844 }, { "type": 1, - "geometry": [[455, 272]], + "geometry": [ + [ + 455, + 272 + ] + ], "tags": { "cluster": true, - "cluster_id": 1188, + "cluster_id": 8356, "point_count": 5, "point_count_abbreviated": 5 }, - "id": 1188 + "id": 8356 }, { "type": 1, - "geometry": [[227, 121]], + "geometry": [ + [ + 227, + 121 + ] + ], "tags": { "cluster": true, - "cluster_id": 1701, + "cluster_id": 12453, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1701 + "id": 12453 }, { "type": 1, - "geometry": [[210, 21]], + "geometry": [ + [ + 210, + 21 + ] + ], "tags": { "scalerank": 5, "name": "Cape Morris Jesup", @@ -298,29 +419,44 @@ }, { "type": 1, - "geometry": [[484, 235]], + "geometry": [ + [ + 484, + 235 + ] + ], "tags": { "cluster": true, - "cluster_id": 1380, + "cluster_id": 9892, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1380 + "id": 9892 }, { "type": 1, - "geometry": [[503, 260]], + "geometry": [ + [ + 503, + 260 + ] + ], "tags": { "cluster": true, - "cluster_id": 1925, + "cluster_id": 14245, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 1925 + "id": 14245 }, { "type": 1, - "geometry": [[502, 308]], + "geometry": [ + [ + 502, + 308 + ] + ], "tags": { "scalerank": 5, "name": "Cape Reinga", @@ -336,18 +472,28 @@ }, { "type": 1, - "geometry": [[475, 165]], + "geometry": [ + [ + 475, + 165 + ] + ], "tags": { "cluster": true, - "cluster_id": 1668, + "cluster_id": 12196, "point_count": 7, "point_count_abbreviated": 7 }, - "id": 1668 + "id": 12196 }, { "type": 1, - "geometry": [[511, 142]], + "geometry": [ + [ + 511, + 142 + ] + ], "tags": { "scalerank": 5, "name": "Cape Navarin", @@ -362,7 +508,12 @@ }, { "type": 1, - "geometry": [[469, 106]], + "geometry": [ + [ + 469, + 106 + ] + ], "tags": { "scalerank": 5, "name": "Cape Lopatka", @@ -377,7 +528,12 @@ }, { "type": 1, - "geometry": [[292, 110]], + "geometry": [ + [ + 292, + 110 + ] + ], "tags": { "scalerank": 5, "name": "Nordkapp", @@ -392,40 +548,60 @@ }, { "type": 1, - "geometry": [[202, 262]], + "geometry": [ + [ + 202, + 262 + ] + ], "tags": { "cluster": true, - "cluster_id": 4134, + "cluster_id": 31910, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 4134 + "id": 31910 }, { "type": 1, - "geometry": [[-28, 235]], + "geometry": [ + [ + -28, + 235 + ] + ], "tags": { "cluster": true, - "cluster_id": 1380, + "cluster_id": 9892, "point_count": 13, "point_count_abbreviated": 13 }, - "id": 1380 + "id": 9892 }, { "type": 1, - "geometry": [[-9, 260]], + "geometry": [ + [ + -9, + 260 + ] + ], "tags": { "cluster": true, - "cluster_id": 1925, + "cluster_id": 14245, "point_count": 4, "point_count_abbreviated": 4 }, - "id": 1925 + "id": 14245 }, { "type": 1, - "geometry": [[-10, 308]], + "geometry": [ + [ + -10, + 308 + ] + ], "tags": { "scalerank": 5, "name": "Cape Reinga", @@ -441,18 +617,28 @@ }, { "type": 1, - "geometry": [[-37, 165]], + "geometry": [ + [ + -37, + 165 + ] + ], "tags": { "cluster": true, - "cluster_id": 1668, + "cluster_id": 12196, "point_count": 7, "point_count_abbreviated": 7 }, - "id": 1668 + "id": 12196 }, { "type": 1, - "geometry": [[-1, 142]], + "geometry": [ + [ + -1, + 142 + ] + ], "tags": { "scalerank": 5, "name": "Cape Navarin", @@ -467,25 +653,35 @@ }, { "type": 1, - "geometry": [[539, 270]], + "geometry": [ + [ + 539, + 270 + ] + ], "tags": { "cluster": true, - "cluster_id": 900, + "cluster_id": 6052, "point_count": 6, "point_count_abbreviated": 6 }, - "id": 900 + "id": 6052 }, { "type": 1, - "geometry": [[538, 115]], + "geometry": [ + [ + 538, + 115 + ] + ], "tags": { "cluster": true, - "cluster_id": 1157, + "cluster_id": 8101, "point_count": 2, "point_count_abbreviated": 2 }, - "id": 1157 + "id": 8101 } ] -} +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index f48cc76b..4876b129 100644 --- a/test/test.js +++ b/test/test.js @@ -6,6 +6,41 @@ const places = require('./fixtures/places.json'); const placesTile = require('./fixtures/places-z0-0-0.json'); const placesTileMin5 = require('./fixtures/places-z0-0-0-min5.json'); +test('Test indexing with minZoom, maxZoom and zoomStep', (t) => { + const minzoom = 10; + const zoomStep = 0.1; + const index = new Supercluster({minZoom: minzoom, maxZoom: 15, zoomStep}); + + t.same(index._zoomToIndex(minzoom), 0); + t.same(index._zoomToIndex(minzoom + 5 * zoomStep), 5); + t.same(index._zoomToIndex(minzoom + (10 * zoomStep)), 10); + t.end(); +}); + +test('Test indexing with minZoom, maxZoom and zoomStep', (t) => { + const minzoom = 10; + const zoomStep = 1; + const index = new Supercluster({minZoom: minzoom, maxZoom: 20, zoomStep}); + + + t.same(index._zoomToIndex(minzoom), 0); + t.same(index._zoomToIndex(minzoom + 5 * zoomStep), 5); + t.same(index._zoomToIndex(minzoom + 10 * zoomStep), 10); + t.end(); +}); + +test('Test indexing with minZoom, maxZoom and zoomStep', (t) => { + const minzoom = 10; + const zoomStep = 0.2; + const index = new Supercluster({minZoom: minzoom, maxZoom: 20, zoomStep}); + + + t.same(index._zoomToIndex(minzoom), 0); + t.same(index._zoomToIndex(minzoom + 5 * zoomStep), 5); + t.same(index._zoomToIndex(minzoom + 10 * zoomStep), 10); + t.end(); +}); + test('generates clusters properly', (t) => { const index = new Supercluster().load(places.features); const tile = index.getTile(0, 0, 0); @@ -69,10 +104,10 @@ test('getLeaves handles null-property features', (t) => { test('returns cluster expansion zoom', (t) => { const index = new Supercluster().load(places.features); t.same(index.getClusterExpansionZoom(164), 1); - t.same(index.getClusterExpansionZoom(196), 1); - t.same(index.getClusterExpansionZoom(581), 2); - t.same(index.getClusterExpansionZoom(1157), 2); - t.same(index.getClusterExpansionZoom(4134), 3); + t.same(index.getClusterExpansionZoom(420), 1); + t.same(index.getClusterExpansionZoom(3493), 2); + t.same(index.getClusterExpansionZoom(8101), 2); + t.same(index.getClusterExpansionZoom(31910), 3); t.end(); }); @@ -83,7 +118,7 @@ test('returns cluster expansion zoom for maxZoom', (t) => { maxZoom: 4, }).load(places.features); - t.same(index.getClusterExpansionZoom(2504), 5); + t.same(index.getClusterExpansionZoom(18856), 5); t.end(); }); From 8f6d0c9c689efbeb8404a54d64a64201597d48bd Mon Sep 17 00:00:00 2001 From: dariovillar Date: Tue, 11 Apr 2023 20:09:45 +0200 Subject: [PATCH 2/2] Correct zoomStep documentation. Remove 1 decimal limitation on zoomSep --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index bd05eabd..1204dc95 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import KDBush from 'kdbush'; const defaultOptions = { minZoom: 0.0, // min zoom to generate clusters on maxZoom: 16.0, // max zoom level to cluster the points on - zoomStep: 1, // Indicate whether to compute clusters for each 0.1 zoom level. It may impact on the performance. + zoomStep: 1, // Indicate the distance in zoom points between cluster computations. E.g. 0.1 = 10 cluster indexes by zoom level. 1 = one cluster index by zoom level. 0.5 = 2 cluster index by zoom level minPoints: 2, // minimum points to form a cluster radius: 40, // cluster radius in pixels extent: 512, // tile extent (radius is calculated relative to it) @@ -26,7 +26,7 @@ const fround = Math.fround || (tmp => ((x) => { tmp[0] = +x; return tmp[0]; }))( export default class Supercluster { constructor(options) { this.options = extend(Object.create(defaultOptions), options); - this.zoomStep = +(this.options.zoomStep).toFixed(1); + this.zoomStep = +(this.options.zoomStep); this.numTreesByZoomLevel = Math.ceil(1 / this.zoomStep); this.zoomRange = this.options.maxZoom - this.options.minZoom; this.numtrees = (this.zoomRange * this.numTreesByZoomLevel) + 2; @@ -63,7 +63,7 @@ export default class Supercluster { // create a new set of clusters for the zoom and index them with a KD-tree clusters = this._cluster(clusters, z); this.trees[this._zoomToIndex(z)] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); - if (log) console.log('z%d: %d clusters in %dms', +z.toFixed(1), clusters.length, +Date.now() - now); + if (log) console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now); } if (log) console.timeEnd('total time'); @@ -320,7 +320,7 @@ export default class Supercluster { } _zoomToIndex(zoom) { - const clampedZoom = Math.max(this.options.minZoom, Math.min(zoom.toFixed(1), this.options.maxZoom + this.zoomStep)); + const clampedZoom = Math.max(this.options.minZoom, Math.min(zoom, this.options.maxZoom + this.zoomStep)); // Get the index of the tree that better suits the zoom level const adjustedZoom = clampedZoom - this.options.minZoom; const stepIndex = Math.round(adjustedZoom * this.numTreesByZoomLevel);