Skip to content

Commit 77bc75d

Browse files
Update k-nearest-neighbors algorithm and added example from @crhallberg (#59)
* Update k-nearest-neighbors algorithm and added example from @crhalberg * Removed test that took too long on circle/ci * removed unnecessary var * Updated knearest to be depth first * No need to sort points here. * Actually... you do need to sort here
1 parent adbbf4c commit 77bc75d

File tree

4 files changed

+177
-25
lines changed

4 files changed

+177
-25
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
8+
<title>QuadTree</title>
9+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.0/p5.min.js"></script>
10+
<script src="../../quadtree.js"></script>
11+
<script src="sketch.js"></script>
12+
</head>
13+
<body>
14+
</body>
15+
</html>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
let qtree;
2+
let searchedQuads = [];
3+
4+
function setup() {
5+
createCanvas(600, 600);
6+
7+
rectMode(CENTER);
8+
9+
qtree = QuadTree.create();
10+
11+
for (let i = 0; i < 1000; i++) {
12+
let p = new Point(
13+
Math.round(Math.random() * width),
14+
Math.round(Math.random() * height),
15+
{
16+
searched: false,
17+
closest: false,
18+
}
19+
);
20+
qtree.insert(p);
21+
}
22+
23+
console.log(qtree);
24+
}
25+
26+
function draw() {
27+
background("#0e141f");
28+
29+
noFill();
30+
stroke("#273445");
31+
drawQuads(qtree);
32+
33+
stroke("#a56de2");
34+
for (let quad of searchedQuads) {
35+
rect(quad.x, quad.y, quad.w, quad.h);
36+
}
37+
38+
noStroke();
39+
qtree.forEach((p) => {
40+
fill("#95a3ab");
41+
if (p.userData.searched) {
42+
fill("#ffa154");
43+
}
44+
if (p.userData.closest) {
45+
fill("#68b723");
46+
}
47+
48+
rect(p.x, p.y, 6, 6);
49+
});
50+
}
51+
52+
function drawQuads(tree) {
53+
rect(tree.boundary.x, tree.boundary.y, tree.boundary.w, tree.boundary.h);
54+
tree.children.map(drawQuads);
55+
}
56+
57+
function kNearest(tree, searchPoint, maxCount = 1, maxDistance = Infinity, furthestDistance = 0, foundSoFar = 0) {
58+
if (typeof searchPoint === "undefined") {
59+
throw TypeError("Method 'closest' needs a point");
60+
}
61+
62+
let found = [];
63+
64+
tree.children.sort((a, b) => a.boundary.distanceFrom(searchPoint) - b.boundary.distanceFrom(searchPoint))
65+
.forEach((child) => {
66+
const distance = child.boundary.distanceFrom(searchPoint);
67+
if (distance > maxDistance) {
68+
return;
69+
} else if (foundSoFar < maxCount || distance < furthestDistance) {
70+
const result = kNearest(child, searchPoint, maxCount, maxDistance, furthestDistance, foundSoFar);
71+
searchedQuads.push(child.boundary);
72+
const childPoints = result.found;
73+
found = found.concat(childPoints);
74+
foundSoFar += childPoints.length;
75+
furthestDistance = result.furthestDistance;
76+
}
77+
});
78+
79+
tree.points.sort((a, b) => a.distanceFrom(searchPoint) - b.distanceFrom(searchPoint))
80+
.forEach((p) => {
81+
const distance = p.distanceFrom(searchPoint);
82+
p.userData.searched = true;
83+
if (distance > maxDistance) {
84+
return;
85+
} else if (foundSoFar < maxCount || distance < furthestDistance) {
86+
found.push(p);
87+
furthestDistance = max(distance, furthestDistance);
88+
foundSoFar++;
89+
}
90+
});
91+
92+
return {
93+
found: found.sort((a, b) => a.distanceFrom(searchPoint) - b.distanceFrom(searchPoint)).slice(0, maxCount),
94+
furthestDistance
95+
};
96+
}
97+
98+
function mousePressed() {
99+
searchedQuads = [];
100+
qtree.forEach((p) => {
101+
p.userData = {
102+
searched: false,
103+
closest: false,
104+
};
105+
});
106+
let mp = new Point(mouseX, mouseY);
107+
let closest = kNearest(qtree, mp).found;
108+
closest.forEach((p) => {
109+
p.userData.closest = true;
110+
});
111+
}

quadtree.js

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -322,35 +322,47 @@ class QuadTree {
322322
return found;
323323
}
324324

325-
closest(point, count = 1, maxDistance = Infinity, furthestFound = 0, foundSoFar = 0) {
326-
if (typeof point === "undefined") {
325+
closest(searchPoint, maxCount = 1, maxDistance = Infinity) {
326+
if (typeof searchPoint === "undefined") {
327327
throw TypeError("Method 'closest' needs a point");
328328
}
329329

330-
var inRangePoints = this.points.filter((p) => {
331-
const distance = p.distanceFrom(point);
332-
if (distance <= maxDistance) {
333-
furthestFound = Math.max(distance, furthestFound);
334-
}
335-
return distance <= maxDistance
336-
});
337-
338-
this.children.forEach(child => {
339-
// if the child quad tree is further away from the point than the furthest point we have found so far
340-
// and we have enough points already
341-
// then we can skip checking this entire quad tree
342-
// as there will be no points in it that we are interested in.
343-
if (
344-
(child.boundary.distanceFrom(point) < furthestFound || foundSoFar < count) &&
345-
child.boundary.distanceFrom(point) < maxDistance
346-
) {
347-
inRangePoints = inRangePoints.concat(child.closest(point, count, maxDistance, furthestFound, inRangePoints.length + foundSoFar));
348-
}
349-
});
330+
return this.kNearest(searchPoint, maxCount, maxDistance, 0, 0).found;
331+
}
350332

351-
return inRangePoints
352-
.sort((a, b) => a.distanceFrom(point) - b.distanceFrom(point))
353-
.slice(0, count);
333+
kNearest(searchPoint, maxCount, maxDistance, furthestDistance, foundSoFar) {
334+
let found = [];
335+
336+
this.children.sort((a, b) => a.boundary.distanceFrom(searchPoint) - b.boundary.distanceFrom(searchPoint))
337+
.forEach((child) => {
338+
const distance = child.boundary.distanceFrom(searchPoint);
339+
if (distance > maxDistance) {
340+
return;
341+
} else if (foundSoFar < maxCount || distance < furthestDistance) {
342+
const result = child.kNearest(searchPoint, maxCount, maxDistance, furthestDistance, foundSoFar);
343+
const childPoints = result.found;
344+
found = found.concat(childPoints);
345+
foundSoFar += childPoints.length;
346+
furthestDistance = result.furthestDistance;
347+
}
348+
});
349+
350+
this.points.sort((a, b) => a.distanceFrom(searchPoint) - b.distanceFrom(searchPoint))
351+
.forEach((p) => {
352+
const distance = p.distanceFrom(searchPoint);
353+
if (distance > maxDistance) {
354+
return;
355+
} else if (foundSoFar < maxCount || distance < furthestDistance) {
356+
found.push(p);
357+
furthestDistance = Math.max(distance, furthestDistance);
358+
foundSoFar++;
359+
}
360+
});
361+
362+
return {
363+
found: found.sort((a, b) => a.distanceFrom(searchPoint) - b.distanceFrom(searchPoint)).slice(0, maxCount),
364+
furthestDistance
365+
};
354366
}
355367

356368
forEach(fn) {

test/k-nearest-neighbors.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,18 @@ describe('K Nearest Neighbors', () => {
9494

9595
expect(qt.closest(point, 100000).length).to.equal(100000);
9696
});
97+
it('Stupid numnber of points', () => {
98+
const rect = new Rectangle(500, 500, 1000, 1000);
99+
let qt = new QuadTree(rect, 100);
100+
101+
for(var i=0; i<1000000; ++i) {
102+
const x = Math.random() * 1000;
103+
const y = Math.random() * 1000;
104+
qt.insert(new Point(x, y));
105+
}
106+
107+
const point = new Point(500, 500);
108+
109+
expect(qt.closest(point, 10).length).to.equal(10);
110+
});
97111
});

0 commit comments

Comments
 (0)