Skip to content
Draft
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
288 changes: 288 additions & 0 deletions src/data-structures/QuadTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
export interface AbstractQuad {
left: number;
top: number;
right: number;
bottom: number;
}

export class Quad implements AbstractQuad {
constructor(left: number, top: number, right?: number, bottom?: number) {
const l = left;
const t = top;
const r = right ?? left; // zero width for points
const b = bottom ?? top; // zero height for points

this.left = Math.min(l, r);
this.top = Math.min(t, b);
this.right = Math.max(l, r);
this.bottom = Math.max(t, b);
}

intersects(op: Quad): Boolean {
return (
this.left <= op.right && // not too far left
this.top <= op.bottom && // not too far up
op.left <= this.right && // not too far right
op.top <= this.bottom // not too far down
);
}

distance(op: Quad) {
if (this.intersects(op)) {
return 0;
}

// Horizontal (closest edges)
const closestX = Math.min(
Math.abs(this.left - op.right),
Math.abs(this.right - op.left),
);
if (
(this.top >= op.top && this.top <= op.bottom) ||
(op.top >= this.top && op.top <= this.bottom)
) {
return closestX;
}

// Vertical (closest edges)
const closestY = Math.min(
Math.abs(this.top - op.bottom),
Math.abs(this.bottom - op.top),
);
if (
(this.left >= op.left && this.left <= op.right) ||
(op.left >= this.left && op.left <= this.right)
) {
return closestY;
}

// Diagonal (closest corner)
return Math.sqrt(closestX ** 2 + closestY ** 2);
}
}

export class QuadNode<T> extends Quad {
value: T | undefined;

constructor(
value: T,
left: number,
top: number,
right?: number,
bottom?: number,
) {
super(left, top, right, bottom);

this.value = value ?? undefined;
}
}

class QuadTree<T> {
bounds: Quad;
values: Set<QuadNode<T>>;
subdivisions: Array<QuadTree<T>> | undefined = undefined;

SIZE_LIMIT = 8;
DEPTH_LIMIT = 8;
depth = 0;

/**
* Create a QuadTree that covers an spatial area.
* @param {number} left The x minimum.
* @param {number} top The y minimum.
* @param {number} right The x maximum.
* @param {number} bottom The y maximum.
*/
constructor(left = -1000, top = -1000, right = 1000, bottom = 1000) {
this.bounds = new Quad(left, top, right, bottom);
this.values = new Set<QuadNode<T>>();
}

setDepth(depth: number) {
this.depth = depth;
}

setSizeLimit(size: number) {
this.SIZE_LIMIT = size;
}

setDepthLimit(depth: number) {
this.DEPTH_LIMIT = depth;
}

/**
* Divide QuadTree into four equal quads.
* Move all values to subdivisions for performance.
*/
protected _subdivide() {
const centerX = (this.bounds.left + this.bounds.right) / 2;
const centerY = (this.bounds.top + this.bounds.bottom) / 2;

this.subdivisions = [
new QuadTree(this.bounds.left, this.bounds.top, centerX, centerY), // nw
new QuadTree(centerX, this.bounds.top, this.bounds.right, centerY), // ne
new QuadTree(centerX, centerY, this.bounds.right, this.bounds.bottom), // se
new QuadTree(this.bounds.left, centerY, centerX, this.bounds.bottom), // sw
];

// Copy configurations
this.subdivisions.forEach((quad) => {
quad.setDepth(this.depth + 1);
quad.setSizeLimit(this.SIZE_LIMIT);
quad.setDepthLimit(this.DEPTH_LIMIT);
});

// Move values
this.values.forEach((node) => this.insert(node));
this.values.clear();
}

/**
* Insert value with positional data.
* @param {QuadTree<T> | T} nodeOrValue
* @param {number?} left
* @param {number?} top
* @param {number?} right
* @param {number?} bottom
* @return Boolean
*/
insert(
nodeOrValue: QuadNode<T> | T,
left?: number,
top?: number,
right?: number,
bottom?: number,
): Boolean {
if (nodeOrValue instanceof QuadNode) {
// QuadNode
return this._insertImpl(nodeOrValue as QuadNode);
}

if (left !== undefined && top !== undefined) {
// convenience QuadNode creation
return this._insertImpl(
new QuadNode(nodeOrValue, left, top, right, bottom)
);
}

throw Error('value needs positional data to insert');
}

/**
* Insert value with positional data.
* @param {QuadTree<T>} node
* @return Boolean
*/
protected _insertImpl(node: QuadNode<T>): Boolean {
if (!this.bounds.intersects(node)) {
return false;
}

if (this.subdivisions) {
return this.subdivisions.reduce(
(added: Boolean, quad) => added || quad.insert(node),
false,
);
}

this.values.add(node);

if (
!this.subdivisions &&
this.depth < this.DEPTH_LIMIT &&
this.values.size === this.SIZE_LIMIT
) {
this._subdivide();
}

return true;
}

query(
quadOrLeft: Quad | number,
top?: number,
right?: number,
bottom?: number,
): Array<QuadNode<T>> {
if (quadOrLeft instanceof Quad) {
return this._queryImpl(quadOrLeft);
}

return this._queryImpl(new Quad(quadOrLeft, top, right, bottom));
}

protected _queryImpl(range: Quad) {
if (!this.bounds.intersects(range)) {
return [];
}

if (this.subdivisions) {
return this.subdivisions.reduce(
(ret, quad) => [...ret, ...quad.query(range)],
[] as Array<QuadNode<T>>,
);
}

return Array.from(this.values).filter((value) => value.intersects(range));
}

closest(pointOrX: Quad | number, yOrCount: number, count?: number): QuadNode | Array<QuadNode> | null {
const point = pointOrX instanceof Quad ? pointOrX : new Quad(pointOrX, yOrCount);
const limit = (pointOrX instanceof Quad ? yOrCount : count) ?? 1;

let closest = [];
let closestDist = Infinity;

function insertInOrder(value, dist) {
const item = { ...value, dist };

if (closest.length < limit) {
closest.push(item);
closest.sort((a, b) => a.dist - b.dist);

closestDist = closest[closest.length - 1].dist;
return;
}

for (let i = 0; i < limit; i++) {
if (dist < closest[i].dist) {
closest.splice(i, 0, item);
break;
}
}

closestDist = closest[closest.length - 1].dist;
}

function _closestImpl(node) {
if (node.subdivisions) {
node.subdivisions.forEach((sub) => {
if (point.distance(sub.bounds) < closestDist) {
_closestImpl(sub);
}
});

return;
}

node.values.forEach((val) => {
const dist = point.distance(val);
if (dist < closestDist) {
insertInOrder(val, dist);
}
});
}

_closestImpl(this);

// If no count specified, return just an object
const returnOne = (pointOrX instanceof Quad ? yOrCount : count) === undefined;
if (returnOne) {
return closest[0] ?? null;
}

return closest.slice(0, limit);
}
}

