Skip to content

Commit

Permalink
Fix #21 - exclusive bound in quadtree.cover.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Feb 10, 2019
1 parent d70e7ce commit e879193
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 77 deletions.
5 changes: 2 additions & 3 deletions src/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ export function addAll(data) {
if (y > y1) y1 = y;
}

// If there were no (valid) points, inherit the existing extent.
if (x1 < x0) x0 = this._x0, x1 = this._x1;
if (y1 < y0) y0 = this._y0, y1 = this._y1;
// If there were no (valid) points, abort.
if (x0 > x1 || y0 > y1) return this;

// Expand the tree to cover the new points.
this.cover(x0, y0).cover(x1, y1);
Expand Down
33 changes: 9 additions & 24 deletions src/cover.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,26 @@ export default function(x, y) {
}

// Otherwise, double repeatedly to cover.
else if (x0 > x || x > x1 || y0 > y || y > y1) {
else {
var z = x1 - x0,
node = this._root,
parent,
i;

switch (i = (y < (y0 + y1) / 2) << 1 | (x < (x0 + x1) / 2)) {
case 0: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x1 = x0 + z, y1 = y0 + z, x > x1 || y > y1);
break;
}
case 1: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x0 = x1 - z, y1 = y0 + z, x0 > x || y > y1);
break;
}
case 2: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x1 = x0 + z, y0 = y1 - z, x > x1 || y0 > y);
break;
}
case 3: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x0 = x1 - z, y0 = y1 - z, x0 > x || y0 > y);
break;
while (x0 > x || x >= x1 || y0 > y || y >= y1) {
i = (y < y0) << 1 | (x < x0);
parent = new Array(4), parent[i] = node, node = parent, z *= 2;
switch (i) {
case 0: x1 = x0 + z, y1 = y0 + z; break;
case 1: x0 = x1 - z, y1 = y0 + z; break;
case 2: x1 = x0 + z, y0 = y1 - z; break;
case 3: x0 = x1 - z, y0 = y1 - z; break;
}
}

if (this._root && this._root.length) this._root = node;
}

// If the quadtree covers the point already, just return.
else return this;

this._x0 = x0;
this._y0 = y0;
this._x1 = x1;
Expand Down
14 changes: 7 additions & 7 deletions test/add-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ var tape = require("tape"),

tape("quadtree.add(datum) creates a new point and adds it to the quadtree", function(test) {
var q = d3_quadtree.quadtree();
test.deepEqual(q.add([0, 0]).root(), {data: [0, 0]});
test.deepEqual(q.add([1, 1]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.add([1, 0]).root(), [{data: [0, 0]}, {data: [1, 0]},, {data: [1, 1]}]);
test.deepEqual(q.add([0, 1]).root(), [{data: [0, 0]}, {data: [1, 0]}, {data: [0, 1]}, {data: [1, 1]}]);
test.deepEqual(q.add([0.4, 0.4]).root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}], {data: [1, 0]}, {data: [0, 1]}, {data: [1, 1]}]);
test.deepEqual(q.add([0.0, 0.0]).root(), {data: [0, 0]});
test.deepEqual(q.add([0.9, 0.9]).root(), [{data: [0, 0]},,, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.9, 0.0]).root(), [{data: [0, 0]}, {data: [0.9, 0]},, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.0, 0.9]).root(), [{data: [0, 0]}, {data: [0.9, 0]}, {data: [0, 0.9]}, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.4, 0.4]).root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}], {data: [0.9, 0]}, {data: [0, 0.9]}, {data: [0.9, 0.9]}]);
test.end();
});

