all these changes

This commit is contained in:
Jake Kasper
2026-04-09 13:19:47 -05:00
parent e83a51a051
commit 65315f36d1
39102 changed files with 7932979 additions and 567 deletions

15
frontend/node_modules/concaveman/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2025, Mapbox
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

53
frontend/node_modules/concaveman/README.md generated vendored Normal file
View File

@@ -0,0 +1,53 @@
## concaveman
A very fast **2D concave hull** algorithm in JavaScript (generates a general outline of a point set).
[![Build Status](https://travis-ci.org/mapbox/concaveman.svg?branch=master)](https://travis-ci.org/mapbox/concaveman)
[![Coverage Status](https://coveralls.io/repos/github/mapbox/concaveman/badge.svg?branch=master)](https://coveralls.io/github/mapbox/concaveman?branch=master)
[![](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects)
<img width="570" alt="sample concave hull" src="https://cloud.githubusercontent.com/assets/25395/12975726/ada2ad10-d0c6-11e5-96c8-6e42c995e0e2.png">
### Usage
```js
import concaveman from 'concaveman';
const points = [[10, 20], [30, 12.5], ...];
const polygon = concaveman(points);
```
Signature: `concaveman(points[, concavity = 2, lengthThreshold = 0])`
- `points` is an array of `[x, y]` points.
- `concavity` is a relative measure of concavity. `1` results in a relatively detailed shape, `Infinity` results in a convex hull.
You can use values lower than `1`, but they can produce pretty crazy shapes.
- `lengthThreshold`: when a segment length is under this threshold, it stops being considered for further detalization.
Higher values result in simpler shapes.
### Algorithm
The algorithm is based on ideas from the paper [A New Concave Hull Algorithm and Concaveness Measure
for n-dimensional Datasets, 2012](https://jise.iis.sinica.edu.tw/JISESearch/fullText?pId=245&code=5A9B97538372AA1)
by Jin-Seo Park and Se-Jong Oh.
This implementation dramatically improves performance over the one stated in the paper
(`O(rn)`, where `r` is a number of output points, to `O(n log n)`)
by introducing a fast _k nearest points to a segment_ algorithm,
a modification of a depth-first kNN R-tree search using a priority queue.
### TypeScript
[TypeScript type definitions](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/concaveman)
are available through `npm install --save @types/concaveman`.
### Dependencies
- [rbush](https://github.com/mourner/rbush) for point indexing
- [tinyqueue](https://github.com/mourner/tinyqueue) as a priority queue
- [point-in-polygon](https://github.com/substack/point-in-polygon) for point in polygon queries
- [robust-predicates](https://github.com/mourner/robust-predicates) for 3-point orientation tests
### C++ Port
In 2019, a [C++ port](https://github.com/sadaszewski/concaveman-cpp) has been created, allowing for efficient usage from C/C++, Python (via cffi) and other languages featuring an FFI and/or plug-in mechanism for C (e.g. a MATLAB MEX file should be easy to prepare).

375
frontend/node_modules/concaveman/index.js generated vendored Normal file
View File

@@ -0,0 +1,375 @@
import RBush from 'rbush';
import Queue from 'tinyqueue';
import pointInPolygon from 'point-in-polygon';
import {orient2d} from 'robust-predicates';
export default function concaveman(points, concavity, lengthThreshold) {
// a relative measure of concavity; higher value means simpler hull
concavity = Math.max(0, concavity === undefined ? 2 : concavity);
// when a segment goes below this length threshold, it won't be drilled down further
lengthThreshold = lengthThreshold || 0;
// start with a convex hull of the points
const hull = fastConvexHull(points);
// index the points with an R-tree
const tree = new RBush(16);
tree.toBBox = function (a) {
return {
minX: a[0],
minY: a[1],
maxX: a[0],
maxY: a[1]
};
};
tree.compareMinX = function (a, b) { return a[0] - b[0]; };
tree.compareMinY = function (a, b) { return a[1] - b[1]; };
tree.load(points);
// turn the convex hull into a linked list and populate the initial edge queue with the nodes
const queue = [];
let last;
for (let i = 0; i < hull.length; i++) {
const p = hull[i];
tree.remove(p);
last = insertNode(p, last);
queue.push(last);
}
// index the segments with an R-tree (for intersection checks)
const segTree = new RBush(16);
for (let i = 0; i < queue.length; i++) segTree.insert(updateBBox(queue[i]));
const sqConcavity = concavity * concavity;
const sqLenThreshold = lengthThreshold * lengthThreshold;
// process edges one by one
while (queue.length) {
const node = queue.shift();
const a = node.p;
const b = node.next.p;
// skip the edge if it's already short enough
const sqLen = getSqDist(a, b);
if (sqLen < sqLenThreshold) continue;
const maxSqLen = sqLen / sqConcavity;
// find the best connection point for the current edge to flex inward to
const p = findCandidate(tree, node.prev.p, a, b, node.next.next.p, maxSqLen, segTree);
// if we found a connection and it satisfies our concavity measure
if (p && Math.min(getSqDist(p, a), getSqDist(p, b)) <= maxSqLen) {
// connect the edge endpoints through this point and add 2 new edges to the queue
queue.push(node);
queue.push(insertNode(p, node));
// update point and segment indexes
tree.remove(p);
segTree.remove(node);
segTree.insert(updateBBox(node));
segTree.insert(updateBBox(node.next));
}
}
// convert the resulting hull linked list to an array of points
let node = last;
const concave = [];
do {
concave.push(node.p);
node = node.next;
} while (node !== last);
concave.push(node.p);
return concave;
}
function findCandidate(tree, a, b, c, d, maxDist, segTree) {
const queue = new Queue([], compareDist);
let node = tree.data;
// search through the point R-tree with a depth-first search using a priority queue
// in the order of distance to the edge (b, c)
while (node) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const dist = node.leaf ? sqSegDist(child, b, c) : sqSegBoxDist(b, c, child);
if (dist > maxDist) continue; // skip the node if it's farther than we ever need
queue.push({
node: child,
dist
});
}
while (queue.length && !queue.peek().node.children) {
const item = queue.pop();
const p = item.node;
// skip all points that are as close to adjacent edges (a,b) and (c,d),
// and points that would introduce self-intersections when connected
const d0 = sqSegDist(p, a, b);
const d1 = sqSegDist(p, c, d);
if (item.dist < d0 && item.dist < d1 &&
noIntersections(b, p, segTree) &&
noIntersections(c, p, segTree)) return p;
}
node = queue.pop();
if (node) node = node.node;
}
return null;
}
function compareDist(a, b) {
return a.dist - b.dist;
}
// square distance from a segment bounding box to the given one
function sqSegBoxDist(a, b, bbox) {
if (inside(a, bbox) || inside(b, bbox)) return 0;
const d1 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.minY, bbox.maxX, bbox.minY);
if (d1 === 0) return 0;
const d2 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.minY, bbox.minX, bbox.maxY);
if (d2 === 0) return 0;
const d3 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.maxX, bbox.minY, bbox.maxX, bbox.maxY);
if (d3 === 0) return 0;
const d4 = sqSegSegDist(a[0], a[1], b[0], b[1], bbox.minX, bbox.maxY, bbox.maxX, bbox.maxY);
if (d4 === 0) return 0;
return Math.min(d1, d2, d3, d4);
}
function inside(a, bbox) {
return a[0] >= bbox.minX &&
a[0] <= bbox.maxX &&
a[1] >= bbox.minY &&
a[1] <= bbox.maxY;
}
// check if the edge (a,b) doesn't intersect any other edges
function noIntersections(a, b, segTree) {
const minX = Math.min(a[0], b[0]);
const minY = Math.min(a[1], b[1]);
const maxX = Math.max(a[0], b[0]);
const maxY = Math.max(a[1], b[1]);
const edges = segTree.search({minX, minY, maxX, maxY});
for (let i = 0; i < edges.length; i++) {
if (intersects(edges[i].p, edges[i].next.p, a, b)) return false;
}
return true;
}
function cross(p1, p2, p3) {
return orient2d(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
}
// check if the edges (p1,q1) and (p2,q2) intersect
function intersects(p1, q1, p2, q2) {
return p1 !== q2 && q1 !== p2 &&
cross(p1, q1, p2) > 0 !== cross(p1, q1, q2) > 0 &&
cross(p2, q2, p1) > 0 !== cross(p2, q2, q1) > 0;
}
// update the bounding box of a node's edge
function updateBBox(node) {
const p1 = node.p;
const p2 = node.next.p;
node.minX = Math.min(p1[0], p2[0]);
node.minY = Math.min(p1[1], p2[1]);
node.maxX = Math.max(p1[0], p2[0]);
node.maxY = Math.max(p1[1], p2[1]);
return node;
}
// speed up convex hull by filtering out points inside quadrilateral formed by 4 extreme points
function fastConvexHull(points) {
let left = points[0];
let top = points[0];
let right = points[0];
let bottom = points[0];
// find the leftmost, rightmost, topmost and bottommost points
for (let i = 0; i < points.length; i++) {
const p = points[i];
if (p[0] < left[0]) left = p;
if (p[0] > right[0]) right = p;
if (p[1] < top[1]) top = p;
if (p[1] > bottom[1]) bottom = p;
}
// filter out points that are inside the resulting quadrilateral
const cull = [left, top, right, bottom];
const filtered = cull.slice();
for (let i = 0; i < points.length; i++) {
if (!pointInPolygon(points[i], cull)) filtered.push(points[i]);
}
// get convex hull around the filtered points
return convexHull(filtered);
}
// create a new node in a doubly linked list
function insertNode(p, prev) {
const node = {
p,
prev: null,
next: null,
minX: 0,
minY: 0,
maxX: 0,
maxY: 0
};
if (!prev) {
node.prev = node;
node.next = node;
} else {
node.next = prev.next;
node.prev = prev;
prev.next.prev = node;
prev.next = node;
}
return node;
}
// square distance between 2 points
function getSqDist(p1, p2) {
const dx = p1[0] - p2[0],
dy = p1[1] - p2[1];
return dx * dx + dy * dy;
}
// square distance from a point to a segment
function sqSegDist(p, p1, p2) {
let x = p1[0],
y = p1[1],
dx = p2[0] - x,
dy = p2[1] - y;
if (dx !== 0 || dy !== 0) {
const t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = p2[0];
y = p2[1];
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = p[0] - x;
dy = p[1] - y;
return dx * dx + dy * dy;
}
// segment to segment distance, ported from http://geomalgorithms.com/a07-_distance.html by Dan Sunday
function sqSegSegDist(x0, y0, x1, y1, x2, y2, x3, y3) {
const ux = x1 - x0;
const uy = y1 - y0;
const vx = x3 - x2;
const vy = y3 - y2;
const wx = x0 - x2;
const wy = y0 - y2;
const a = ux * ux + uy * uy;
const b = ux * vx + uy * vy;
const c = vx * vx + vy * vy;
const d = ux * wx + uy * wy;
const e = vx * wx + vy * wy;
const D = a * c - b * b;
let sN, tN;
let sD = D;
let tD = D;
if (D === 0) {
sN = 0;
sD = 1;
tN = e;
tD = c;
} else {
sN = b * e - c * d;
tN = a * e - b * d;
if (sN < 0) {
sN = 0;
tN = e;
tD = c;
} else if (sN > sD) {
sN = sD;
tN = e + b;
tD = c;
}
}
if (tN < 0.0) {
tN = 0.0;
if (-d < 0.0) sN = 0.0;
else if (-d > a) sN = sD;
else {
sN = -d;
sD = a;
}
} else if (tN > tD) {
tN = tD;
if ((-d + b) < 0.0) sN = 0;
else if (-d + b > a) sN = sD;
else {
sN = -d + b;
sD = a;
}
}
const sc = sN === 0 ? 0 : sN / sD;
const tc = tN === 0 ? 0 : tN / tD;
const cx = (1 - sc) * x0 + sc * x1;
const cy = (1 - sc) * y0 + sc * y1;
const cx2 = (1 - tc) * x2 + tc * x3;
const cy2 = (1 - tc) * y2 + tc * y3;
const dx = cx2 - cx;
const dy = cy2 - cy;
return dx * dx + dy * dy;
}
function compareByX(a, b) {
return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0];
}
function convexHull(points) {
points.sort(compareByX);
const lower = [];
for (let i = 0; i < points.length; i++) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) {
lower.pop();
}
lower.push(points[i]);
}
const upper = [];
for (let ii = points.length - 1; ii >= 0; ii--) {
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[ii]) <= 0) {
upper.pop();
}
upper.push(points[ii]);
}
upper.pop();
lower.pop();
return lower.concat(upper);
}

View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2024, Vladimir Agafonkin
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

View File

@@ -0,0 +1,28 @@
## quickselect
A tiny and fast [selection algorithm](https://en.wikipedia.org/wiki/Selection_algorithm) in JavaScript
(specifically, [Floyd-Rivest selection](https://en.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm)).
```js
quickselect(array, k[, left, right, compareFn]);
```
Rearranges items so that all items in the `[left, k]` are the smallest.
The `k`-th element will have the `(k - left + 1)`-th smallest value in `[left, right]`.
- `array`: the array to partially sort (in place)
- `k`: middle index for partial sorting (as defined above)
- `left`: left index of the range to sort (`0` by default)
- `right`: right index (last index of the array by default)
- `compareFn`: compare function
Example:
```js
const arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39];
quickselect(arr, 8);
// arr is [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95]
// ^^ middle index
```

View File

@@ -0,0 +1,12 @@
/**
* Rearranges items so that all items in the [left, k] are the smallest.
* The k-th element will have the (k - left + 1)-th smallest value in [left, right].
*
* @template T
* @param {T[]} arr the array to partially sort (in place)
* @param {number} k middle index for partial sorting (as defined above)
* @param {number} [left=0] left index of the range to sort
* @param {number} [right=arr.length-1] right index
* @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
*/
export default function quickselect<T>(arr: T[], k: number, left?: number | undefined, right?: number | undefined, compare?: ((a: T, b: T) => number) | undefined): void;

View File

@@ -0,0 +1,74 @@
/**
* Rearranges items so that all items in the [left, k] are the smallest.
* The k-th element will have the (k - left + 1)-th smallest value in [left, right].
*
* @template T
* @param {T[]} arr the array to partially sort (in place)
* @param {number} k middle index for partial sorting (as defined above)
* @param {number} [left=0] left index of the range to sort
* @param {number} [right=arr.length-1] right index
* @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
*/
export default function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
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));
quickselect(arr, k, newLeft, newRight, compare);
}
const t = arr[k];
let i = left;
/** @type {number} */
let j = right;
swap(arr, left, k);
if (compare(arr[right], t) > 0) swap(arr, left, right);
while (i < j) {
swap(arr, i, j);
i++;
j--;
while (compare(arr[i], t) < 0) i++;
while (compare(arr[j], t) > 0) j--;
}
if (compare(arr[left], t) === 0) swap(arr, left, j);
else {
j++;
swap(arr, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
/**
* @template T
* @param {T[]} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* @template T
* @param {T} a
* @param {T} b
* @returns {number}
*/
function defaultCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}

View File

@@ -0,0 +1,39 @@
{
"name": "quickselect",
"version": "3.0.0",
"type": "module",
"description": "A tiny and fast selection algorithm in JavaScript.",
"repository": "github:mourner/quickselect",
"module": "index.js",
"main": "index.js",
"exports": "./index.js",
"devDependencies": {
"eslint": "^9.6.0",
"eslint-config-mourner": "^4.0.1",
"esm": "^3.2.25",
"rollup": "^4.18.0",
"tape": "^5.8.1",
"typescript": "^5.5.3"
},
"scripts": {
"pretest": "eslint *.js && tsc",
"test": "node test.js",
"bench": "node bench.js"
},
"files": [
"index.js",
"index.d.ts"
],
"types": "index.d.ts",
"keywords": [
"selection",
"algorithm",
"quickselect",
"sort",
"partial",
"floyd",
"rivest"
],
"author": "Vladimir Agafonkin",
"license": "ISC"
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Volodymyr Agafonkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,234 @@
RBush
=====
RBush is a high-performance JavaScript library for 2D **spatial indexing** of points and rectangles.
It's based on an optimized **R-tree** data structure with **bulk insertion** support.
*Spatial index* is a special data structure for points and rectangles
that allows you to perform queries like "all items within this bounding box" very efficiently
(e.g. hundreds of times faster than looping over all items).
It's most commonly used in maps and data visualizations.
[![Node](https://github.com/mourner/rbush/actions/workflows/node.yml/badge.svg)](https://github.com/mourner/rbush/actions/workflows/node.yml)
[![](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects)
![](https://img.shields.io/bundlephobia/minzip/rbush)
## Demos
The demos contain visualization of trees generated from 50k bulk-loaded random points.
Open web console to see benchmarks;
click on buttons to insert or remove items;
click to perform search under the cursor.
* [randomly clustered data](http://mourner.github.io/rbush/viz/viz-cluster.html)
* [uniformly distributed random data](http://mourner.github.io/rbush/viz/viz-uniform.html)
## Usage
### Installing RBush
Install with NPM: `npm install rbush`, then import as a module:
```js
import RBush from 'rbush';
```
Or use as a module directly in the browser with [jsDelivr](https://www.jsdelivr.com/esm):
```html
<script type="module">
import RBush from 'https://cdn.jsdelivr.net/npm/rbush/+esm';
</script>
```
Alternatively, there's a browser bundle with an `RBush` global variable:
```html
<script src="https://cdn.jsdelivr.net/npm/rbush"></script>
```
### Creating a Tree
```js
const tree = new RBush();
```
An optional argument to `RBush` defines the maximum number of entries in a tree node.
`9` (used by default) is a reasonable choice for most applications.
Higher value means faster insertion and slower search, and vice versa.
```js
const tree = new RBush(16);
```
### Adding Data
Insert an item:
```js
const item = {
minX: 20,
minY: 40,
maxX: 30,
maxY: 50,
foo: 'bar'
};
tree.insert(item);
```
### Removing Data
Remove a previously inserted item:
```js
tree.remove(item);
```
By default, RBush removes objects by reference.
However, you can pass a custom `equals` function to compare by value for removal,
which is useful when you only have a copy of the object you need removed (e.g. loaded from server):
```js
tree.remove(itemCopy, (a, b) => {
return a.id === b.id;
});
```
Remove all items:
```js
tree.clear();
```
### Data Format
By default, RBush assumes the format of data points to be an object
with `minX`, `minY`, `maxX` and `maxY` properties.
You can customize this by overriding `toBBox`, `compareMinX` and `compareMinY` methods like this:
```js
class MyRBush extends RBush {
toBBox([x, y]) { return {minX: x, minY: y, maxX: x, maxY: y}; }
compareMinX(a, b) { return a.x - b.x; }
compareMinY(a, b) { return a.y - b.y; }
}
const tree = new MyRBush();
tree.insert([20, 50]); // accepts [x, y] points
```
If you're indexing a static list of points (you don't need to add/remove points after indexing), you should use [kdbush](https://github.com/mourner/kdbush) which performs point indexing 5-8x faster than RBush.
### Bulk-Inserting Data
Bulk-insert the given data into the tree:
```js
tree.load([item1, item2, ...]);
```
Bulk insertion is usually ~2-3 times faster than inserting items one by one.
After bulk loading (bulk insertion into an empty tree),
subsequent query performance is also ~20-30% better.
Note that when you do bulk insertion into an existing tree,
it bulk-loads the given data into a separate tree
and inserts the smaller tree into the larger tree.
This means that bulk insertion works very well for clustered data
(where items in one update are close to each other),
but makes query performance worse if the data is scattered.
### Search
```js
const result = tree.search({
minX: 40,
minY: 20,
maxX: 80,
maxY: 70
});
```
Returns an array of data items (points or rectangles) that the given bounding box intersects.
Note that the `search` method accepts a bounding box in `{minX, minY, maxX, maxY}` format
regardless of the data format.
```js
const allItems = tree.all();
```
Returns all items of the tree.
### Collisions
```js
const result = tree.collides({minX: 40, minY: 20, maxX: 80, maxY: 70});
```
Returns `true` if there are any items intersecting the given bounding box, otherwise `false`.
### Export and Import
```js
// export data as JSON object
const treeData = tree.toJSON();
// import previously exported data
const tree = rbush(9).fromJSON(treeData);
```
Importing and exporting as JSON allows you to use RBush on both the server (using Node.js) and the browser combined,
e.g. first indexing the data on the server and and then importing the resulting tree data on the client for searching.
Note that the `nodeSize` option passed to the constructor must be the same in both trees for export/import to work properly.
### K-Nearest Neighbors
For "_k_ nearest neighbors around a point" type of queries for RBush,
check out [rbush-knn](https://github.com/mourner/rbush-knn).
## Performance
The following sample performance test was done by generating
random uniformly distributed rectangles of ~0.01% area and setting `maxEntries` to `16`
(see `debug/perf.js` script).
Performed with Node.js v6.2.2 on a Retina Macbook Pro 15 (mid-2012).
Test | RBush | [old RTree](https://github.com/imbcmdth/RTree) | Improvement
---------------------------- | ------ | ------ | ----
insert 1M items one by one | 3.18s | 7.83s | 2.5x
1000 searches of 0.01% area | 0.03s | 0.93s | 30x
1000 searches of 1% area | 0.35s | 2.27s | 6.5x
1000 searches of 10% area | 2.18s | 9.53s | 4.4x
remove 1000 items one by one | 0.02s | 1.18s | 50x
bulk-insert 1M items | 1.25s | n/a | 6.7x
## Algorithms Used
* single insertion: non-recursive R-tree insertion with overlap minimizing split routine from R\*-tree (split is very effective in JS, while other R\*-tree modifications like reinsertion on overflow and overlap minimizing subtree search are too slow and not worth it)
* single deletion: non-recursive R-tree deletion using depth-first tree traversal with free-at-empty strategy (entries in underflowed nodes are not reinserted, instead underflowed nodes are kept in the tree and deleted only when empty, which is a good compromise of query vs removal performance)
* bulk loading: OMT algorithm (Overlap Minimizing Top-down Bulk Loading) combined with FloydRivest selection algorithm
* bulk insertion: STLT algorithm (Small-Tree-Large-Tree)
* search: standard non-recursive R-tree search
## Papers
* [R-trees: a Dynamic Index Structure For Spatial Searching](http://www-db.deis.unibo.it/courses/SI-LS/papers/Gut84.pdf)
* [The R*-tree: An Efficient and Robust Access Method for Points and Rectangles+](http://dbs.mathematik.uni-marburg.de/publications/myPapers/1990/BKSS90.pdf)
* [OMT: Overlap Minimizing Top-down Bulk Loading Algorithm for R-tree](http://ftp.informatik.rwth-aachen.de/Publications/CEUR-WS/Vol-74/files/FORUM_18.pdf)
* [Bulk Insertions into R-Trees Using the Small-Tree-Large-Tree Approach](http://www.cs.arizona.edu/~bkmoon/papers/dke06-bulk.pdf)
* [R-Trees: Theory and Applications (book)](http://www.apress.com/9781852339777)
## Development
```bash
npm ci # install dependencies
npm test # lint the code and run tests
npm run perf # run performance benchmarks
npm run cov # report test coverage
```
## Compatibility
RBush v4+ is published as a ES module and no longer supports CommonJS environments. It works universally in modern browsers, but you can transpile the code on your end to support IE11.

View File

@@ -0,0 +1,512 @@
import quickselect from 'quickselect';
export default class RBush {
constructor(maxEntries = 9) {
// max entries in a node is 9 by default; min node fill is 40% for best performance
this._maxEntries = Math.max(4, maxEntries);
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
this.clear();
}
all() {
return this._all(this.data, []);
}
search(bbox) {
let node = this.data;
const result = [];
if (!intersects(bbox, node)) return result;
const toBBox = this.toBBox;
const nodesToSearch = [];
while (node) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const childBBox = node.leaf ? toBBox(child) : child;
if (intersects(bbox, childBBox)) {
if (node.leaf) result.push(child);
else if (contains(bbox, childBBox)) this._all(child, result);
else nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return result;
}
collides(bbox) {
let node = this.data;
if (!intersects(bbox, node)) return false;
const nodesToSearch = [];
while (node) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const childBBox = node.leaf ? this.toBBox(child) : child;
if (intersects(bbox, childBBox)) {
if (node.leaf || contains(bbox, childBBox)) return true;
nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return false;
}
load(data) {
if (!(data && data.length)) return this;
if (data.length < this._minEntries) {
for (let i = 0; i < data.length; i++) {
this.insert(data[i]);
}
return this;
}
// recursively build the tree with the given data from scratch using OMT algorithm
let node = this._build(data.slice(), 0, data.length - 1, 0);
if (!this.data.children.length) {
// save as is if tree is empty
this.data = node;
} else if (this.data.height === node.height) {
// split root if trees have the same height
this._splitRoot(this.data, node);
} else {
if (this.data.height < node.height) {
// swap trees if inserted one is bigger
const tmpNode = this.data;
this.data = node;
node = tmpNode;
}
// insert the small tree into the large tree at appropriate level
this._insert(node, this.data.height - node.height - 1, true);
}
return this;
}
insert(item) {
if (item) this._insert(item, this.data.height - 1);
return this;
}
clear() {
this.data = createNode([]);
return this;
}
remove(item, equalsFn) {
if (!item) return this;
let node = this.data;
const bbox = this.toBBox(item);
const path = [];
const indexes = [];
let i, parent, goingUp;
// depth-first iterative tree traversal
while (node || path.length) {
if (!node) { // go up
node = path.pop();
parent = path[path.length - 1];
i = indexes.pop();
goingUp = true;
}
if (node.leaf) { // check current node
const index = findItem(item, node.children, equalsFn);
if (index !== -1) {
// item found, remove the item and condense tree upwards
node.children.splice(index, 1);
path.push(node);
this._condense(path);
return this;
}
}
if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
path.push(node);
indexes.push(i);
i = 0;
parent = node;
node = node.children[0];
} else if (parent) { // go right
i++;
node = parent.children[i];
goingUp = false;
} else node = null; // nothing found
}
return this;
}
toBBox(item) { return item; }
compareMinX(a, b) { return a.minX - b.minX; }
compareMinY(a, b) { return a.minY - b.minY; }
toJSON() { return this.data; }
fromJSON(data) {
this.data = data;
return this;
}
_all(node, result) {
const nodesToSearch = [];
while (node) {
if (node.leaf) result.push(...node.children);
else nodesToSearch.push(...node.children);
node = nodesToSearch.pop();
}
return result;
}
_build(items, left, right, height) {
const N = right - left + 1;
let M = this._maxEntries;
let node;
if (N <= M) {
// reached leaf level; return leaf
node = createNode(items.slice(left, right + 1));
calcBBox(node, this.toBBox);
return node;
}
if (!height) {
// target height of the bulk-loaded tree
height = Math.ceil(Math.log(N) / Math.log(M));
// target number of root entries to maximize storage utilization
M = Math.ceil(N / Math.pow(M, height - 1));
}
node = createNode([]);
node.leaf = false;
node.height = height;
// split the items into M mostly square tiles
const N2 = Math.ceil(N / M);
const N1 = N2 * Math.ceil(Math.sqrt(M));
multiSelect(items, left, right, N1, this.compareMinX);
for (let i = left; i <= right; i += N1) {
const right2 = Math.min(i + N1 - 1, right);
multiSelect(items, i, right2, N2, this.compareMinY);
for (let j = i; j <= right2; j += N2) {
const right3 = Math.min(j + N2 - 1, right2);
// pack each entry recursively
node.children.push(this._build(items, j, right3, height - 1));
}
}
calcBBox(node, this.toBBox);
return node;
}
_chooseSubtree(bbox, node, level, path) {
while (true) {
path.push(node);
if (node.leaf || path.length - 1 === level) break;
let minArea = Infinity;
let minEnlargement = Infinity;
let targetNode;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const area = bboxArea(child);
const enlargement = enlargedArea(bbox, child) - area;
// choose entry with the least area enlargement
if (enlargement < minEnlargement) {
minEnlargement = enlargement;
minArea = area < minArea ? area : minArea;
targetNode = child;
} else if (enlargement === minEnlargement) {
// otherwise choose one with the smallest area
if (area < minArea) {
minArea = area;
targetNode = child;
}
}
}
node = targetNode || node.children[0];
}
return node;
}
_insert(item, level, isNode) {
const bbox = isNode ? item : this.toBBox(item);
const insertPath = [];
// find the best node for accommodating the item, saving all nodes along the path too
const node = this._chooseSubtree(bbox, this.data, level, insertPath);
// put the item into the node
node.children.push(item);
extend(node, bbox);
// split on node overflow; propagate upwards if necessary
while (level >= 0) {
if (insertPath[level].children.length > this._maxEntries) {
this._split(insertPath, level);
level--;
} else break;
}
// adjust bboxes along the insertion path
this._adjustParentBBoxes(bbox, insertPath, level);
}
// split overflowed node into two
_split(insertPath, level) {
const node = insertPath[level];
const M = node.children.length;
const m = this._minEntries;
this._chooseSplitAxis(node, m, M);
const splitIndex = this._chooseSplitIndex(node, m, M);
const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
newNode.height = node.height;
newNode.leaf = node.leaf;
calcBBox(node, this.toBBox);
calcBBox(newNode, this.toBBox);
if (level) insertPath[level - 1].children.push(newNode);
else this._splitRoot(node, newNode);
}
_splitRoot(node, newNode) {
// split root node
this.data = createNode([node, newNode]);
this.data.height = node.height + 1;
this.data.leaf = false;
calcBBox(this.data, this.toBBox);
}
_chooseSplitIndex(node, m, M) {
let index;
let minOverlap = Infinity;
let minArea = Infinity;
for (let i = m; i <= M - m; i++) {
const bbox1 = distBBox(node, 0, i, this.toBBox);
const bbox2 = distBBox(node, i, M, this.toBBox);
const overlap = intersectionArea(bbox1, bbox2);
const area = bboxArea(bbox1) + bboxArea(bbox2);
// choose distribution with minimum overlap
if (overlap < minOverlap) {
minOverlap = overlap;
index = i;
minArea = area < minArea ? area : minArea;
} else if (overlap === minOverlap) {
// otherwise choose distribution with minimum area
if (area < minArea) {
minArea = area;
index = i;
}
}
}
return index || M - m;
}
// sorts node children by the best axis for split
_chooseSplitAxis(node, m, M) {
const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
const xMargin = this._allDistMargin(node, m, M, compareMinX);
const yMargin = this._allDistMargin(node, m, M, compareMinY);
// if total distributions margin value is minimal for x, sort by minX,
// otherwise it's already sorted by minY
if (xMargin < yMargin) node.children.sort(compareMinX);
}
// total margin of all possible split distributions where each node is at least m full
_allDistMargin(node, m, M, compare) {
node.children.sort(compare);
const toBBox = this.toBBox;
const leftBBox = distBBox(node, 0, m, toBBox);
const rightBBox = distBBox(node, M - m, M, toBBox);
let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
for (let i = m; i < M - m; i++) {
const child = node.children[i];
extend(leftBBox, node.leaf ? toBBox(child) : child);
margin += bboxMargin(leftBBox);
}
for (let i = M - m - 1; i >= m; i--) {
const child = node.children[i];
extend(rightBBox, node.leaf ? toBBox(child) : child);
margin += bboxMargin(rightBBox);
}
return margin;
}
_adjustParentBBoxes(bbox, path, level) {
// adjust bboxes along the given tree path
for (let i = level; i >= 0; i--) {
extend(path[i], bbox);
}
}
_condense(path) {
// go through the path, removing empty nodes and updating bboxes
for (let i = path.length - 1, siblings; i >= 0; i--) {
if (path[i].children.length === 0) {
if (i > 0) {
siblings = path[i - 1].children;
siblings.splice(siblings.indexOf(path[i]), 1);
} else this.clear();
} else calcBBox(path[i], this.toBBox);
}
}
}
function findItem(item, items, equalsFn) {
if (!equalsFn) return items.indexOf(item);
for (let i = 0; i < items.length; i++) {
if (equalsFn(item, items[i])) return i;
}
return -1;
}
// calculate node's bbox from bboxes of its children
function calcBBox(node, toBBox) {
distBBox(node, 0, node.children.length, toBBox, node);
}
// min bounding rectangle of node children from k to p-1
function distBBox(node, k, p, toBBox, destNode) {
if (!destNode) destNode = createNode(null);
destNode.minX = Infinity;
destNode.minY = Infinity;
destNode.maxX = -Infinity;
destNode.maxY = -Infinity;
for (let i = k; i < p; i++) {
const child = node.children[i];
extend(destNode, node.leaf ? toBBox(child) : child);
}
return destNode;
}
function extend(a, b) {
a.minX = Math.min(a.minX, b.minX);
a.minY = Math.min(a.minY, b.minY);
a.maxX = Math.max(a.maxX, b.maxX);
a.maxY = Math.max(a.maxY, b.maxY);
return a;
}
function compareNodeMinX(a, b) { return a.minX - b.minX; }
function compareNodeMinY(a, b) { return a.minY - b.minY; }
function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
function enlargedArea(a, b) {
return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
(Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
}
function intersectionArea(a, b) {
const minX = Math.max(a.minX, b.minX);
const minY = Math.max(a.minY, b.minY);
const maxX = Math.min(a.maxX, b.maxX);
const maxY = Math.min(a.maxY, b.maxY);
return Math.max(0, maxX - minX) *
Math.max(0, maxY - minY);
}
function contains(a, b) {
return a.minX <= b.minX &&
a.minY <= b.minY &&
b.maxX <= a.maxX &&
b.maxY <= a.maxY;
}
function intersects(a, b) {
return b.minX <= a.maxX &&
b.minY <= a.maxY &&
b.maxX >= a.minX &&
b.maxY >= a.minY;
}
function createNode(children) {
return {
children,
height: 1,
leaf: true,
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
};
}
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
// combines selection algorithm with binary divide & conquer approach
function multiSelect(arr, left, right, n, compare) {
const stack = [left, right];
while (stack.length) {
right = stack.pop();
left = stack.pop();
if (right - left <= n) continue;
const mid = left + Math.ceil((right - left) / n / 2) * n;
quickselect(arr, mid, left, right, compare);
stack.push(left, mid, mid, right);
}
}

View File

@@ -0,0 +1,52 @@
{
"name": "rbush",
"version": "4.0.1",
"description": "High-performance 2D spatial index for rectangles (based on R*-tree with bulk loading and bulk insertion algorithms)",
"homepage": "https://github.com/mourner/rbush",
"repository": {
"type": "git",
"url": "git://github.com/mourner/rbush.git"
},
"keywords": [
"spatial",
"tree",
"search",
"rectangle",
"index",
"math"
],
"author": "Volodymyr Agafonkin",
"license": "MIT",
"type": "module",
"exports": "./index.js",
"main": "index.js",
"module": "index.js",
"browser": "rbush.min.js",
"jsdelivr": "rbush.min.js",
"unpkg": "rbush.min.js",
"devDependencies": {
"@rollup/plugin-buble": "^1.0.3",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"benchmark": "^2.1.4",
"eslint": "^9.9.0",
"eslint-config-mourner": "^4.0.2",
"rollup": "^4.21.0"
},
"scripts": {
"pretest": "eslint index.js test/test.js bench/*.js",
"test": "node --test",
"perf": "node ./bench/perf.js",
"cov": "node --test --experimental-test-coverage",
"build": "rollup -c",
"prepublishOnly": "npm run test && npm run build"
},
"files": [
"index.js",
"rbush.js",
"rbush.min.js"
],
"dependencies": {
"quickselect": "^3.0.0"
}
}

View File

@@ -0,0 +1,594 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.RBush = factory());
})(this, (function () { 'use strict';
/**
* Rearranges items so that all items in the [left, k] are the smallest.
* The k-th element will have the (k - left + 1)-th smallest value in [left, right].
*
* @template T
* @param {T[]} arr the array to partially sort (in place)
* @param {number} k middle index for partial sorting (as defined above)
* @param {number} [left=0] left index of the range to sort
* @param {number} [right=arr.length-1] right index
* @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
*/
function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
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));
quickselect(arr, k, newLeft, newRight, compare);
}
const t = arr[k];
let i = left;
/** @type {number} */
let j = right;
swap(arr, left, k);
if (compare(arr[right], t) > 0) swap(arr, left, right);
while (i < j) {
swap(arr, i, j);
i++;
j--;
while (compare(arr[i], t) < 0) i++;
while (compare(arr[j], t) > 0) j--;
}
if (compare(arr[left], t) === 0) swap(arr, left, j);
else {
j++;
swap(arr, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
/**
* @template T
* @param {T[]} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* @template T
* @param {T} a
* @param {T} b
* @returns {number}
*/
function defaultCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
class RBush {
constructor(maxEntries = 9) {
// max entries in a node is 9 by default; min node fill is 40% for best performance
this._maxEntries = Math.max(4, maxEntries);
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
this.clear();
}
all() {
return this._all(this.data, []);
}
search(bbox) {
let node = this.data;
const result = [];
if (!intersects(bbox, node)) return result;
const toBBox = this.toBBox;
const nodesToSearch = [];
while (node) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const childBBox = node.leaf ? toBBox(child) : child;
if (intersects(bbox, childBBox)) {
if (node.leaf) result.push(child);
else if (contains(bbox, childBBox)) this._all(child, result);
else nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return result;
}
collides(bbox) {
let node = this.data;
if (!intersects(bbox, node)) return false;
const nodesToSearch = [];
while (node) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const childBBox = node.leaf ? this.toBBox(child) : child;
if (intersects(bbox, childBBox)) {
if (node.leaf || contains(bbox, childBBox)) return true;
nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return false;
}
load(data) {
if (!(data && data.length)) return this;
if (data.length < this._minEntries) {
for (let i = 0; i < data.length; i++) {
this.insert(data[i]);
}
return this;
}
// recursively build the tree with the given data from scratch using OMT algorithm
let node = this._build(data.slice(), 0, data.length - 1, 0);
if (!this.data.children.length) {
// save as is if tree is empty
this.data = node;
} else if (this.data.height === node.height) {
// split root if trees have the same height
this._splitRoot(this.data, node);
} else {
if (this.data.height < node.height) {
// swap trees if inserted one is bigger
const tmpNode = this.data;
this.data = node;
node = tmpNode;
}
// insert the small tree into the large tree at appropriate level
this._insert(node, this.data.height - node.height - 1, true);
}
return this;
}
insert(item) {
if (item) this._insert(item, this.data.height - 1);
return this;
}
clear() {
this.data = createNode([]);
return this;
}
remove(item, equalsFn) {
if (!item) return this;
let node = this.data;
const bbox = this.toBBox(item);
const path = [];
const indexes = [];
let i, parent, goingUp;
// depth-first iterative tree traversal
while (node || path.length) {
if (!node) { // go up
node = path.pop();
parent = path[path.length - 1];
i = indexes.pop();
goingUp = true;
}
if (node.leaf) { // check current node
const index = findItem(item, node.children, equalsFn);
if (index !== -1) {
// item found, remove the item and condense tree upwards
node.children.splice(index, 1);
path.push(node);
this._condense(path);
return this;
}
}
if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
path.push(node);
indexes.push(i);
i = 0;
parent = node;
node = node.children[0];
} else if (parent) { // go right
i++;
node = parent.children[i];
goingUp = false;
} else node = null; // nothing found
}
return this;
}
toBBox(item) { return item; }
compareMinX(a, b) { return a.minX - b.minX; }
compareMinY(a, b) { return a.minY - b.minY; }
toJSON() { return this.data; }
fromJSON(data) {
this.data = data;
return this;
}
_all(node, result) {
const nodesToSearch = [];
while (node) {
if (node.leaf) result.push(...node.children);
else nodesToSearch.push(...node.children);
node = nodesToSearch.pop();
}
return result;
}
_build(items, left, right, height) {
const N = right - left + 1;
let M = this._maxEntries;
let node;
if (N <= M) {
// reached leaf level; return leaf
node = createNode(items.slice(left, right + 1));
calcBBox(node, this.toBBox);
return node;
}
if (!height) {
// target height of the bulk-loaded tree
height = Math.ceil(Math.log(N) / Math.log(M));
// target number of root entries to maximize storage utilization
M = Math.ceil(N / Math.pow(M, height - 1));
}
node = createNode([]);
node.leaf = false;
node.height = height;
// split the items into M mostly square tiles
const N2 = Math.ceil(N / M);
const N1 = N2 * Math.ceil(Math.sqrt(M));
multiSelect(items, left, right, N1, this.compareMinX);
for (let i = left; i <= right; i += N1) {
const right2 = Math.min(i + N1 - 1, right);
multiSelect(items, i, right2, N2, this.compareMinY);
for (let j = i; j <= right2; j += N2) {
const right3 = Math.min(j + N2 - 1, right2);
// pack each entry recursively
node.children.push(this._build(items, j, right3, height - 1));
}
}
calcBBox(node, this.toBBox);
return node;
}
_chooseSubtree(bbox, node, level, path) {
while (true) {
path.push(node);
if (node.leaf || path.length - 1 === level) break;
let minArea = Infinity;
let minEnlargement = Infinity;
let targetNode;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const area = bboxArea(child);
const enlargement = enlargedArea(bbox, child) - area;
// choose entry with the least area enlargement
if (enlargement < minEnlargement) {
minEnlargement = enlargement;
minArea = area < minArea ? area : minArea;
targetNode = child;
} else if (enlargement === minEnlargement) {
// otherwise choose one with the smallest area
if (area < minArea) {
minArea = area;
targetNode = child;
}
}
}
node = targetNode || node.children[0];
}
return node;
}
_insert(item, level, isNode) {
const bbox = isNode ? item : this.toBBox(item);
const insertPath = [];
// find the best node for accommodating the item, saving all nodes along the path too
const node = this._chooseSubtree(bbox, this.data, level, insertPath);
// put the item into the node
node.children.push(item);
extend(node, bbox);
// split on node overflow; propagate upwards if necessary
while (level >= 0) {
if (insertPath[level].children.length > this._maxEntries) {
this._split(insertPath, level);
level--;
} else break;
}
// adjust bboxes along the insertion path
this._adjustParentBBoxes(bbox, insertPath, level);
}
// split overflowed node into two
_split(insertPath, level) {
const node = insertPath[level];
const M = node.children.length;
const m = this._minEntries;
this._chooseSplitAxis(node, m, M);
const splitIndex = this._chooseSplitIndex(node, m, M);
const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
newNode.height = node.height;
newNode.leaf = node.leaf;
calcBBox(node, this.toBBox);
calcBBox(newNode, this.toBBox);
if (level) insertPath[level - 1].children.push(newNode);
else this._splitRoot(node, newNode);
}
_splitRoot(node, newNode) {
// split root node
this.data = createNode([node, newNode]);
this.data.height = node.height + 1;
this.data.leaf = false;
calcBBox(this.data, this.toBBox);
}
_chooseSplitIndex(node, m, M) {
let index;
let minOverlap = Infinity;
let minArea = Infinity;
for (let i = m; i <= M - m; i++) {
const bbox1 = distBBox(node, 0, i, this.toBBox);
const bbox2 = distBBox(node, i, M, this.toBBox);
const overlap = intersectionArea(bbox1, bbox2);
const area = bboxArea(bbox1) + bboxArea(bbox2);
// choose distribution with minimum overlap
if (overlap < minOverlap) {
minOverlap = overlap;
index = i;
minArea = area < minArea ? area : minArea;
} else if (overlap === minOverlap) {
// otherwise choose distribution with minimum area
if (area < minArea) {
minArea = area;
index = i;
}
}
}
return index || M - m;
}
// sorts node children by the best axis for split
_chooseSplitAxis(node, m, M) {
const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
const xMargin = this._allDistMargin(node, m, M, compareMinX);
const yMargin = this._allDistMargin(node, m, M, compareMinY);
// if total distributions margin value is minimal for x, sort by minX,
// otherwise it's already sorted by minY
if (xMargin < yMargin) node.children.sort(compareMinX);
}
// total margin of all possible split distributions where each node is at least m full
_allDistMargin(node, m, M, compare) {
node.children.sort(compare);
const toBBox = this.toBBox;
const leftBBox = distBBox(node, 0, m, toBBox);
const rightBBox = distBBox(node, M - m, M, toBBox);
let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
for (let i = m; i < M - m; i++) {
const child = node.children[i];
extend(leftBBox, node.leaf ? toBBox(child) : child);
margin += bboxMargin(leftBBox);
}
for (let i = M - m - 1; i >= m; i--) {
const child = node.children[i];
extend(rightBBox, node.leaf ? toBBox(child) : child);
margin += bboxMargin(rightBBox);
}
return margin;
}
_adjustParentBBoxes(bbox, path, level) {
// adjust bboxes along the given tree path
for (let i = level; i >= 0; i--) {
extend(path[i], bbox);
}
}
_condense(path) {
// go through the path, removing empty nodes and updating bboxes
for (let i = path.length - 1, siblings; i >= 0; i--) {
if (path[i].children.length === 0) {
if (i > 0) {
siblings = path[i - 1].children;
siblings.splice(siblings.indexOf(path[i]), 1);
} else this.clear();
} else calcBBox(path[i], this.toBBox);
}
}
}
function findItem(item, items, equalsFn) {
if (!equalsFn) return items.indexOf(item);
for (let i = 0; i < items.length; i++) {
if (equalsFn(item, items[i])) return i;
}
return -1;
}
// calculate node's bbox from bboxes of its children
function calcBBox(node, toBBox) {
distBBox(node, 0, node.children.length, toBBox, node);
}
// min bounding rectangle of node children from k to p-1
function distBBox(node, k, p, toBBox, destNode) {
if (!destNode) destNode = createNode(null);
destNode.minX = Infinity;
destNode.minY = Infinity;
destNode.maxX = -Infinity;
destNode.maxY = -Infinity;
for (let i = k; i < p; i++) {
const child = node.children[i];
extend(destNode, node.leaf ? toBBox(child) : child);
}
return destNode;
}
function extend(a, b) {
a.minX = Math.min(a.minX, b.minX);
a.minY = Math.min(a.minY, b.minY);
a.maxX = Math.max(a.maxX, b.maxX);
a.maxY = Math.max(a.maxY, b.maxY);
return a;
}
function compareNodeMinX(a, b) { return a.minX - b.minX; }
function compareNodeMinY(a, b) { return a.minY - b.minY; }
function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
function enlargedArea(a, b) {
return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
(Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
}
function intersectionArea(a, b) {
const minX = Math.max(a.minX, b.minX);
const minY = Math.max(a.minY, b.minY);
const maxX = Math.min(a.maxX, b.maxX);
const maxY = Math.min(a.maxY, b.maxY);
return Math.max(0, maxX - minX) *
Math.max(0, maxY - minY);
}
function contains(a, b) {
return a.minX <= b.minX &&
a.minY <= b.minY &&
b.maxX <= a.maxX &&
b.maxY <= a.maxY;
}
function intersects(a, b) {
return b.minX <= a.maxX &&
b.minY <= a.maxY &&
b.maxX >= a.minX &&
b.maxY >= a.minY;
}
function createNode(children) {
return {
children,
height: 1,
leaf: true,
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
};
}
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
// combines selection algorithm with binary divide & conquer approach
function multiSelect(arr, left, right, n, compare) {
const stack = [left, right];
while (stack.length) {
right = stack.pop();
left = stack.pop();
if (right - left <= n) continue;
const mid = left + Math.ceil((right - left) / n / 2) * n;
quickselect(arr, mid, left, right, compare);
stack.push(left, mid, mid, right);
}
}
return RBush;
}));

File diff suppressed because one or more lines are too long

42
frontend/node_modules/concaveman/package.json generated vendored Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "concaveman",
"version": "2.0.0",
"description": "Fast 2D concave hull algorithm in JavaScript (generates an outline of a point set)",
"type": "module",
"main": "index.js",
"exports": "./index.js",
"dependencies": {
"point-in-polygon": "^1.1.0",
"rbush": "^4.0.1",
"robust-predicates": "^3.0.2",
"tinyqueue": "^3.0.0"
},
"devDependencies": {
"eslint": "^9.31.0",
"eslint-config-mourner": "^4.0.2"
},
"scripts": {
"pretest": "eslint index.js test/*.js",
"test": "node test/test.js"
},
"repository": {
"type": "git",
"url": "git://github.com/mapbox/concaveman.git"
},
"keywords": [
"concave",
"hull",
"algorithm",
"geometry",
"shape",
"outline"
],
"eslintConfig": {
"extends": "mourner"
},
"files": [
"index.js"
],
"author": "Vladimir Agafonkin",
"license": "ISC"
}