@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
JavaScript
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
};