Skip to content
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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:8.9
- image: circleci/node:lts

# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
Expand Down
204 changes: 123 additions & 81 deletions quadtree.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ class Circle {
}

class QuadTree {
constructor(boundary, capacity) {
DEFAULT_CAPACITY = 8;
MAX_DEPTH = 8;

constructor(boundary, capacity = this.DEFAULT_CAPACITY, _depth = 0) {
if (!boundary) {
throw TypeError('boundary is null or undefined');
}
Expand All @@ -157,10 +160,13 @@ class QuadTree {
if (capacity < 1) {
throw RangeError('capacity must be greater than 0');
}

this.boundary = boundary;
this.capacity = capacity;
this.points = [];
this.divided = false;

this.depth = _depth;
}

get children() {
Expand All @@ -177,7 +183,6 @@ class QuadTree {
}

static create() {
let DEFAULT_CAPACITY = 8;
if (arguments.length === 0) {
if (typeof width === "undefined") {
throw new TypeError("No global width defined");
Expand All @@ -186,119 +191,148 @@ class QuadTree {
throw new TypeError("No global height defined");
}
let bounds = new Rectangle(width / 2, height / 2, width, height);
return new QuadTree(bounds, DEFAULT_CAPACITY);
return new QuadTree(bounds, this.DEFAULT_CAPACITY);
}
if (arguments[0] instanceof Rectangle) {
let capacity = arguments[1] || DEFAULT_CAPACITY;
let capacity = arguments[1] || this.DEFAULT_CAPACITY;
return new QuadTree(arguments[0], capacity);
}
if (typeof arguments[0] === "number" &&
typeof arguments[1] === "number" &&
typeof arguments[2] === "number" &&
typeof arguments[3] === "number") {
let capacity = arguments[4] || DEFAULT_CAPACITY;
let capacity = arguments[4] || this.DEFAULT_CAPACITY;
return new QuadTree(new Rectangle(arguments[0], arguments[1], arguments[2], arguments[3]), capacity);
}
throw new TypeError('Invalid parameters');
}

toJSON(isChild) {
let obj = { points: this.points };
toJSON() {
let obj = {};

if (this.divided) {
if (this.northeast.points.length > 0) {
obj.ne = this.northeast.toJSON(true);
if (this.northeast.divided || this.northeast.points.length > 0) {
obj.ne = this.northeast.toJSON();
}
if (this.northwest.points.length > 0) {
obj.nw = this.northwest.toJSON(true);
if (this.northwest.divided || this.northwest.points.length > 0) {
obj.nw = this.northwest.toJSON();
}
if (this.southeast.points.length > 0) {
obj.se = this.southeast.toJSON(true);
if (this.southeast.divided || this.southeast.points.length > 0) {
obj.se = this.southeast.toJSON();
}
if (this.southwest.points.length > 0) {
obj.sw = this.southwest.toJSON(true);
if (this.southwest.divided || this.southwest.points.length > 0) {
obj.sw = this.southwest.toJSON();
}
} else {
obj.points = this.points;
}
if (!isChild) {

if (this.depth === 0) {
obj.capacity = this.capacity;
obj.x = this.boundary.x;
obj.y = this.boundary.y;
obj.w = this.boundary.w;
obj.h = this.boundary.h;
}

return obj;
}

static fromJSON(obj, x, y, w, h, capacity) {
static fromJSON(obj, x, y, w, h, capacity, depth) {
if (typeof x === "undefined") {
if ("x" in obj) {
x = obj.x;
y = obj.y;
w = obj.w;
h = obj.h;
capacity = obj.capacity;
depth = 0;
} else {
throw TypeError("JSON missing boundary information");
}
}
let qt = new QuadTree(new Rectangle(x, y, w, h), capacity);
qt.points = obj.points;

let qt = new QuadTree(new Rectangle(x, y, w, h), capacity, depth);

qt.points = obj.points ?? null;
qt.divided = qt.points === null; // points are set to null on subdivide

if (
"ne" in obj ||
"nw" in obj ||
"se" in obj ||
"sw" in obj
) {
let x = qt.boundary.x;
let y = qt.boundary.y;
let w = qt.boundary.w / 2;
let h = qt.boundary.h / 2;
const x = qt.boundary.x;
const y = qt.boundary.y;
const w = qt.boundary.w / 2;
const h = qt.boundary.h / 2;

if ("ne" in obj) {
qt.northeast = QuadTree.fromJSON(obj.ne, x + w/2, y - h/2, w, h, capacity);
qt.northeast = QuadTree.fromJSON(obj.ne, x + w/2, y - h/2, w, h, capacity, depth + 1);
} else {
qt.northeast = new QuadTree(qt.boundary.subdivide('ne'), capacity);
qt.northeast = new QuadTree(qt.boundary.subdivide('ne'), capacity, depth + 1);
}
if ("nw" in obj) {
qt.northwest = QuadTree.fromJSON(obj.nw, x - w/2, y - h/2, w, h, capacity);
qt.northwest = QuadTree.fromJSON(obj.nw, x - w/2, y - h/2, w, h, capacity, depth + 1);
} else {
qt.northwest = new QuadTree(qt.boundary.subdivide('nw'), capacity);
qt.northwest = new QuadTree(qt.boundary.subdivide('nw'), capacity, depth + 1);
}
if ("se" in obj) {
qt.southeast = QuadTree.fromJSON(obj.se, x + w/2, y + h/2, w, h, capacity);
qt.southeast = QuadTree.fromJSON(obj.se, x + w/2, y + h/2, w, h, capacity, depth + 1);
} else {
qt.southeast = new QuadTree(qt.boundary.subdivide('se'), capacity);
qt.southeast = new QuadTree(qt.boundary.subdivide('se'), capacity, depth + 1);
}
if ("sw" in obj) {
qt.southwest = QuadTree.fromJSON(obj.sw, x - w/2, y + h/2, w, h, capacity);
qt.southwest = QuadTree.fromJSON(obj.sw, x - w/2, y + h/2, w, h, capacity, depth + 1);
} else {
qt.southwest = new QuadTree(qt.boundary.subdivide('sw'), capacity);
qt.southwest = new QuadTree(qt.boundary.subdivide('sw'), capacity, depth + 1);
}

qt.divided = true;
}

return qt;
}

subdivide() {
this.northeast = new QuadTree(this.boundary.subdivide('ne'), this.capacity);
this.northwest = new QuadTree(this.boundary.subdivide('nw'), this.capacity);
this.southeast = new QuadTree(this.boundary.subdivide('se'), this.capacity);
this.southwest = new QuadTree(this.boundary.subdivide('sw'), this.capacity);
this.northeast = new QuadTree(this.boundary.subdivide('ne'), this.capacity, this.depth + 1);
this.northwest = new QuadTree(this.boundary.subdivide('nw'), this.capacity, this.depth + 1);
this.southeast = new QuadTree(this.boundary.subdivide('se'), this.capacity, this.depth + 1);
this.southwest = new QuadTree(this.boundary.subdivide('sw'), this.capacity, this.depth + 1);

this.divided = true;

// Move points to children.
// This improves performance by placing points
// in the smallest available rectangle.
for (const p of this.points) {
const inserted =
this.northeast.insert(p) ||
this.northwest.insert(p) ||
this.southeast.insert(p) ||
this.southwest.insert(p);

if (!inserted) {
throw RangeError('capacity must be greater than 0');
}
}

this.points = null;
}

insert(point) {
if (!this.boundary.contains(point)) {
return false;
}

if (this.points.length < this.capacity) {
this.points.push(point);
return true;
}

if (!this.divided) {
if (
this.points.length < this.capacity ||
this.depth === this.MAX_DEPTH
) {
this.points.push(point);
return true;
}

this.subdivide();
}

Expand All @@ -319,16 +353,18 @@ class QuadTree {
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;
}

for (const p of this.points) {
if (range.contains(p)) {
found.push(p);
}
}

return found;
Expand All @@ -346,46 +382,50 @@ class QuadTree {
kNearest(searchPoint, maxCount, sqMaxDistance, furthestSqDistance, foundSoFar) {
let found = [];

this.children.sort((a, b) => a.boundary.sqDistanceFrom(searchPoint) - b.boundary.sqDistanceFrom(searchPoint))
.forEach((child) => {
const sqDist = child.boundary.sqDistanceFrom(searchPoint);
if (sqDist > sqMaxDistance) {
return;
} else if (foundSoFar < maxCount || sqDist < furthestSqDistance) {
const result = child.kNearest(searchPoint, maxCount, sqMaxDistance, furthestSqDistance, foundSoFar);
const childPoints = result.found;
found = found.concat(childPoints);
foundSoFar += childPoints.length;
furthestSqDistance = result.furthestSqDistance;
}
});

this.points
.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint))
.forEach((p) => {
const sqDist = p.sqDistanceFrom(searchPoint);
if (sqDist > sqMaxDistance) {
return;
} else if (foundSoFar < maxCount || sqDist < furthestSqDistance) {
found.push(p);
furthestSqDistance = Math.max(sqDist, furthestSqDistance);
foundSoFar++;
}
});
if (this.divided) {
this.children
.sort((a, b) => a.boundary.sqDistanceFrom(searchPoint) - b.boundary.sqDistanceFrom(searchPoint))
.forEach((child) => {
const sqDistance = child.boundary.sqDistanceFrom(searchPoint);
if (sqDistance > sqMaxDistance) {
return;
} else if (foundSoFar < maxCount || sqDistance < furthestSqDistance) {
const result = child.kNearest(searchPoint, maxCount, sqMaxDistance, furthestSqDistance, foundSoFar);
const childPoints = result.found;
found = found.concat(childPoints);
foundSoFar += childPoints.length;
furthestSqDistance = result.furthestSqDistance;
}
});
} else {
this.points
.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint))
.forEach((p) => {
const sqDistance = p.sqDistanceFrom(searchPoint);
if (sqDistance > sqMaxDistance) {
return;
} else if (foundSoFar < maxCount || sqDistance < furthestSqDistance) {
found.push(p);
furthestSqDistance = Math.max(sqDistance, furthestSqDistance);
foundSoFar++;
}
});
}

return {
found: found.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint)).slice(0, maxCount),
furthestDistance: Math.sqrt(furthestSqDistance),
furthestSqDistance: Math.sqrt(furthestSqDistance),
};
}

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);
} else {
this.points.forEach(fn);
}
}

Expand All @@ -411,14 +451,16 @@ class QuadTree {
}

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 (
this.northwest.length +
this.northeast.length +
this.southwest.length +
this.southeast.length
);
}
return count;

return this.points.length;
}
}

Expand Down
Loading