/* * 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. */ import quickSelect from './quickSelect'; import { VectorArray } from 'zrender/src/core/vector'; type KDTreePoint = { array: VectorArray }; class KDTreeNode { left: KDTreeNode; right: KDTreeNode; axis: number; data: T; constructor(axis: number, data: T) { 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 represent the actual data * @param {Number} [dimension] * Point dimension. * Default will use the first point's length as dimension. */ class KDTree { dimension: number; root: KDTreeNode; // Use one stack to avoid allocation // each time searching the nearest point private _stack: KDTreeNode[] = []; // Again avoid allocating a new array // each time searching nearest N points private _nearstNList: { dist: number, node: KDTreeNode }[] = []; constructor(points: T[], dimension?: number) { if (!points.length) { return; } if (!dimension) { dimension = points[0].array.length; } this.dimension = dimension; this.root = this._buildTree(points, 0, points.length - 1, 0); } /** * Recursively build the tree. */ private _buildTree(points: T[], left: number, right: number, axis: number): KDTreeNode { if (right < left) { return null; } let medianIndex = Math.floor((left + right) / 2); medianIndex = quickSelect( points, left, right, medianIndex, function (a: T, b: T) { return a.array[axis] - b.array[axis]; } ); const median = points[medianIndex]; const node = new KDTreeNode(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 target Target point * @param squaredDistance Squared distance function * @return Nearest point */ nearest(target: T, squaredDistance: (a: T, b: T) => number) { let curr = this.root; const stack = this._stack; let idx = 0; let minDist = Infinity; let 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]; let currDist = target.array[curr.axis] - curr.data.array[curr.axis]; const isLeft = currDist < 0; let 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; }; _addNearest(found: number, dist: number, node: KDTreeNode) { const nearestNList = this._nearstNList; let i = found - 1; // Insert to the right position // Sort from small to large for (; 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 target Target point * @param N * @param squaredDistance Squared distance function * @param output Output nearest N points */ nearestN( target: T, N: number, squaredDistance: (a: T, b: T) => number, output: T[] ) { if (N <= 0) { output.length = 0; return output; } let curr = this.root; const stack = this._stack; let idx = 0; const nearestNList = this._nearstNList; for (let i = 0; i < N; i++) { // Allocate if (!nearestNList[i]) { nearestNList[i] = { dist: 0, node: null }; } nearestNList[i].dist = 0; nearestNList[i].node = null; } const currDist = squaredDistance(curr.data, target); let 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]; let currDist = target.array[curr.axis] - curr.data.array[curr.axis]; const isLeft = currDist < 0; let 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 (let i = 0; i < found; i++) { output[i] = nearestNList[i].node.data; } output.length = found; return output; } } export default KDTree;