|
function Node(location, axis, subnodes, datum) { |
|
this.location = location; |
|
this.axis = axis; |
|
this.subnodes = subnodes; // = children nodes = [left child, right child] |
|
this.datum = datum; |
|
}; |
|
|
|
Node.prototype.toArray = function() { |
|
var array = [ |
|
this.location, |
|
this.subnodes[0] ? this.subnodes[0].toArray() : null, |
|
this.subnodes[0] ? this.subnodes[1].toArray() : null |
|
]; |
|
array.axis = this.axis; |
|
return array; |
|
}; |
|
|
|
Node.prototype.flatten = function() { |
|
var left = this.subnodes[0] ? this.subnodes[0].flatten() : null, |
|
right = this.subnodes[1] ? this.subnodes[1].flatten() : null; |
|
return left && right ? [this].concat(left, right) : |
|
left ? [this].concat(left) : |
|
right ? [this].concat(right) : |
|
[this]; |
|
}; |
|
|
|
// Nearest neighbor search (1-NN) |
|
Node.prototype.find = function(target) { |
|
var guess = this, |
|
bestDist = Infinity, |
|
scannedNodes = []; // keep track of these just for testing purpose |
|
|
|
search(this); |
|
|
|
return { |
|
node: guess, |
|
distance: bestDist, |
|
scannedNodes: scannedNodes |
|
}; |
|
|
|
// 1-NN algorithm outlined here: |
|
// http://web.stanford.edu/class/cs106l/handouts/assignment-3-kdtree.pdf |
|
function search(node) { |
|
if (node === null) return; |
|
|
|
scannedNodes.push(node); |
|
|
|
// If the current location is better than the best known location, |
|
// update the best known location |
|
var nodeDist = distance(node.location, target); |
|
if (nodeDist < bestDist) { |
|
bestDist = nodeDist; |
|
guess = node; |
|
} |
|
|
|
// Recursively search the half of the tree that contains the target |
|
var side = target[node.axis] < node.location[node.axis] ? "left" : "right"; |
|
if (side == "left") { |
|
search(node.subnodes[0]); |
|
var otherNode = node.subnodes[1]; |
|
} |
|
else { |
|
search(node.subnodes[1]); |
|
var otherNode = node.subnodes[0]; |
|
} |
|
|
|
// If the candidate hypersphere crosses this splitting plane, look on the |
|
// other side of the plane by examining the other subtree |
|
if (otherNode !== null) { |
|
var i = node.axis; |
|
var delta = Math.abs(node.location[i] - target[i]); |
|
if (delta < bestDist) { |
|
search(otherNode); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
// Only works for 2D |
|
Node.prototype.lines = function(extent) { |
|
var x0 = extent[0][0], |
|
y0 = extent[0][1], |
|
x1 = extent[1][0], |
|
y1 = extent[1][1], |
|
x = this.location[0], |
|
y = this.location[1]; |
|
|
|
if (this.axis == 0) { |
|
var line = [[x, y0], [x, y1]]; |
|
var left = this.subnodes[0] ? |
|
this.subnodes[0].lines([[x0, y0], [x, y1]]) : null; |
|
var right = this.subnodes[1] ? |
|
this.subnodes[1].lines([[x, y0], [x1, y1]]) : null; |
|
} |
|
else if (this.axis == 1) { |
|
var line = [[x0, y], [x1, y]]; |
|
var left = this.subnodes[0] ? |
|
this.subnodes[0].lines([[x0, y0], [x1, y]]) : null; |
|
var right = this.subnodes[1] ? |
|
this.subnodes[1].lines([[x0, y], [x1, y1]]) : null; |
|
} |
|
|
|
return left && right ? [line].concat(left, right) : |
|
left ? [line].concat(left) : |
|
right ? [line].concat(right) : |
|
[line]; |
|
} |
|
|
|
function KDTree() { |
|
var x = function(d) { return d[0]; }, |
|
y = function(d) { return d[1]; }; |
|
|
|
function tree(data) { |
|
var points = data.map(function(d) { |
|
var point = [x(d), y(d)]; |
|
point.datum = d; |
|
return point; |
|
}); |
|
|
|
return treeify(points, 0); |
|
} |
|
|
|
tree.x = function(_) { |
|
if (!arguments.length) return x; |
|
x = _; |
|
return tree; |
|
}; |
|
|
|
tree.y = function(_) { |
|
if (!arguments.length) return y; |
|
y = _; |
|
return tree; |
|
}; |
|
|
|
return tree; |
|
|
|
// Adapted from https://en.wikipedia.org/wiki/K-d_tree |
|
function treeify(points, depth) { |
|
try { var k = points[0].length; } |
|
catch (e) { return null; } |
|
|
|
// Select axis based on depth so that axis cycles through all valid values |
|
var axis = depth % k; |
|
|
|
// TODO: To speed up, consider splitting points based on approximation of |
|
// median; take median of random sample of points (perhaps of 1/10th |
|
// of the points) |
|
|
|
// Sort point list and choose median as pivot element |
|
points.sort(function(a, b) { return a[axis] - b[axis]; }); |
|
i_median = Math.floor(points.length / 2); |
|
|
|
// Create node and construct subtrees |
|
var point = points[i_median], |
|
left_points = points.slice(0, i_median), |
|
right_points = points.slice(i_median + 1); |
|
|
|
return new Node( |
|
point, |
|
axis, |
|
[treeify(left_points, depth + 1), treeify(right_points, depth + 1)], |
|
point.datum |
|
); |
|
} |
|
} |
|
|
|
function min(array, accessor) { |
|
return array |
|
.map(function(d) { return accessor(d); }) |
|
.reduce(function(a, b) { return a < b ? a : b; }); |
|
} |
|
|
|
function max(array, accessor) { |
|
return array |
|
.map(function(d) { return accessor(d); }) |
|
.reduce(function(a, b) { return a > b ? a : b; }); |
|
} |
|
|
|
function get(key) { return function(d) { return d[key]; }; } |
|
|
|
// TODO: Make distance function work for k-dimensions |
|
|
|
// Euclidean distance between two 2D points |
|
function distance(p0, p1) { |
|
return Math.sqrt(Math.pow(p1[0] - p0[0], 2) + Math.pow(p1[1] - p0[1], 2)); |
|
} |