Skip to content

Commit 2f6bf78

Browse files
committed
use Int32 storage with higher precision
1 parent cf20790 commit 2f6bf78

1 file changed

Lines changed: 67 additions & 46 deletions

File tree

index.js

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ const defaultOptions = {
2020
map: props => props // props => ({sum: props.my_value})
2121
};
2222

23-
const fround = Math.fround || (tmp => ((x) => { tmp[0] = +x; return tmp[0]; }))(new Float32Array(1));
23+
// Int32 encoding of source coords in [0, 1]: (coord - 0.5) * SCALE in [-2^30, 2^30].
24+
// Keeps every stored value inside V8's 31-bit SMI fast path.
25+
const SCALE = 0x80000000; // 2^31
26+
const INV_SCALE = 1 / SCALE;
27+
const encode = c => (c - 0.5) * SCALE;
28+
const decode = v => v * INV_SCALE + 0.5;
2429

2530
const OFFSET_ZOOM = 2;
2631
const OFFSET_ID = 3;
@@ -45,28 +50,26 @@ export default class Supercluster {
4550
if (log) console.time(timerId);
4651

4752
this.points = points;
53+
const stride = this.stride;
54+
const notProcessed = maxZoom + 1; // sentinel for "not yet processed at any zoom"
4855

4956
// generate a cluster object for each point and index input points into a KD-tree
50-
const data = [];
51-
57+
const data = new Int32Array(points.length * stride);
58+
let w = 0;
5259
for (let i = 0; i < points.length; i++) {
5360
const p = points[i];
5461
if (!p.geometry) continue;
5562

5663
const [lng, lat] = p.geometry.coordinates;
57-
const x = fround(lngX(lng));
58-
const y = fround(latY(lat));
59-
// store internal point/cluster data in flat numeric arrays for performance
60-
data.push(
61-
x, y, // projected point coordinates
62-
Infinity, // the last zoom the point was processed at
63-
i, // index of the source feature in the original input array
64-
-1, // parent cluster id
65-
1 // number of points in a cluster
66-
);
67-
if (this.options.reduce) data.push(0); // noop
64+
// store internal point/cluster data in flat typed arrays for performance
65+
writeSlot(data, w, encode(lngX(lng)), encode(latY(lat)), notProcessed, i, 1);
66+
w += stride;
6867
}
69-
let tree = this.trees[maxZoom + 1] = this._createTree(data);
68+
const numInput = w / stride;
69+
const inputSlab = w === data.length ? data : data.subarray(0, w);
70+
let prev = inputSlab;
71+
let prevNum = numInput;
72+
let tree = this.trees[maxZoom + 1] = this._createTree(prev, prevNum);
7073

7174
if (log) console.timeEnd(timerId);
7275

@@ -75,8 +78,12 @@ export default class Supercluster {
7578
for (let z = maxZoom; z >= minZoom; z--) {
7679
const now = +Date.now();
7780

78-
// create a new set of clusters for the zoom and index them with a KD-tree
79-
tree = this.trees[z] = this._createTree(this._cluster(tree, z));
81+
// allocate a tight Int32 slab for this zoom; output is strictly <= input length
82+
const out = new Int32Array(prev.length);
83+
const written = this._cluster(prev, prevNum, z, out);
84+
tree = this.trees[z] = this._createTree(out, written);
85+
prev = out;
86+
prevNum = written;
8087

8188
if (log) console.log('z%d: %d clusters in %dms', z, tree.numItems, +Date.now() - now);
8289
}
@@ -102,7 +109,7 @@ export default class Supercluster {
102109
}
103110

104111
const tree = this.trees[this._limitZoom(zoom)];
105-
const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
112+
const ids = tree.range(encode(lngX(minLng)), encode(latY(maxLat)), encode(lngX(maxLng)), encode(latY(minLat)));
106113
const data = tree.data;
107114
const clusters = [];
108115
for (const id of ids) {
@@ -121,12 +128,12 @@ export default class Supercluster {
121128
if (!tree) throw new Error(errorMsg);
122129

123130
const data = tree.data;
124-
if (originId * this.stride >= data.length) throw new Error(errorMsg);
131+
if (originId >= tree.numItems) throw new Error(errorMsg);
125132

126133
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
127134
const x = data[originId * this.stride];
128135
const y = data[originId * this.stride + 1];
129-
const ids = tree.within(x, y, r);
136+
const ids = tree.within(x, y, r * SCALE);
130137
const children = [];
131138
for (const id of ids) {
132139
const k = id * this.stride;
@@ -155,25 +162,23 @@ export default class Supercluster {
155162
const z2 = Math.pow(2, z);
156163
const {extent, radius} = this.options;
157164
const p = radius / extent;
158-
const top = (y - p) / z2;
159-
const bottom = (y + 1 + p) / z2;
165+
const top = encode((y - p) / z2);
166+
const bottom = encode((y + 1 + p) / z2);
160167

161-
const tile = {
162-
features: []
163-
};
168+
const tile = {features: []};
164169

165170
this._addTileFeatures(
166-
tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom),
171+
tree.range(encode((x - p) / z2), top, encode((x + 1 + p) / z2), bottom),
167172
tree.data, x, y, z2, tile);
168173

169174
if (x === 0) {
170175
this._addTileFeatures(
171-
tree.range(1 - p / z2, top, 1, bottom),
176+
tree.range(encode(1 - p / z2), top, encode(1), bottom),
172177
tree.data, z2, y, z2, tile);
173178
}
174179
if (x === z2 - 1) {
175180
this._addTileFeatures(
176-
tree.range(0, top, p / z2, bottom),
181+
tree.range(encode(0), top, encode(p / z2), bottom),
177182
tree.data, -1, y, z2, tile);
178183
}
179184

@@ -219,9 +224,10 @@ export default class Supercluster {
219224
return skipped;
220225
}
221226

222-
_createTree(data) {
223-
const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
224-
for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
227+
_createTree(data, numItems) {
228+
const tree = new KDBush(numItems, this.options.nodeSize, Int32Array);
229+
const stride = this.stride;
230+
for (let i = 0; i < numItems; i++) tree.add(data[i * stride], data[i * stride + 1]);
225231
tree.finish();
226232
tree.data = data;
227233
return tree;
@@ -235,8 +241,8 @@ export default class Supercluster {
235241
let tags, px, py;
236242
if (isCluster) {
237243
tags = getClusterProperties(data, k, this.clusterProps);
238-
px = data[k];
239-
py = data[k + 1];
244+
px = decode(data[k]);
245+
py = decode(data[k + 1]);
240246
} else {
241247
const p = this.points[data[k + OFFSET_ID]];
242248
tags = p.properties;
@@ -274,23 +280,25 @@ export default class Supercluster {
274280
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
275281
}
276282

277-
_cluster(tree, zoom) {
278-
const {radius, extent, reduce, minPoints} = this.options;
279-
const r = radius / (extent * Math.pow(2, zoom));
280-
const data = tree.data;
281-
const nextData = [];
283+
_cluster(data, numItems, zoom, out) {
284+
const {radius, extent, reduce, minPoints, maxZoom} = this.options;
285+
const r = radius / (extent * Math.pow(2, zoom)) * SCALE;
286+
const notProcessed = maxZoom + 1;
287+
const tree = this.trees[zoom + 1];
282288
const stride = this.stride;
289+
const limit = numItems * stride;
290+
let cursor = 0;
283291

284292
// loop through each point
285-
for (let i = 0; i < data.length; i += stride) {
293+
for (let i = 0; i < limit; i += stride) {
286294
// if we've already visited the point at this zoom level, skip it
287295
if (data[i + OFFSET_ZOOM] <= zoom) continue;
288296
data[i + OFFSET_ZOOM] = zoom;
289297

290298
// find all nearby points
291299
const x = data[i];
292300
const y = data[i + 1];
293-
const neighborIds = tree.within(data[i], data[i + 1], r);
301+
const neighborIds = tree.within(x, y, r);
294302

295303
const numPointsOrigin = data[i + OFFSET_NUM];
296304
let numPoints = numPointsOrigin;
@@ -336,24 +344,26 @@ export default class Supercluster {
336344
}
337345

338346
data[i + OFFSET_PARENT] = id;
339-
nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
340-
if (reduce) nextData.push(clusterPropIndex);
347+
writeSlot(out, cursor, wx / numPoints, wy / numPoints, notProcessed, id, numPoints, reduce ? clusterPropIndex : undefined);
348+
cursor += stride;
341349

342350
} else { // left points as unclustered
343-
for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
351+
for (let j = 0; j < stride; j++) out[cursor + j] = data[i + j];
352+
cursor += stride;
344353

345354
if (numPoints > 1) {
346355
for (const neighborId of neighborIds) {
347356
const k = neighborId * stride;
348357
if (data[k + OFFSET_ZOOM] <= zoom) continue;
349358
data[k + OFFSET_ZOOM] = zoom;
350-
for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
359+
for (let j = 0; j < stride; j++) out[cursor + j] = data[k + j];
360+
cursor += stride;
351361
}
352362
}
353363
}
354364
}
355365

356-
return nextData;
366+
return cursor / stride;
357367
}
358368

359369
// get index of the point from which the cluster originated
@@ -377,14 +387,25 @@ export default class Supercluster {
377387
}
378388
}
379389

390+
// write one stride-tuple (cluster or input point) into a typed slab
391+
function writeSlot(data, k, x, y, zoom, id, num, propIndex) {
392+
data[k] = x;
393+
data[k + 1] = y;
394+
data[k + 2] = zoom;
395+
data[k + 3] = id;
396+
data[k + 4] = -1; // parent cluster id
397+
data[k + 5] = num;
398+
if (propIndex !== undefined) data[k + 6] = propIndex;
399+
}
400+
380401
function getClusterJSON(data, i, clusterProps) {
381402
return {
382403
type: 'Feature',
383404
id: data[i + OFFSET_ID],
384405
properties: getClusterProperties(data, i, clusterProps),
385406
geometry: {
386407
type: 'Point',
387-
coordinates: [xLng(data[i]), yLat(data[i + 1])]
408+
coordinates: [xLng(decode(data[i])), yLat(decode(data[i + 1]))]
388409
}
389410
};
390411
}

0 commit comments

Comments
 (0)