UNPKG

@thi.ng/geom-accel

Version:

n-D spatial indexing data structures with a shared ES6 Map/Set-like API

239 lines (238 loc) 6.92 kB
import { assert } from "@thi.ng/errors/assert"; import { equals } from "@thi.ng/vectors/equals"; import { hash2, hash3 } from "@thi.ng/vectors/hash"; class AHashGrid { invSize; tableSize; keyFn; indices; entries; items; constructor(key, cellSize, capacity, items) { this.invSize = 1 / cellSize; this.tableSize = capacity << 1; this.indices = new Uint32Array(this.tableSize + 1); this.entries = new Uint32Array(capacity); this.keyFn = key; items ? this.build(items) : this.items = []; } get length() { return this.items.length; } clear() { this.items = []; this.indices.fill(0); this.entries.fill(0); } /** * Computes number of hash collisions and max cell occupancy. */ stats() { const { indices, items, keyFn } = this; const bins = new Uint32Array(indices.length); let max = 0; let collisions = 0; const numP = items.length; for (let i = 0; i < numP; i++) { const n = ++bins[this.hashPos(keyFn(items[i]))]; if (n > 1) collisions++; max = Math.max(max, n); } return { collisions, max }; } /** * (Re)builds the hash grid from given items. All previous contents will be * lost! `items` must be less than max. capacity configured via ctor. * * @param items */ build(items) { const { entries, indices, keyFn, tableSize } = this; const num = items.length; assert(items.length <= entries.length, `too many items`); this.items = items; indices.fill(0); entries.fill(0); for (let i = 0; i < num; i++) { indices[this.hashPos(keyFn(items[i]))]++; } let prefixSum = 0; for (let i = 0; i < tableSize; i++) { prefixSum += indices[i]; indices[i] = prefixSum; } indices[tableSize] = prefixSum; for (let i = 0; i < num; i++) { entries[--indices[this.hashPos(keyFn(items[i]))]] = i; } } /** * Returns true if an item with given `key` vector has been indexed by the * hash grid. The optional `equiv` predicate can be used to customize the * key equality test (called for all items matching the `keys` hash). * * @remarks * Default predicate is: [thi.ng/vectors * equals()](https://docs.thi.ng/umbrella/vectors/functions/equals.html) * * @param key * @param equiv */ has(key, equiv = equals) { const { entries, indices, items, keyFn } = this; const h = this.hashPos(key); for (let i = indices[h], j = indices[h + 1]; i < j; i++) { if (equiv(keyFn(items[entries[i]]), key)) return true; } return false; } /** * Returns array of all items (if any) which have been indexed using given * lookup `key` AND which are passing the given `equiv` predicate (which can * be used to customize the key equality test). * * @remarks * Default predicate is: [thi.ng/vectors * equals()](https://docs.thi.ng/umbrella/vectors/functions/equals.html) * * @param key * @param equiv */ get(key, equiv = equals) { const { entries, indices, items, keyFn } = this; const h = this.hashPos(key); const res = []; for (let i = indices[h], j = indices[h + 1]; i < j; i++) { const val = items[entries[i]]; if (equiv(keyFn(val), key)) res.push(val); } return res; } } class HashGrid2 extends AHashGrid { empty() { return new HashGrid2( this.keyFn, 1 / this.invSize, this.entries.length ); } queryNeighborhood(neighborhood, opts = {}) { const { entries, indices, items, keyFn, tableSize } = this; const { xmin, xmax, ymin, ymax } = this.queryBounds(neighborhood, opts); let x, y, i, j, h, val; for (x = xmin; x <= xmax; x++) { for (y = ymin; y <= ymax; y++) { h = hash2(x, y) % tableSize; for (i = indices[h], j = indices[h + 1]; i < j; i++) { val = items[entries[i]]; neighborhood.consider(keyFn(val), val); } } } return neighborhood; } hasNeighborhood(neighborhood, opts = {}) { const { entries, indices, items, keyFn, tableSize } = this; const { xmin, xmax, ymin, ymax } = this.queryBounds(neighborhood, opts); let x, y, i, j, h; for (x = xmin; x <= xmax; x++) { for (y = ymin; y <= ymax; y++) { h = hash2(x, y) % tableSize; for (i = indices[h], j = indices[h + 1]; i < j; i++) { if (neighborhood.includesPosition(keyFn(items[entries[i]]))) return true; } } } return false; } hashPos(p) { const s = this.invSize; return hash2(p[0] * s, p[1] * s) % this.tableSize; } queryBounds(neighborhood, opts = {}) { const { invSize } = this; const [qx, qy] = neighborhood.target; const r = opts.r ?? neighborhood.radius; return { xmin: (qx - r) * invSize | 0, xmax: (qx + r) * invSize | 0, ymin: (qy - r) * invSize | 0, ymax: (qy + r) * invSize | 0 }; } } class HashGrid3 extends AHashGrid { empty() { return new HashGrid3( this.keyFn, 1 / this.invSize, this.entries.length ); } queryNeighborhood(neighborhood, opts = {}) { const { entries, indices, items, keyFn, tableSize } = this; const { xmin, xmax, ymin, ymax, zmin, zmax } = this.queryBounds( neighborhood, opts ); let x, y, z, i, j, h, val; for (x = xmin; x <= xmax; x++) { for (y = ymin; y <= ymax; y++) { for (z = zmin; z <= zmax; z++) { h = hash3(x, y, z) % tableSize; for (i = indices[h], j = indices[h + 1]; i < j; i++) { val = items[entries[i]]; neighborhood.consider(keyFn(val), val); } } } } return neighborhood; } hasNeighborhood(neighborhood, opts = {}) { const { entries, indices, items, keyFn, tableSize } = this; const { xmin, xmax, ymin, ymax, zmin, zmax } = this.queryBounds( neighborhood, opts ); let x, y, z, i, j, h; for (x = xmin; x <= xmax; x++) { for (y = ymin; y <= ymax; y++) { for (z = zmin; z <= zmax; z++) { h = hash3(x, y, z) % tableSize; for (i = indices[h], j = indices[h + 1]; i < j; i++) { if (neighborhood.includesPosition( keyFn(items[entries[i]]) )) return true; } } } } return false; } hashPos(p) { const s = this.invSize; return hash3(p[0] * s, p[1] * s, p[2] * s) % this.tableSize; } queryBounds(neighborhood, opts = {}) { const { invSize } = this; const [qx, qy, qz] = neighborhood.target; const r = opts.r ?? neighborhood.radius; return { xmin: (qx - r) * invSize | 0, xmax: (qx + r) * invSize | 0, ymin: (qy - r) * invSize | 0, ymax: (qy + r) * invSize | 0, zmin: (qz - r) * invSize | 0, zmax: (qz + r) * invSize | 0 }; } } export { AHashGrid, HashGrid2, HashGrid3 };