|
/** |
|
* k-d Tree JavaScript - V 1.01 |
|
* |
|
* https://github.com/ubilabs/kd-tree-javascript |
|
* |
|
* @author Mircea Pricop <[email protected]>, 2012 |
|
* @author Martin Kleppe <[email protected]>, 2012 |
|
* @author Ubilabs http://ubilabs.net, 2012 |
|
* @license MIT License <http://www.opensource.org/licenses/mit-license.php> |
|
*/ |
|
|
|
(function (root, factory) { |
|
if (typeof define === 'function' && define.amd) { |
|
define(['exports'], factory); |
|
} else if (typeof exports === 'object') { |
|
factory(exports); |
|
} else { |
|
factory((root.commonJsStrict = {})); |
|
} |
|
}(this, function (exports) { |
|
function Node(obj, dimension, parent) { |
|
this.obj = obj; |
|
this.left = null; |
|
this.right = null; |
|
this.parent = parent; |
|
this.dimension = dimension; |
|
} |
|
|
|
function kdTree(points, metric, dimensions) { |
|
|
|
var self = this; |
|
|
|
function buildTree(points, depth, parent) { |
|
var dim = depth % dimensions.length, |
|
median, |
|
node; |
|
|
|
if (points.length === 0) { |
|
return null; |
|
} |
|
if (points.length === 1) { |
|
return new Node(points[0], dim, parent); |
|
} |
|
|
|
points.sort(function (a, b) { |
|
return a[dimensions[dim]] - b[dimensions[dim]]; |
|
}); |
|
|
|
median = Math.floor(points.length / 2); |
|
node = new Node(points[median], dim, parent); |
|
node.left = buildTree(points.slice(0, median), depth + 1, node); |
|
node.right = buildTree(points.slice(median + 1), depth + 1, node); |
|
|
|
return node; |
|
} |
|
|
|
// Reloads a serialied tree |
|
function loadTree (data) { |
|
// Just need to restore the `parent` parameter |
|
self.root = data; |
|
|
|
function restoreParent (root) { |
|
if (root.left) { |
|
root.left.parent = root; |
|
restoreParent(root.left); |
|
} |
|
|
|
if (root.right) { |
|
root.right.parent = root; |
|
restoreParent(root.right); |
|
} |
|
} |
|
|
|
restoreParent(self.root); |
|
} |
|
|
|
// If points is not an array, assume we're loading a pre-built tree |
|
if (!Array.isArray(points)) loadTree(points, metric, dimensions); |
|
else this.root = buildTree(points, 0, null); |
|
|
|
// Convert to a JSON serializable structure; this just requires removing |
|
// the `parent` property |
|
this.toJSON = function (src) { |
|
if (!src) src = this.root; |
|
var dest = new Node(src.obj, src.dimension, null); |
|
if (src.left) dest.left = self.toJSON(src.left); |
|
if (src.right) dest.right = self.toJSON(src.right); |
|
// if (src.parent) dest.parent = self.toJSON(src.parent); |
|
return dest; |
|
}; |
|
|
|
this.insert = function (point) { |
|
function innerSearch(node, parent) { |
|
|
|
if (node === null) { |
|
return parent; |
|
} |
|
|
|
var dimension = dimensions[node.dimension]; |
|
if (point[dimension] < node.obj[dimension]) { |
|
return innerSearch(node.left, node); |
|
} else { |
|
return innerSearch(node.right, node); |
|
} |
|
} |
|
|
|
var insertPosition = innerSearch(this.root, null), |
|
newNode, |
|
dimension; |
|
|
|
if (insertPosition === null) { |
|
this.root = new Node(point, 0, null); |
|
return; |
|
} |
|
|
|
newNode = new Node(point, (insertPosition.dimension + 1) % dimensions.length, insertPosition); |
|
dimension = dimensions[insertPosition.dimension]; |
|
|
|
if (point[dimension] < insertPosition.obj[dimension]) { |
|
insertPosition.left = newNode; |
|
} else { |
|
insertPosition.right = newNode; |
|
} |
|
}; |
|
|
|
this.remove = function (point) { |
|
var node; |
|
|
|
function nodeSearch(node) { |
|
if (node === null) { |
|
return null; |
|
} |
|
|
|
if (node.obj === point) { |
|
return node; |
|
} |
|
|
|
var dimension = dimensions[node.dimension]; |
|
|
|
if (point[dimension] < node.obj[dimension]) { |
|
return nodeSearch(node.left, node); |
|
} else { |
|
return nodeSearch(node.right, node); |
|
} |
|
} |
|
|
|
function removeNode(node) { |
|
var nextNode, |
|
nextObj, |
|
pDimension; |
|
|
|
function findMin(node, dim) { |
|
var dimension, |
|
own, |
|
left, |
|
right, |
|
min; |
|
|
|
if (node === null) { |
|
return null; |
|
} |
|
|
|
dimension = dimensions[dim]; |
|
|
|
if (node.dimension === dim) { |
|
if (node.left !== null) { |
|
return findMin(node.left, dim); |
|
} |
|
return node; |
|
} |
|
|
|
own = node.obj[dimension]; |
|
left = findMin(node.left, dim); |
|
right = findMin(node.right, dim); |
|
min = node; |
|
|
|
if (left !== null && left.obj[dimension] < own) { |
|
min = left; |
|
} |
|
if (right !== null && right.obj[dimension] < min.obj[dimension]) { |
|
min = right; |
|
} |
|
return min; |
|
} |
|
|
|
if (node.left === null && node.right === null) { |
|
if (node.parent === null) { |
|
self.root = null; |
|
return; |
|
} |
|
|
|
pDimension = dimensions[node.parent.dimension]; |
|
|
|
if (node.obj[pDimension] < node.parent.obj[pDimension]) { |
|
node.parent.left = null; |
|
} else { |
|
node.parent.right = null; |
|
} |
|
return; |
|
} |
|
|
|
// If the right subtree is not empty, swap with the minimum element on the |
|
// node's dimension. If it is empty, we swap the left and right subtrees and |
|
// do the same. |
|
if (node.right !== null) { |
|
nextNode = findMin(node.right, node.dimension); |
|
nextObj = nextNode.obj; |
|
removeNode(nextNode); |
|
node.obj = nextObj; |
|
} else { |
|
nextNode = findMin(node.left, node.dimension); |
|
nextObj = nextNode.obj; |
|
removeNode(nextNode); |
|
node.right = node.left; |
|
node.left = null; |
|
node.obj = nextObj; |
|
} |
|
|
|
} |
|
|
|
node = nodeSearch(self.root); |
|
|
|
if (node === null) { return; } |
|
|
|
removeNode(node); |
|
}; |
|
|
|
this.nearest = function (point, maxNodes, maxDistance) { |
|
var i, |
|
result, |
|
bestNodes; |
|
|
|
bestNodes = new BinaryHeap( |
|
function (e) { return -e[1]; } |
|
); |
|
|
|
function nearestSearch(node) { |
|
var bestChild, |
|
dimension = dimensions[node.dimension], |
|
ownDistance = metric(point, node.obj), |
|
linearPoint = {}, |
|
linearDistance, |
|
otherChild, |
|
i; |
|
|
|
function saveNode(node, distance) { |
|
bestNodes.push([node, distance]); |
|
if (bestNodes.size() > maxNodes) { |
|
bestNodes.pop(); |
|
} |
|
} |
|
|
|
for (i = 0; i < dimensions.length; i += 1) { |
|
if (i === node.dimension) { |
|
linearPoint[dimensions[i]] = point[dimensions[i]]; |
|
} else { |
|
linearPoint[dimensions[i]] = node.obj[dimensions[i]]; |
|
} |
|
} |
|
|
|
linearDistance = metric(linearPoint, node.obj); |
|
|
|
if (node.right === null && node.left === null) { |
|
if (bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[1]) { |
|
saveNode(node, ownDistance); |
|
} |
|
return; |
|
} |
|
|
|
if (node.right === null) { |
|
bestChild = node.left; |
|
} else if (node.left === null) { |
|
bestChild = node.right; |
|
} else { |
|
if (point[dimension] < node.obj[dimension]) { |
|
bestChild = node.left; |
|
} else { |
|
bestChild = node.right; |
|
} |
|
} |
|
|
|
nearestSearch(bestChild); |
|
|
|
if (bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[1]) { |
|
saveNode(node, ownDistance); |
|
} |
|
|
|
if (bestNodes.size() < maxNodes || Math.abs(linearDistance) < bestNodes.peek()[1]) { |
|
if (bestChild === node.left) { |
|
otherChild = node.right; |
|
} else { |
|
otherChild = node.left; |
|
} |
|
if (otherChild !== null) { |
|
nearestSearch(otherChild); |
|
} |
|
} |
|
} |
|
|
|
if (maxDistance) { |
|
for (i = 0; i < maxNodes; i += 1) { |
|
bestNodes.push([null, maxDistance]); |
|
} |
|
} |
|
|
|
if(self.root) |
|
nearestSearch(self.root); |
|
|
|
result = []; |
|
|
|
for (i = 0; i < Math.min(maxNodes, bestNodes.content.length); i += 1) { |
|
if (bestNodes.content[i][0]) { |
|
result.push([bestNodes.content[i][0].obj, bestNodes.content[i][1]]); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
this.pointsBFS = function () { |
|
var result = []; |
|
if (!this.root) return result; |
|
var queue = []; |
|
var bounds = { x1: 0, y1: 0, x2: 255, y2: 255 }; |
|
var bundle = { node: this.root, bounds: bounds, depth: 0 }; |
|
queue.push(bundle); |
|
while(queue.length > 0) { |
|
var bundle = queue.shift(); |
|
|
|
var node = bundle.node; |
|
var bounds = bundle.bounds; |
|
var depth = bundle.depth; |
|
var object = node.obj; |
|
|
|
object.dimension = node.dimension; |
|
object.depth = depth; |
|
node.children = [node.left, node.right]; |
|
object.node = node |
|
|
|
var leftBounds, rightBounds; |
|
switch(object.dimension) { |
|
case 0: |
|
object.x1 = object.x2 = object.red; |
|
object.y1 = bounds.y1; |
|
object.y2 = bounds.y2; |
|
leftBounds = { x1: bounds.x1, y1: bounds.y1, x2: object.x2, y2: bounds.y2 }; |
|
rightBounds = { x1: object.x1, y1: bounds.y1, x2: bounds.x2, y2: bounds.y2 }; |
|
break; |
|
case 1: |
|
object.y1 = object.y2 = object.green; |
|
object.x1 = bounds.x1; |
|
object.x2 = bounds.x2; |
|
leftBounds = { x1: bounds.x1, y1: bounds.y1, x2: bounds.x2, y2: object.y2 }; |
|
rightBounds = { x1: bounds.x1, y1: object.y1, x2: bounds.x2, y2: bounds.y2 }; |
|
break; |
|
} |
|
|
|
result.push(object); |
|
//console.log(depth); |
|
|
|
// Traverse the tree |
|
if (node.left) { |
|
queue.push({ node: node.left, bounds: leftBounds, depth: depth + 1 }); |
|
}; |
|
if (node.right) { |
|
queue.push({ node: node.right, bounds: rightBounds, depth: depth + 1 }); |
|
}; |
|
} // while |
|
return result; |
|
}; |
|
|
|
this.d3tree = function () { |
|
function dfs (node) { |
|
if (!node) return "null"; // d3 expects JSON like nulls |
|
var object = {}; |
|
object.name = node.obj.toString(); |
|
if (node.parent) { |
|
object.parent = node.parent.obj.toString(); |
|
} else { |
|
object.parent = "null"; // d3 expects JSON like nulls |
|
} |
|
object.children = [dfs(node.left), dfs(node.right)]; |
|
return object; |
|
} |
|
return dfs(this.root); |
|
}; |
|
|
|
this.balanceFactor = function () { |
|
function height(node) { |
|
if (node === null) { |
|
return 0; |
|
} |
|
return Math.max(height(node.left), height(node.right)) + 1; |
|
} |
|
|
|
function count(node) { |
|
if (node === null) { |
|
return 0; |
|
} |
|
return count(node.left) + count(node.right) + 1; |
|
} |
|
|
|
return height(self.root) / (Math.log(count(self.root)) / Math.log(2)); |
|
}; |
|
} |
|
|
|
// Binary heap implementation from: |
|
// http://eloquentjavascript.net/appendix2.html |
|
|
|
function BinaryHeap(scoreFunction){ |
|
this.content = []; |
|
this.scoreFunction = scoreFunction; |
|
} |
|
|
|
BinaryHeap.prototype = { |
|
push: function(element) { |
|
// Add the new element to the end of the array. |
|
this.content.push(element); |
|
// Allow it to bubble up. |
|
this.bubbleUp(this.content.length - 1); |
|
}, |
|
|
|
pop: function() { |
|
// Store the first element so we can return it later. |
|
var result = this.content[0]; |
|
// Get the element at the end of the array. |
|
var end = this.content.pop(); |
|
// If there are any elements left, put the end element at the |
|
// start, and let it sink down. |
|
if (this.content.length > 0) { |
|
this.content[0] = end; |
|
this.sinkDown(0); |
|
} |
|
return result; |
|
}, |
|
|
|
peek: function() { |
|
return this.content[0]; |
|
}, |
|
|
|
remove: function(node) { |
|
var len = this.content.length; |
|
// To remove a value, we must search through the array to find |
|
// it. |
|
for (var i = 0; i < len; i++) { |
|
if (this.content[i] == node) { |
|
// When it is found, the process seen in 'pop' is repeated |
|
// to fill up the hole. |
|
var end = this.content.pop(); |
|
if (i != len - 1) { |
|
this.content[i] = end; |
|
if (this.scoreFunction(end) < this.scoreFunction(node)) |
|
this.bubbleUp(i); |
|
else |
|
this.sinkDown(i); |
|
} |
|
return; |
|
} |
|
} |
|
throw new Error("Node not found."); |
|
}, |
|
|
|
size: function() { |
|
return this.content.length; |
|
}, |
|
|
|
bubbleUp: function(n) { |
|
// Fetch the element that has to be moved. |
|
var element = this.content[n]; |
|
// When at 0, an element can not go up any further. |
|
while (n > 0) { |
|
// Compute the parent element's index, and fetch it. |
|
var parentN = Math.floor((n + 1) / 2) - 1, |
|
parent = this.content[parentN]; |
|
// Swap the elements if the parent is greater. |
|
if (this.scoreFunction(element) < this.scoreFunction(parent)) { |
|
this.content[parentN] = element; |
|
this.content[n] = parent; |
|
// Update 'n' to continue at the new position. |
|
n = parentN; |
|
} |
|
// Found a parent that is less, no need to move it further. |
|
else { |
|
break; |
|
} |
|
} |
|
}, |
|
|
|
sinkDown: function(n) { |
|
// Look up the target element and its score. |
|
var length = this.content.length, |
|
element = this.content[n], |
|
elemScore = this.scoreFunction(element); |
|
|
|
while(true) { |
|
// Compute the indices of the child elements. |
|
var child2N = (n + 1) * 2, child1N = child2N - 1; |
|
// This is used to store the new position of the element, |
|
// if any. |
|
var swap = null; |
|
// If the first child exists (is inside the array)... |
|
if (child1N < length) { |
|
// Look it up and compute its score. |
|
var child1 = this.content[child1N], |
|
child1Score = this.scoreFunction(child1); |
|
// If the score is less than our element's, we need to swap. |
|
if (child1Score < elemScore) |
|
swap = child1N; |
|
} |
|
// Do the same checks for the other child. |
|
if (child2N < length) { |
|
var child2 = this.content[child2N], |
|
child2Score = this.scoreFunction(child2); |
|
if (child2Score < (swap == null ? elemScore : child1Score)){ |
|
swap = child2N; |
|
} |
|
} |
|
|
|
// If the element needs to be moved, swap it, and continue. |
|
if (swap != null) { |
|
this.content[n] = this.content[swap]; |
|
this.content[swap] = element; |
|
n = swap; |
|
} |
|
// Otherwise, we are done. |
|
else { |
|
break; |
|
} |
|
} |
|
} |
|
}; |
|
|
|
this.kdTree = kdTree; |
|
|
|
exports.kdTree = kdTree; |
|
exports.BinaryHeap = BinaryHeap; |
|
})); |