using System; using System.Collections.Generic; using System.Collections.ObjectModel; using GeoAPI.Geometries; using GisSharpBlog.NetTopologySuite.Geometries; namespace GisSharpBlog.NetTopologySuite.Index.KdTree { /// /// An implementation of a 2-D KD-Tree. KD-trees provide fast range searching on point data. /// /// /// This implementation supports detecting and snapping points which are closer than a given /// tolerance value. If the same point (up to tolerance) is inserted more than once a new node is /// not created but the count of the existing node is incremented. /// /// The type of the user data object /// David Skea /// Martin Davis public class KdTree where T : class { private KdNode _root; // ReSharper disable once UnusedField.Compiler private KdNode _last = null; private long _numberOfNodes; private readonly double _tolerance; /// /// Creates a new instance of a KdTree with a snapping tolerance of 0.0. /// (I.e. distinct points will not be snapped) /// public KdTree() : this(0.0) { } /// /// Creates a new instance of a KdTree, specifying a snapping distance tolerance. /// Points which lie closer than the tolerance to a point already /// in the tree will be treated as identical to the existing point. /// /// The tolerance distance for considering two points equal public KdTree(double tolerance) { _tolerance = tolerance; } /// /// Tests whether the index contains any items. /// public bool IsEmpty { get { if (_root == null) return true; return false; } } /// /// Inserts a new point in the kd-tree, with no data. /// /// The point to insert /// The kdnode containing the point public KdNode Insert(ICoordinate p) { return Insert(p, null); } /// /// Inserts a new point into the kd-tree. /// /// The point to insert /// A data item for the point /// /// A new KdNode if a new point is inserted, else an existing /// node is returned with its counter incremented. This can be checked /// by testing returnedNode.getCount() > 1. /// public KdNode Insert(ICoordinate p, T data) { if (_root == null) { _root = new KdNode(p, data); return _root; } var currentNode = _root; var leafNode = _root; var isOddLevel = true; var isLessThan = true; /** * Traverse the tree, * first cutting the plane left-right (by X ordinate) * then top-bottom (by Y ordinate) */ while (currentNode != _last) { // test if point is already a node if (currentNode != null) { var isInTolerance = p.Distance(currentNode.Coordinate) <= _tolerance; // check if point is already in tree (up to tolerance) and if so simply // return existing node if (isInTolerance) { currentNode.Increment(); return currentNode; } } if (isOddLevel) { // ReSharper disable once PossibleNullReferenceException isLessThan = p.X < currentNode.X; } else { // ReSharper disable once PossibleNullReferenceException isLessThan = p.Y < currentNode.Y; } leafNode = currentNode; currentNode = isLessThan ? currentNode.Left : currentNode.Right; isOddLevel = !isOddLevel; } // no node found, add new leaf node to tree _numberOfNodes = _numberOfNodes + 1; var node = new KdNode(p, data); node.Left = null; node.Right = null; if (isLessThan) { leafNode.Left = node; } else { leafNode.Right = node; } return node; } private static void QueryNode(KdNode currentNode, KdNode bottomNode, Envelope queryEnv, bool odd, ICollection> result) { if (currentNode == null) return; if (currentNode == bottomNode) return; double min; double max; double discriminant; if (odd) { min = queryEnv.MinX; max = queryEnv.MaxX; discriminant = currentNode.X; } else { min = queryEnv.MinY; max = queryEnv.MaxY; discriminant = currentNode.Y; } bool searchLeft = min < discriminant; bool searchRight = discriminant <= max; if (searchLeft) { QueryNode(currentNode.Left, bottomNode, queryEnv, !odd, result); } if (queryEnv.Contains(currentNode.Coordinate)) { result.Add(currentNode); } if (searchRight) { QueryNode(currentNode.Right, bottomNode, queryEnv, !odd, result); } } /// /// Performs a range search of the points in the index. /// /// The range rectangle to query /// A collection of the KdNodes found public ICollection> Query(Envelope queryEnv) { KdNode last = null; ICollection> result = new Collection>(); QueryNode(_root, _last, queryEnv, true, result); return result; } /// /// Performs a range search of the points in the index. /// /// The range rectangle to query /// A collection to accumulate the result nodes into public void Query(Envelope queryEnv, ICollection> result) { QueryNode(_root, _last, queryEnv, true, result); } private static void NearestNeighbor(KdNode currentNode, KdNode bottomNode, ICoordinate queryCoordinate, ref KdNode closestNode, ref double closestDistanceSq) { while (true) { if (currentNode == null) return; if (currentNode == bottomNode) return; var distSq = Math.Pow(currentNode.X - queryCoordinate.X, 2) + Math.Pow(currentNode.Y - queryCoordinate.Y, 2); if (distSq < closestDistanceSq) { closestNode = currentNode; closestDistanceSq = distSq; } var searchLeft = false; var searchRight = false; if (currentNode.Left != null) searchLeft = (Math.Pow(currentNode.Left.X - queryCoordinate.X, 2) + Math.Pow(currentNode.Left.Y - queryCoordinate.Y, 2)) < closestDistanceSq; if (currentNode.Right != null) searchRight = (Math.Pow(currentNode.Right.X - queryCoordinate.X, 2) + Math.Pow(currentNode.Right.Y - queryCoordinate.Y, 2)) < closestDistanceSq; if (searchLeft) { NearestNeighbor(currentNode.Left, bottomNode, queryCoordinate, ref closestNode, ref closestDistanceSq); } if (searchRight) { currentNode = currentNode.Right; continue; } break; } } /// /// Performs a nearest neighbor search of the points in the index. /// /// The point to search the nearset neighbor for public KdNode NearestNeighbor(ICoordinate coord) { KdNode result = null; var closestDistSq = double.MaxValue; NearestNeighbor(_root, _last, coord, ref result, ref closestDistSq); return result; } } }