Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Or use it with an ordinary script tag in the browser:

#### `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) or [MultiPoint](https://tools.ietf.org/html/rfc7946#section-3.1.3) — a MultiPoint is clustered as an individual point per coordinate, each inheriting the feature's `properties` and `id`. Once loaded, index is immutable.

#### `getClusters(bbox, zoom)`

Expand Down
65 changes: 40 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,55 @@ export default class Supercluster {
const timerId = `z${notProcessed}: ${points.length} points`;
if (log) console.time(timerId);

this.numPoints = points.length;
const stride = this.stride;

// MultiPoint features expand into one internal point per coordinate, so count first
let numPoints = points.length;
for (const p of points) {
const g = p.geometry;
if (g && g.type === 'MultiPoint') numPoints += g.coordinates.length - 1;
}
this.numPoints = numPoints;

// retain only per-point fields used by output paths; drop the GeoJSON wrappers
const props = this.props = new Array(points.length);
const props = this.props = new Array(numPoints);
// original Float64 mercator coords for drift-free single-point output
const coords = this.coords = new Float64Array(points.length * 2);
const coords = this.coords = new Float64Array(numPoints * 2);
let ids = null;

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

const [lng, lat] = p.geometry.coordinates;
const px = lngX(lng);
const py = latY(lat);
coords[2 * i] = px;
coords[2 * i + 1] = py;
// store internal point/cluster data in flat typed arrays for performance
data[w] = encode(px);
data[w + 1] = encode(py);
data[w + OFFSET_ZOOM] = notProcessed;
data[w + OFFSET_ID] = i;
data[w + OFFSET_PARENT] = -1;
data[w + OFFSET_NUM] = 1;
props[i] = p.properties;
if (p.id !== undefined) {
if (!ids) ids = new Array(points.length);
ids[i] = p.id;
let i = 0; // index of the individual point (multiple per MultiPoint feature)
for (const p of points) {
const g = p.geometry;
if (!g) { // keep the slot so point indices stay aligned with input features
i++;
continue;
}
const cs = g.coordinates;
const multi = g.type === 'MultiPoint';

for (let c = 0, n = multi ? cs.length : 1; c < n; c++, i++) {
const [lng, lat] = multi ? cs[c] : cs;
const px = lngX(lng);
const py = latY(lat);
coords[2 * i] = px;
coords[2 * i + 1] = py;
// store internal point/cluster data in flat typed arrays for performance
data[w] = encode(px);
data[w + 1] = encode(py);
data[w + OFFSET_ZOOM] = notProcessed;
data[w + OFFSET_ID] = i;
data[w + OFFSET_PARENT] = -1;
data[w + OFFSET_NUM] = 1;
props[i] = p.properties;
if (p.id !== undefined) {
if (!ids) ids = new Array(numPoints);
ids[i] = p.id;
}
w += stride;
}
w += stride;
}
this.ids = ids;
let prev = w === data.length ? data : data.subarray(0, w);
Expand Down
23 changes: 23 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,29 @@ test('preserves single-point tile coords at GL JS params (extent 8192, z18)', ()
assert.ok(checked > 50, `expected to check >50 points, got ${checked}`);
});

test('expands MultiPoint features into individual points', () => {
const index = new Supercluster().load([{
type: 'Feature',
id: 'mp',
properties: {a: 1},
geometry: {type: 'MultiPoint', coordinates: [[0, 0], [0.001, 0.001], [50, 50]]}
}, {
type: 'Feature',
properties: {b: 2},
geometry: {type: 'Point', coordinates: [0, 0]}
}]);

const clusters = index.getClusters([-180, -85, 180, 85], 5);
assert.equal(clusters.length, 2);
assert.equal(clusters[0].properties.point_count, 3);
assert.deepEqual(clusters[1].geometry.coordinates, [49.999999999999986, 50]);
assert.deepEqual(clusters[1].properties, {a: 1});
assert.equal(clusters[1].id, 'mp');

const leaves = index.getLeaves(clusters[0].properties.cluster_id);
assert.deepEqual(leaves.map(l => l.properties), [{a: 1}, {b: 2}, {a: 1}]);
});

test('does not throw on zero items', () => {
assert.doesNotThrow(() => {
const index = new Supercluster().load([]);
Expand Down
Loading