/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var quickSelect = require("./quickSelect"); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * K-Dimension Tree * * @module echarts/data/KDTree * @author Yi Shen(https://github.com/pissang) */ function Node(axis, data) { this.left = null; this.right = null; this.axis = axis; this.data = data; } /** * @constructor * @alias module:echarts/data/KDTree * @param {Array} points List of points. * each point needs an array property to repesent the actual data * @param {Number} [dimension] * Point dimension. * Default will use the first point's length as dimensiont */ var KDTree = function (points, dimension) { if (!points.length) { return; } if (!dimension) { dimension = points[0].array.length; } this.dimension = dimension; this.root = this._buildTree(points, 0, points.length - 1, 0); // Use one stack to avoid allocation // each time searching the nearest point this._stack = []; // Again avoid allocating a new array // each time searching nearest N points this._nearstNList = []; }; /** * Resursively build the tree */ KDTree.prototype._buildTree = function (points, left, right, axis) { if (right < left) { return null; } var medianIndex = Math.floor((left + right) / 2); medianIndex = quickSelect(points, left, right, medianIndex, function (a, b) { return a.array[axis] - b.array[axis]; }); var median = points[medianIndex]; var node = new Node(axis, median); axis = (axis + 1) % this.dimension; if (right > left) { node.left = this._buildTree(points, left, medianIndex - 1, axis); node.right = this._buildTree(points, medianIndex + 1, right, axis); } return node; }; /** * Find nearest point * @param {Array} target Target point * @param {Function} squaredDistance Squared distance function * @return {Array} Nearest point */ KDTree.prototype.nearest = function (target, squaredDistance) { var curr = this.root; var stack = this._stack; var idx = 0; var minDist = Infinity; var nearestNode = null; if (curr.data !== target) { minDist = squaredDistance(curr.data, target); nearestNode = curr; } if (target.array[curr.axis] < curr.data.array[curr.axis]) { // Left first curr.right && (stack[idx++] = curr.right); curr.left && (stack[idx++] = curr.left); } else { // Right first curr.left && (stack[idx++] = curr.left); curr.right && (stack[idx++] = curr.right); } while (idx--) { curr = stack[idx]; var currDist = target.array[curr.axis] - curr.data.array[curr.axis]; var isLeft = currDist < 0; var needsCheckOtherSide = false; currDist = currDist * currDist; // Intersecting right hyperplane with minDist hypersphere if (currDist < minDist) { currDist = squaredDistance(curr.data, target); if (currDist < minDist && curr.data !== target) { minDist = currDist; nearestNode = curr; } needsCheckOtherSide = true; } if (isLeft) { if (needsCheckOtherSide) { curr.right && (stack[idx++] = curr.right); } // Search in the left area curr.left && (stack[idx++] = curr.left); } else { if (needsCheckOtherSide) { curr.left && (stack[idx++] = curr.left); } // Search the right area curr.right && (stack[idx++] = curr.right); } } return nearestNode.data; }; KDTree.prototype._addNearest = function (found, dist, node) { var nearestNList = this._nearstNList; // Insert to the right position // Sort from small to large for (var i = found - 1; i > 0; i--) { if (dist >= nearestNList[i - 1].dist) { break; } else { nearestNList[i].dist = nearestNList[i - 1].dist; nearestNList[i].node = nearestNList[i - 1].node; } } nearestNList[i].dist = dist; nearestNList[i].node = node; }; /** * Find nearest N points * @param {Array} target Target point * @param {number} N * @param {Function} squaredDistance Squared distance function * @param {Array} [output] Output nearest N points */ KDTree.prototype.nearestN = function (target, N, squaredDistance, output) { if (N <= 0) { output.length = 0; return output; } var curr = this.root; var stack = this._stack; var idx = 0; var nearestNList = this._nearstNList; for (var i = 0; i < N; i++) { // Allocate if (!nearestNList[i]) { nearestNList[i] = {}; } nearestNList[i].dist = 0; nearestNList[i].node = null; } var currDist = squaredDistance(curr.data, target); var found = 0; if (curr.data !== target) { found++; this._addNearest(found, currDist, curr); } if (target.array[curr.axis] < curr.data.array[curr.axis]) { // Left first curr.right && (stack[idx++] = curr.right); curr.left && (stack[idx++] = curr.left); } else { // Right first curr.left && (stack[idx++] = curr.left); curr.right && (stack[idx++] = curr.right); } while (idx--) { curr = stack[idx]; var currDist = target.array[curr.axis] - curr.data.array[curr.axis]; var isLeft = currDist < 0; var needsCheckOtherSide = false; currDist = currDist * currDist; // Intersecting right hyperplane with minDist hypersphere if (found < N || currDist < nearestNList[found - 1].dist) { currDist = squaredDistance(curr.data, target); if ((found < N || currDist < nearestNList[found - 1].dist) && curr.data !== target) { if (found < N) { found++; } this._addNearest(found, currDist, curr); } needsCheckOtherSide = true; } if (isLeft) { if (needsCheckOtherSide) { curr.right && (stack[idx++] = curr.right); } // Search in the left area curr.left && (stack[idx++] = curr.left); } else { if (needsCheckOtherSide) { curr.left && (stack[idx++] = curr.left); } // Search the right area curr.right && (stack[idx++] = curr.right); } } // Copy to output for (var i = 0; i < found; i++) { output[i] = nearestNList[i].node.data; } output.length = found; return output; }; var _default = KDTree; module.exports = _default;