Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for floating point fix in overlapRectangle #34

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 7 additions & 3 deletions dist/rtree.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,12 @@ function Rectangle(x, y, w, h) { // new Rectangle(bounds) or new Rectangle(x, y,
*/
Rectangle.overlapRectangle = function (a, b) {
//if(!((a.h||a.w)&&(b.h||b.w))){ not faster resist the urge!
if ((a.h === 0 && a.w === 0) || (b.h === 0 && b.w === 0)) {
return a.x <= (b.x + b.w) && (a.x + a.w) >= b.x && a.y <= (b.y + b.h) && (a.y + a.h) >= b.y;
var eps = 1e-6;
if ((a.h < eps && a.w < eps) || (b.h < eps && b.w < eps)) {
return (a.x - (b.x + b.w) < eps) &&
((a.x + a.w) - b.x > -eps) &&
(a.y - (b.y + b.h) < eps) &&
((a.y + a.h) - b.y > -eps);
}
else {
return a.x < (b.x + b.w) && (a.x + a.w) > b.x && a.y < (b.y + b.h) && (a.y + a.h) > b.y;
Expand Down Expand Up @@ -908,4 +912,4 @@ if (typeof Array.isArray !== 'function') {

},{"./rectangle":3}]},{},[2])
(2)
});
});
2 changes: 1 addition & 1 deletion dist/rtree.min.js

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions lib/rectangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ function Rectangle(x, y, w, h) { // new Rectangle(bounds) or new Rectangle(x, y,
*/
Rectangle.overlapRectangle = function (a, b) {
//if(!((a.h||a.w)&&(b.h||b.w))){ not faster resist the urge!
if ((a.h === 0 && a.w === 0) || (b.h === 0 && b.w === 0)) {
return a.x <= (b.x + b.w) && (a.x + a.w) >= b.x && a.y <= (b.y + b.h) && (a.y + a.h) >= b.y;
var eps = 1e-6;
if ((a.h < eps && a.w < eps) || (b.h < eps && b.w < eps)) {
return (a.x - (b.x + b.w) < eps) &&
((a.x + a.w) - b.x > -eps) &&
(a.y - (b.y + b.h) < eps) &&
((a.y + a.h) - b.y > -eps);
}
else {
return a.x < (b.x + b.w) && (a.x + a.w) > b.x && a.y < (b.y + b.h) && (a.y + a.h) > b.y;
Expand Down Expand Up @@ -196,4 +200,4 @@ Rectangle.squarifiedRatio = function (l, w, fill) {
var lgeo = larea / (lperi * lperi);
return larea * fill / lgeo;
};
module.exports = Rectangle;
module.exports = Rectangle;
281 changes: 141 additions & 140 deletions lib/rtree.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,146 +33,7 @@ function RTree(width) {
}
return done;
};
/* find the best specific node(s) for object to be deleted from
* [ leaf node parent ] = removeSubtree(rectangle, object, root)
* @private
*/
var removeSubtree = function (rect, obj, root) {
var hitStack = []; // Contains the elements that overlap
var countStack = []; // Contains the elements that overlap
var retArray = [];
var currentDepth = 1;
var tree, i, ltree;
if (!rect || !rectangle.overlapRectangle(rect, root)) {
return retArray;
}
var retObj = {x: rect.x, y: rect.y, w: rect.w, h: rect.h, target: obj};

countStack.push(root.nodes.length);
hitStack.push(root);
while (hitStack.length > 0) {
tree = hitStack.pop();
i = countStack.pop() - 1;
if ('target' in retObj) { // will this ever be false?
while (i >= 0) {
ltree = tree.nodes[i];
if (rectangle.overlapRectangle(retObj, ltree)) {
if ((retObj.target && 'leaf' in ltree && ltree.leaf === retObj.target) || (!retObj.target && ('leaf' in ltree || rectangle.containsRectangle(ltree, retObj)))) {
// A Match !!
// Yup we found a match...
// we can cancel search and start walking up the list
if ('nodes' in ltree) {// If we are deleting a node not a leaf...
retArray = flatten(tree.nodes.splice(i, 1));
} else {
retArray = tree.nodes.splice(i, 1);
}
// Resize MBR down...
rectangle.makeMBR(tree.nodes, tree);
delete retObj.target;
//if (tree.nodes.length < minWidth) { // Underflow
// retObj.nodes = searchSubtree(tree, true, [], tree);
//}
break;
} else if ('nodes' in ltree) { // Not a Leaf
currentDepth++;
countStack.push(i);
hitStack.push(tree);
tree = ltree;
i = ltree.nodes.length;
}
}
i--;
}

} else if ('nodes' in retObj) { // We are unsplitting

tree.nodes.splice(i + 1, 1); // Remove unsplit node
if (tree.nodes.length > 0) {
rectangle.makeMBR(tree.nodes, tree);
}
for (var t = 0;t < retObj.nodes.length;t++) {
insertSubtree(retObj.nodes[t], tree);
}
retObj.nodes = [];
if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
hitStack.push(tree);
countStack.push(1);
} else if (hitStack.length > 0 && tree.nodes.length < minWidth) { // Underflow..AGAIN!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
} else {
delete retObj.nodes; // Just start resizing
}
} else { // we are just resizing
rectangle.makeMBR(tree.nodes, tree);
}
currentDepth -= 1;
}
return retArray;
};

/* choose the best damn node for rectangle to be inserted into
* [ leaf node parent ] = chooseLeafSubtree(rectangle, root to start search at)
* @private
*/
var chooseLeafSubtree = function (rect, root) {
var bestChoiceIndex = -1;
var bestChoiceStack = [];
var bestChoiceArea;
var first = true;
bestChoiceStack.push(root);
var nodes = root.nodes;

while (first || bestChoiceIndex !== -1) {
if (first) {
first = false;
} else {
bestChoiceStack.push(nodes[bestChoiceIndex]);
nodes = nodes[bestChoiceIndex].nodes;
bestChoiceIndex = -1;
}

for (var i = nodes.length - 1; i >= 0; i--) {
var ltree = nodes[i];
if ('leaf' in ltree) {
// Bail out of everything and start inserting
bestChoiceIndex = -1;
break;
}
// Area of new enlarged rectangle
var oldLRatio = rectangle.squarifiedRatio(ltree.w, ltree.h, ltree.nodes.length + 1);

// Enlarge rectangle to fit new rectangle
var nw = Math.max(ltree.x + ltree.w, rect.x + rect.w) - Math.min(ltree.x, rect.x);
var nh = Math.max(ltree.y + ltree.h, rect.y + rect.h) - Math.min(ltree.y, rect.y);

// Area of new enlarged rectangle
var lratio = rectangle.squarifiedRatio(nw, nh, ltree.nodes.length + 2);

if (bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) {
bestChoiceArea = Math.abs(lratio - oldLRatio);
bestChoiceIndex = i;
}
}
}

return bestChoiceStack;
};

/* split a set of nodes into two roughly equally-filled nodes
* [ an array of two new arrays of nodes ] = linearSplit(array of nodes)
* @private
*/
var linearSplit = function (nodes) {
var n = pickLinear(nodes);
while (nodes.length > 0) {
pickNext(nodes, n[0], n[1]);
}
return n;
};


/* insert the best source rectangle into the best fitting parent node: a or b
* [] = pickNext(array of source nodes, target node array a, target node array b)
* @private
Expand Down Expand Up @@ -391,6 +252,146 @@ function RTree(width) {
}
};

/* find the best specific node(s) for object to be deleted from
* [ leaf node parent ] = removeSubtree(rectangle, object, root)
* @private
*/
var removeSubtree = function (rect, obj, root) {
var hitStack = []; // Contains the elements that overlap
var countStack = []; // Contains the elements that overlap
var retArray = [];
var currentDepth = 1;
var tree, i, ltree;
if (!rect || !rectangle.overlapRectangle(rect, root)) {
return retArray;
}
var retObj = {x: rect.x, y: rect.y, w: rect.w, h: rect.h, target: obj};

countStack.push(root.nodes.length);
hitStack.push(root);
while (hitStack.length > 0) {
tree = hitStack.pop();
i = countStack.pop() - 1;
if ('target' in retObj) { // will this ever be false?
while (i >= 0) {
ltree = tree.nodes[i];
if (rectangle.overlapRectangle(retObj, ltree)) {
if ((retObj.target && 'leaf' in ltree && ltree.leaf === retObj.target) || (!retObj.target && ('leaf' in ltree || rectangle.containsRectangle(ltree, retObj)))) {
// A Match !!
// Yup we found a match...
// we can cancel search and start walking up the list
if ('nodes' in ltree) {// If we are deleting a node not a leaf...
retArray = flatten(tree.nodes.splice(i, 1));
} else {
retArray = tree.nodes.splice(i, 1);
}
// Resize MBR down...
rectangle.makeMBR(tree.nodes, tree);
delete retObj.target;
//if (tree.nodes.length < minWidth) { // Underflow
// retObj.nodes = searchSubtree(tree, true, [], tree);
//}
break;
} else if ('nodes' in ltree) { // Not a Leaf
currentDepth++;
countStack.push(i);
hitStack.push(tree);
tree = ltree;
i = ltree.nodes.length;
}
}
i--;
}

} else if ('nodes' in retObj) { // We are unsplitting

tree.nodes.splice(i + 1, 1); // Remove unsplit node
if (tree.nodes.length > 0) {
rectangle.makeMBR(tree.nodes, tree);
}
for (var t = 0;t < retObj.nodes.length;t++) {
insertSubtree(retObj.nodes[t], tree);
}
retObj.nodes = [];
if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
hitStack.push(tree);
countStack.push(1);
} else if (hitStack.length > 0 && tree.nodes.length < minWidth) { // Underflow..AGAIN!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
} else {
delete retObj.nodes; // Just start resizing
}
} else { // we are just resizing
rectangle.makeMBR(tree.nodes, tree);
}
currentDepth -= 1;
}
return retArray;
};

/* choose the best damn node for rectangle to be inserted into
* [ leaf node parent ] = chooseLeafSubtree(rectangle, root to start search at)
* @private
*/
var chooseLeafSubtree = function (rect, root) {
var bestChoiceIndex = -1;
var bestChoiceStack = [];
var bestChoiceArea;
var first = true;
bestChoiceStack.push(root);
var nodes = root.nodes;

while (first || bestChoiceIndex !== -1) {
if (first) {
first = false;
} else {
bestChoiceStack.push(nodes[bestChoiceIndex]);
nodes = nodes[bestChoiceIndex].nodes;
bestChoiceIndex = -1;
}

for (var i = nodes.length - 1; i >= 0; i--) {
var ltree = nodes[i];
if ('leaf' in ltree) {
// Bail out of everything and start inserting
bestChoiceIndex = -1;
break;
}
// Area of new enlarged rectangle
var oldLRatio = rectangle.squarifiedRatio(ltree.w, ltree.h, ltree.nodes.length + 1);

// Enlarge rectangle to fit new rectangle
var nw = Math.max(ltree.x + ltree.w, rect.x + rect.w) - Math.min(ltree.x, rect.x);
var nh = Math.max(ltree.y + ltree.h, rect.y + rect.h) - Math.min(ltree.y, rect.y);

// Area of new enlarged rectangle
var lratio = rectangle.squarifiedRatio(nw, nh, ltree.nodes.length + 2);

if (bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) {
bestChoiceArea = Math.abs(lratio - oldLRatio);
bestChoiceIndex = i;
}
}
}

return bestChoiceStack;
};

/* split a set of nodes into two roughly equally-filled nodes
* [ an array of two new arrays of nodes ] = linearSplit(array of nodes)
* @private
*/
var linearSplit = function (nodes) {
var n = pickLinear(nodes);
while (nodes.length > 0) {
pickNext(nodes, n[0], n[1]);
}
return n;
};

this.insertSubtree = insertSubtree;
/* quick 'n' dirty function for plugins or manually drawing the tree
* [ tree ] = RTree.getTree(): returns the raw tree data. useful for adding
Expand Down
24 changes: 24 additions & 0 deletions test/searchexact.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
var test = require('tape');
var RTree = require('../lib');
test('RTree Searching for exact point', function(t) {
var rt = new RTree();
t.plan(1);
var data = [];
for(var i = 0; i < 1000; i++) {
data[i] = {x: Math.random() * 10, y: Math.random() * 10, w: 0, h: 0};
rt.insert(data[i], {});
}

var len = 0;
var bounds;
for(var i = 0; i < 1000; i++) {
bounds = {
x: data[i].x,
y: data[i].y,
w: 0,
h: 0
};
len += rt.search(bounds).length;
}
t.equal(len, 1000, '1k In-Bounds Searches');
});
3 changes: 2 additions & 1 deletion test/searching.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ test('RTree Searching', function(t) {
i--;
}
t.equals(len, 0, '1k Out-of-Bounds Searches');

i = 1000;
len = 0;
while (i > 0) {
Expand All @@ -37,4 +38,4 @@ test('RTree Searching', function(t) {
i--;
}
t.notEqual(len, 0, '1k In-Bounds Searches');
});
});