namespace QuadtreeCircle{
export class Point {
x = 0;
y = 0;
userData = null;
constructor(x, y, data) {
this.x = x;
this.y = y;
this.userData = data;
}
}
export class Rectangle {
x = 0;
y = 0;
w = 0;
h = 0;
constructor(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
get left() {
return this.x - this.w / 2;
}
get right() {
return this.x + this.w / 2;
}
get top() {
return this.y - this.h / 2;
}
get bottom() {
return this.y + this.h / 2;
}
contains(point) {
return (point.x >= this.x - this.w &&
point.x <= this.x + this.w &&
point.y >= this.y - this.h &&
point.y <= this.y + this.h);
}
intersects(range) {
return !(range.x - range.w > this.x + this.w ||
range.x + range.w < this.x - this.w ||
range.y - range.h > this.y + this.h ||
range.y + range.h < this.y - this.h);
}
}
// circle class for a circle shaped query
export class Circle {
x = 0;
y = 0;
r = 0;
rSquared = 0;
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
this.rSquared = this.r * this.r;
}
contains(point) {
// check if the point is in the circle by checking if the euclidean distance of
// the point and the center of the circle if smaller or equal to the radius of
// the circle
let d = Math.pow((point.x - this.x), 2) + Math.pow((point.y - this.y), 2);
return d <= this.rSquared;
}
intersects(range) {
let xDist = Math.abs(range.x - this.x);
let yDist = Math.abs(range.y - this.y);
// radius of the circle
let r = this.r;
let w = range.w;
let h = range.h;
let edges = Math.pow((xDist - w), 2) + Math.pow((yDist - h), 2);
// no intersection
if (xDist > (r + w) || yDist > (r + h))
return false;
// intersection within the circle
if (xDist <= w || yDist <= h)
return true;
// intersection on the edge of the circle
return edges <= this.rSquared;
}
}
export class QuadTree {
boundary : Rectangle = null;
capacity = 1;
points = [];
divided = false;
width = 0;
northeast : QuadTree = null;
northwest : QuadTree = null;
southeast : QuadTree = null;
southwest : QuadTree = null;
constructor(boundary, capacity) {
if (!boundary) {
throw TypeError('boundary is null or undefined');
}
if (!(boundary instanceof Rectangle)) {
throw TypeError('boundary should be a Rectangle');
}
if (typeof capacity !== 'number') {
throw TypeError(`capacity should be a number but is a ${typeof capacity}`);
}
if (capacity < 1) {
throw RangeError('capacity must be greater than 0');
}
this.boundary = boundary;
this.capacity = capacity;
this.points = [];
this.divided = false;
}
subdivide() {
let x = this.boundary.x;
let y = this.boundary.y;
let w = this.boundary.w / 2;
let h = this.boundary.h / 2;
let ne = new Rectangle(x + w, y - h, w, h);
this.northeast = new QuadTree(ne, this.capacity);
let nw = new Rectangle(x - w, y - h, w, h);
this.northwest = new QuadTree(nw, this.capacity);
let se = new Rectangle(x + w, y + h, w, h);
this.southeast = new QuadTree(se, this.capacity);
let sw = new Rectangle(x - w, y + h, w, h);
this.southwest = new QuadTree(sw, this.capacity);
this.divided = true;
}
insert(point) {
if (!this.boundary.contains(point)) {
return false;
}
if (this.points.length < this.capacity) {
this.points.push(point);
return true;
}
if (!this.divided) {
this.subdivide();
}
return (this.northeast.insert(point) || this.northwest.insert(point) ||
this.southeast.insert(point) || this.southwest.insert(point));
}
query(range : Circle, found = []) {
if (!range.intersects(this.boundary)) {
return found;
}
for (let p of this.points) {
if (range.contains(p)) {
found.push(p);
}
}
if (this.divided) {
this.northwest.query(range, found);
this.northeast.query(range, found);
this.southwest.query(range, found);
this.southeast.query(range, found);
}
return found;
}
closest(point, count, maxDistance) {
if (typeof point === "undefined") {
throw TypeError("Method 'closest' needs a point");
}
if (typeof count === "undefined") {
count = 1;
}
// Limit to number of points in this QuadTree
if (this.length == 0) {
return [];
}
if (this.length < count) {
return this.points;
}
if (typeof maxDistance === "undefined") {
// A circle that contains the entire QuadTree
const outerReach = Math.sqrt(
Math.pow(this.boundary.w, 2) + Math.pow(this.boundary.h, 2)
);
// Distance of query point from center
const pointDistance = Math.sqrt(
Math.pow(point.x, 2) + Math.pow(point.y, 2)
);
// One QuadTree size away from the query point
maxDistance = outerReach + pointDistance;
}
// Binary search with Circle queries
let inner = 0;
let outer = maxDistance;
let limit = 8; // Limit to avoid infinite loops caused by ties
let points;
while (limit > 0) {
const radius = (inner + outer) / 2;
const range = new Circle(point.x, point.y, radius);
points = this.query(range);
if (points.length === count) {
return points; // Return when we hit the right size
} else if (points.length < count) {
inner = radius;
} else {
outer = radius;
limit --;
}
}
// Sort by squared distance
points.sort(
(a, b) => {
const aDist = Math.pow(point.x - a.x, 2) +
Math.pow(point.y - a.y, 2);
const bDist = Math.pow(point.x - b.x, 2) +
Math.pow(point.y - b.y, 2);
return aDist - bDist;
}
);
// Slice to return correct count (breaks ties)
return points.slice(0, count);
}
forEach(fn) {
this.points.forEach(fn);
if (this.divided) {
this.northeast.forEach(fn);
this.northwest.forEach(fn);
this.southeast.forEach(fn);
this.southwest.forEach(fn);
}
}
merge(other, capacity) {
let left = Math.min(this.boundary.left, other.boundary.left);
let right = Math.max(this.boundary.right, other.boundary.right);
let top = Math.min(this.boundary.top, other.boundary.top);
let bottom = Math.max(this.boundary.bottom, other.boundary.bottom);
let height = bottom - top;
let width = right - left;
let midX = left + width / 2;
let midY = top + height / 2;
let boundary = new Rectangle(midX, midY, width, height);
let result = new QuadTree(boundary, capacity);
this.forEach(point => result.insert(point));
other.forEach(point => result.insert(point));
return result;
}
get length() {
let count = this.points.length;
if (this.divided) {
count += this.northwest.length;
count += this.northeast.length;
count += this.southwest.length;
count += this.southeast.length;
}
return count;
}
}
}