export default QuadTree;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import List from './data-structures/List';
import NDArray from './data-structures/NDArray';
import Node from './data-structures/Node';
import PriorityQueue from './data-structures/PriorityQueue';
import QuadTree from './data-structures/QuadTree';
import Queue from './data-structures/Queue';
import Stack from './data-structures/Stack';
import Trie from './data-structures/Trie';
Expand Down Expand Up @@ -73,6 +74,7 @@ export {
NDArray,
Node,
PriorityQueue,
QuadTree,
Queue,
Stack,
Trie,
Expand Down
61 changes: 61 additions & 0 deletions test/data-structures/QuadTree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { QuadNode, QuadTree } from '../../src';

// #todo QuadTree.test.ts

describe('Quad', () => {
describe('normal', () => {});
describe('swap bounds', () => {});
describe('missing info', () => {});
});

describe('QuadNode', () => {
describe('normal', () => {});
describe('swap bounds', () => {});
describe('missing info', () => {});
});

describe('QuadTree', () => {
describe('add', () => {
test('add in bounds', () => {
const qtree = new QuadTree(0, 0, 100, 100);
expect(qtree.add(new QuadNode('point', 10, 10))).toBe(true);
expect(qtree.add(new QuadNode('corner point', 0, 0))).toBe(true);
expect(qtree.add(new QuadNode('far corner point', 100, 100))).toBe(true);
expect(qtree.add(new QuadNode('inside quad', 50, 50, 70, 70))).toBe(true);
expect(qtree.add(new QuadNode('overflow quad', -50, -50, 10, 10))).toBe(
true,
);
expect(qtree.add(new QuadNode('through quad', 20, -50, 40, 200))).toBe(
true,
);
});

test('reject out of bounds', () => {
const qtree = new QuadTree(20, 20, 40, 40);
expect(qtree.add(new QuadNode('point', -10, -10))).toBe(false);
expect(qtree.add(new QuadNode('quad past left', -50, -50, -10, 20))).toBe(
false,
);
expect(qtree.add(new QuadNode('quad past top', -50, -50, 20, -10))).toBe(
false,
);
expect(
qtree.add(new QuadNode('quad past right', 110, 10, 200, 200)),
).toBe(false);
expect(
qtree.add(new QuadNode('quad past bottom', 10, 110, 200, 200)),
).toBe(false);
});
});

describe('get', () => {});

describe('division', () => {
describe('split at size limit', () => {});
describe('move values to subdivisions', () => {});
});

describe('size limits', () => {});

describe('depth limits', () => {});
});