supercluster
Advanced tools
Comparing version 7.1.5 to 8.0.0
@@ -7,190 +7,330 @@ (function (global, factory) { | ||
function sortKD(ids, coords, nodeSize, left, right, depth) { | ||
if (right - left <= nodeSize) { return; } | ||
const ARRAY_TYPES = [ | ||
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, | ||
Int32Array, Uint32Array, Float32Array, Float64Array | ||
]; | ||
var m = (left + right) >> 1; | ||
/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */ | ||
select(ids, coords, m, left, right, depth % 2); | ||
const VERSION = 1; // serialized format version | ||
const HEADER_SIZE = 8; | ||
sortKD(ids, coords, nodeSize, left, m - 1, depth + 1); | ||
sortKD(ids, coords, nodeSize, m + 1, right, depth + 1); | ||
} | ||
class KDBush { | ||
function select(ids, coords, k, left, right, inc) { | ||
while (right > left) { | ||
if (right - left > 600) { | ||
var n = right - left + 1; | ||
var m = k - left + 1; | ||
var z = Math.log(n); | ||
var s = 0.5 * Math.exp(2 * z / 3); | ||
var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); | ||
var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); | ||
var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); | ||
select(ids, coords, k, newLeft, newRight, inc); | ||
/** | ||
* Creates an index from raw `ArrayBuffer` data. | ||
* @param {ArrayBuffer} data | ||
*/ | ||
static from(data) { | ||
if (!(data instanceof ArrayBuffer)) { | ||
throw new Error('Data must be an instance of ArrayBuffer.'); | ||
} | ||
const [magic, versionAndType] = new Uint8Array(data, 0, 2); | ||
if (magic !== 0xdb) { | ||
throw new Error('Data does not appear to be in a KDBush format.'); | ||
} | ||
const version = versionAndType >> 4; | ||
if (version !== VERSION) { | ||
throw new Error(`Got v${version} data when expected v${VERSION}.`); | ||
} | ||
const ArrayType = ARRAY_TYPES[versionAndType & 0x0f]; | ||
if (!ArrayType) { | ||
throw new Error('Unrecognized array type.'); | ||
} | ||
const [nodeSize] = new Uint16Array(data, 2, 1); | ||
const [numItems] = new Uint32Array(data, 4, 1); | ||
var t = coords[2 * k + inc]; | ||
var i = left; | ||
var j = right; | ||
return new KDBush(numItems, nodeSize, ArrayType, data); | ||
} | ||
swapItem(ids, coords, left, k); | ||
if (coords[2 * right + inc] > t) { swapItem(ids, coords, left, right); } | ||
/** | ||
* Creates an index that will hold a given number of items. | ||
* @param {number} numItems | ||
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default). | ||
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default). | ||
* @param {ArrayBuffer} [data] (For internal use only) | ||
*/ | ||
constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) { | ||
if (isNaN(numItems) || numItems <= 0) throw new Error(`Unpexpected numItems value: ${numItems}.`); | ||
while (i < j) { | ||
swapItem(ids, coords, i, j); | ||
i++; | ||
j--; | ||
while (coords[2 * i + inc] < t) { i++; } | ||
while (coords[2 * j + inc] > t) { j--; } | ||
this.numItems = +numItems; | ||
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535); | ||
this.ArrayType = ArrayType; | ||
this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array; | ||
const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType); | ||
const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT; | ||
const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT; | ||
const padCoords = (8 - idsByteSize % 8) % 8; | ||
if (arrayTypeIndex < 0) { | ||
throw new Error(`Unexpected typed array class: ${ArrayType}.`); | ||
} | ||
if (coords[2 * left + inc] === t) { swapItem(ids, coords, left, j); } | ||
else { | ||
j++; | ||
swapItem(ids, coords, j, right); | ||
if (data && (data instanceof ArrayBuffer)) { // reconstruct an index from a buffer | ||
this.data = data; | ||
this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems); | ||
this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2); | ||
this._pos = numItems * 2; | ||
this._finished = true; | ||
} else { // initialize a new index | ||
this.data = new ArrayBuffer(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords); | ||
this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems); | ||
this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2); | ||
this._pos = 0; | ||
this._finished = false; | ||
// set header | ||
new Uint8Array(this.data, 0, 2).set([0xdb, (VERSION << 4) + arrayTypeIndex]); | ||
new Uint16Array(this.data, 2, 1)[0] = nodeSize; | ||
new Uint32Array(this.data, 4, 1)[0] = numItems; | ||
} | ||
} | ||
if (j <= k) { left = j + 1; } | ||
if (k <= j) { right = j - 1; } | ||
/** | ||
* Add a point to the index. | ||
* @param {number} x | ||
* @param {number} y | ||
* @returns {number} An incremental index associated with the added item (starting from `0`). | ||
*/ | ||
add(x, y) { | ||
const index = this._pos >> 1; | ||
this.ids[index] = index; | ||
this.coords[this._pos++] = x; | ||
this.coords[this._pos++] = y; | ||
return index; | ||
} | ||
} | ||
function swapItem(ids, coords, i, j) { | ||
swap(ids, i, j); | ||
swap(coords, 2 * i, 2 * j); | ||
swap(coords, 2 * i + 1, 2 * j + 1); | ||
} | ||
/** | ||
* Perform indexing of the added points. | ||
*/ | ||
finish() { | ||
const numAdded = this._pos >> 1; | ||
if (numAdded !== this.numItems) { | ||
throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`); | ||
} | ||
// kd-sort both arrays for efficient search | ||
sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0); | ||
function swap(arr, i, j) { | ||
var tmp = arr[i]; | ||
arr[i] = arr[j]; | ||
arr[j] = tmp; | ||
} | ||
this._finished = true; | ||
return this; | ||
} | ||
function range(ids, coords, minX, minY, maxX, maxY, nodeSize) { | ||
var stack = [0, ids.length - 1, 0]; | ||
var result = []; | ||
var x, y; | ||
/** | ||
* Search the index for items within a given bounding box. | ||
* @param {number} minX | ||
* @param {number} minY | ||
* @param {number} maxX | ||
* @param {number} maxY | ||
* @returns {number[]} An array of indices correponding to the found items. | ||
*/ | ||
range(minX, minY, maxX, maxY) { | ||
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().'); | ||
while (stack.length) { | ||
var axis = stack.pop(); | ||
var right = stack.pop(); | ||
var left = stack.pop(); | ||
const {ids, coords, nodeSize} = this; | ||
const stack = [0, ids.length - 1, 0]; | ||
const result = []; | ||
if (right - left <= nodeSize) { | ||
for (var i = left; i <= right; i++) { | ||
x = coords[2 * i]; | ||
y = coords[2 * i + 1]; | ||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) { result.push(ids[i]); } | ||
// recursively search for items in range in the kd-sorted arrays | ||
while (stack.length) { | ||
const axis = stack.pop() || 0; | ||
const right = stack.pop() || 0; | ||
const left = stack.pop() || 0; | ||
// if we reached "tree node", search linearly | ||
if (right - left <= nodeSize) { | ||
for (let i = left; i <= right; i++) { | ||
const x = coords[2 * i]; | ||
const y = coords[2 * i + 1]; | ||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]); | ||
} | ||
continue; | ||
} | ||
continue; | ||
} | ||
var m = Math.floor((left + right) / 2); | ||
// otherwise find the middle index | ||
const m = (left + right) >> 1; | ||
x = coords[2 * m]; | ||
y = coords[2 * m + 1]; | ||
// include the middle item if it's in range | ||
const x = coords[2 * m]; | ||
const y = coords[2 * m + 1]; | ||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]); | ||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) { result.push(ids[m]); } | ||
// queue search in halves that intersect the query | ||
if (axis === 0 ? minX <= x : minY <= y) { | ||
stack.push(left); | ||
stack.push(m - 1); | ||
stack.push(1 - axis); | ||
} | ||
if (axis === 0 ? maxX >= x : maxY >= y) { | ||
stack.push(m + 1); | ||
stack.push(right); | ||
stack.push(1 - axis); | ||
} | ||
} | ||
var nextAxis = (axis + 1) % 2; | ||
if (axis === 0 ? minX <= x : minY <= y) { | ||
stack.push(left); | ||
stack.push(m - 1); | ||
stack.push(nextAxis); | ||
} | ||
if (axis === 0 ? maxX >= x : maxY >= y) { | ||
stack.push(m + 1); | ||
stack.push(right); | ||
stack.push(nextAxis); | ||
} | ||
return result; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Search the index for items within a given radius. | ||
* @param {number} qx | ||
* @param {number} qy | ||
* @param {number} r Query radius. | ||
* @returns {number[]} An array of indices correponding to the found items. | ||
*/ | ||
within(qx, qy, r) { | ||
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().'); | ||
function within(ids, coords, qx, qy, r, nodeSize) { | ||
var stack = [0, ids.length - 1, 0]; | ||
var result = []; | ||
var r2 = r * r; | ||
const {ids, coords, nodeSize} = this; | ||
const stack = [0, ids.length - 1, 0]; | ||
const result = []; | ||
const r2 = r * r; | ||
while (stack.length) { | ||
var axis = stack.pop(); | ||
var right = stack.pop(); | ||
var left = stack.pop(); | ||
// recursively search for items within radius in the kd-sorted arrays | ||
while (stack.length) { | ||
const axis = stack.pop() || 0; | ||
const right = stack.pop() || 0; | ||
const left = stack.pop() || 0; | ||
if (right - left <= nodeSize) { | ||
for (var i = left; i <= right; i++) { | ||
if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) { result.push(ids[i]); } | ||
// if we reached "tree node", search linearly | ||
if (right - left <= nodeSize) { | ||
for (let i = left; i <= right; i++) { | ||
if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) result.push(ids[i]); | ||
} | ||
continue; | ||
} | ||
continue; | ||
} | ||
var m = Math.floor((left + right) / 2); | ||
// otherwise find the middle index | ||
const m = (left + right) >> 1; | ||
var x = coords[2 * m]; | ||
var y = coords[2 * m + 1]; | ||
// include the middle item if it's in range | ||
const x = coords[2 * m]; | ||
const y = coords[2 * m + 1]; | ||
if (sqDist(x, y, qx, qy) <= r2) result.push(ids[m]); | ||
if (sqDist(x, y, qx, qy) <= r2) { result.push(ids[m]); } | ||
// queue search in halves that intersect the query | ||
if (axis === 0 ? qx - r <= x : qy - r <= y) { | ||
stack.push(left); | ||
stack.push(m - 1); | ||
stack.push(1 - axis); | ||
} | ||
if (axis === 0 ? qx + r >= x : qy + r >= y) { | ||
stack.push(m + 1); | ||
stack.push(right); | ||
stack.push(1 - axis); | ||
} | ||
} | ||
var nextAxis = (axis + 1) % 2; | ||
if (axis === 0 ? qx - r <= x : qy - r <= y) { | ||
stack.push(left); | ||
stack.push(m - 1); | ||
stack.push(nextAxis); | ||
} | ||
if (axis === 0 ? qx + r >= x : qy + r >= y) { | ||
stack.push(m + 1); | ||
stack.push(right); | ||
stack.push(nextAxis); | ||
} | ||
return result; | ||
} | ||
return result; | ||
} | ||
function sqDist(ax, ay, bx, by) { | ||
var dx = ax - bx; | ||
var dy = ay - by; | ||
return dx * dx + dy * dy; | ||
/** | ||
* @param {Uint16Array | Uint32Array} ids | ||
* @param {InstanceType<TypedArrayConstructor>} coords | ||
* @param {number} nodeSize | ||
* @param {number} left | ||
* @param {number} right | ||
* @param {number} axis | ||
*/ | ||
function sort(ids, coords, nodeSize, left, right, axis) { | ||
if (right - left <= nodeSize) return; | ||
const m = (left + right) >> 1; // middle index | ||
// sort ids and coords around the middle index so that the halves lie | ||
// either left/right or top/bottom correspondingly (taking turns) | ||
select(ids, coords, m, left, right, axis); | ||
// recursively kd-sort first half and second half on the opposite axis | ||
sort(ids, coords, nodeSize, left, m - 1, 1 - axis); | ||
sort(ids, coords, nodeSize, m + 1, right, 1 - axis); | ||
} | ||
var defaultGetX = function (p) { return p[0]; }; | ||
var defaultGetY = function (p) { return p[1]; }; | ||
/** | ||
* Custom Floyd-Rivest selection algorithm: sort ids and coords so that | ||
* [left..k-1] items are smaller than k-th item (on either x or y axis) | ||
* @param {Uint16Array | Uint32Array} ids | ||
* @param {InstanceType<TypedArrayConstructor>} coords | ||
* @param {number} k | ||
* @param {number} left | ||
* @param {number} right | ||
* @param {number} axis | ||
*/ | ||
function select(ids, coords, k, left, right, axis) { | ||
var KDBush = function KDBush(points, getX, getY, nodeSize, ArrayType) { | ||
if ( getX === void 0 ) getX = defaultGetX; | ||
if ( getY === void 0 ) getY = defaultGetY; | ||
if ( nodeSize === void 0 ) nodeSize = 64; | ||
if ( ArrayType === void 0 ) ArrayType = Float64Array; | ||
while (right > left) { | ||
if (right - left > 600) { | ||
const n = right - left + 1; | ||
const m = k - left + 1; | ||
const z = Math.log(n); | ||
const s = 0.5 * Math.exp(2 * z / 3); | ||
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); | ||
const newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); | ||
const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); | ||
select(ids, coords, k, newLeft, newRight, axis); | ||
} | ||
this.nodeSize = nodeSize; | ||
this.points = points; | ||
const t = coords[2 * k + axis]; | ||
let i = left; | ||
let j = right; | ||
var IndexArrayType = points.length < 65536 ? Uint16Array : Uint32Array; | ||
swapItem(ids, coords, left, k); | ||
if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right); | ||
var ids = this.ids = new IndexArrayType(points.length); | ||
var coords = this.coords = new ArrayType(points.length * 2); | ||
while (i < j) { | ||
swapItem(ids, coords, i, j); | ||
i++; | ||
j--; | ||
while (coords[2 * i + axis] < t) i++; | ||
while (coords[2 * j + axis] > t) j--; | ||
} | ||
for (var i = 0; i < points.length; i++) { | ||
ids[i] = i; | ||
coords[2 * i] = getX(points[i]); | ||
coords[2 * i + 1] = getY(points[i]); | ||
if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j); | ||
else { | ||
j++; | ||
swapItem(ids, coords, j, right); | ||
} | ||
if (j <= k) left = j + 1; | ||
if (k <= j) right = j - 1; | ||
} | ||
} | ||
sortKD(ids, coords, nodeSize, 0, ids.length - 1, 0); | ||
}; | ||
/** | ||
* @param {Uint16Array | Uint32Array} ids | ||
* @param {InstanceType<TypedArrayConstructor>} coords | ||
* @param {number} i | ||
* @param {number} j | ||
*/ | ||
function swapItem(ids, coords, i, j) { | ||
swap(ids, i, j); | ||
swap(coords, 2 * i, 2 * j); | ||
swap(coords, 2 * i + 1, 2 * j + 1); | ||
} | ||
KDBush.prototype.range = function range$1 (minX, minY, maxX, maxY) { | ||
return range(this.ids, this.coords, minX, minY, maxX, maxY, this.nodeSize); | ||
}; | ||
/** | ||
* @param {InstanceType<TypedArrayConstructor>} arr | ||
* @param {number} i | ||
* @param {number} j | ||
*/ | ||
function swap(arr, i, j) { | ||
const tmp = arr[i]; | ||
arr[i] = arr[j]; | ||
arr[j] = tmp; | ||
} | ||
KDBush.prototype.within = function within$1 (x, y, r) { | ||
return within(this.ids, this.coords, x, y, r, this.nodeSize); | ||
}; | ||
/** | ||
* @param {number} ax | ||
* @param {number} ay | ||
* @param {number} bx | ||
* @param {number} by | ||
*/ | ||
function sqDist(ax, ay, bx, by) { | ||
const dx = ax - bx; | ||
const dy = ay - by; | ||
return dx * dx + dy * dy; | ||
} | ||
var defaultOptions = { | ||
const defaultOptions = { | ||
minZoom: 0, // min zoom to generate clusters on | ||
@@ -211,377 +351,370 @@ maxZoom: 16, // max zoom level to cluster the points on | ||
// properties to use for individual points when running the reducer | ||
map: function (props) { return props; } // props => ({sum: props.my_value}) | ||
map: props => props // props => ({sum: props.my_value}) | ||
}; | ||
var fround = Math.fround || (function (tmp) { return (function (x) { tmp[0] = +x; return tmp[0]; }); })(new Float32Array(1)); | ||
const fround = Math.fround || (tmp => ((x) => { tmp[0] = +x; return tmp[0]; }))(new Float32Array(1)); | ||
var Supercluster = function Supercluster(options) { | ||
this.options = extend(Object.create(defaultOptions), options); | ||
this.trees = new Array(this.options.maxZoom + 1); | ||
}; | ||
const OFFSET_ZOOM = 2; | ||
const OFFSET_ID = 3; | ||
const OFFSET_PARENT = 4; | ||
const OFFSET_NUM = 5; | ||
const OFFSET_PROP = 6; | ||
Supercluster.prototype.load = function load (points) { | ||
var ref = this.options; | ||
var log = ref.log; | ||
var minZoom = ref.minZoom; | ||
var maxZoom = ref.maxZoom; | ||
var nodeSize = ref.nodeSize; | ||
class Supercluster { | ||
constructor(options) { | ||
this.options = Object.assign(Object.create(defaultOptions), options); | ||
this.trees = new Array(this.options.maxZoom + 1); | ||
this.stride = this.options.reduce ? 7 : 6; | ||
this.clusterProps = []; | ||
} | ||
if (log) { console.time('total time'); } | ||
load(points) { | ||
const {log, minZoom, maxZoom} = this.options; | ||
var timerId = "prepare " + (points.length) + " points"; | ||
if (log) { console.time(timerId); } | ||
if (log) console.time('total time'); | ||
this.points = points; | ||
const timerId = `prepare ${ points.length } points`; | ||
if (log) console.time(timerId); | ||
// generate a cluster object for each point and index input points into a KD-tree | ||
var clusters = []; | ||
for (var i = 0; i < points.length; i++) { | ||
if (!points[i].geometry) { continue; } | ||
clusters.push(createPointCluster(points[i], i)); | ||
} | ||
this.trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); | ||
this.points = points; | ||
if (log) { console.timeEnd(timerId); } | ||
// generate a cluster object for each point and index input points into a KD-tree | ||
const data = []; | ||
// cluster points on max zoom, then cluster the results on previous zoom, etc.; | ||
// results in a cluster hierarchy across zoom levels | ||
for (var z = maxZoom; z >= minZoom; z--) { | ||
var now = +Date.now(); | ||
for (let i = 0; i < points.length; i++) { | ||
const p = points[i]; | ||
if (!p.geometry) continue; | ||
// 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); | ||
const [lng, lat] = p.geometry.coordinates; | ||
const x = fround(lngX(lng)); | ||
const y = fround(latY(lat)); | ||
// store internal point/cluster data in flat numeric arrays for performance | ||
data.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) data.push(0); // noop | ||
} | ||
let tree = this.trees[maxZoom + 1] = this._createTree(data); | ||
if (log) { console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now); } | ||
} | ||
if (log) console.timeEnd(timerId); | ||
if (log) { console.timeEnd('total time'); } | ||
// 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--) { | ||
const now = +Date.now(); | ||
return this; | ||
}; | ||
// create a new set of clusters for the zoom and index them with a KD-tree | ||
tree = this.trees[z] = this._createTree(this._cluster(tree, z)); | ||
Supercluster.prototype.getClusters = function getClusters (bbox, zoom) { | ||
var minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180; | ||
var minLat = Math.max(-90, Math.min(90, bbox[1])); | ||
var maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180; | ||
var maxLat = Math.max(-90, Math.min(90, bbox[3])); | ||
if (log) console.log('z%d: %d clusters in %dms', z, tree.numItems, +Date.now() - now); | ||
} | ||
if (bbox[2] - bbox[0] >= 360) { | ||
minLng = -180; | ||
maxLng = 180; | ||
} else if (minLng > maxLng) { | ||
var easternHem = this.getClusters([minLng, minLat, 180, maxLat], zoom); | ||
var westernHem = this.getClusters([-180, minLat, maxLng, maxLat], zoom); | ||
return easternHem.concat(westernHem); | ||
if (log) console.timeEnd('total time'); | ||
return this; | ||
} | ||
var tree = this.trees[this._limitZoom(zoom)]; | ||
var ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat)); | ||
var clusters = []; | ||
for (var i = 0, list = ids; i < list.length; i += 1) { | ||
var id = list[i]; | ||
getClusters(bbox, zoom) { | ||
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180; | ||
const minLat = Math.max(-90, Math.min(90, bbox[1])); | ||
let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180; | ||
const maxLat = Math.max(-90, Math.min(90, bbox[3])); | ||
var c = tree.points[id]; | ||
clusters.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]); | ||
if (bbox[2] - bbox[0] >= 360) { | ||
minLng = -180; | ||
maxLng = 180; | ||
} else if (minLng > maxLng) { | ||
const easternHem = this.getClusters([minLng, minLat, 180, maxLat], zoom); | ||
const westernHem = this.getClusters([-180, minLat, maxLng, maxLat], zoom); | ||
return easternHem.concat(westernHem); | ||
} | ||
const tree = this.trees[this._limitZoom(zoom)]; | ||
const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat)); | ||
const data = tree.data; | ||
const clusters = []; | ||
for (const id of ids) { | ||
const k = this.stride * id; | ||
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]); | ||
} | ||
return clusters; | ||
} | ||
return clusters; | ||
}; | ||
Supercluster.prototype.getChildren = function getChildren (clusterId) { | ||
var originId = this._getOriginId(clusterId); | ||
var originZoom = this._getOriginZoom(clusterId); | ||
var errorMsg = 'No cluster with the specified id.'; | ||
getChildren(clusterId) { | ||
const originId = this._getOriginId(clusterId); | ||
const originZoom = this._getOriginZoom(clusterId); | ||
const errorMsg = 'No cluster with the specified id.'; | ||
var index = this.trees[originZoom]; | ||
if (!index) { throw new Error(errorMsg); } | ||
const tree = this.trees[originZoom]; | ||
if (!tree) throw new Error(errorMsg); | ||
var origin = index.points[originId]; | ||
if (!origin) { throw new Error(errorMsg); } | ||
const data = tree.data; | ||
if (originId * this.stride >= data.length) throw new Error(errorMsg); | ||
var r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)); | ||
var ids = index.within(origin.x, origin.y, r); | ||
var children = []; | ||
for (var i = 0, list = ids; i < list.length; i += 1) { | ||
var id = list[i]; | ||
var c = index.points[id]; | ||
if (c.parentId === clusterId) { | ||
children.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]); | ||
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)); | ||
const x = data[originId * this.stride]; | ||
const y = data[originId * this.stride + 1]; | ||
const ids = tree.within(x, y, r); | ||
const children = []; | ||
for (const id of ids) { | ||
const k = id * this.stride; | ||
if (data[k + OFFSET_PARENT] === clusterId) { | ||
children.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]); | ||
} | ||
} | ||
} | ||
if (children.length === 0) { throw new Error(errorMsg); } | ||
if (children.length === 0) throw new Error(errorMsg); | ||
return children; | ||
}; | ||
return children; | ||
} | ||
Supercluster.prototype.getLeaves = function getLeaves (clusterId, limit, offset) { | ||
limit = limit || 10; | ||
offset = offset || 0; | ||
getLeaves(clusterId, limit, offset) { | ||
limit = limit || 10; | ||
offset = offset || 0; | ||
var leaves = []; | ||
this._appendLeaves(leaves, clusterId, limit, offset, 0); | ||
const leaves = []; | ||
this._appendLeaves(leaves, clusterId, limit, offset, 0); | ||
return leaves; | ||
}; | ||
return leaves; | ||
} | ||
Supercluster.prototype.getTile = function getTile (z, x, y) { | ||
var tree = this.trees[this._limitZoom(z)]; | ||
var z2 = Math.pow(2, z); | ||
var ref = this.options; | ||
var extent = ref.extent; | ||
var radius = ref.radius; | ||
var p = radius / extent; | ||
var top = (y - p) / z2; | ||
var bottom = (y + 1 + p) / z2; | ||
getTile(z, x, y) { | ||
const tree = this.trees[this._limitZoom(z)]; | ||
const z2 = Math.pow(2, z); | ||
const {extent, radius} = this.options; | ||
const p = radius / extent; | ||
const top = (y - p) / z2; | ||
const bottom = (y + 1 + p) / z2; | ||
var tile = { | ||
features: [] | ||
}; | ||
const tile = { | ||
features: [] | ||
}; | ||
this._addTileFeatures( | ||
tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), | ||
tree.points, x, y, z2, tile); | ||
if (x === 0) { | ||
this._addTileFeatures( | ||
tree.range(1 - p / z2, top, 1, bottom), | ||
tree.points, z2, y, z2, tile); | ||
} | ||
if (x === z2 - 1) { | ||
this._addTileFeatures( | ||
tree.range(0, top, p / z2, bottom), | ||
tree.points, -1, y, z2, tile); | ||
} | ||
tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), | ||
tree.data, x, y, z2, tile); | ||
return tile.features.length ? tile : null; | ||
}; | ||
if (x === 0) { | ||
this._addTileFeatures( | ||
tree.range(1 - p / z2, top, 1, bottom), | ||
tree.data, z2, y, z2, tile); | ||
} | ||
if (x === z2 - 1) { | ||
this._addTileFeatures( | ||
tree.range(0, top, p / z2, bottom), | ||
tree.data, -1, y, z2, tile); | ||
} | ||
Supercluster.prototype.getClusterExpansionZoom = function getClusterExpansionZoom (clusterId) { | ||
var expansionZoom = this._getOriginZoom(clusterId) - 1; | ||
while (expansionZoom <= this.options.maxZoom) { | ||
var children = this.getChildren(clusterId); | ||
expansionZoom++; | ||
if (children.length !== 1) { break; } | ||
clusterId = children[0].properties.cluster_id; | ||
return tile.features.length ? tile : null; | ||
} | ||
return expansionZoom; | ||
}; | ||
Supercluster.prototype._appendLeaves = function _appendLeaves (result, clusterId, limit, offset, skipped) { | ||
var children = this.getChildren(clusterId); | ||
getClusterExpansionZoom(clusterId) { | ||
let expansionZoom = this._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; | ||
} | ||
for (var i = 0, list = children; i < list.length; i += 1) { | ||
var child = list[i]; | ||
_appendLeaves(result, clusterId, limit, offset, skipped) { | ||
const children = this.getChildren(clusterId); | ||
var props = child.properties; | ||
for (const child of children) { | ||
const props = child.properties; | ||
if (props && props.cluster) { | ||
if (skipped + props.point_count <= offset) { | ||
// skip the whole cluster | ||
skipped += props.point_count; | ||
if (props && props.cluster) { | ||
if (skipped + props.point_count <= offset) { | ||
// skip the whole cluster | ||
skipped += props.point_count; | ||
} else { | ||
// enter the cluster | ||
skipped = this._appendLeaves(result, props.cluster_id, limit, offset, skipped); | ||
// exit the cluster | ||
} | ||
} else if (skipped < offset) { | ||
// skip a single point | ||
skipped++; | ||
} else { | ||
// enter the cluster | ||
skipped = this._appendLeaves(result, props.cluster_id, limit, offset, skipped); | ||
// exit the cluster | ||
// add a single point | ||
result.push(child); | ||
} | ||
} else if (skipped < offset) { | ||
// skip a single point | ||
skipped++; | ||
} else { | ||
// add a single point | ||
result.push(child); | ||
if (result.length === limit) break; | ||
} | ||
if (result.length === limit) { break; } | ||
return skipped; | ||
} | ||
return skipped; | ||
}; | ||
_createTree(data) { | ||
const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array); | ||
for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]); | ||
tree.finish(); | ||
tree.data = data; | ||
return tree; | ||
} | ||
Supercluster.prototype._addTileFeatures = function _addTileFeatures (ids, points, x, y, z2, tile) { | ||
for (var i$1 = 0, list = ids; i$1 < list.length; i$1 += 1) { | ||
var i = list[i$1]; | ||
_addTileFeatures(ids, data, x, y, z2, tile) { | ||
for (const i of ids) { | ||
const k = i * this.stride; | ||
const isCluster = data[k + OFFSET_NUM] > 1; | ||
var c = points[i]; | ||
var isCluster = c.numPoints; | ||
let tags, px, py; | ||
if (isCluster) { | ||
tags = getClusterProperties(data, k, this.clusterProps); | ||
px = data[k]; | ||
py = data[k + 1]; | ||
} else { | ||
const p = this.points[data[k + OFFSET_ID]]; | ||
tags = p.properties; | ||
const [lng, lat] = p.geometry.coordinates; | ||
px = lngX(lng); | ||
py = latY(lat); | ||
} | ||
var tags = (void 0), px = (void 0), py = (void 0); | ||
if (isCluster) { | ||
tags = getClusterProperties(c); | ||
px = c.x; | ||
py = c.y; | ||
} else { | ||
var p = this.points[c.index]; | ||
tags = p.properties; | ||
px = lngX(p.geometry.coordinates[0]); | ||
py = latY(p.geometry.coordinates[1]); | ||
} | ||
const f = { | ||
type: 1, | ||
geometry: [[ | ||
Math.round(this.options.extent * (px * z2 - x)), | ||
Math.round(this.options.extent * (py * z2 - y)) | ||
]], | ||
tags | ||
}; | ||
var f = { | ||
type: 1, | ||
geometry: [[ | ||
Math.round(this.options.extent * (px * z2 - x)), | ||
Math.round(this.options.extent * (py * z2 - y)) | ||
]], | ||
tags: tags | ||
}; | ||
// assign id | ||
let id; | ||
if (isCluster || this.options.generateId) { | ||
// optionally generate id for points | ||
id = data[k + OFFSET_ID]; | ||
} else { | ||
// keep id if already assigned | ||
id = this.points[data[k + OFFSET_ID]].id; | ||
} | ||
// assign id | ||
var id = (void 0); | ||
if (isCluster) { | ||
id = c.id; | ||
} else if (this.options.generateId) { | ||
// optionally generate id | ||
id = c.index; | ||
} else if (this.points[c.index].id) { | ||
// keep id if already assigned | ||
id = this.points[c.index].id; | ||
if (id !== undefined) f.id = id; | ||
tile.features.push(f); | ||
} | ||
} | ||
if (id !== undefined) { f.id = id; } | ||
tile.features.push(f); | ||
_limitZoom(z) { | ||
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1)); | ||
} | ||
}; | ||
Supercluster.prototype._limitZoom = function _limitZoom (z) { | ||
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1)); | ||
}; | ||
_cluster(tree, zoom) { | ||
const {radius, extent, reduce, minPoints} = this.options; | ||
const r = radius / (extent * Math.pow(2, zoom)); | ||
const data = tree.data; | ||
const nextData = []; | ||
const stride = this.stride; | ||
Supercluster.prototype._cluster = function _cluster (points, zoom) { | ||
var clusters = []; | ||
var ref = this.options; | ||
var radius = ref.radius; | ||
var extent = ref.extent; | ||
var reduce = ref.reduce; | ||
var minPoints = ref.minPoints; | ||
var r = radius / (extent * Math.pow(2, zoom)); | ||
// loop through each point | ||
for (let i = 0; i < data.length; i += 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; | ||
// loop through each point | ||
for (var i = 0; i < points.length; i++) { | ||
var p = points[i]; | ||
// if we've already visited the point at this zoom level, skip it | ||
if (p.zoom <= zoom) { continue; } | ||
p.zoom = zoom; | ||
// find all nearby points | ||
const x = data[i]; | ||
const y = data[i + 1]; | ||
const neighborIds = tree.within(data[i], data[i + 1], r); | ||
// find all nearby points | ||
var tree = this.trees[zoom + 1]; | ||
var neighborIds = tree.within(p.x, p.y, r); | ||
const numPointsOrigin = data[i + OFFSET_NUM]; | ||
let numPoints = numPointsOrigin; | ||
var numPointsOrigin = p.numPoints || 1; | ||
var numPoints = numPointsOrigin; | ||
// count the number of points in a potential cluster | ||
for (const neighborId of neighborIds) { | ||
const k = neighborId * stride; | ||
// filter out neighbors that are already processed | ||
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM]; | ||
} | ||
// count the number of points in a potential cluster | ||
for (var i$1 = 0, list = neighborIds; i$1 < list.length; i$1 += 1) { | ||
var neighborId = list[i$1]; | ||
// if there were neighbors to merge, and there are enough points to form a cluster | ||
if (numPoints > numPointsOrigin && numPoints >= minPoints) { | ||
let wx = x * numPointsOrigin; | ||
let wy = y * numPointsOrigin; | ||
var b = tree.points[neighborId]; | ||
// filter out neighbors that are already processed | ||
if (b.zoom > zoom) { numPoints += b.numPoints || 1; } | ||
} | ||
let clusterProperties; | ||
let clusterPropIndex = -1; | ||
// if there were neighbors to merge, and there are enough points to form a cluster | ||
if (numPoints > numPointsOrigin && numPoints >= minPoints) { | ||
var wx = p.x * numPointsOrigin; | ||
var wy = p.y * numPointsOrigin; | ||
// 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; | ||
var clusterProperties = reduce && numPointsOrigin > 1 ? this._map(p, true) : null; | ||
for (const neighborId of neighborIds) { | ||
const k = neighborId * stride; | ||
// encode both zoom and point index on which the cluster originated -- offset by total length of features | ||
var id = (i << 5) + (zoom + 1) + this.points.length; | ||
if (data[k + OFFSET_ZOOM] <= zoom) continue; | ||
data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice) | ||
for (var i$2 = 0, list$1 = neighborIds; i$2 < list$1.length; i$2 += 1) { | ||
var neighborId$1 = list$1[i$2]; | ||
const numPoints2 = data[k + OFFSET_NUM]; | ||
wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center | ||
wy += data[k + 1] * numPoints2; | ||
var b$1 = tree.points[neighborId$1]; | ||
data[k + OFFSET_PARENT] = id; | ||
if (b$1.zoom <= zoom) { continue; } | ||
b$1.zoom = zoom; // save the zoom (so it doesn't get processed twice) | ||
var numPoints2 = b$1.numPoints || 1; | ||
wx += b$1.x * numPoints2; // accumulate coordinates for calculating weighted center | ||
wy += b$1.y * numPoints2; | ||
b$1.parentId = id; | ||
if (reduce) { | ||
if (!clusterProperties) { clusterProperties = this._map(p, true); } | ||
reduce(clusterProperties, this._map(b$1)); | ||
if (reduce) { | ||
if (!clusterProperties) { | ||
clusterProperties = this._map(data, i, true); | ||
clusterPropIndex = this.clusterProps.length; | ||
this.clusterProps.push(clusterProperties); | ||
} | ||
reduce(clusterProperties, this._map(data, k)); | ||
} | ||
} | ||
} | ||
p.parentId = id; | ||
clusters.push(createCluster(wx / numPoints, wy / numPoints, id, numPoints, clusterProperties)); | ||
data[i + OFFSET_PARENT] = id; | ||
nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints); | ||
if (reduce) nextData.push(clusterPropIndex); | ||
} else { // left points as unclustered | ||
clusters.push(p); | ||
} else { // left points as unclustered | ||
for (let j = 0; j < stride; j++) nextData.push(data[i + j]); | ||
if (numPoints > 1) { | ||
for (var i$3 = 0, list$2 = neighborIds; i$3 < list$2.length; i$3 += 1) { | ||
var neighborId$2 = list$2[i$3]; | ||
var b$2 = tree.points[neighborId$2]; | ||
if (b$2.zoom <= zoom) { continue; } | ||
b$2.zoom = zoom; | ||
clusters.push(b$2); | ||
if (numPoints > 1) { | ||
for (const neighborId of neighborIds) { | ||
const k = neighborId * stride; | ||
if (data[k + OFFSET_ZOOM] <= zoom) continue; | ||
data[k + OFFSET_ZOOM] = zoom; | ||
for (let j = 0; j < stride; j++) nextData.push(data[k + j]); | ||
} | ||
} | ||
} | ||
} | ||
return nextData; | ||
} | ||
return clusters; | ||
}; | ||
// get index of the point from which the cluster originated | ||
_getOriginId(clusterId) { | ||
return (clusterId - this.points.length) >> 5; | ||
} | ||
// get index of the point from which the cluster originated | ||
Supercluster.prototype._getOriginId = function _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; | ||
} | ||
// get zoom of the point from which the cluster originated | ||
Supercluster.prototype._getOriginZoom = function _getOriginZoom (clusterId) { | ||
return (clusterId - this.points.length) % 32; | ||
}; | ||
Supercluster.prototype._map = function _map (point, clone) { | ||
if (point.numPoints) { | ||
return clone ? extend({}, point.properties) : point.properties; | ||
_map(data, i, clone) { | ||
if (data[i + OFFSET_NUM] > 1) { | ||
const props = this.clusterProps[data[i + OFFSET_PROP]]; | ||
return clone ? Object.assign({}, props) : props; | ||
} | ||
const original = this.points[data[i + OFFSET_ID]].properties; | ||
const result = this.options.map(original); | ||
return clone && result === original ? Object.assign({}, result) : result; | ||
} | ||
var original = this.points[point.index].properties; | ||
var result = this.options.map(original); | ||
return clone && result === original ? extend({}, result) : result; | ||
}; | ||
function createCluster(x, y, id, numPoints, properties) { | ||
return { | ||
x: fround(x), // weighted cluster center; round for consistency with Float32Array index | ||
y: fround(y), | ||
zoom: Infinity, // the last zoom the cluster was processed at | ||
id: id, // encodes index of the first child of the cluster and its zoom level | ||
parentId: -1, // parent cluster id | ||
numPoints: numPoints, | ||
properties: properties | ||
}; | ||
} | ||
function createPointCluster(p, id) { | ||
var ref = p.geometry.coordinates; | ||
var x = ref[0]; | ||
var y = ref[1]; | ||
function getClusterJSON(data, i, clusterProps) { | ||
return { | ||
x: fround(lngX(x)), // projected point coordinates | ||
y: fround(latY(y)), | ||
zoom: Infinity, // the last zoom the point was processed at | ||
index: id, // index of the source feature in the original input array, | ||
parentId: -1 // parent cluster id | ||
}; | ||
} | ||
function getClusterJSON(cluster) { | ||
return { | ||
type: 'Feature', | ||
id: cluster.id, | ||
properties: getClusterProperties(cluster), | ||
id: data[i + OFFSET_ID], | ||
properties: getClusterProperties(data, i, clusterProps), | ||
geometry: { | ||
type: 'Point', | ||
coordinates: [xLng(cluster.x), yLat(cluster.y)] | ||
coordinates: [xLng(data[i]), yLat(data[i + 1])] | ||
} | ||
@@ -591,10 +724,12 @@ }; | ||
function getClusterProperties(cluster) { | ||
var count = cluster.numPoints; | ||
var abbrev = | ||
count >= 10000 ? ((Math.round(count / 1000)) + "k") : | ||
count >= 1000 ? ((Math.round(count / 100) / 10) + "k") : count; | ||
return extend(extend({}, cluster.properties), { | ||
function getClusterProperties(data, i, clusterProps) { | ||
const count = data[i + OFFSET_NUM]; | ||
const abbrev = | ||
count >= 10000 ? `${Math.round(count / 1000) }k` : | ||
count >= 1000 ? `${Math.round(count / 100) / 10 }k` : count; | ||
const propIndex = data[i + OFFSET_PROP]; | ||
const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]); | ||
return Object.assign(properties, { | ||
cluster: true, | ||
cluster_id: cluster.id, | ||
cluster_id: data[i + OFFSET_ID], | ||
point_count: count, | ||
@@ -610,4 +745,4 @@ point_count_abbreviated: abbrev | ||
function latY(lat) { | ||
var sin = Math.sin(lat * Math.PI / 180); | ||
var y = (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI); | ||
const sin = Math.sin(lat * Math.PI / 180); | ||
const y = (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI); | ||
return y < 0 ? 0 : y > 1 ? 1 : y; | ||
@@ -621,20 +756,8 @@ } | ||
function yLat(y) { | ||
var y2 = (180 - y * 360) * Math.PI / 180; | ||
const y2 = (180 - y * 360) * Math.PI / 180; | ||
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90; | ||
} | ||
function extend(dest, src) { | ||
for (var id in src) { dest[id] = src[id]; } | ||
return dest; | ||
} | ||
function getX(p) { | ||
return p.x; | ||
} | ||
function getY(p) { | ||
return p.y; | ||
} | ||
return Supercluster; | ||
})); |
@@ -1,1 +0,1 @@ | ||
!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?module.exports=o():"function"==typeof define&&define.amd?define(o):(t="undefined"!=typeof globalThis?globalThis:t||self).Supercluster=o()}(this,(function(){"use strict";function t(e,n,r,i,s,a){if(!(s-i<=r)){var p=i+s>>1;o(e,n,p,i,s,a%2),t(e,n,r,i,p-1,a+1),t(e,n,r,p+1,s,a+1)}}function o(t,n,r,i,s,a){for(;s>i;){if(s-i>600){var p=s-i+1,h=r-i+1,u=Math.log(p),f=.5*Math.exp(2*u/3),d=.5*Math.sqrt(u*f*(p-f)/p)*(h-p/2<0?-1:1);o(t,n,r,Math.max(i,Math.floor(r-h*f/p+d)),Math.min(s,Math.floor(r+(p-h)*f/p+d)),a)}var l=n[2*r+a],m=i,c=s;for(e(t,n,i,r),n[2*s+a]>l&&e(t,n,i,s);m<c;){for(e(t,n,m,c),m++,c--;n[2*m+a]<l;)m++;for(;n[2*c+a]>l;)c--}n[2*i+a]===l?e(t,n,i,c):e(t,n,++c,s),c<=r&&(i=c+1),r<=c&&(s=c-1)}}function e(t,o,e,r){n(t,e,r),n(o,2*e,2*r),n(o,2*e+1,2*r+1)}function n(t,o,e){var n=t[o];t[o]=t[e],t[e]=n}function r(t,o,e,n){var r=t-e,i=o-n;return r*r+i*i}var i=function(t){return t[0]},s=function(t){return t[1]},a=function(o,e,n,r,a){void 0===e&&(e=i),void 0===n&&(n=s),void 0===r&&(r=64),void 0===a&&(a=Float64Array),this.nodeSize=r,this.points=o;for(var p=o.length<65536?Uint16Array:Uint32Array,h=this.ids=new p(o.length),u=this.coords=new a(2*o.length),f=0;f<o.length;f++)h[f]=f,u[2*f]=e(o[f]),u[2*f+1]=n(o[f]);t(h,u,r,0,h.length-1,0)};a.prototype.range=function(t,o,e,n){return function(t,o,e,n,r,i,s){for(var a,p,h=[0,t.length-1,0],u=[];h.length;){var f=h.pop(),d=h.pop(),l=h.pop();if(d-l<=s)for(var m=l;m<=d;m++)a=o[2*m],p=o[2*m+1],a>=e&&a<=r&&p>=n&&p<=i&&u.push(t[m]);else{var c=Math.floor((l+d)/2);a=o[2*c],p=o[2*c+1],a>=e&&a<=r&&p>=n&&p<=i&&u.push(t[c]);var v=(f+1)%2;(0===f?e<=a:n<=p)&&(h.push(l),h.push(c-1),h.push(v)),(0===f?r>=a:i>=p)&&(h.push(c+1),h.push(d),h.push(v))}}return u}(this.ids,this.coords,t,o,e,n,this.nodeSize)},a.prototype.within=function(t,o,e){return function(t,o,e,n,i,s){for(var a=[0,t.length-1,0],p=[],h=i*i;a.length;){var u=a.pop(),f=a.pop(),d=a.pop();if(f-d<=s)for(var l=d;l<=f;l++)r(o[2*l],o[2*l+1],e,n)<=h&&p.push(t[l]);else{var m=Math.floor((d+f)/2),c=o[2*m],v=o[2*m+1];r(c,v,e,n)<=h&&p.push(t[m]);var g=(u+1)%2;(0===u?e-i<=c:n-i<=v)&&(a.push(d),a.push(m-1),a.push(g)),(0===u?e+i>=c:n+i>=v)&&(a.push(m+1),a.push(f),a.push(g))}}return p}(this.ids,this.coords,t,o,e,this.nodeSize)};var p,h={minZoom:0,maxZoom:16,minPoints:2,radius:40,extent:512,nodeSize:64,log:!1,generateId:!1,reduce:null,map:function(t){return t}},u=Math.fround||(p=new Float32Array(1),function(t){return p[0]=+t,p[0]}),f=function(t){this.options=y(Object.create(h),t),this.trees=new Array(this.options.maxZoom+1)};function d(t,o,e,n,r){return{x:u(t),y:u(o),zoom:1/0,id:e,parentId:-1,numPoints:n,properties:r}}function l(t,o){var e=t.geometry.coordinates,n=e[0],r=e[1];return{x:u(v(n)),y:u(g(r)),zoom:1/0,index:o,parentId:-1}}function m(t){return{type:"Feature",id:t.id,properties:c(t),geometry:{type:"Point",coordinates:[(n=t.x,360*(n-.5)),(o=t.y,e=(180-360*o)*Math.PI/180,360*Math.atan(Math.exp(e))/Math.PI-90)]}};var o,e,n}function c(t){var o=t.numPoints,e=o>=1e4?Math.round(o/1e3)+"k":o>=1e3?Math.round(o/100)/10+"k":o;return y(y({},t.properties),{cluster:!0,cluster_id:t.id,point_count:o,point_count_abbreviated:e})}function v(t){return t/360+.5}function g(t){var o=Math.sin(t*Math.PI/180),e=.5-.25*Math.log((1+o)/(1-o))/Math.PI;return e<0?0:e>1?1:e}function y(t,o){for(var e in o)t[e]=o[e];return t}function x(t){return t.x}function M(t){return t.y}return f.prototype.load=function(t){var o=this.options,e=o.log,n=o.minZoom,r=o.maxZoom,i=o.nodeSize;e&&console.time("total time");var s="prepare "+t.length+" points";e&&console.time(s),this.points=t;for(var p=[],h=0;h<t.length;h++)t[h].geometry&&p.push(l(t[h],h));this.trees[r+1]=new a(p,x,M,i,Float32Array),e&&console.timeEnd(s);for(var u=r;u>=n;u--){var f=+Date.now();p=this._cluster(p,u),this.trees[u]=new a(p,x,M,i,Float32Array),e&&console.log("z%d: %d clusters in %dms",u,p.length,+Date.now()-f)}return e&&console.timeEnd("total time"),this},f.prototype.getClusters=function(t,o){var e=((t[0]+180)%360+360)%360-180,n=Math.max(-90,Math.min(90,t[1])),r=180===t[2]?180:((t[2]+180)%360+360)%360-180,i=Math.max(-90,Math.min(90,t[3]));if(t[2]-t[0]>=360)e=-180,r=180;else if(e>r){var s=this.getClusters([e,n,180,i],o),a=this.getClusters([-180,n,r,i],o);return s.concat(a)}for(var p=this.trees[this._limitZoom(o)],h=[],u=0,f=p.range(v(e),g(i),v(r),g(n));u<f.length;u+=1){var d=f[u],l=p.points[d];h.push(l.numPoints?m(l):this.points[l.index])}return h},f.prototype.getChildren=function(t){var o=this._getOriginId(t),e=this._getOriginZoom(t),n="No cluster with the specified id.",r=this.trees[e];if(!r)throw new Error(n);var i=r.points[o];if(!i)throw new Error(n);for(var s=this.options.radius/(this.options.extent*Math.pow(2,e-1)),a=[],p=0,h=r.within(i.x,i.y,s);p<h.length;p+=1){var u=h[p],f=r.points[u];f.parentId===t&&a.push(f.numPoints?m(f):this.points[f.index])}if(0===a.length)throw new Error(n);return a},f.prototype.getLeaves=function(t,o,e){o=o||10,e=e||0;var n=[];return this._appendLeaves(n,t,o,e,0),n},f.prototype.getTile=function(t,o,e){var n=this.trees[this._limitZoom(t)],r=Math.pow(2,t),i=this.options,s=i.extent,a=i.radius/s,p=(e-a)/r,h=(e+1+a)/r,u={features:[]};return this._addTileFeatures(n.range((o-a)/r,p,(o+1+a)/r,h),n.points,o,e,r,u),0===o&&this._addTileFeatures(n.range(1-a/r,p,1,h),n.points,r,e,r,u),o===r-1&&this._addTileFeatures(n.range(0,p,a/r,h),n.points,-1,e,r,u),u.features.length?u:null},f.prototype.getClusterExpansionZoom=function(t){for(var o=this._getOriginZoom(t)-1;o<=this.options.maxZoom;){var e=this.getChildren(t);if(o++,1!==e.length)break;t=e[0].properties.cluster_id}return o},f.prototype._appendLeaves=function(t,o,e,n,r){for(var i=0,s=this.getChildren(o);i<s.length;i+=1){var a=s[i],p=a.properties;if(p&&p.cluster?r+p.point_count<=n?r+=p.point_count:r=this._appendLeaves(t,p.cluster_id,e,n,r):r<n?r++:t.push(a),t.length===e)break}return r},f.prototype._addTileFeatures=function(t,o,e,n,r,i){for(var s=0,a=t;s<a.length;s+=1){var p=o[a[s]],h=p.numPoints,u=void 0,f=void 0,d=void 0;if(h)u=c(p),f=p.x,d=p.y;else{var l=this.points[p.index];u=l.properties,f=v(l.geometry.coordinates[0]),d=g(l.geometry.coordinates[1])}var m={type:1,geometry:[[Math.round(this.options.extent*(f*r-e)),Math.round(this.options.extent*(d*r-n))]],tags:u},y=void 0;h?y=p.id:this.options.generateId?y=p.index:this.points[p.index].id&&(y=this.points[p.index].id),void 0!==y&&(m.id=y),i.features.push(m)}},f.prototype._limitZoom=function(t){return Math.max(this.options.minZoom,Math.min(Math.floor(+t),this.options.maxZoom+1))},f.prototype._cluster=function(t,o){for(var e=[],n=this.options,r=n.radius,i=n.extent,s=n.reduce,a=n.minPoints,p=r/(i*Math.pow(2,o)),h=0;h<t.length;h++){var u=t[h];if(!(u.zoom<=o)){u.zoom=o;for(var f=this.trees[o+1],l=f.within(u.x,u.y,p),m=u.numPoints||1,c=m,v=0,g=l;v<g.length;v+=1){var y=g[v],x=f.points[y];x.zoom>o&&(c+=x.numPoints||1)}if(c>m&&c>=a){for(var M=u.x*m,_=u.y*m,w=s&&m>1?this._map(u,!0):null,P=(h<<5)+(o+1)+this.points.length,z=0,Z=l;z<Z.length;z+=1){var I=Z[z],F=f.points[I];if(!(F.zoom<=o)){F.zoom=o;var b=F.numPoints||1;M+=F.x*b,_+=F.y*b,F.parentId=P,s&&(w||(w=this._map(u,!0)),s(w,this._map(F)))}}u.parentId=P,e.push(d(M/c,_/c,P,c,w))}else if(e.push(u),c>1)for(var A=0,C=l;A<C.length;A+=1){var T=C[A],E=f.points[T];E.zoom<=o||(E.zoom=o,e.push(E))}}}return e},f.prototype._getOriginId=function(t){return t-this.points.length>>5},f.prototype._getOriginZoom=function(t){return(t-this.points.length)%32},f.prototype._map=function(t,o){if(t.numPoints)return o?y({},t.properties):t.properties;var e=this.points[t.index].properties,n=this.options.map(e);return o&&n===e?y({},n):n},f})); | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Supercluster=e()}(this,(function(){"use strict";const t=[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];class e{static from(s){if(!(s instanceof ArrayBuffer))throw new Error("Data must be an instance of ArrayBuffer.");const[n,i]=new Uint8Array(s,0,2);if(219!==n)throw new Error("Data does not appear to be in a KDBush format.");const o=i>>4;if(1!==o)throw new Error(`Got v${o} data when expected v1.`);const r=t[15&i];if(!r)throw new Error("Unrecognized array type.");const[h]=new Uint16Array(s,2,1),[a]=new Uint32Array(s,4,1);return new e(a,h,r,s)}constructor(e,s=64,n=Float64Array,i){if(isNaN(e)||e<=0)throw new Error(`Unpexpected numItems value: ${e}.`);this.numItems=+e,this.nodeSize=Math.min(Math.max(+s,2),65535),this.ArrayType=n,this.IndexArrayType=e<65536?Uint16Array:Uint32Array;const o=t.indexOf(this.ArrayType),r=2*e*this.ArrayType.BYTES_PER_ELEMENT,h=e*this.IndexArrayType.BYTES_PER_ELEMENT,a=(8-h%8)%8;if(o<0)throw new Error(`Unexpected typed array class: ${n}.`);i&&i instanceof ArrayBuffer?(this.data=i,this.ids=new this.IndexArrayType(this.data,8,e),this.coords=new this.ArrayType(this.data,8+h+a,2*e),this._pos=2*e,this._finished=!0):(this.data=new ArrayBuffer(8+r+h+a),this.ids=new this.IndexArrayType(this.data,8,e),this.coords=new this.ArrayType(this.data,8+h+a,2*e),this._pos=0,this._finished=!1,new Uint8Array(this.data,0,2).set([219,16+o]),new Uint16Array(this.data,2,1)[0]=s,new Uint32Array(this.data,4,1)[0]=e)}add(t,e){const s=this._pos>>1;return this.ids[s]=s,this.coords[this._pos++]=t,this.coords[this._pos++]=e,s}finish(){const t=this._pos>>1;if(t!==this.numItems)throw new Error(`Added ${t} items when expected ${this.numItems}.`);return s(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(t,e,s,n){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:i,coords:o,nodeSize:r}=this,h=[0,i.length-1,0],a=[];for(;h.length;){const c=h.pop()||0,p=h.pop()||0,u=h.pop()||0;if(p-u<=r){for(let r=u;r<=p;r++){const h=o[2*r],c=o[2*r+1];h>=t&&h<=s&&c>=e&&c<=n&&a.push(i[r])}continue}const d=u+p>>1,f=o[2*d],l=o[2*d+1];f>=t&&f<=s&&l>=e&&l<=n&&a.push(i[d]),(0===c?t<=f:e<=l)&&(h.push(u),h.push(d-1),h.push(1-c)),(0===c?s>=f:n>=l)&&(h.push(d+1),h.push(p),h.push(1-c))}return a}within(t,e,s){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:n,coords:i,nodeSize:o}=this,h=[0,n.length-1,0],a=[],c=s*s;for(;h.length;){const p=h.pop()||0,u=h.pop()||0,d=h.pop()||0;if(u-d<=o){for(let s=d;s<=u;s++)r(i[2*s],i[2*s+1],t,e)<=c&&a.push(n[s]);continue}const f=d+u>>1,l=i[2*f],m=i[2*f+1];r(l,m,t,e)<=c&&a.push(n[f]),(0===p?t-s<=l:e-s<=m)&&(h.push(d),h.push(f-1),h.push(1-p)),(0===p?t+s>=l:e+s>=m)&&(h.push(f+1),h.push(u),h.push(1-p))}return a}}function s(t,e,i,o,r,h){if(r-o<=i)return;const a=o+r>>1;n(t,e,a,o,r,h),s(t,e,i,o,a-1,1-h),s(t,e,i,a+1,r,1-h)}function n(t,e,s,o,r,h){for(;r>o;){if(r-o>600){const i=r-o+1,a=s-o+1,c=Math.log(i),p=.5*Math.exp(2*c/3),u=.5*Math.sqrt(c*p*(i-p)/i)*(a-i/2<0?-1:1);n(t,e,s,Math.max(o,Math.floor(s-a*p/i+u)),Math.min(r,Math.floor(s+(i-a)*p/i+u)),h)}const a=e[2*s+h];let c=o,p=r;for(i(t,e,o,s),e[2*r+h]>a&&i(t,e,o,r);c<p;){for(i(t,e,c,p),c++,p--;e[2*c+h]<a;)c++;for(;e[2*p+h]>a;)p--}e[2*o+h]===a?i(t,e,o,p):(p++,i(t,e,p,r)),p<=s&&(o=p+1),s<=p&&(r=p-1)}}function i(t,e,s,n){o(t,s,n),o(e,2*s,2*n),o(e,2*s+1,2*n+1)}function o(t,e,s){const n=t[e];t[e]=t[s],t[s]=n}function r(t,e,s,n){const i=t-s,o=e-n;return i*i+o*o}const h={minZoom:0,maxZoom:16,minPoints:2,radius:40,extent:512,nodeSize:64,log:!1,generateId:!1,reduce:null,map:t=>t},a=Math.fround||(c=new Float32Array(1),t=>(c[0]=+t,c[0]));var c;const p=3,u=5,d=6;function f(t,e,s){return{type:"Feature",id:t[e+p],properties:l(t,e,s),geometry:{type:"Point",coordinates:[(n=t[e],360*(n-.5)),y(t[e+1])]}};var n}function l(t,e,s){const n=t[e+u],i=n>=1e4?`${Math.round(n/1e3)}k`:n>=1e3?Math.round(n/100)/10+"k":n,o=t[e+d],r=-1===o?{}:Object.assign({},s[o]);return Object.assign(r,{cluster:!0,cluster_id:t[e+p],point_count:n,point_count_abbreviated:i})}function m(t){return t/360+.5}function g(t){const e=Math.sin(t*Math.PI/180),s=.5-.25*Math.log((1+e)/(1-e))/Math.PI;return s<0?0:s>1?1:s}function y(t){const e=(180-360*t)*Math.PI/180;return 360*Math.atan(Math.exp(e))/Math.PI-90}return class{constructor(t){this.options=Object.assign(Object.create(h),t),this.trees=new Array(this.options.maxZoom+1),this.stride=this.options.reduce?7:6,this.clusterProps=[]}load(t){const{log:e,minZoom:s,maxZoom:n}=this.options;e&&console.time("total time");const i=`prepare ${t.length} points`;e&&console.time(i),this.points=t;const o=[];for(let e=0;e<t.length;e++){const s=t[e];if(!s.geometry)continue;const[n,i]=s.geometry.coordinates,r=a(m(n)),h=a(g(i));o.push(r,h,1/0,e,-1,1),this.options.reduce&&o.push(0)}let r=this.trees[n+1]=this._createTree(o);e&&console.timeEnd(i);for(let t=n;t>=s;t--){const s=+Date.now();r=this.trees[t]=this._createTree(this._cluster(r,t)),e&&console.log("z%d: %d clusters in %dms",t,r.numItems,+Date.now()-s)}return e&&console.timeEnd("total time"),this}getClusters(t,e){let s=((t[0]+180)%360+360)%360-180;const n=Math.max(-90,Math.min(90,t[1]));let i=180===t[2]?180:((t[2]+180)%360+360)%360-180;const o=Math.max(-90,Math.min(90,t[3]));if(t[2]-t[0]>=360)s=-180,i=180;else if(s>i){const t=this.getClusters([s,n,180,o],e),r=this.getClusters([-180,n,i,o],e);return t.concat(r)}const r=this.trees[this._limitZoom(e)],h=r.range(m(s),g(o),m(i),g(n)),a=r.data,c=[];for(const t of h){const e=this.stride*t;c.push(a[e+u]>1?f(a,e,this.clusterProps):this.points[a[e+p]])}return c}getChildren(t){const e=this._getOriginId(t),s=this._getOriginZoom(t),n="No cluster with the specified id.",i=this.trees[s];if(!i)throw new Error(n);const o=i.data;if(e*this.stride>=o.length)throw new Error(n);const r=this.options.radius/(this.options.extent*Math.pow(2,s-1)),h=o[e*this.stride],a=o[e*this.stride+1],c=i.within(h,a,r),d=[];for(const e of c){const s=e*this.stride;o[s+4]===t&&d.push(o[s+u]>1?f(o,s,this.clusterProps):this.points[o[s+p]])}if(0===d.length)throw new Error(n);return d}getLeaves(t,e,s){e=e||10,s=s||0;const n=[];return this._appendLeaves(n,t,e,s,0),n}getTile(t,e,s){const n=this.trees[this._limitZoom(t)],i=Math.pow(2,t),{extent:o,radius:r}=this.options,h=r/o,a=(s-h)/i,c=(s+1+h)/i,p={features:[]};return this._addTileFeatures(n.range((e-h)/i,a,(e+1+h)/i,c),n.data,e,s,i,p),0===e&&this._addTileFeatures(n.range(1-h/i,a,1,c),n.data,i,s,i,p),e===i-1&&this._addTileFeatures(n.range(0,a,h/i,c),n.data,-1,s,i,p),p.features.length?p:null}getClusterExpansionZoom(t){let e=this._getOriginZoom(t)-1;for(;e<=this.options.maxZoom;){const s=this.getChildren(t);if(e++,1!==s.length)break;t=s[0].properties.cluster_id}return e}_appendLeaves(t,e,s,n,i){const o=this.getChildren(e);for(const e of o){const o=e.properties;if(o&&o.cluster?i+o.point_count<=n?i+=o.point_count:i=this._appendLeaves(t,o.cluster_id,s,n,i):i<n?i++:t.push(e),t.length===s)break}return i}_createTree(t){const s=new e(t.length/this.stride|0,this.options.nodeSize,Float32Array);for(let e=0;e<t.length;e+=this.stride)s.add(t[e],t[e+1]);return s.finish(),s.data=t,s}_addTileFeatures(t,e,s,n,i,o){for(const r of t){const t=r*this.stride,h=e[t+u]>1;let a,c,d;if(h)a=l(e,t,this.clusterProps),c=e[t],d=e[t+1];else{const s=this.points[e[t+p]];a=s.properties;const[n,i]=s.geometry.coordinates;c=m(n),d=g(i)}const f={type:1,geometry:[[Math.round(this.options.extent*(c*i-s)),Math.round(this.options.extent*(d*i-n))]],tags:a};let y;y=h||this.options.generateId?e[t+p]:this.points[e[t+p]].id,void 0!==y&&(f.id=y),o.features.push(f)}}_limitZoom(t){return Math.max(this.options.minZoom,Math.min(Math.floor(+t),this.options.maxZoom+1))}_cluster(t,e){const{radius:s,extent:n,reduce:i,minPoints:o}=this.options,r=s/(n*Math.pow(2,e)),h=t.data,a=[],c=this.stride;for(let s=0;s<h.length;s+=c){if(h[s+2]<=e)continue;h[s+2]=e;const n=h[s],p=h[s+1],d=t.within(h[s],h[s+1],r),f=h[s+u];let l=f;for(const t of d){const s=t*c;h[s+2]>e&&(l+=h[s+u])}if(l>f&&l>=o){let t,o=n*f,r=p*f,m=-1;const g=((s/c|0)<<5)+(e+1)+this.points.length;for(const n of d){const a=n*c;if(h[a+2]<=e)continue;h[a+2]=e;const p=h[a+u];o+=h[a]*p,r+=h[a+1]*p,h[a+4]=g,i&&(t||(t=this._map(h,s,!0),m=this.clusterProps.length,this.clusterProps.push(t)),i(t,this._map(h,a)))}h[s+4]=g,a.push(o/l,r/l,1/0,g,-1,l),i&&a.push(m)}else{for(let t=0;t<c;t++)a.push(h[s+t]);if(l>1)for(const t of d){const s=t*c;if(!(h[s+2]<=e)){h[s+2]=e;for(let t=0;t<c;t++)a.push(h[s+t])}}}}return a}_getOriginId(t){return t-this.points.length>>5}_getOriginZoom(t){return(t-this.points.length)%32}_map(t,e,s){if(t[e+u]>1){const n=this.clusterProps[t[e+d]];return s?Object.assign({},n):n}const n=this.points[t[e+p]].properties,i=this.options.map(n);return s&&i===n?Object.assign({},i):i}}})); |
243
index.js
@@ -25,10 +25,18 @@ | ||
const OFFSET_ZOOM = 2; | ||
const OFFSET_ID = 3; | ||
const OFFSET_PARENT = 4; | ||
const OFFSET_NUM = 5; | ||
const OFFSET_PROP = 6; | ||
export default class Supercluster { | ||
constructor(options) { | ||
this.options = extend(Object.create(defaultOptions), options); | ||
this.options = Object.assign(Object.create(defaultOptions), options); | ||
this.trees = new Array(this.options.maxZoom + 1); | ||
this.stride = this.options.reduce ? 7 : 6; | ||
this.clusterProps = []; | ||
} | ||
load(points) { | ||
const {log, minZoom, maxZoom, nodeSize} = this.options; | ||
const {log, minZoom, maxZoom} = this.options; | ||
@@ -43,8 +51,22 @@ if (log) console.time('total time'); | ||
// generate a cluster object for each point and index input points into a KD-tree | ||
let clusters = []; | ||
const data = []; | ||
for (let i = 0; i < points.length; i++) { | ||
if (!points[i].geometry) continue; | ||
clusters.push(createPointCluster(points[i], i)); | ||
const p = points[i]; | ||
if (!p.geometry) continue; | ||
const [lng, lat] = p.geometry.coordinates; | ||
const x = fround(lngX(lng)); | ||
const y = fround(latY(lat)); | ||
// store internal point/cluster data in flat numeric arrays for performance | ||
data.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) data.push(0); // noop | ||
} | ||
this.trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array); | ||
let tree = this.trees[maxZoom + 1] = this._createTree(data); | ||
@@ -59,6 +81,5 @@ if (log) console.timeEnd(timerId); | ||
// 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); | ||
tree = this.trees[z] = this._createTree(this._cluster(tree, z)); | ||
if (log) console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now); | ||
if (log) console.log('z%d: %d clusters in %dms', z, tree.numItems, +Date.now() - now); | ||
} | ||
@@ -88,6 +109,7 @@ | ||
const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat)); | ||
const data = tree.data; | ||
const clusters = []; | ||
for (const id of ids) { | ||
const c = tree.points[id]; | ||
clusters.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]); | ||
const k = this.stride * id; | ||
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]); | ||
} | ||
@@ -102,15 +124,17 @@ return clusters; | ||
const index = this.trees[originZoom]; | ||
if (!index) throw new Error(errorMsg); | ||
const tree = this.trees[originZoom]; | ||
if (!tree) throw new Error(errorMsg); | ||
const origin = index.points[originId]; | ||
if (!origin) throw new Error(errorMsg); | ||
const data = tree.data; | ||
if (originId * this.stride >= data.length) throw new Error(errorMsg); | ||
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1)); | ||
const ids = index.within(origin.x, origin.y, r); | ||
const x = data[originId * this.stride]; | ||
const y = data[originId * this.stride + 1]; | ||
const ids = tree.within(x, y, r); | ||
const children = []; | ||
for (const id of ids) { | ||
const c = index.points[id]; | ||
if (c.parentId === clusterId) { | ||
children.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]); | ||
const k = id * this.stride; | ||
if (data[k + OFFSET_PARENT] === clusterId) { | ||
children.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]); | ||
} | ||
@@ -148,3 +172,3 @@ } | ||
tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), | ||
tree.points, x, y, z2, tile); | ||
tree.data, x, y, z2, tile); | ||
@@ -154,3 +178,3 @@ if (x === 0) { | ||
tree.range(1 - p / z2, top, 1, bottom), | ||
tree.points, z2, y, z2, tile); | ||
tree.data, z2, y, z2, tile); | ||
} | ||
@@ -160,3 +184,3 @@ if (x === z2 - 1) { | ||
tree.range(0, top, p / z2, bottom), | ||
tree.points, -1, y, z2, tile); | ||
tree.data, -1, y, z2, tile); | ||
} | ||
@@ -206,17 +230,26 @@ | ||
_addTileFeatures(ids, points, x, y, z2, tile) { | ||
_createTree(data) { | ||
const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array); | ||
for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]); | ||
tree.finish(); | ||
tree.data = data; | ||
return tree; | ||
} | ||
_addTileFeatures(ids, data, x, y, z2, tile) { | ||
for (const i of ids) { | ||
const c = points[i]; | ||
const isCluster = c.numPoints; | ||
const k = i * this.stride; | ||
const isCluster = data[k + OFFSET_NUM] > 1; | ||
let tags, px, py; | ||
if (isCluster) { | ||
tags = getClusterProperties(c); | ||
px = c.x; | ||
py = c.y; | ||
tags = getClusterProperties(data, k, this.clusterProps); | ||
px = data[k]; | ||
py = data[k + 1]; | ||
} else { | ||
const p = this.points[c.index]; | ||
const p = this.points[data[k + OFFSET_ID]]; | ||
tags = p.properties; | ||
px = lngX(p.geometry.coordinates[0]); | ||
py = latY(p.geometry.coordinates[1]); | ||
const [lng, lat] = p.geometry.coordinates; | ||
px = lngX(lng); | ||
py = latY(lat); | ||
} | ||
@@ -235,10 +268,8 @@ | ||
let id; | ||
if (isCluster) { | ||
id = c.id; | ||
} else if (this.options.generateId) { | ||
// optionally generate id | ||
id = c.index; | ||
} else if (this.points[c.index].id) { | ||
if (isCluster || this.options.generateId) { | ||
// optionally generate id for points | ||
id = data[k + OFFSET_ID]; | ||
} else { | ||
// keep id if already assigned | ||
id = this.points[c.index].id; | ||
id = this.points[data[k + OFFSET_ID]].id; | ||
} | ||
@@ -256,19 +287,21 @@ | ||
_cluster(points, zoom) { | ||
const clusters = []; | ||
_cluster(tree, zoom) { | ||
const {radius, extent, reduce, minPoints} = this.options; | ||
const r = radius / (extent * Math.pow(2, zoom)); | ||
const data = tree.data; | ||
const nextData = []; | ||
const stride = this.stride; | ||
// loop through each point | ||
for (let i = 0; i < points.length; i++) { | ||
const p = points[i]; | ||
for (let i = 0; i < data.length; i += stride) { | ||
// if we've already visited the point at this zoom level, skip it | ||
if (p.zoom <= zoom) continue; | ||
p.zoom = zoom; | ||
if (data[i + OFFSET_ZOOM] <= zoom) continue; | ||
data[i + OFFSET_ZOOM] = zoom; | ||
// find all nearby points | ||
const tree = this.trees[zoom + 1]; | ||
const neighborIds = tree.within(p.x, p.y, r); | ||
const x = data[i]; | ||
const y = data[i + 1]; | ||
const neighborIds = tree.within(data[i], data[i + 1], r); | ||
const numPointsOrigin = p.numPoints || 1; | ||
const numPointsOrigin = data[i + OFFSET_NUM]; | ||
let numPoints = numPointsOrigin; | ||
@@ -278,5 +311,5 @@ | ||
for (const neighborId of neighborIds) { | ||
const b = tree.points[neighborId]; | ||
const k = neighborId * stride; | ||
// filter out neighbors that are already processed | ||
if (b.zoom > zoom) numPoints += b.numPoints || 1; | ||
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM]; | ||
} | ||
@@ -286,40 +319,46 @@ | ||
if (numPoints > numPointsOrigin && numPoints >= minPoints) { | ||
let wx = p.x * numPointsOrigin; | ||
let wy = p.y * numPointsOrigin; | ||
let wx = x * numPointsOrigin; | ||
let wy = y * numPointsOrigin; | ||
let clusterProperties = reduce && numPointsOrigin > 1 ? this._map(p, true) : null; | ||
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 << 5) + (zoom + 1) + this.points.length; | ||
const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length; | ||
for (const neighborId of neighborIds) { | ||
const b = tree.points[neighborId]; | ||
const k = neighborId * stride; | ||
if (b.zoom <= zoom) continue; | ||
b.zoom = zoom; // save the zoom (so it doesn't get processed twice) | ||
if (data[k + OFFSET_ZOOM] <= zoom) continue; | ||
data[k + OFFSET_ZOOM] = zoom; // save the zoom (so it doesn't get processed twice) | ||
const numPoints2 = b.numPoints || 1; | ||
wx += b.x * numPoints2; // accumulate coordinates for calculating weighted center | ||
wy += b.y * numPoints2; | ||
const numPoints2 = data[k + OFFSET_NUM]; | ||
wx += data[k] * numPoints2; // accumulate coordinates for calculating weighted center | ||
wy += data[k + 1] * numPoints2; | ||
b.parentId = id; | ||
data[k + OFFSET_PARENT] = id; | ||
if (reduce) { | ||
if (!clusterProperties) clusterProperties = this._map(p, true); | ||
reduce(clusterProperties, this._map(b)); | ||
if (!clusterProperties) { | ||
clusterProperties = this._map(data, i, true); | ||
clusterPropIndex = this.clusterProps.length; | ||
this.clusterProps.push(clusterProperties); | ||
} | ||
reduce(clusterProperties, this._map(data, k)); | ||
} | ||
} | ||
p.parentId = id; | ||
clusters.push(createCluster(wx / numPoints, wy / numPoints, id, numPoints, clusterProperties)); | ||
data[i + OFFSET_PARENT] = id; | ||
nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints); | ||
if (reduce) nextData.push(clusterPropIndex); | ||
} else { // left points as unclustered | ||
clusters.push(p); | ||
for (let j = 0; j < stride; j++) nextData.push(data[i + j]); | ||
if (numPoints > 1) { | ||
for (const neighborId of neighborIds) { | ||
const b = tree.points[neighborId]; | ||
if (b.zoom <= zoom) continue; | ||
b.zoom = zoom; | ||
clusters.push(b); | ||
const k = neighborId * stride; | ||
if (data[k + OFFSET_ZOOM] <= zoom) continue; | ||
data[k + OFFSET_ZOOM] = zoom; | ||
for (let j = 0; j < stride; j++) nextData.push(data[k + j]); | ||
} | ||
@@ -330,3 +369,3 @@ } | ||
return clusters; | ||
return nextData; | ||
} | ||
@@ -344,43 +383,21 @@ | ||
_map(point, clone) { | ||
if (point.numPoints) { | ||
return clone ? extend({}, point.properties) : point.properties; | ||
_map(data, i, clone) { | ||
if (data[i + OFFSET_NUM] > 1) { | ||
const props = this.clusterProps[data[i + OFFSET_PROP]]; | ||
return clone ? Object.assign({}, props) : props; | ||
} | ||
const original = this.points[point.index].properties; | ||
const original = this.points[data[i + OFFSET_ID]].properties; | ||
const result = this.options.map(original); | ||
return clone && result === original ? extend({}, result) : result; | ||
return clone && result === original ? Object.assign({}, result) : result; | ||
} | ||
} | ||
function createCluster(x, y, id, numPoints, properties) { | ||
function getClusterJSON(data, i, clusterProps) { | ||
return { | ||
x: fround(x), // weighted cluster center; round for consistency with Float32Array index | ||
y: fround(y), | ||
zoom: Infinity, // the last zoom the cluster was processed at | ||
id, // encodes index of the first child of the cluster and its zoom level | ||
parentId: -1, // parent cluster id | ||
numPoints, | ||
properties | ||
}; | ||
} | ||
function createPointCluster(p, id) { | ||
const [x, y] = p.geometry.coordinates; | ||
return { | ||
x: fround(lngX(x)), // projected point coordinates | ||
y: fround(latY(y)), | ||
zoom: Infinity, // the last zoom the point was processed at | ||
index: id, // index of the source feature in the original input array, | ||
parentId: -1 // parent cluster id | ||
}; | ||
} | ||
function getClusterJSON(cluster) { | ||
return { | ||
type: 'Feature', | ||
id: cluster.id, | ||
properties: getClusterProperties(cluster), | ||
id: data[i + OFFSET_ID], | ||
properties: getClusterProperties(data, i, clusterProps), | ||
geometry: { | ||
type: 'Point', | ||
coordinates: [xLng(cluster.x), yLat(cluster.y)] | ||
coordinates: [xLng(data[i]), yLat(data[i + 1])] | ||
} | ||
@@ -390,10 +407,12 @@ }; | ||
function getClusterProperties(cluster) { | ||
const count = cluster.numPoints; | ||
function getClusterProperties(data, i, clusterProps) { | ||
const count = data[i + OFFSET_NUM]; | ||
const abbrev = | ||
count >= 10000 ? `${Math.round(count / 1000) }k` : | ||
count >= 1000 ? `${Math.round(count / 100) / 10 }k` : count; | ||
return extend(extend({}, cluster.properties), { | ||
const propIndex = data[i + OFFSET_PROP]; | ||
const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]); | ||
return Object.assign(properties, { | ||
cluster: true, | ||
cluster_id: cluster.id, | ||
cluster_id: data[i + OFFSET_ID], | ||
point_count: count, | ||
@@ -422,13 +441,1 @@ point_count_abbreviated: abbrev | ||
} | ||
function extend(dest, src) { | ||
for (const id in src) dest[id] = src[id]; | ||
return dest; | ||
} | ||
function getX(p) { | ||
return p.x; | ||
} | ||
function getY(p) { | ||
return p.y; | ||
} |
{ | ||
"name": "supercluster", | ||
"version": "7.1.5", | ||
"version": "8.0.0", | ||
"description": "A very fast geospatial point clustering library.", | ||
"main": "dist/supercluster.js", | ||
"type": "module", | ||
"exports": "./index.js", | ||
"module": "index.js", | ||
"jsdelivr": "dist/supercluster.min.js", | ||
"unpkg": "dist/supercluster.min.js", | ||
"sideEffects": false, | ||
"scripts": { | ||
"pretest": "eslint index.js bench.js test/test.js demo/index.js demo/worker.js", | ||
"test": "tape -r esm test/test.js", | ||
"test": "node test/test.js", | ||
"cov": "c8 npm run test", | ||
"bench": "node --expose-gc -r esm bench.js", | ||
"bench": "node --expose-gc bench.js", | ||
"build": "mkdirp dist && rollup -c", | ||
@@ -34,18 +37,18 @@ "prepublishOnly": "npm run test && npm run build" | ||
"dependencies": { | ||
"kdbush": "^3.0.0" | ||
"kdbush": "^4.0.1" | ||
}, | ||
"devDependencies": { | ||
"@rollup/plugin-buble": "^0.21.3", | ||
"@rollup/plugin-node-resolve": "^13.1.3", | ||
"c8": "^7.11.0", | ||
"eslint": "^8.12.0", | ||
"@rollup/plugin-node-resolve": "^15.0.2", | ||
"@rollup/plugin-terser": "^0.4.1", | ||
"c8": "^7.13.0", | ||
"eslint": "^8.39.0", | ||
"eslint-config-mourner": "^3.0.0", | ||
"esm": "^3.2.25", | ||
"mkdirp": "^1.0.4", | ||
"rollup": "^2.70.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"tape": "^5.5.2" | ||
"mkdirp": "^3.0.1", | ||
"rollup": "^3.21.0" | ||
}, | ||
"eslintConfig": { | ||
"extends": "mourner", | ||
"parserOptions": { | ||
"ecmaVersion": 2020 | ||
}, | ||
"rules": { | ||
@@ -52,0 +55,0 @@ "camelcase": 0 |
@@ -1,2 +0,2 @@ | ||
# supercluster [![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects) [![Build Status](https://travis-ci.org/mapbox/supercluster.svg?branch=main)](https://travis-ci.org/mapbox/supercluster) | ||
# supercluster [![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects) [![Build Status](https://travis-ci.com/mapbox/supercluster.svg?branch=main)](https://travis-ci.com/mapbox/supercluster) | ||
@@ -3,0 +3,0 @@ A very fast JavaScript library for geospatial point clustering for browsers and Node. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
57346
7
1021
Yes
1
+ Addedkdbush@4.0.2(transitive)
- Removedkdbush@3.0.0(transitive)
Updatedkdbush@^4.0.1