/** * kdtree * * Is a modified and google closure adapted kdtree implementation * of https://github.com/ubilabs/kd-tree-javascript. * * @author Andrey Antukh , 2016 * @author Mircea Pricop , 2012 * @author Martin Kleppe , 2012 * @author Ubilabs http://ubilabs.net, 2012 * @license MIT License */ goog.provide("kdtree"); goog.provide("kdtree.Point2d"); goog.provide("kdtree.KDTree"); goog.require('goog.array'); goog.require('goog.asserts'); goog.scope(function() { "use strict"; const assert = goog.asserts.assert; const assertNumber = goog.asserts.assertNumber; const every = goog.array.every; class Point2d { constructor(x, y, data) { this.type = Point2d; this.x = x; this.y = y; this.data = data; } static empty() { return new Point2d(0, 0, null); } get(index) { if (index === 0) { return this.x; } else { return this.y; } } set(index, value) { if (index === 0) { this.x = value; } else { this.y = value; } return this; } } class Node { constructor(obj, dimension, parent) { this.obj = obj; this.left = null; this.right = null; this.parent = parent; this.dimension = dimension; } } function buildTree(points, depth, parent, dimensions) { const dim = depth % dimensions; if (points.length === 0) { return null; } if (points.length === 1) { return new Node(points[0], dim, parent); } points.sort((a, b) => { return a.get(dim) - b.get(dim); }); const median = Math.floor(points.length / 2); const node = new Node(points[median], dim, parent); node.left = buildTree(points.slice(0, median), depth + 1, node, dimensions); node.right = buildTree(points.slice(median + 1), depth + 1, node, dimensions); return node; } function findMin(node, dim) { let dimension, own, left, right, min; if (node === null) { return null; } if (node.dimension === dim) { if (node.left !== null) { return findMin(node.left, dim); } return node; } own = node.obj.get(dim); left = findMin(node.left, dim); right = findMin(node.right, dim); min = node; if (left !== null && left.obj.get(dim) < own) { min = left; } if (right !== null && right.obj.get(dim) < min.obj.get(dim)) { min = right; } return min; } function innerSearch(point, node, parent) { if (node === null) { return parent; } if (point.get(dim) < node.obj.get(dim)) { return innerSearch(point, node.left, node); } else { return innerSearch(point, node.right, node); } } function nodeSearch(point, node) { if (node === null) { return null; } if (node.obj === point) { return node; } if (point.get(node.dimension) < node.obj.get(node.dimension)) { return nodeSearch(point, node.left); } else { return nodeSearch(point, node.right); } } class KDTree { constructor(points, metric, dimensions) { assert(points.length !== 0); assertNumber(dimensions); this.root = buildTree(points, 0, null, dimensions); this.metric = metric; this.dimensions = dimensions; } insert(point) { const insertPosition = innerSearch(point, this.root, null); if (insertPosition === null) { this.root = new Node(point, 0, null); return; } const newNode = new Node(point, (insertPosition.dimension + 1) % this.dimensions, insertPosition); const dimension = insertPosition.dimension; if (point.get(dimension) < insertPosition.obj.get(dimension)) { insertPosition.left = newNode; } else { insertPosition.right = newNode; } } remove(point) { const node = nodeSearch(point, this.root); if (node === null) { return; } if (node.left === null && node.right === null) { if (node.parent === null) { this.root = null; return; } const pdim = node.parent.dimension; if (node.obj.get(pdim) < node.parent.obj.get(pdim)) { 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. let nextNode, nextObj; 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; } } nearest(point, maxNodes, maxDistance) { let i, result, bestNodes; if (maxNodes === undefined) { maxNodes = 1; } bestNodes = new BinaryHeap(function (e) { return -e[1]; }); const nearestSearch = (node) => { const ownDistance = self.metric(point, node.obj); const dimension = node.dimension; const pointType = node.obj.type; const linearPoint = pointType.empty(); let otherChild, linearDistance, bestChild, i; function saveNode(node, distance) { bestNodes.push([node, distance]); if (bestNodes.size() > maxNodes) { bestNodes.pop(); } } for (i = 0; i < this.dimensions; i += 1) { if (i === node.dimension) { linearPoint.set(i, point.get(i)); } else { linearPoint.set(i, node.obj.get(i)); } } linearDistance = this.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.get(dimension) < node.obj.get(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(this.root) { nearestSearch(this.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; } balanceFactor() { 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(this.root) / (Math.log(count(this.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; } } } }; function distance2d(a, b){ return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2); } function point2d(x, y, data) { return new Point2d(x, y, data); } function create2d(points) { return new KDTree(points, distance2d, 2); }; // Types kdtree.KDTree = KDTree; // Factory functions kdtree.point2d = point2d; kdtree.create2d = create2d; });