From b2920b4452c7b38c2b49fe50eda22d7615a31d04 Mon Sep 17 00:00:00 2001 From: Sander Verwimp <91965164+sanDer153@users.noreply.github.com> Date: Fri, 22 Aug 2025 23:10:52 +0100 Subject: [PATCH] fix: make load destructive to avoid extra copy in memory --- README.md | 2 +- index.js | 1 + package-lock.json | 4 ++-- package.json | 2 +- test/test.js | 26 +++++++++++++------------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b5e747c8..a57e84fc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ import Supercluster from 'mutable-supercluster'; #### `load(points)` -Loads an array of [GeoJSON Feature](https://tools.ietf.org/html/rfc7946#section-3.2) objects. Each feature's `geometry` must be a [GeoJSON Point](https://tools.ietf.org/html/rfc7946#section-3.1.2). Once loaded, index is immutable. +Loads an array of [GeoJSON Feature](https://tools.ietf.org/html/rfc7946#section-3.2) objects. Each feature's `geometry` must be a [GeoJSON Point](https://tools.ietf.org/html/rfc7946#section-3.1.2). Loading the points is destructive, so the provided list will be cleared. #### `getClusters(bbox, zoom)` diff --git a/index.js b/index.js index acd7d2a5..6b8c82f9 100644 --- a/index.js +++ b/index.js @@ -62,6 +62,7 @@ export default class Supercluster { if (log) console.time(timerId); this.points = structuredClone(points); + points.length = 0; // generate a cluster object for each point and index input points into a R-tree const currentClusterData = []; diff --git a/package-lock.json b/package-lock.json index 1cbfc823..c67bbb85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.0.4", "license": "ISC", "dependencies": { "lodash-es": "^4.17.21", diff --git a/package.json b/package.json index 85646d7b..e5fce81a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mutable-supercluster", - "version": "1.0.3", + "version": "1.0.4", "description": "A library for fast and mutable geospatial point clustering.", "main": "dist/mutable-supercluster.js", "type": "module", diff --git a/test/test.js b/test/test.js index 28eb737d..7d7bc508 100644 --- a/test/test.js +++ b/test/test.js @@ -19,27 +19,27 @@ const placesTileMin5 = JSON.parse(readFileSync(new URL('./fixtures/places-z0-0-0 const sortedTileMin5Features = placesTileMin5.features.sort(compareFn); test('generates clusters properly', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); const tile = index.getTile(0, 0, 0); tile.features.sort(compareFn); assert.deepEqual(tile.features, sortedTileFeatures); }); test('supports minPoints option', () => { - const index = new Supercluster({minPoints: 5, getId}).load(places.features); + const index = new Supercluster({minPoints: 5, getId}).load(structuredClone(places.features)); const tile = index.getTile(0, 0, 0); tile.features.sort(compareFn); assert.deepEqual(tile.features, sortedTileMin5Features); }); test('returns children of a cluster', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); const childCounts = index.getChildren(164).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 index = new Supercluster({getId}).load(structuredClone(places.features)); const leafNames = index.getLeaves(164, 10, 5).map(p => p.properties.name); assert.deepEqual(leafNames, [ 'I. de Cozumel', @@ -56,13 +56,13 @@ test('returns leaves of a cluster', () => { }); test('generates unique ids with generateId option', () => { - const index = new Supercluster({generateId: true, getId}).load(places.features); + const index = new Supercluster({generateId: true, getId}).load(structuredClone(places.features)); const ids = index.getTile(0, 0, 0).features.filter(f => !f.tags.cluster).map(f => f.id); assert.deepEqual(ids, [62, 24, 22, 12, 28, 20, 125, 119, 30, 118, 81, 21, 81, 118]); }); test('getLeaves handles null-property features', () => { - const index = new Supercluster({getId}).load(places.features.concat([{ + const index = new Supercluster({getId}).load(structuredClone(places.features).concat([{ type: 'Feature', properties: null, geometry: { @@ -75,7 +75,7 @@ test('getLeaves handles null-property features', () => { }); test('returns cluster expansion zoom', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); assert.deepEqual(index.getClusterExpansionZoom(164), 1); assert.deepEqual(index.getClusterExpansionZoom(196), 1); assert.deepEqual(index.getClusterExpansionZoom(581), 2); @@ -89,7 +89,7 @@ test('returns cluster expansion zoom for maxZoom', () => { extent: 256, maxZoom: 4, getId - }).load(places.features); + }).load(structuredClone(places.features)); assert.deepEqual(index.getClusterExpansionZoom(2504), 5); }); @@ -100,7 +100,7 @@ test('aggregates cluster properties with reduce', () => { reduce: (a, b) => { a.sum += b.sum; }, radius: 100, getId - }).load(places.features); + }).load(structuredClone(places.features)); assert.deepEqual(index.getTile(1, 0, 0).features.map(f => f.tags.sum).filter(Boolean), [8, 19, 12, 23, 146, 34, 8, 29, 84, 63, 35, 80]); @@ -150,7 +150,7 @@ test('returns clusters when query crosses international dateline', () => { }); test('does not crash on weird bbox values', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); assert.equal(index.getClusters([129.426390, -103.720017, -445.930843, 114.518236], 1).length, 26); assert.equal(index.getClusters([112.207836, -84.578666, -463.149397, 120.169159], 1).length, 27); assert.equal(index.getClusters([129.886277, -82.332680, -445.470956, 120.390930], 1).length, 26); @@ -161,7 +161,7 @@ test('does not crash on weird bbox values', () => { }); test('does not crash on non-integer zoom values', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); assert.ok(index.getClusters([179, -10, -177, 10], 1.25)); }); @@ -195,7 +195,7 @@ test('does not throw on zero items', () => { }); test('update properties succeeds', () => { - const index = new Supercluster({getId}).load(places.features); + const index = new Supercluster({getId}).load(structuredClone(places.features)); const leafNames = index.getLeaves(164, 3, 5).map(p => p.properties.name); assert.deepEqual(leafNames, [ 'I. de Cozumel', @@ -213,7 +213,7 @@ test('update properties succeeds', () => { }); test('update properties with different location fails', () => { - const index = new Supercluster({getId}).load(places.features); + 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.