Skip to content

Commit 3d028d5

Browse files
crhallbergChris Hallberg
andauthored
Move Points to Children (#61)
* Move points to children, set to null. Adjust tests. * Bump node version to LTS. * Remove maxDepth from constructor to simplify code. Consolidate with DEFAULT_CAPACITY. Co-authored-by: Chris Hallberg <crhallberg@gmail.com>
1 parent c083a49 commit 3d028d5

File tree

2 files changed

+138
-95
lines changed

2 files changed

+138
-95
lines changed

quadtree.js

Lines changed: 123 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ class Circle {
144144
}
145145

146146
class QuadTree {
147-
constructor(boundary, capacity) {
147+
DEFAULT_CAPACITY = 8;
148+
MAX_DEPTH = 8;
149+
150+
constructor(boundary, capacity = this.DEFAULT_CAPACITY, _depth = 0) {
148151
if (!boundary) {
149152
throw TypeError('boundary is null or undefined');
150153
}
@@ -157,10 +160,13 @@ class QuadTree {
157160
if (capacity < 1) {
158161
throw RangeError('capacity must be greater than 0');
159162
}
163+
160164
this.boundary = boundary;
161165
this.capacity = capacity;
162166
this.points = [];
163167
this.divided = false;
168+
169+
this.depth = _depth;
164170
}
165171

166172
get children() {
@@ -177,7 +183,6 @@ class QuadTree {
177183
}
178184

179185
static create() {
180-
let DEFAULT_CAPACITY = 8;
181186
if (arguments.length === 0) {
182187
if (typeof width === "undefined") {
183188
throw new TypeError("No global width defined");
@@ -186,119 +191,148 @@ class QuadTree {
186191
throw new TypeError("No global height defined");
187192
}
188193
let bounds = new Rectangle(width / 2, height / 2, width, height);
189-
return new QuadTree(bounds, DEFAULT_CAPACITY);
194+
return new QuadTree(bounds, this.DEFAULT_CAPACITY);
190195
}
191196
if (arguments[0] instanceof Rectangle) {
192-
let capacity = arguments[1] || DEFAULT_CAPACITY;
197+
let capacity = arguments[1] || this.DEFAULT_CAPACITY;
193198
return new QuadTree(arguments[0], capacity);
194199
}
195200
if (typeof arguments[0] === "number" &&
196201
typeof arguments[1] === "number" &&
197202
typeof arguments[2] === "number" &&
198203
typeof arguments[3] === "number") {
199-
let capacity = arguments[4] || DEFAULT_CAPACITY;
204+
let capacity = arguments[4] || this.DEFAULT_CAPACITY;
200205
return new QuadTree(new Rectangle(arguments[0], arguments[1], arguments[2], arguments[3]), capacity);
201206
}
202207
throw new TypeError('Invalid parameters');
203208
}
204209

205-
toJSON(isChild) {
206-
let obj = { points: this.points };
210+
toJSON() {
211+
let obj = {};
212+
207213
if (this.divided) {
208-
if (this.northeast.points.length > 0) {
209-
obj.ne = this.northeast.toJSON(true);
214+
if (this.northeast.divided || this.northeast.points.length > 0) {
215+
obj.ne = this.northeast.toJSON();
210216
}
211-
if (this.northwest.points.length > 0) {
212-
obj.nw = this.northwest.toJSON(true);
217+
if (this.northwest.divided || this.northwest.points.length > 0) {
218+
obj.nw = this.northwest.toJSON();
213219
}
214-
if (this.southeast.points.length > 0) {
215-
obj.se = this.southeast.toJSON(true);
220+
if (this.southeast.divided || this.southeast.points.length > 0) {
221+
obj.se = this.southeast.toJSON();
216222
}
217-
if (this.southwest.points.length > 0) {
218-
obj.sw = this.southwest.toJSON(true);
223+
if (this.southwest.divided || this.southwest.points.length > 0) {
224+
obj.sw = this.southwest.toJSON();
219225
}
226+
} else {
227+
obj.points = this.points;
220228
}
221-
if (!isChild) {
229+
230+
if (this.depth === 0) {
222231
obj.capacity = this.capacity;
223232
obj.x = this.boundary.x;
224233
obj.y = this.boundary.y;
225234
obj.w = this.boundary.w;
226235
obj.h = this.boundary.h;
227236
}
237+
228238
return obj;
229239
}
230240

231-
static fromJSON(obj, x, y, w, h, capacity) {
241+
static fromJSON(obj, x, y, w, h, capacity, depth) {
232242
if (typeof x === "undefined") {
233243
if ("x" in obj) {
234244
x = obj.x;
235245
y = obj.y;
236246
w = obj.w;
237247
h = obj.h;
238248
capacity = obj.capacity;
249+
depth = 0;
239250
} else {
240251
throw TypeError("JSON missing boundary information");
241252
}
242253
}
243-
let qt = new QuadTree(new Rectangle(x, y, w, h), capacity);
244-
qt.points = obj.points;
254+
255+
let qt = new QuadTree(new Rectangle(x, y, w, h), capacity, depth);
256+
257+
qt.points = obj.points ?? null;
258+
qt.divided = qt.points === null; // points are set to null on subdivide
259+
245260
if (
246261
"ne" in obj ||
247262
"nw" in obj ||
248263
"se" in obj ||
249264
"sw" in obj
250265
) {
251-
let x = qt.boundary.x;
252-
let y = qt.boundary.y;
253-
let w = qt.boundary.w / 2;
254-
let h = qt.boundary.h / 2;
266+
const x = qt.boundary.x;
267+
const y = qt.boundary.y;
268+
const w = qt.boundary.w / 2;
269+
const h = qt.boundary.h / 2;
255270

256271
if ("ne" in obj) {
257-
qt.northeast = QuadTree.fromJSON(obj.ne, x + w/2, y - h/2, w, h, capacity);
272+
qt.northeast = QuadTree.fromJSON(obj.ne, x + w/2, y - h/2, w, h, capacity, depth + 1);
258273
} else {
259-
qt.northeast = new QuadTree(qt.boundary.subdivide('ne'), capacity);
274+
qt.northeast = new QuadTree(qt.boundary.subdivide('ne'), capacity, depth + 1);
260275
}
261276
if ("nw" in obj) {
262-
qt.northwest = QuadTree.fromJSON(obj.nw, x - w/2, y - h/2, w, h, capacity);
277+
qt.northwest = QuadTree.fromJSON(obj.nw, x - w/2, y - h/2, w, h, capacity, depth + 1);
263278
} else {
264-
qt.northwest = new QuadTree(qt.boundary.subdivide('nw'), capacity);
279+
qt.northwest = new QuadTree(qt.boundary.subdivide('nw'), capacity, depth + 1);
265280
}
266281
if ("se" in obj) {
267-
qt.southeast = QuadTree.fromJSON(obj.se, x + w/2, y + h/2, w, h, capacity);
282+
qt.southeast = QuadTree.fromJSON(obj.se, x + w/2, y + h/2, w, h, capacity, depth + 1);
268283
} else {
269-
qt.southeast = new QuadTree(qt.boundary.subdivide('se'), capacity);
284+
qt.southeast = new QuadTree(qt.boundary.subdivide('se'), capacity, depth + 1);
270285
}
271286
if ("sw" in obj) {
272-
qt.southwest = QuadTree.fromJSON(obj.sw, x - w/2, y + h/2, w, h, capacity);
287+
qt.southwest = QuadTree.fromJSON(obj.sw, x - w/2, y + h/2, w, h, capacity, depth + 1);
273288
} else {
274-
qt.southwest = new QuadTree(qt.boundary.subdivide('sw'), capacity);
289+
qt.southwest = new QuadTree(qt.boundary.subdivide('sw'), capacity, depth + 1);
275290
}
276-
277-
qt.divided = true;
278291
}
292+
279293
return qt;
280294
}
281295

282296
subdivide() {
283-
this.northeast = new QuadTree(this.boundary.subdivide('ne'), this.capacity);
284-
this.northwest = new QuadTree(this.boundary.subdivide('nw'), this.capacity);
285-
this.southeast = new QuadTree(this.boundary.subdivide('se'), this.capacity);
286-
this.southwest = new QuadTree(this.boundary.subdivide('sw'), this.capacity);
297+
this.northeast = new QuadTree(this.boundary.subdivide('ne'), this.capacity, this.depth + 1);
298+
this.northwest = new QuadTree(this.boundary.subdivide('nw'), this.capacity, this.depth + 1);
299+
this.southeast = new QuadTree(this.boundary.subdivide('se'), this.capacity, this.depth + 1);
300+
this.southwest = new QuadTree(this.boundary.subdivide('sw'), this.capacity, this.depth + 1);
287301

288302
this.divided = true;
303+
304+
// Move points to children.
305+
// This improves performance by placing points
306+
// in the smallest available rectangle.
307+
for (const p of this.points) {
308+
const inserted =
309+
this.northeast.insert(p) ||
310+
this.northwest.insert(p) ||
311+
this.southeast.insert(p) ||
312+
this.southwest.insert(p);
313+
314+
if (!inserted) {
315+
throw RangeError('capacity must be greater than 0');
316+
}
317+
}
318+
319+
this.points = null;
289320
}
290321

291322
insert(point) {
292323
if (!this.boundary.contains(point)) {
293324
return false;
294325
}
295326

296-
if (this.points.length < this.capacity) {
297-
this.points.push(point);
298-
return true;
299-
}
300-
301327
if (!this.divided) {
328+
if (
329+
this.points.length < this.capacity ||
330+
this.depth === this.MAX_DEPTH
331+
) {
332+
this.points.push(point);
333+
return true;
334+
}
335+
302336
this.subdivide();
303337
}
304338

@@ -319,16 +353,18 @@ class QuadTree {
319353
return found;
320354
}
321355

322-
for (let p of this.points) {
323-
if (range.contains(p)) {
324-
found.push(p);
325-
}
326-
}
327356
if (this.divided) {
328357
this.northwest.query(range, found);
329358
this.northeast.query(range, found);
330359
this.southwest.query(range, found);
331360
this.southeast.query(range, found);
361+
return found;
362+
}
363+
364+
for (const p of this.points) {
365+
if (range.contains(p)) {
366+
found.push(p);
367+
}
332368
}
333369

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

349-
this.children.sort((a, b) => a.boundary.sqDistanceFrom(searchPoint) - b.boundary.sqDistanceFrom(searchPoint))
350-
.forEach((child) => {
351-
const sqDist = child.boundary.sqDistanceFrom(searchPoint);
352-
if (sqDist > sqMaxDistance) {
353-
return;
354-
} else if (foundSoFar < maxCount || sqDist < furthestSqDistance) {
355-
const result = child.kNearest(searchPoint, maxCount, sqMaxDistance, furthestSqDistance, foundSoFar);
356-
const childPoints = result.found;
357-
found = found.concat(childPoints);
358-
foundSoFar += childPoints.length;
359-
furthestSqDistance = result.furthestSqDistance;
360-
}
361-
});
362-
363-
this.points
364-
.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint))
365-
.forEach((p) => {
366-
const sqDist = p.sqDistanceFrom(searchPoint);
367-
if (sqDist > sqMaxDistance) {
368-
return;
369-
} else if (foundSoFar < maxCount || sqDist < furthestSqDistance) {
370-
found.push(p);
371-
furthestSqDistance = Math.max(sqDist, furthestSqDistance);
372-
foundSoFar++;
373-
}
374-
});
385+
if (this.divided) {
386+
this.children
387+
.sort((a, b) => a.boundary.sqDistanceFrom(searchPoint) - b.boundary.sqDistanceFrom(searchPoint))
388+
.forEach((child) => {
389+
const sqDistance = child.boundary.sqDistanceFrom(searchPoint);
390+
if (sqDistance > sqMaxDistance) {
391+
return;
392+
} else if (foundSoFar < maxCount || sqDistance < furthestSqDistance) {
393+
const result = child.kNearest(searchPoint, maxCount, sqMaxDistance, furthestSqDistance, foundSoFar);
394+
const childPoints = result.found;
395+
found = found.concat(childPoints);
396+
foundSoFar += childPoints.length;
397+
furthestSqDistance = result.furthestSqDistance;
398+
}
399+
});
400+
} else {
401+
this.points
402+
.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint))
403+
.forEach((p) => {
404+
const sqDistance = p.sqDistanceFrom(searchPoint);
405+
if (sqDistance > sqMaxDistance) {
406+
return;
407+
} else if (foundSoFar < maxCount || sqDistance < furthestSqDistance) {
408+
found.push(p);
409+
furthestSqDistance = Math.max(sqDistance, furthestSqDistance);
410+
foundSoFar++;
411+
}
412+
});
413+
}
375414

376415
return {
377416
found: found.sort((a, b) => a.sqDistanceFrom(searchPoint) - b.sqDistanceFrom(searchPoint)).slice(0, maxCount),
378-
furthestDistance: Math.sqrt(furthestSqDistance),
417+
furthestSqDistance: Math.sqrt(furthestSqDistance),
379418
};
380419
}
381420

382421
forEach(fn) {
383-
this.points.forEach(fn);
384422
if (this.divided) {
385423
this.northeast.forEach(fn);
386424
this.northwest.forEach(fn);
387425
this.southeast.forEach(fn);
388426
this.southwest.forEach(fn);
427+
} else {
428+
this.points.forEach(fn);
389429
}
390430
}
391431

@@ -411,14 +451,16 @@ class QuadTree {
411451
}
412452

413453
get length() {
414-
let count = this.points.length;
415454
if (this.divided) {
416-
count += this.northwest.length;
417-
count += this.northeast.length;
418-
count += this.southwest.length;
419-
count += this.southeast.length;
455+
return (
456+
this.northwest.length +
457+
this.northeast.length +
458+
this.southwest.length +
459+
this.southeast.length
460+
);
420461
}
421-
return count;
462+
463+
return this.points.length;
422464
}
423465
}
424466

0 commit comments

Comments
 (0)