Skip to content

Commit

Permalink
Add JSDoc-based TypeScript checks + first-class types (#47)
Browse files Browse the repository at this point in the history
* add JSDoc-based TypeScript checks + first-class types

* minot updates

* case conventions

* minor updates

* fixes
  • Loading branch information
mourner committed Apr 21, 2023
1 parent f3d24b3 commit eaca470
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 17 deletions.
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"
]
}

0 comments on commit eaca470

Please sign in to comment.