| /** @module kd |
| * A k-d tree implementation, which is used to find the closest point in |
| * something like a 2D scatter plot. See https://en.wikipedia.org/wiki/K-d_tree |
| * for more details. |
| * |
| * Forked from https://github.com/Pandinosaurus/kd-tree-javascript and |
| * then massively trimmed down to just find the single closest point, and also |
| * ported to ES6 syntax, then ported to TypeScript. |
| * |
| * https://github.com/Pandinosaurus/kd-tree-javascript is a fork of |
| * https://github.com/ubilabs/kd-tree-javascript |
| * |
| * @author Mircea Pricop <pricop@ubilabs.net>, 2012 |
| * @author Martin Kleppe <kleppe@ubilabs.net>, 2012 |
| * @author Ubilabs http://ubilabs.net, 2012 |
| * @license MIT License <http://www.opensource.org/licenses/mit-license.php> |
| */ |
| |
| export interface KDPoint { |
| x: number; |
| y: number; |
| } |
| |
| /** @class A single node in the k-d Tree. */ |
| class Node<Point extends KDPoint> { |
| obj: Point; |
| |
| left: Node<Point> | null = null; |
| |
| right: Node<Point> | null = null; |
| |
| parent: Node<Point> | null; |
| |
| dimension: number; |
| |
| constructor(obj: Point, dimension: number, parent: Node<Point> | null) { |
| this.obj = obj; |
| this.parent = parent; |
| this.dimension = dimension; |
| } |
| } |
| |
| type Dimensions = keyof KDPoint; |
| |
| /** |
| * @class The k-d tree. |
| */ |
| export class KDTree<Point extends KDPoint> { |
| private dimensions: Dimensions[]; |
| |
| private root: Node<Point> | null; |
| |
| private metric: (a: KDPoint, b: KDPoint)=> number; |
| |
| /** |
| * The constructor. |
| * |
| * @param {Array} points - An array of points, something with the shape |
| * {x:x, y:y}. |
| * @param {function} metric - A function that calculates the distance |
| * between two points. |
| * @param {Array} dimensions - The dimensions to use in our points, for |
| * example ['x', 'y']. |
| */ |
| constructor( |
| points: Point[], |
| metric: (a: KDPoint, b: KDPoint)=> number, |
| dimensions: Dimensions[], |
| ) { |
| this.dimensions = dimensions; |
| this.metric = metric; |
| this.root = this._buildTree(points, 0, null); |
| } |
| |
| /** |
| * Find the nearest Node to the given point. |
| * |
| * @param {Object} point - {x:x, y:y} |
| * @returns {Object} The closest point object passed into the constructor. |
| * We pass back the original object since it might have extra info |
| * beyond just the coordinates, such as trace id. |
| */ |
| nearest(point: KDPoint): Point { |
| let bestNode = { |
| node: this.root, |
| distance: Number.MAX_VALUE, |
| }; |
| |
| const saveNode = (node: Node<Point>, distance: number) => { |
| bestNode = { |
| node: node, |
| distance: distance, |
| }; |
| }; |
| |
| const nearestSearch = (node: Node<Point>) => { |
| const dimension = this.dimensions[node.dimension]; |
| const ownDistance = this.metric(point, node.obj); |
| |
| if (node.right === null && node.left === null) { |
| if (ownDistance < bestNode.distance) { |
| saveNode(node, ownDistance); |
| } |
| return; |
| } |
| |
| let bestChild = null; |
| let otherChild = null; |
| // If we get here we know that at least one of .left and .right is |
| // non-null, so bestChild is guaranteed to be non-null. |
| 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; |
| otherChild = node.right; |
| } else { |
| bestChild = node.right; |
| otherChild = node.left; |
| } |
| |
| nearestSearch(bestChild!); |
| |
| if (ownDistance < bestNode.distance) { |
| saveNode(node, ownDistance); |
| } |
| |
| // Find distance to hyperplane. |
| const pointOnHyperplane = { |
| x: 0, |
| y: 0, |
| }; |
| for (let i = 0; i < this.dimensions.length; i++) { |
| if (i === node.dimension) { |
| pointOnHyperplane[this.dimensions[i]] = point[this.dimensions[i]]; |
| } else { |
| pointOnHyperplane[this.dimensions[i]] = node.obj[this.dimensions[i]]; |
| } |
| } |
| |
| // If the hyperplane is closer than the current best point then we |
| // need to search down the other side of the tree. |
| if ( |
| otherChild !== null |
| && this.metric(pointOnHyperplane, node.obj) < bestNode.distance |
| ) { |
| nearestSearch(otherChild); |
| } |
| }; |
| |
| if (this.root) { |
| nearestSearch(this.root); |
| } |
| |
| return bestNode.node!.obj; |
| } |
| |
| /** |
| * Builds the from parent Node on down. |
| * |
| * @param {Array} points - An array of {x:x, y:y}. |
| * @param {Number} depth - The current depth from the root node. |
| * @param {Node} parent - The parent Node. |
| */ |
| private _buildTree( |
| points: Point[], |
| depth: number, |
| parent: Node<Point> | null, |
| ): Node<Point> | null { |
| // Every step deeper into the tree we switch to using another axis. |
| const dim = depth % this.dimensions.length; |
| |
| if (points.length === 0) { |
| return null; |
| } |
| if (points.length === 1) { |
| return new Node(points[0], dim, parent); |
| } |
| |
| points.sort((a, b) => a[this.dimensions[dim]] - b[this.dimensions[dim]]); |
| |
| const median = Math.floor(points.length / 2); |
| const node = new Node(points[median], dim, parent); |
| node.left = this._buildTree(points.slice(0, median), depth + 1, node); |
| node.right = this._buildTree(points.slice(median + 1), depth + 1, node); |
| |
| return node; |
| } |
| } |