UNPKG

gis-tools-ts

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

417 lines 15.4 kB
import { FlatQueue } from './flatqueue.js'; /** * # BoxIndex * * ## Description * An Index for points and rectangles * * A really fast static spatial index for 2D points and rectangles in JavaScript. * Uses either a fast simple Hilbert curve algorithm or a more complex Hilbert curve (S2) algorithm. * * This is a partial port/typescript port of the [flatbush](https://github.com/mourner/flatbush) * codebase. * * ## Usage * ```ts * import { BoxIndex } from 'gis-tools-ts'; * import type { BoxIndexAccessor } from 'gis-tools-ts'; * * interface Item { minX: number; minY: number; maxX: number; maxY: number } * * // define how to access the minX, minY, maxX, and maxY properties * const accessor: BoxIndexAccessor<Item> = (item: Item) => [item.minX, item.minY, item.maxX, item.maxY]; * // create the index * const flatbush = new BoxIndex<Item>(items, accessor); * * // make a bounding box query * const found = index.search(minX, minY, maxX, maxY); * * // make a k-nearest-neighbors query * const neighborIds = index.neighbors(x, y, 5); * ``` * * ## Links * - <https://github.com/mourner/flatbush> */ export class BoxIndex { items; accessor; #nodeSize; #numItems; #levelBounds; #pos = 0; minX = Infinity; minY = Infinity; maxX = -Infinity; maxY = -Infinity; #boxes; #indices; // a priority queue for k-nearest-neighbors queries #queue = new FlatQueue(); #indexed = false; /** * Create a BoxIndex index that will hold a given number of items. * @param items - The items to index. * @param accessor - A function for accessing the minX, minY, maxX, and maxY properties of the items. * @param [nodeSize] Size of the tree node (16 by default). */ constructor(items, accessor, nodeSize = 16) { this.items = items; this.accessor = accessor; this.#numItems = items.length; this.#nodeSize = Math.min(Math.max(nodeSize, 2), 65535); // calculate the total number of nodes in the R-tree to allocate space for // and the index of each tree level (used in search later) let n = this.#numItems; let numNodes = n; this.#levelBounds = [n * 4]; if (n !== 0) { do { n = Math.ceil(n / this.#nodeSize); numNodes += n; this.#levelBounds.push(numNodes * 4); } while (n !== 1); } // setup the index buffers this.#boxes = new Array(numNodes * 4); this.#indices = new Array(numNodes); // add the items for (const item of this.items) { const [minX, minY, maxX, maxY] = this.accessor(item); this.#add(minX, minY, maxX, maxY); } // spatially index the items this.#index(); } /** * Add a given rectangle to the index. * @param minX - The minimum x coordinate of the query point. * @param minY - The minimum y coordinate of the query point. * @param maxX - The maximum x coordinate of the query point. * @param maxY - The maximum y coordinate of the query point. * @returns A zero-based, incremental number that represents the newly added rectangle. */ #add(minX, minY, maxX, maxY) { const index = this.#pos >> 2; const boxes = this.#boxes; this.#indices[index] = index; boxes[this.#pos++] = minX; boxes[this.#pos++] = minY; boxes[this.#pos++] = maxX; boxes[this.#pos++] = maxY; if (minX < this.minX) this.minX = minX; if (minY < this.minY) this.minY = minY; if (maxX > this.maxX) this.maxX = maxX; if (maxY > this.maxY) this.maxY = maxY; return index; } /** Perform indexing of the added rectangles/points after all data has been added about the items. */ #index() { if (this.#indexed || this.items.length === 0) { return; } this.#indexed = true; if (this.#pos >> 2 !== this.#numItems) { throw new Error(`Added ${this.#pos >> 2} items when expected ${this.#numItems}.`); } const boxes = this.#boxes; if (this.#numItems <= this.#nodeSize) { // only one node, skip sorting and just fill the root box boxes[this.#pos++] = this.minX; boxes[this.#pos++] = this.minY; boxes[this.#pos++] = this.maxX; boxes[this.#pos++] = this.maxY; return; } const width = this.maxX - this.minX; const height = this.maxY - this.minY; const hilbertValues = new Uint32Array(this.#numItems); const hilbertMax = (1 << 16) - 1; // map item centers into Hilbert coordinate space and calculate Hilbert values for (let i = 0, pos = 0; i < this.#numItems; i++) { const minX = boxes[pos++]; const minY = boxes[pos++]; const maxX = boxes[pos++]; const maxY = boxes[pos++]; const x = Math.floor((hilbertMax * ((minX + maxX) / 2 - this.minX)) / width); const y = Math.floor((hilbertMax * ((minY + maxY) / 2 - this.minY)) / height); hilbertValues[i] = hilbert(x, y); } // sort items by their Hilbert value (for packing later) sort(hilbertValues, boxes, this.#indices, 0, this.#numItems - 1, this.#nodeSize); // generate nodes at each tree level, bottom-up for (let i = 0, pos = 0; i < this.#levelBounds.length - 1; i++) { const end = this.#levelBounds[i]; // generate a parent node for each block of consecutive <nodeSize> nodes while (pos < end) { const nodeIndex = pos; // calculate bbox for the new node let nodeMinX = boxes[pos++]; let nodeMinY = boxes[pos++]; let nodeMaxX = boxes[pos++]; let nodeMaxY = boxes[pos++]; for (let j = 1; j < this.#nodeSize && pos < end; j++) { nodeMinX = Math.min(nodeMinX, boxes[pos++]); nodeMinY = Math.min(nodeMinY, boxes[pos++]); nodeMaxX = Math.max(nodeMaxX, boxes[pos++]); nodeMaxY = Math.max(nodeMaxY, boxes[pos++]); } // add the new node to the tree data this.#indices[this.#pos >> 2] = nodeIndex; boxes[this.#pos++] = nodeMinX; boxes[this.#pos++] = nodeMinY; boxes[this.#pos++] = nodeMaxX; boxes[this.#pos++] = nodeMaxY; } } } /** * Search the index by a bounding box. * @param minX - The minimum x coordinate of the query point. * @param minY - The minimum y coordinate of the query point. * @param maxX - The maximum x coordinate of the query point. * @param maxY - The maximum y coordinate of the query point. * @param [filterFn] An optional function that is called on every found item; if supplied, only items for which this function returns true will be included in the results array. * @returns An array of indices of items intersecting or touching the given bounding box. */ search(minX, minY, maxX, maxY, filterFn) { let nodeIndex = this.#boxes.length - 4; const queue = []; const results = []; while (nodeIndex !== undefined) { // find the end index of the node const end = Math.min(nodeIndex + this.#nodeSize * 4, upperBound(nodeIndex, this.#levelBounds)); // search through child nodes for (let pos = nodeIndex ?? 0; pos < end; pos += 4) { // check if node bbox intersects with query bbox const x0 = this.#boxes[pos]; if (maxX < x0) continue; const y0 = this.#boxes[pos + 1]; if (maxY < y0) continue; const x1 = this.#boxes[pos + 2]; if (minX > x1) continue; const y1 = this.#boxes[pos + 3]; if (minY > y1) continue; const index = this.#indices[pos >> 2] | 0; if (nodeIndex >= this.#numItems * 4) { queue.push(index); // node; add it to the search queue } else { const item = this.items.at(index); if (item !== undefined && (filterFn?.(item) ?? true)) results.push(item); // leaf item } } nodeIndex = queue.pop(); } return results; } /** * Search items in order of distance from the given point. * @param x - The x coordinate of the query point. * @param y - The y coordinate of the query point. * @param [maxResults] - The maximum number of results to return. * @param [maxDistance] - The maximum distance to search. * @param [filterFn] An optional function for filtering the results. * @returns An array of indices of items found. */ neighbors(x, y, maxResults = Infinity, maxDistance = Infinity, filterFn) { let nodeIndex = this.#boxes.length - 4; const q = this.#queue; const results = []; const maxDistSquared = maxDistance * maxDistance; outer: while (nodeIndex !== undefined) { // find the end index of the node const end = Math.min(nodeIndex + this.#nodeSize * 4, upperBound(nodeIndex, this.#levelBounds)); // add child nodes to the queue for (let pos = nodeIndex; pos < end; pos += 4) { const index = this.#indices[pos >> 2] | 0; const minX = this.#boxes[pos]; const minY = this.#boxes[pos + 1]; const maxX = this.#boxes[pos + 2]; const maxY = this.#boxes[pos + 3]; const dx = x < minX ? minX - x : x > maxX ? x - maxX : 0; const dy = y < minY ? minY - y : y > maxY ? y - maxY : 0; const dist = dx * dx + dy * dy; if (dist > maxDistSquared) continue; if (nodeIndex >= this.#numItems * 4) { q.push(index << 1, dist); // node (use even id) } else { q.push((index << 1) + 1, dist); // leaf item (use odd id) } } // pop items from the queue // @ts-expect-error q.length check eliminates undefined values while (q.length !== 0 && (q.peek() & 1) !== 0) { const dist = q.peekValue(); if (dist === undefined || dist > maxDistSquared) break outer; const idx = (q.pop() ?? 0) >> 1; const item = this.items.at(idx); if (item !== undefined && (filterFn?.(item) ?? true)) results.push(item); if (results.length === maxResults) break outer; } nodeIndex = q.length !== 0 ? (q.pop() ?? 0) >> 1 : undefined; } q.clear(); return results; } } /** * Binary search for the first value in the array bigger than the given. * @param value - the value to search for * @param arr - the array to search * @returns the first value in the array bigger than the given */ function upperBound(value, arr) { let i = 0; let j = arr.length - 1; while (i < j) { const m = (i + j) >> 1; if (arr[m] > value) { j = m; } else { i = m + 1; } } return arr[i]; } /** * Custom quicksort that partially sorts bbox data alongside the hilbert values. * @param values - the hilbert values * @param boxes - the boxes * @param indices - the indices * @param left - the left index * @param right - the right index * @param nodeSize - the node size */ function sort(values, boxes, indices, left, right, nodeSize) { if (Math.floor(left / nodeSize) >= Math.floor(right / nodeSize)) return; // apply median of three method const start = values[left]; const mid = values[(left + right) >> 1]; const end = values[right]; let pivot = end; const x = Math.max(start, mid); if (end > x) { pivot = x; } else if (x === start) { pivot = Math.max(mid, end); } else if (x === mid) { pivot = Math.max(start, end); } let i = left - 1; let j = right + 1; while (true) { do i++; while (values[i] < pivot); do j--; while (values[j] > pivot); if (i >= j) break; swap(values, boxes, indices, i, j); } sort(values, boxes, indices, left, j, nodeSize); sort(values, boxes, indices, j + 1, right, nodeSize); } /** * Swap two values and two corresponding boxes. * @param values - the hilbert values * @param boxes - the boxes * @param indices - the indices * @param i - index * @param j - index */ function swap(values, boxes, indices, i, j) { const temp = values[i]; values[i] = values[j]; values[j] = temp; const k = 4 * i; const m = 4 * j; const a = boxes[k]; const b = boxes[k + 1]; const c = boxes[k + 2]; const d = boxes[k + 3]; boxes[k] = boxes[m]; boxes[k + 1] = boxes[m + 1]; boxes[k + 2] = boxes[m + 2]; boxes[k + 3] = boxes[m + 3]; boxes[m] = a; boxes[m + 1] = b; boxes[m + 2] = c; boxes[m + 3] = d; const e = indices[i]; indices[i] = indices[j]; indices[j] = e; } /** * Fast Hilbert curve algorithm by http://threadlocalmutex.com/ * Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain) * @param x - x coordinate * @param y - y coordinate * @returns the Hilbert value (32-bit integer) */ function hilbert(x, y) { let a = x ^ y; let b = 0xffff ^ a; let c = 0xffff ^ (x | y); let d = x & (y ^ 0xffff); let A = a | (b >> 1); let B = (a >> 1) ^ a; let C = (c >> 1) ^ (b & (d >> 1)) ^ c; let D = (a & (c >> 1)) ^ (d >> 1) ^ d; a = A; b = B; c = C; d = D; A = (a & (a >> 2)) ^ (b & (b >> 2)); B = (a & (b >> 2)) ^ (b & ((a ^ b) >> 2)); C ^= (a & (c >> 2)) ^ (b & (d >> 2)); D ^= (b & (c >> 2)) ^ ((a ^ b) & (d >> 2)); a = A; b = B; c = C; d = D; A = (a & (a >> 4)) ^ (b & (b >> 4)); B = (a & (b >> 4)) ^ (b & ((a ^ b) >> 4)); C ^= (a & (c >> 4)) ^ (b & (d >> 4)); D ^= (b & (c >> 4)) ^ ((a ^ b) & (d >> 4)); a = A; b = B; c = C; d = D; C ^= (a & (c >> 8)) ^ (b & (d >> 8)); D ^= (b & (c >> 8)) ^ ((a ^ b) & (d >> 8)); a = C ^ (C >> 1); b = D ^ (D >> 1); let i0 = x ^ y; let i1 = b | (0xffff ^ (i0 | a)); i0 = (i0 | (i0 << 8)) & 0x00ff00ff; i0 = (i0 | (i0 << 4)) & 0x0f0f0f0f; i0 = (i0 | (i0 << 2)) & 0x33333333; i0 = (i0 | (i0 << 1)) & 0x55555555; i1 = (i1 | (i1 << 8)) & 0x00ff00ff; i1 = (i1 | (i1 << 4)) & 0x0f0f0f0f; i1 = (i1 | (i1 << 2)) & 0x33333333; i1 = (i1 | (i1 << 1)) & 0x55555555; return ((i1 << 1) | i0) >>> 0; } //# sourceMappingURL=boxIndex.js.map