Expand All @@ -22,7 +22,7 @@ tape("quadtree.add(datum) handles points being on the perimeter of the quadtree

tape("quadtree.add(datum) handles points being to the top of the quadtree bounds", function(test) {
var q = d3_quadtree.quadtree().extent([[0, 0], [2, 2]]);
test.deepEqual(q.add([1, -1]).extent(), [[0, -2], [4, 2]]);
test.deepEqual(q.add([1, -1]).extent(), [[0, -4], [8, 4]]);
test.end();
});

Expand All @@ -40,7 +40,7 @@ tape("quadtree.add(datum) handles points being to the bottom of the quadtree bou

tape("quadtree.add(datum) handles points being to the left of the quadtree bounds", function(test) {
var q = d3_quadtree.quadtree().extent([[0, 0], [2, 2]]);
test.deepEqual(q.add([-1, 1]).extent(), [[-2, 0], [2, 4]]);
test.deepEqual(q.add([-1, 1]).extent(), [[-4, 0], [4, 8]]);
test.end();
});

Expand Down
20 changes: 10 additions & 10 deletions test/addAll-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ var tape = require("tape"),

tape("quadtree.addAll(data) creates new points and adds them to the quadtree", function(test) {
var q = d3_quadtree.quadtree();
test.deepEqual(q.add([0, 0]).root(), {data: [0, 0]});
test.deepEqual(q.add([1, 1]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.add([1, 0]).root(), [{data: [0, 0]}, {data: [1, 0]},, {data: [1, 1]}]);
test.deepEqual(q.add([0, 1]).root(), [{data: [0, 0]}, {data: [1, 0]}, {data: [0, 1]}, {data: [1, 1]}]);
test.deepEqual(q.add([0.4, 0.4]).root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}], {data: [1, 0]}, {data: [0, 1]}, {data: [1, 1]}]);
test.deepEqual(q.add([0.0, 0.0]).root(), {data: [0, 0]});
test.deepEqual(q.add([0.9, 0.9]).root(), [{data: [0, 0]},,, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.9, 0.0]).root(), [{data: [0, 0]}, {data: [0.9, 0]},, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.0, 0.9]).root(), [{data: [0, 0]}, {data: [0.9, 0]}, {data: [0, 0.9]}, {data: [0.9, 0.9]}]);
test.deepEqual(q.add([0.4, 0.4]).root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}], {data: [0.9, 0]}, {data: [0, 0.9]}, {data: [0.9, 0.9]}]);
test.end();
});

tape("quadtree.addAll(data) ignores points with NaN coordinates", function(test) {
var q = d3_quadtree.quadtree();
test.deepEqual(q.addAll([[NaN, 0], [0, NaN]]).root(), undefined);
test.equal(q.extent(), undefined);
test.deepEqual(q.addAll([[0, 0], [1, 1]]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.addAll([[NaN, 0], [0, NaN]]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.addAll([[0, 0], [0.9, 0.9]]).root(), [{data: [0, 0]},,, {data: [0.9, 0.9]}]);
test.deepEqual(q.addAll([[NaN, 0], [0, NaN]]).root(), [{data: [0, 0]},,, {data: [0.9, 0.9]}]);
test.deepEqual(q.extent(), [[0, 0], [1, 1]]);
test.end();
});
Expand All @@ -27,12 +27,12 @@ tape("quadtree.addAll(data) correctly handles the empty array", function(test) {
test.equal(q.extent(), undefined);
test.deepEqual(q.addAll([[0, 0], [1, 1]]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.addAll([]).root(), [{data: [0, 0]},,, {data: [1, 1]}]);
test.deepEqual(q.extent(), [[0, 0], [1, 1]]);
test.deepEqual(q.extent(), [[0, 0], [2, 2]]);
test.end();
});

tape("quadtree.addAll(data) computes the extent of the data before adding", function(test) {
var q = d3_quadtree.quadtree().addAll([[0.4, 0.4], [0, 0], [1, 1]]);
test.deepEqual(q.root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}],,, {data: [1, 1]}]);
var q = d3_quadtree.quadtree().addAll([[0.4, 0.4], [0, 0], [0.9, 0.9]]);
test.deepEqual(q.root(), [[{data: [0, 0]},,, {data: [0.4, 0.4]}],,, {data: [0.9, 0.9]}]);
test.end();
});
16 changes: 8 additions & 8 deletions test/copy-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ tape("quadtree.copy() isolates changes to the extent", function(test) {
var q0 = d3_quadtree.quadtree().extent([[0, 0], [1, 1]]),
q1 = q0.copy();
q0.add([2, 2]);
test.deepEqual(q1.extent(), [[0, 0], [1, 1]]);
test.deepEqual(q1.extent(), [[0, 0], [2, 2]]);
q1.add([-1, -1]);
test.deepEqual(q0.extent(), [[0, 0], [2, 2]]);
test.deepEqual(q0.extent(), [[0, 0], [4, 4]]);
test.end();
});

