|
//_____________________________________________________________________________ |
|
// Bounded priority queue class -- used in k-NN search |
|
|
|
function BPQ(capacity) { |
|
this.capacity = capacity; |
|
this.elements = []; |
|
} |
|
|
|
BPQ.prototype.isFull = function() { |
|
return this.elements.length === this.capacity; |
|
}; |
|
|
|
BPQ.prototype.isEmpty = function() { |
|
return this.elements.length === 0; |
|
}; |
|
|
|
BPQ.prototype.maxPriority = function() { |
|
return this.elements[this.elements.length - 1].priority; |
|
}; |
|
|
|
Object.defineProperty(BPQ.prototype, "values", { |
|
get: function() { return this.elements.map(function(d) { return d.value; }); } |
|
}); |
|
|
|
// TODO: make more efficient? |
|
BPQ.prototype.add = function(value, priority) { |
|
var q = this.elements, |
|
d = { value: value, priority: priority }; |
|
if (this.isEmpty()) { q.push(d); } |
|
else { |
|
for (var i = 0; i < q.length; i++) { |
|
if (priority < q[i].priority) { |
|
q.splice(i, 0, d); |
|
break; |
|
} |
|
else if ( (i == q.length-1) && !this.isFull() ) { |
|
q.push(d); |
|
} |
|
} |
|
} |
|
this.elements = q.slice(0, this.capacity); |
|
}; |
|
|
|
//______________________________________________________________________________ |
|
// Node class -- defines each node in the k-d tree |
|
|
|
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]; |
|
}; |
|
|
|
// k-NN search |
|
Node.prototype.find = function(target, k) { |
|
k = k || 1; |
|
|
|
var queue = new BPQ(k), |
|
scannedNodes = []; |
|
|
|
search(this); |
|
|
|
return { |
|
nearestNodes: queue.values, |
|
scannedNodes: scannedNodes, |
|
maxDistance: queue.maxPriority() |
|
}; |
|
|
|
// 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); |
|
|
|
// Add current point to BPQ |
|
queue.add(node, distance(node.location, target)); |
|
|
|
// Recursively search the half of the tree that contains the test point |
|
if (target[node.axis] < node.location[node.axis]) { |
|
// Check left |
|
search(node.subnodes[0]); |
|
var otherNode = node.subnodes[1]; |
|
} |
|
else { |
|
// Check right |
|
search(node.subnodes[1]); |
|
var otherNode = node.subnodes[0]; |
|
} |
|
|
|
// If candidate hypersphere crosses this splitting plane, look on the |
|
// other side of the plane by examining the other subtree |
|
var delta = Math.abs(node.location[node.axis] - target[node.axis]); |
|
if (!queue.isFull() || delta < queue.maxPriority()) { |
|
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]; |
|
} |
|
|
|
|
|
//______________________________________________________________________________ |
|
// k-d tree generator |
|
|
|
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 |
|
); |
|
} |
|
} |
|
|
|
|
|
//______________________________________________________________________________ |
|
// Helper functions |
|
|
|
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)); |
|
} |