Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSDoc-based TypeScript checks + first-class types #47

Merged
merged 5 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ flatbush.js
yarn.lock
pnpm-lock.yaml
node_modules
index.d.ts
111 changes: 96 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
import FlatQueue from 'flatqueue';

const ARRAY_TYPES = [
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float32Array, Float64Array
];

const ARRAY_TYPES = [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array];
const VERSION = 3; // serialized format version

/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */

export default class Flatbush {

/**
* Recreate a Flatbush index from raw `ArrayBuffer` or `SharedArrayBuffer` data.
* @param {ArrayBuffer | SharedArrayBuffer} data
* @returns {Flatbush} index
*/
static from(data) {
// @ts-expect-error duck typing array buffers
if (!data || data.byteLength === undefined || data.buffer) {
throw new Error('Data must be an instance of ArrayBuffer or SharedArrayBuffer.');
}
const [magic, versionAndType] = new Uint8Array(data, 0, 2);
if (magic !== 0xfb) {
throw new Error('Data does not appear to be in a Flatbush format.');
}
if (versionAndType >> 4 !== VERSION) {
throw new Error(`Got v${versionAndType >> 4} data when expected v${VERSION}.`);
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);

return new Flatbush(numItems, nodeSize, ARRAY_TYPES[versionAndType & 0x0f], undefined, data);
return new Flatbush(numItems, nodeSize, ArrayType, undefined, data);
}

/**
* Create a Flatbush index that will hold a given number of items.
* @param {number} numItems
* @param {number} [nodeSize=16] Size of the tree node (16 by default).
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
* @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used to store data (`ArrayBuffer` by default).
* @param {ArrayBuffer | SharedArrayBuffer} [data] (Only used internally)
*/
constructor(numItems, nodeSize = 16, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data) {
if (numItems === undefined) throw new Error('Missing required argument: numItems.');
if (isNaN(numItems) || numItems <= 0) throw new Error(`Unexpected numItems value: ${numItems}.`);
Expand All @@ -44,7 +61,7 @@ export default class Flatbush {
this._levelBounds.push(numNodes * 4);
} while (n !== 1);

this.ArrayType = ArrayType || Float64Array;
this.ArrayType = ArrayType;
this.IndexArrayType = numNodes < 16384 ? Uint16Array : Uint32Array;

const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
Expand All @@ -54,6 +71,7 @@ export default class Flatbush {
throw new Error(`Unexpected typed array class: ${ArrayType}.`);
}

// @ts-expect-error duck typing array buffers
if (data && data.byteLength !== undefined && !data.buffer) {
this.data = data;
this._boxes = new this.ArrayType(this.data, 8, numNodes * 4);
Expand Down Expand Up @@ -81,9 +99,18 @@ export default class Flatbush {
}

// a priority queue for k-nearest-neighbors queries
/** @type FlatQueue<number> */
this._queue = new FlatQueue();
}

/**
* Add a given rectangle to the index.
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {number} 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;
Expand All @@ -101,6 +128,7 @@ export default class Flatbush {
return index;
}

/** Perform indexing of the added rectangles. */
finish() {
if (this._pos >> 2 !== this.numItems) {
throw new Error(`Added ${this._pos >> 2} items when expected ${this.numItems}.`);
Expand Down Expand Up @@ -165,11 +193,21 @@ export default class Flatbush {
}
}

/**
* Search the index by a bounding box.
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @param {(index: number) => boolean} [filterFn] An optional function for filtering the results.
* @returns {number[]} An array of indices of items intersecting or touching the given bounding box.
*/
search(minX, minY, maxX, maxY, filterFn) {
if (this._pos !== this._boxes.length) {
throw new Error('Data not yet indexed - call index.finish().');
}

/** @type number | undefined */
let nodeIndex = this._boxes.length - 4;
const queue = [];
const results = [];
Expand All @@ -179,7 +217,7 @@ export default class Flatbush {
const end = Math.min(nodeIndex + this.nodeSize * 4, upperBound(nodeIndex, this._levelBounds));

// search through child nodes
for (let pos = nodeIndex; pos < end; pos += 4) {
for (let /** @type number */ pos = nodeIndex; pos < end; pos += 4) {
// check if node bbox intersects with query bbox
if (maxX < this._boxes[pos]) continue; // maxX < nodeMinX
if (maxY < this._boxes[pos + 1]) continue; // maxY < nodeMinY
Expand All @@ -202,11 +240,21 @@ export default class Flatbush {
return results;
}

/**
* Search items in order of distance from the given point.
* @param {number} x
* @param {number} y
* @param {number} [maxResults=Infinity]
* @param {number} [maxDistance=Infinity]
* @param {(index: number) => boolean} [filterFn] An optional function for filtering the results.
* @returns {number[]} An array of indices of items found.
*/
neighbors(x, y, maxResults = Infinity, maxDistance = Infinity, filterFn) {
if (this._pos !== this._boxes.length) {
throw new Error('Data not yet indexed - call index.finish().');
}

/** @type number | undefined */
let nodeIndex = this._boxes.length - 4;
const q = this._queue;
const results = [];
Expand All @@ -233,12 +281,15 @@ export default class Flatbush {
}

// pop items from the queue
// @ts-expect-error q.length check eliminates undefined values
while (q.length && (q.peek() & 1)) {
const dist = q.peekValue();
// @ts-expect-error
if (dist > maxDistSquared) {
q.clear();
return results;
}
// @ts-expect-error
results.push(q.pop() >> 1);

if (results.length === maxResults) {
Expand All @@ -247,6 +298,7 @@ export default class Flatbush {
}
}

// @ts-expect-error
nodeIndex = q.length ? q.pop() >> 1 : undefined;
}

Expand All @@ -255,11 +307,21 @@ export default class Flatbush {
}
}

/**
* 1D distance from a value to a range.
* @param {number} k
* @param {number} min
* @param {number} max
*/
function axisDist(k, min, max) {
return k < min ? min - k : k <= max ? 0 : k - max;
}

// binary search for the first value in the array bigger than the given
/**
* Binary search for the first value in the array bigger than the given.
* @param {number} value
* @param {number[]} arr
*/
function upperBound(value, arr) {
let i = 0;
let j = arr.length - 1;
Expand All @@ -274,7 +336,15 @@ function upperBound(value, arr) {
return arr[i];
}

// custom quicksort that partially sorts bbox data alongside the hilbert values
/**
* Custom quicksort that partially sorts bbox data alongside the hilbert values.
* @param {Uint32Array} values
* @param {InstanceType<TypedArrayConstructor>} boxes
* @param {Uint16Array | Uint32Array} indices
* @param {number} left
* @param {number} right
* @param {number} nodeSize
*/
function sort(values, boxes, indices, left, right, nodeSize) {
if (Math.floor(left / nodeSize) >= Math.floor(right / nodeSize)) return;

Expand All @@ -293,7 +363,14 @@ function sort(values, boxes, indices, left, right, nodeSize) {
sort(values, boxes, indices, j + 1, right, nodeSize);
}

// swap two values and two corresponding boxes
/**
* Swap two values and two corresponding boxes.
* @param {Uint32Array} values
* @param {InstanceType<TypedArrayConstructor>} boxes
* @param {Uint16Array | Uint32Array} indices
* @param {number} i
* @param {number} j
*/
function swap(values, boxes, indices, i, j) {
const temp = values[i];
values[i] = values[j];
Expand All @@ -320,8 +397,12 @@ function swap(values, boxes, indices, i, j) {
indices[j] = e;
}

// Fast Hilbert curve algorithm by http://threadlocalmutex.com/
// Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain)
/**
* Fast Hilbert curve algorithm by http://threadlocalmutex.com/
* Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain)
* @param {number} x
* @param {number} y
*/
function hilbert(x, y) {
let a = x ^ y;
let b = 0xFFFF ^ a;
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
"module": "index.js",
"exports": "./index.js",
"sideEffects": false,
"types": "index.d.ts",
"scripts": {
"pretest": "eslint index.js test.js bench.js",
"test": "node --test --test-reporter spec test.js",
"test": "tsc && node test.js",
"build": "rollup index.js -o flatbush.js -n Flatbush -f umd -p node-resolve",
"prepublishOnly": "npm run build"
},
"files": [
"index.js",
"index.d.ts",
"flatbush.js"
],
"repository": {
Expand Down Expand Up @@ -46,6 +48,7 @@
"eslint-config-mourner": "^3.0.0",
"rbush": "^3.0.1",
"rbush-knn": "^3.0.1",
"rollup": "^3.20.2"
"rollup": "^3.20.2",
"typescript": "^5.0.4"
}
}
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"emitDeclarationOnly": true,
"declaration": true,
"target": "es2017",
"moduleResolution": "nodenext"
},
"files": [
"index.js"
]
}
Loading