Expand All @@ -39,13 +39,13 @@ tape("quadtree.copy() isolates changes to the root when not a leaf", function(te
q0 = d3_quadtree.quadtree().extent([[0, 0], [4, 4]]).addAll([p0, p1]),
q1 = q0.copy();
q0.add(p2);
test.deepEqual(q0.extent(), [[0, 0], [4, 4]]);
test.deepEqual(q0.root(), [{data: [1, 1]},,, [{data: [2, 2]},,, {data: [3, 3]}]]);
test.deepEqual(q1.extent(), [[0, 0], [4, 4]]);
test.deepEqual(q1.root(), [{data: [1, 1]},,, {data: [2, 2]}]);
test.deepEqual(q0.extent(), [[0, 0], [8, 8]]);
test.deepEqual(q0.root(), [[{data: [1, 1]},,, [{data: [2, 2]},,, {data: [3, 3]}]],,, ]);
test.deepEqual(q1.extent(), [[0, 0], [8, 8]]);
test.deepEqual(q1.root(), [[{data: [1, 1]},,, {data: [2, 2]}],,, ]);
q1 = q0.copy();
q0.remove(p2);
test.deepEqual(q1.extent(), [[0, 0], [4, 4]]);
test.deepEqual(q1.root(), [{data: [1, 1]},,, [{data: [2, 2]},,, {data: [3, 3]}]]);
test.deepEqual(q1.extent(), [[0, 0], [8, 8]]);
test.deepEqual(q1.root(), [[{data: [1, 1]},,, [{data: [2, 2]},,, {data: [3, 3]}]],,, ]);
test.end();
});
32 changes: 16 additions & 16 deletions test/cover-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ tape("quadtree.cover(x, y) sets a trivial extent if the extent was undefined", f
});

