From 01b665312476bfda1b82cc24b6ed2f53000aedca Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Fri, 4 Nov 2022 17:43:06 -0400 Subject: [PATCH 1/2] feat: begin QuadTree implementation. --- src/data-structures/QuadTree.ts | 184 ++++++++++++++++++++++++++ src/index.ts | 2 + test/data-structures/QuadTree.test.ts | 61 +++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/data-structures/QuadTree.ts create mode 100644 test/data-structures/QuadTree.test.ts diff --git a/src/data-structures/QuadTree.ts b/src/data-structures/QuadTree.ts new file mode 100644 index 0000000..1764dec --- /dev/null +++ b/src/data-structures/QuadTree.ts @@ -0,0 +1,184 @@ +export interface AbstractQuad { + left: number; + top: number; + right: number; + bottom: number; +} + +export class Quad implements AbstractQuad { + public left: number; + public top: number; + public right: number; + public bottom: number; + + 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 !( + ( + op.right < this.left || // too far left + op.bottom < this.top || // too far up + this.right < op.right || // too far right + this.bottom < op.top + ) // too far down + ); + } +} + +export class QuadNode 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 { + public bounds: Quad; + public values: Set>; + public subdivisions: Array> | undefined = undefined; + + public SIZE_LIMIT = 8; + public DEPTH_LIMIT = 8; + public 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>(); + } + + public setDepth(depth: number) { + this.depth = depth; + } + + public setSizeLimit(size: number) { + this.SIZE_LIMIT = size; + } + + public 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.add(node)); + } + + /** + * Insert value with positional data. + * @param {QuadTree} node + * @return Boolean + */ + protected _insertImpl(node: QuadNode): Boolean { + if (!this.bounds.intersects(node)) { + return false; + } + + if ( + !this.subdivisions && + this.depth < this.DEPTH_LIMIT && + this.values.size === this.SIZE_LIMIT + ) { + this._subdivide(); + } + + if (this.subdivisions) { + return this.subdivisions.reduce( + (added: Boolean, quad) => added || quad.add(node), + false, + ); + } + + this.values.add(node); + + return true; + } + + /** + * Insert value with positional data. + * @param {QuadTree} node + * @return Boolean + */ + public insert(node: QuadNode): Boolean { + return this._insertImpl(node); + } + + /** + * #todo fix this overload + * + * public insert(node: QuadNode): Boolean; + * public insert(value: T, left: number, top: number, right?: number, bottom?: number): Boolean; + * public insert(nodeOrValue: QuadNode|T, left?: number, top?: number, right?: number, bottom?: number): Boolean { + * if (left !== undefined && top !== undefined) { + * // convenience QuadNode creation + * return this._insertImpl(new QuadNode(nodeOrValue, left, right, top, bottom)); + * } + + * // QuadNode + * return this._insertImpl(nodeOrValue); + * } + */ + + public get(range: Quad): Array> { + if (!this.bounds.intersects(range)) { + return []; + } + + if (this.subdivisions) { + return this.subdivisions.reduce( + (ret, quad) => [...ret, ...quad.get(range)], + [] as Array>, + ); + } + + return Array.from(this.values).filter(value => value.intersects(range)); + } +} + +export default QuadTree; diff --git a/src/index.ts b/src/index.ts index 071eb0e..cf05943 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -73,6 +74,7 @@ export { NDArray, Node, PriorityQueue, + QuadTree, Queue, Stack, Trie, diff --git a/test/data-structures/QuadTree.test.ts b/test/data-structures/QuadTree.test.ts new file mode 100644 index 0000000..abee19b --- /dev/null +++ b/test/data-structures/QuadTree.test.ts @@ -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', () => {}); +}); From 954f39cd10c4cec833d38e8b79f2ee3d964e1aa2 Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 8 Feb 2023 13:18:31 -0500 Subject: [PATCH 2/2] feat: add closest. fix query. --- src/data-structures/QuadTree.ts | 220 +++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 58 deletions(-) diff --git a/src/data-structures/QuadTree.ts b/src/data-structures/QuadTree.ts index 1764dec..a7c1267 100644 --- a/src/data-structures/QuadTree.ts +++ b/src/data-structures/QuadTree.ts @@ -6,11 +6,6 @@ export interface AbstractQuad { } export class Quad implements AbstractQuad { - public left: number; - public top: number; - public right: number; - public bottom: number; - constructor(left: number, top: number, right?: number, bottom?: number) { const l = left; const t = top; @@ -24,15 +19,46 @@ export class Quad implements AbstractQuad { } intersects(op: Quad): Boolean { - return !( - ( - op.right < this.left || // too far left - op.bottom < this.top || // too far up - this.right < op.right || // too far right - this.bottom < op.top - ) // too far down + 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 extends Quad { @@ -52,13 +78,13 @@ export class QuadNode extends Quad { } class QuadTree { - public bounds: Quad; - public values: Set>; - public subdivisions: Array> | undefined = undefined; + bounds: Quad; + values: Set>; + subdivisions: Array> | undefined = undefined; - public SIZE_LIMIT = 8; - public DEPTH_LIMIT = 8; - public depth = 0; + SIZE_LIMIT = 8; + DEPTH_LIMIT = 8; + depth = 0; /** * Create a QuadTree that covers an spatial area. @@ -72,15 +98,15 @@ class QuadTree { this.values = new Set>(); } - public setDepth(depth: number) { + setDepth(depth: number) { this.depth = depth; } - public setSizeLimit(size: number) { + setSizeLimit(size: number) { this.SIZE_LIMIT = size; } - public setDepthLimit(depth: number) { + setDepthLimit(depth: number) { this.DEPTH_LIMIT = depth; } @@ -100,14 +126,46 @@ class QuadTree { ]; // Copy configurations - this.subdivisions.forEach(quad => { + 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.add(node)); + this.values.forEach((node) => this.insert(node)); + this.values.clear(); + } + + /** + * Insert value with positional data. + * @param {QuadTree | T} nodeOrValue + * @param {number?} left + * @param {number?} top + * @param {number?} right + * @param {number?} bottom + * @return Boolean + */ + insert( + nodeOrValue: QuadNode | 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'); } /** @@ -120,64 +178,110 @@ class QuadTree { return false; } - if ( - !this.subdivisions && - this.depth < this.DEPTH_LIMIT && - this.values.size === this.SIZE_LIMIT - ) { - this._subdivide(); - } - if (this.subdivisions) { return this.subdivisions.reduce( - (added: Boolean, quad) => added || quad.add(node), + (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; } - /** - * Insert value with positional data. - * @param {QuadTree} node - * @return Boolean - */ - public insert(node: QuadNode): Boolean { - return this._insertImpl(node); - } + query( + quadOrLeft: Quad | number, + top?: number, + right?: number, + bottom?: number, + ): Array> { + if (quadOrLeft instanceof Quad) { + return this._queryImpl(quadOrLeft); + } - /** - * #todo fix this overload - * - * public insert(node: QuadNode): Boolean; - * public insert(value: T, left: number, top: number, right?: number, bottom?: number): Boolean; - * public insert(nodeOrValue: QuadNode|T, left?: number, top?: number, right?: number, bottom?: number): Boolean { - * if (left !== undefined && top !== undefined) { - * // convenience QuadNode creation - * return this._insertImpl(new QuadNode(nodeOrValue, left, right, top, bottom)); - * } - - * // QuadNode - * return this._insertImpl(nodeOrValue); - * } - */ + return this._queryImpl(new Quad(quadOrLeft, top, right, bottom)); + } - public get(range: Quad): Array> { + protected _queryImpl(range: Quad) { if (!this.bounds.intersects(range)) { return []; } if (this.subdivisions) { return this.subdivisions.reduce( - (ret, quad) => [...ret, ...quad.get(range)], + (ret, quad) => [...ret, ...quad.query(range)], [] as Array>, ); } - return Array.from(this.values).filter(value => value.intersects(range)); + return Array.from(this.values).filter((value) => value.intersects(range)); + } + + closest(pointOrX: Quad | number, yOrCount: number, count?: number): QuadNode | Array | 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); } }