/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2016, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.process.spatialstatistics.core; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.util.logging.Logging; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.index.strtree.AbstractNode; import com.vividsolutions.jts.index.strtree.Boundable; import com.vividsolutions.jts.index.strtree.ItemBoundable; import com.vividsolutions.jts.index.strtree.ItemDistance; import com.vividsolutions.jts.index.strtree.STRtree; import com.vividsolutions.jts.util.PriorityQueue; /** * K-Nearest Neighbor Search. * * @author Minpa Lee, MangoSystem * * @source https://github.com/jiayuasu/JTSplus/blob/master/src/main/java/com/vividsolutions/jts/index/strtree/STRtree.java */ public class KnnSearch { protected static final Logger LOGGER = Logging.getLogger(KnnSearch.class); private STRtree spatialIndex; public KnnSearch(STRtree spatialIndex) { this.spatialIndex = spatialIndex; } public Object[] kNearestNeighbour(Envelope env, Object item, ItemDistance itemDistance, int k) { Boundable bnd = new ItemBoundable(env, item); BoundablePair bp = new BoundablePair(spatialIndex.getRoot(), bnd, itemDistance); return nearestNeighbour(bp, Double.POSITIVE_INFINITY, k); } private Object[] nearestNeighbour(BoundablePair initBndPair, double maxDistance, int k) { double distanceLowerBound = maxDistance; // initialize internal structures PriorityQueue priQ = new PriorityQueue(); // initialize queue priQ.add(initBndPair); List<Object> kNearestNeighbors = new ArrayList<Object>(); List<Double> kNearestDistances = new ArrayList<Double>(); while (!priQ.isEmpty() && distanceLowerBound >= 0.0) { // pop head of queue and expand one side of pair BoundablePair bndPair = (BoundablePair) priQ.poll(); double currentDistance = bndPair.getDistance(); /** * If the distance for the first node in the queue is >= the current maximum distance in the k queue , all other nodes in the queue must * also have a greater distance. So the current minDistance must be the true minimum, and we are done. */ if (currentDistance >= distanceLowerBound && kNearestDistances.size() >= k) { break; } /** * If the pair members are leaves then their distance is the exact lower bound. Update the distanceLowerBound to reflect this (which must * be smaller, due to the test immediately prior to this). */ if (bndPair.isLeaves()) { if (kNearestDistances.size() > 0 && kNearestDistances.size() < k) { int position = Collections.binarySearch(kNearestDistances, currentDistance); if (position < 0) { position = -position - 1; } kNearestNeighbors.add(position, ((ItemBoundable) bndPair.getBoundable(0)).getItem()); kNearestDistances.add(position, currentDistance); } else if (kNearestDistances.size() >= k) { if (currentDistance < kNearestDistances.get(kNearestDistances.size() - 1)) { int position = Collections.binarySearch(kNearestDistances, currentDistance); if (position < 0) { position = -position - 1; } kNearestNeighbors.add(position, ((ItemBoundable) bndPair.getBoundable(0)).getItem()); kNearestDistances.add(position, currentDistance); // assert kNearestNeighbors.size() > k; kNearestNeighbors.remove(kNearestNeighbors.size() - 1); kNearestDistances.remove(kNearestDistances.size() - 1); } } else if (kNearestDistances.size() == 0) { kNearestNeighbors.add(((ItemBoundable) bndPair.getBoundable(0)).getItem()); kNearestDistances.add(currentDistance); } else { try { throw new Exception("Should never reach here"); } catch (Exception e1) { LOGGER.log(Level.WARNING, e1.getMessage()); } } distanceLowerBound = kNearestDistances.get(kNearestDistances.size() - 1); } else { /** * Otherwise, expand one side of the pair, (the choice of which side to expand is heuristically determined) and insert the new * expanded pairs into the queue */ bndPair.expandToQueue(priQ, distanceLowerBound); } } // done - return items with min distance return kNearestNeighbors.toArray(new Object[kNearestNeighbors.size()]); } /** * A pair of {@link Boundable}s, whose leaf items support a distance metric between them. Used to compute the distance between the members, and to * expand a member relative to the other in order to produce new branches of the Branch-and-Bound evaluation tree. Provides an ordering based on * the distance between the members, which allows building a priority queue by minimum distance. * * @author Martin Davis * * @source https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/index/strtree/BoundablePair.java * */ @SuppressWarnings("rawtypes") static final class BoundablePair implements Comparable { private Boundable boundable1; private Boundable boundable2; private double distance; private ItemDistance itemDistance; public BoundablePair(Boundable boundable1, Boundable boundable2, ItemDistance itemDistance) { this.boundable1 = boundable1; this.boundable2 = boundable2; this.itemDistance = itemDistance; distance = distance(); } /** * Gets one of the member {@link Boundable}s in the pair (indexed by [0, 1]). * * @param i the index of the member to return (0 or 1) * @return the chosen member */ public Boundable getBoundable(int i) { if (i == 0) return boundable1; return boundable2; } /** * Computes the distance between the {@link Boundable}s in this pair. The boundables are either composites or leaves. If either is composite, * the distance is computed as the minimum distance between the bounds. If both are leaves, the distance is computed by * {@link #itemDistance(ItemBoundable, ItemBoundable)}. * * @return */ private double distance() { // if items, compute exact distance if (isLeaves()) { return itemDistance .distance((ItemBoundable) boundable1, (ItemBoundable) boundable2); } // otherwise compute distance between bounds of boundables return ((Envelope) boundable1.getBounds()) .distance(((Envelope) boundable2.getBounds())); } /** * Gets the minimum possible distance between the Boundables in this pair. If the members are both items, this will be the exact distance * between them. Otherwise, this distance will be a lower bound on the distances between the items in the members. * * @return the exact or lower bound distance for this pair */ public double getDistance() { return distance; } /** * Compares two pairs based on their minimum distances */ public int compareTo(Object o) { BoundablePair nd = (BoundablePair) o; if (distance < nd.distance) return -1; if (distance > nd.distance) return 1; return 0; } /** * Tests if both elements of the pair are leaf nodes * * @return true if both pair elements are leaf nodes */ public boolean isLeaves() { return !(isComposite(boundable1) || isComposite(boundable2)); } public static boolean isComposite(Object item) { return (item instanceof AbstractNode); } private static double area(Boundable b) { return ((Envelope) b.getBounds()).getArea(); } /** * For a pair which is not a leaf (i.e. has at least one composite boundable) computes a list of new pairs from the expansion of the larger * boundable. * */ public void expandToQueue(PriorityQueue priQ, double minDistance) { boolean isComp1 = isComposite(boundable1); boolean isComp2 = isComposite(boundable2); /** * HEURISTIC: If both boundable are composite, choose the one with largest area to expand. Otherwise, simply expand whichever is * composite. */ if (isComp1 && isComp2) { if (area(boundable1) > area(boundable2)) { expand(boundable1, boundable2, priQ, minDistance); return; } else { expand(boundable2, boundable1, priQ, minDistance); return; } } else if (isComp1) { expand(boundable1, boundable2, priQ, minDistance); return; } else if (isComp2) { expand(boundable2, boundable1, priQ, minDistance); return; } throw new IllegalArgumentException("neither boundable is composite"); } private void expand(Boundable bndComposite, Boundable bndOther, PriorityQueue priQ, double minDistance) { List children = ((AbstractNode) bndComposite).getChildBoundables(); for (Iterator i = children.iterator(); i.hasNext();) { Boundable child = (Boundable) i.next(); BoundablePair bp = new BoundablePair(child, bndOther, itemDistance); // only add to queue if this pair might contain the closest points // MD - it's actually faster to construct the object rather than called distance(child, bndOther)! if (bp.getDistance() < minDistance) { priQ.add(bp); } } } } }