tape("quadtree.cover(x, y) sets a non-trivial squarified and centered extent if the extent was trivial", function(test) {
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(1, 2).extent(), [[0, 0], [2, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(1, 2).extent(), [[0, 0], [4, 4]]);
test.end();
});

Expand All @@ -17,36 +17,36 @@ tape("quadtree.cover(x, y) ignores invalid points", function(test) {
});

tape("quadtree.cover(x, y) repeatedly doubles the existing extent if the extent was non-trivial", function(test) {
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, -1).extent(), [[-2, -2], [2, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(1, -1).extent(), [[0, -2], [4, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, -1).extent(), [[0, -2], [4, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, -1).extent(), [[-4, -4], [4, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(1, -1).extent(), [[0, -4], [8, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, -1).extent(), [[0, -4], [8, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, 1).extent(), [[0, 0], [4, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, 3).extent(), [[0, 0], [4, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(1, 3).extent(), [[0, 0], [4, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, 3).extent(), [[-2, 0], [2, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, 1).extent(), [[-2, 0], [2, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, -3).extent(), [[-6, -6], [2, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, -3).extent(), [[0, -6], [8, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(5, -3).extent(), [[0, -6], [8, 2]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, 3).extent(), [[-4, 0], [4, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-1, 1).extent(), [[-4, 0], [4, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, -3).extent(), [[-4, -4], [4, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, -3).extent(), [[0, -4], [8, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(5, -3).extent(), [[0, -4], [8, 4]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(5, 3).extent(), [[0, 0], [8, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(5, 5).extent(), [[0, 0], [8, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(3, 5).extent(), [[0, 0], [8, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, 5).extent(), [[-6, 0], [2, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, 3).extent(), [[-6, 0], [2, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, 5).extent(), [[-4, 0], [4, 8]]);
test.deepEqual(d3_quadtree.quadtree().cover(0, 0).cover(2, 2).cover(-3, 3).extent(), [[-4, 0], [4, 8]]);
test.end();
});

tape("quadtree.cover(x, y) repeatedly wraps the root node if it has children", function(test) {
var q = d3_quadtree.quadtree().add([0, 0]).add([2, 2]);
test.deepEqual(q.root(), [{data: [0, 0]},,, {data: [2, 2]}]);
test.deepEqual(q.copy().cover(3, 3).root(), [[{data: [0, 0]},,, {data: [2, 2]}],,, ]);
test.deepEqual(q.copy().cover(3, 3).root(), [{data: [0, 0]},,, {data: [2, 2]}]);
test.deepEqual(q.copy().cover(-1, 3).root(), [,[{data: [0, 0]},,, {data: [2, 2]}],, ]);
test.deepEqual(q.copy().cover(3, -1).root(), [,, [{data: [0, 0]},,, {data: [2, 2]}], ]);
test.deepEqual(q.copy().cover(-1, -1).root(), [,,, [{data: [0, 0]},,, {data: [2, 2]}]]);
test.deepEqual(q.copy().cover(5, 5).root(), [[[{data: [0, 0]},,, {data: [2, 2]}],,, ],,, ]);
test.deepEqual(q.copy().cover(-3, 5).root(), [, [,[{data: [0, 0]},,, {data: [2, 2]}],, ],, ]);
test.deepEqual(q.copy().cover(5, -3).root(), [,, [,, [{data: [0, 0]},,, {data: [2, 2]}], ], ]);
test.deepEqual(q.copy().cover(-3, -3).root(), [,,, [,,, [{data: [0, 0]},,, {data: [2, 2]}]]]);
test.deepEqual(q.copy().cover(5, 5).root(), [[{data: [0, 0]},,, {data: [2, 2]}],,, ]);
test.deepEqual(q.copy().cover(-3, 5).root(), [,[{data: [0, 0]},,, {data: [2, 2]}],, ]);
test.deepEqual(q.copy().cover(5, -3).root(), [,, [{data: [0, 0]},,, {data: [2, 2]}], ]);
test.deepEqual(q.copy().cover(-3, -3).root(), [,,, [{data: [0, 0]},,, {data: [2, 2]}]]);
test.end();
});

Expand Down
4 changes: 2 additions & 2 deletions test/extent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tape("quadtree.extent(extent) extends the extent", function(test) {
tape("quadtree.extent() can be inferred by quadtree.cover", function(test) {
var q = d3_quadtree.quadtree();
test.deepEqual(q.cover(0, 0).extent(), [[0, 0], [1, 1]]);
test.deepEqual(q.cover(2, 4).extent(), [[0, 0], [4, 4]]);
test.deepEqual(q.cover(2, 4).extent(), [[0, 0], [8, 8]]);
test.end();
});

Expand All @@ -18,7 +18,7 @@ tape("quadtree.extent() can be inferred by quadtree.add", function(test) {
q.add([0, 0]);
test.deepEqual(q.extent(), [[0, 0], [1, 1]]);
q.add([2, 4]);
test.deepEqual(q.extent(), [[0, 0], [4, 4]]);
test.deepEqual(q.extent(), [[0, 0], [8, 8]]);
test.end();
});

Expand Down
4 changes: 2 additions & 2 deletions test/remove-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ tape("quadtree.remove(datum) removes a non-root point in the quadtree", function
p1 = [1, 1],
q = d3_quadtree.quadtree().addAll([p0, p1]);
test.equal(q.remove(p0), q);
test.deepEqual(q.extent(), [[0, 0], [1, 1]]);
test.deepEqual(q.extent(), [[0, 0], [2, 2]]);
test.equal(q.root().data, p1);
test.deepEqual(p0, [0, 0]);
test.deepEqual(p1, [1, 1]);
Expand All @@ -61,7 +61,7 @@ tape("quadtree.remove(datum) removes another non-root point in the quadtree", fu
p1 = [1, 1],
q = d3_quadtree.quadtree().addAll([p0, p1]);
test.equal(q.remove(p1), q);
test.deepEqual(q.extent(), [[0, 0], [1, 1]]);
test.deepEqual(q.extent(), [[0, 0], [2, 2]]);
test.equal(q.root().data, p0);
test.deepEqual(p0, [0, 0]);
test.deepEqual(p1, [1, 1]);
Expand Down
10 changes: 5 additions & 5 deletions test/visit-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ tape("quadtree.visit(callback) visits each node in a quadtree", function(test) {
.addAll([[0, 0], [1, 0], [0, 1], [1, 1]]);
test.equal(q.visit(function(node, x0, y0, x1, y1) { results.push([x0, y0, x1, y1]); }), q);
test.deepEqual(results, [
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 0.5, 0.5],
[0.5, 0.0, 1.0, 0.5],
[0.0, 0.5, 0.5, 1.0],
[0.5, 0.5, 1.0, 1.0]
[0, 0, 2, 2],
[0, 0, 1, 1],
[1, 0, 2, 1],
[0, 1, 1, 2],
[1, 1, 2, 2]
]);
test.end();
});
Expand Down

0 comments on commit e879193

Please sign in to comment.