From ffeaf1b81c50e1022b0888914639e93f3bd6da6d Mon Sep 17 00:00:00 2001 From: Izumi Kawashima Date: Fri, 17 May 2019 03:41:54 +0900 Subject: [PATCH] k-nearest neighbors search in R-tree (#725) --- vtm-tests/test/org/oscim/utils/RTreeTest.java | 159 +++++++++++++++++- vtm/src/org/oscim/utils/QuadTree.java | 12 ++ vtm/src/org/oscim/utils/RTree.java | 80 ++++++++- vtm/src/org/oscim/utils/SpatialIndex.java | 5 + 4 files changed, 247 insertions(+), 9 deletions(-) diff --git a/vtm-tests/test/org/oscim/utils/RTreeTest.java b/vtm-tests/test/org/oscim/utils/RTreeTest.java index 06a518e4..f2a3a7fa 100644 --- a/vtm-tests/test/org/oscim/utils/RTreeTest.java +++ b/vtm-tests/test/org/oscim/utils/RTreeTest.java @@ -1,5 +1,7 @@ /* + * Copyright 2014 Hannes Janetzek * Copyright 2016 devemux86 + * Copyright 2019 Izumi Kawashima * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -19,9 +21,11 @@ package org.oscim.utils; import org.junit.Assert; import org.junit.Test; import org.oscim.core.Box; +import org.oscim.core.Point; import org.oscim.utils.SpatialIndex.SearchCb; import java.util.ArrayList; +import java.util.List; import java.util.Random; import static org.junit.Assert.assertEquals; @@ -39,11 +43,17 @@ public class RTreeTest { this.max = max.clone(); } + Item(int xmin, int ymin, int xmax, int ymax, int val) { + this.val = val; + this.min = new double[]{xmin, ymin}; + this.max = new double[]{xmax, ymax}; + } + @Override public String toString() { - // return val + "/" - // + Arrays.toString(min) + "/" - // + Arrays.toString(max); +// return val + "/" +// + Arrays.toString(min) + "/" +// + Arrays.toString(max); return String.valueOf(val); } } @@ -314,8 +324,7 @@ public class RTreeTest { int cnt = 0; - for (@SuppressWarnings("unused") - Item it : t) { + for (@SuppressWarnings("unused") Item it : t) { //System.out.println(it.val); cnt++; } @@ -326,6 +335,146 @@ public class RTreeTest { } + /** + * Use values from https://github.com/mourner/rbush-knn/blob/master/test.js + */ + private List generateKnnTestFixture() { + List items = new ArrayList(); + + items.add(new Item(87, 55, 87, 56, items.size())); + items.add(new Item(38, 13, 39, 16, items.size())); + items.add(new Item(7, 47, 8, 47, items.size())); + items.add(new Item(89, 9, 91, 12, items.size())); + items.add(new Item(4, 58, 5, 60, items.size())); + items.add(new Item(0, 11, 1, 12, items.size())); + items.add(new Item(0, 5, 0, 6, items.size())); + items.add(new Item(69, 78, 73, 78, items.size())); + + items.add(new Item(56, 77, 57, 81, items.size())); + items.add(new Item(23, 7, 24, 9, items.size())); + items.add(new Item(68, 24, 70, 26, items.size())); + items.add(new Item(31, 47, 33, 50, items.size())); + items.add(new Item(11, 13, 14, 15, items.size())); + items.add(new Item(1, 80, 1, 80, items.size())); + items.add(new Item(72, 90, 72, 91, items.size())); + items.add(new Item(59, 79, 61, 83, items.size())); + + items.add(new Item(98, 77, 101, 77, items.size())); + items.add(new Item(11, 55, 14, 56, items.size())); + items.add(new Item(98, 4, 100, 6, items.size())); + items.add(new Item(21, 54, 23, 58, items.size())); + items.add(new Item(44, 74, 48, 74, items.size())); + items.add(new Item(70, 57, 70, 61, items.size())); + items.add(new Item(32, 9, 33, 12, items.size())); + items.add(new Item(43, 87, 44, 91, items.size())); + + items.add(new Item(38, 60, 38, 60, items.size())); + items.add(new Item(62, 48, 66, 50, items.size())); + items.add(new Item(16, 87, 19, 91, items.size())); + items.add(new Item(5, 98, 9, 99, items.size())); + items.add(new Item(9, 89, 10, 90, items.size())); + items.add(new Item(89, 2, 92, 6, items.size())); + items.add(new Item(41, 95, 45, 98, items.size())); + items.add(new Item(57, 36, 61, 40, items.size())); + + items.add(new Item(50, 1, 52, 1, items.size())); + items.add(new Item(93, 87, 96, 88, items.size())); + items.add(new Item(29, 42, 33, 42, items.size())); + items.add(new Item(34, 43, 36, 44, items.size())); + items.add(new Item(41, 64, 42, 65, items.size())); + items.add(new Item(87, 3, 88, 4, items.size())); + items.add(new Item(56, 50, 56, 52, items.size())); + items.add(new Item(32, 13, 35, 15, items.size())); + + items.add(new Item(3, 8, 5, 11, items.size())); + items.add(new Item(16, 33, 18, 33, items.size())); + items.add(new Item(35, 39, 38, 40, items.size())); + items.add(new Item(74, 54, 78, 56, items.size())); + items.add(new Item(92, 87, 95, 90, items.size())); + items.add(new Item(12, 97, 16, 98, items.size())); + items.add(new Item(76, 39, 78, 40, items.size())); + items.add(new Item(16, 93, 18, 95, items.size())); + + items.add(new Item(62, 40, 64, 42, items.size())); + items.add(new Item(71, 87, 71, 88, items.size())); + items.add(new Item(60, 85, 63, 86, items.size())); + items.add(new Item(39, 52, 39, 56, items.size())); + items.add(new Item(15, 18, 19, 18, items.size())); + items.add(new Item(91, 62, 94, 63, items.size())); + items.add(new Item(10, 16, 10, 18, items.size())); + items.add(new Item(5, 86, 8, 87, items.size())); + + items.add(new Item(85, 85, 88, 86, items.size())); + items.add(new Item(44, 84, 44, 88, items.size())); + items.add(new Item(3, 94, 3, 97, items.size())); + items.add(new Item(79, 74, 81, 78, items.size())); + items.add(new Item(21, 63, 24, 66, items.size())); + items.add(new Item(16, 22, 16, 22, items.size())); + items.add(new Item(68, 97, 72, 97, items.size())); + items.add(new Item(39, 65, 42, 65, items.size())); + + items.add(new Item(51, 68, 52, 69, items.size())); + items.add(new Item(61, 38, 61, 42, items.size())); + items.add(new Item(31, 65, 31, 65, items.size())); + items.add(new Item(16, 6, 19, 6, items.size())); + items.add(new Item(66, 39, 66, 41, items.size())); + items.add(new Item(57, 32, 59, 35, items.size())); + items.add(new Item(54, 80, 58, 84, items.size())); + items.add(new Item(5, 67, 7, 71, items.size())); + + items.add(new Item(49, 96, 51, 98, items.size())); + items.add(new Item(29, 45, 31, 47, items.size())); + items.add(new Item(31, 72, 33, 74, items.size())); + items.add(new Item(94, 25, 95, 26, items.size())); + items.add(new Item(14, 7, 18, 8, items.size())); + items.add(new Item(29, 0, 31, 1, items.size())); + items.add(new Item(48, 38, 48, 40, items.size())); + items.add(new Item(34, 29, 34, 32, items.size())); + + items.add(new Item(99, 21, 100, 25, items.size())); + items.add(new Item(79, 3, 79, 4, items.size())); + items.add(new Item(87, 1, 87, 5, items.size())); + items.add(new Item(9, 77, 9, 81, items.size())); + items.add(new Item(23, 25, 25, 29, items.size())); + items.add(new Item(83, 48, 86, 51, items.size())); + items.add(new Item(79, 94, 79, 95, items.size())); + items.add(new Item(33, 95, 33, 99, items.size())); + + items.add(new Item(1, 14, 1, 14, items.size())); + items.add(new Item(33, 77, 34, 77, items.size())); + items.add(new Item(94, 56, 98, 59, items.size())); + items.add(new Item(75, 25, 78, 26, items.size())); + items.add(new Item(17, 73, 20, 74, items.size())); + items.add(new Item(11, 3, 12, 4, items.size())); + items.add(new Item(45, 12, 47, 12, items.size())); + items.add(new Item(38, 39, 39, 39, items.size())); + + items.add(new Item(99, 3, 103, 5, items.size())); + items.add(new Item(41, 92, 44, 96, items.size())); + items.add(new Item(79, 40, 79, 41, items.size())); + items.add(new Item(29, 2, 29, 4, items.size())); + + return items; + } + + @Test + public void shouldWorkKnn() { + List items = generateKnnTestFixture(); + + RTree t = new RTree(); + for (Item item : items) + t.insert(item.min, item.max, item); + + List result = t.searchKNearestNeighbors(new Point(40, 40), 10, Double.POSITIVE_INFINITY, null); + Assert.assertEquals(10, result.size()); + + result = t.searchKNearestNeighbors(new Point(40, 40), 10, 17, null); + Assert.assertEquals(10, result.size()); + + result = t.searchKNearestNeighbors(new Point(40, 60), 90, Double.POSITIVE_INFINITY, result); + Assert.assertEquals(90, result.size()); + } + public static void main(String[] args) { RTreeTest t = new RTreeTest(); t.shouldWork2(); diff --git a/vtm/src/org/oscim/utils/QuadTree.java b/vtm/src/org/oscim/utils/QuadTree.java index fc0db28f..ee25487f 100644 --- a/vtm/src/org/oscim/utils/QuadTree.java +++ b/vtm/src/org/oscim/utils/QuadTree.java @@ -1,6 +1,7 @@ package org.oscim.utils; import org.oscim.core.Box; +import org.oscim.core.Point; import org.oscim.utils.pool.Pool; import org.oscim.utils.quadtree.BoxTree; import org.oscim.utils.quadtree.BoxTree.BoxItem; @@ -78,4 +79,15 @@ public class QuadTree extends BoxTree, T> implements SpatialIndex< boxPool.release(box); return finished; } + + @Override + public List searchKNearestNeighbors(Point center, int k, double maxDistance, List results) { + // TODO + return results; + } + + @Override + public void searchKNearestNeighbors(Point center, int k, double maxDistance, SearchCb cb, Object context) { + // TODO + } } diff --git a/vtm/src/org/oscim/utils/RTree.java b/vtm/src/org/oscim/utils/RTree.java index 04c149e0..fd8cc306 100644 --- a/vtm/src/org/oscim/utils/RTree.java +++ b/vtm/src/org/oscim/utils/RTree.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Hannes Janetzek + * Copyright 2019 Izumi Kawashima * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -17,6 +18,7 @@ package org.oscim.utils; import org.oscim.core.Box; +import org.oscim.core.Point; import org.oscim.utils.RTree.Branch; import org.oscim.utils.RTree.Node; import org.oscim.utils.RTree.Rect; @@ -28,14 +30,13 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.PriorityQueue; /** * Implementation of RTree, a multidimensional bounding rectangle tree. * - * @author 1983 Original algorithm and test code by Antonin Guttman and Michael - * Stonebraker, UC Berkely - * @author 1994 ANCI C ported from original test code by Melinda Green - - * melinda@superliminal.com + * @author 1983 Original algorithm and test code by Antonin Guttman and Michael Stonebraker, UC Berkely + * @author 1994 ANCI C ported from original test code by Melinda Green - melinda@superliminal.com * @author 1995 Sphere volume fix for degeneracy problem submitted by Paul Brook * @author 2004 Templated C++ port by Greg Douglas * @author 2008 Portability issues fixed by Maxence Laurent @@ -69,6 +70,17 @@ public class RTree implements SpatialIndex, Iterable { } } + class KnnItem implements Comparable { + Branch branch; + boolean isLeaf; + double squareDistance; + + @Override + public int compareTo(KnnItem o) { + return Double.compare(squareDistance, o.squareDistance); + } + } + /** * Node for each branch level */ @@ -173,6 +185,10 @@ public class RTree implements SpatialIndex, Iterable { ymax = max[1]; } + double axisDistance(double k, double min, double max) { + return k < min ? min - k : k <= max ? 0 : k - max; + } + /** * Calculate the n-dimensional volume of a rectangle */ @@ -247,6 +263,12 @@ public class RTree implements SpatialIndex, Iterable { add(node.branch[idx]); } } + + double squareDistance(Point xy) { + double dx = axisDistance(xy.x, xmin, xmax); + double dy = axisDistance(xy.y, ymin, ymax); + return dx * dx + dy * dy; + } } /** @@ -370,6 +392,56 @@ public class RTree implements SpatialIndex, Iterable { return results; } + /** + * See https://github.com/mourner/rbush-knn/blob/master/index.js + */ + @Override + public List searchKNearestNeighbors(Point center, int k, double maxDistance, List results) { + if (results == null) + results = new ArrayList<>(16); + + PriorityQueue queue = new PriorityQueue<>(); + double maxSquareDistance = maxDistance * maxDistance; + + Node node = mRoot; + while (node != null) { + for (int idx = 0; idx < node.count; idx++) { + Branch[] branch = node.branch; + double squareDistance = branch[idx].squareDistance(center); + if (squareDistance <= maxSquareDistance) { + KnnItem knnItem = new KnnItem(); + knnItem.branch = branch[idx]; + knnItem.isLeaf = node.level == 0; + knnItem.squareDistance = squareDistance; + queue.add(knnItem); + } + } + + while (!queue.isEmpty() && queue.peek().isLeaf) { + KnnItem knnItem = queue.poll(); + T obj = (T) (knnItem.branch); + results.add(obj); + if (results.size() >= k) + return results; + } + + KnnItem knnItem = queue.poll(); + if (knnItem != null) + node = (Node) knnItem.branch.node; + else + node = null; + } + + return results; + } + + @Override + public void searchKNearestNeighbors(Point center, int k, double maxDistance, SearchCb cb, Object context) { + List results = searchKNearestNeighbors(center, k, maxDistance, null); + for (T result : results) + cb.call(result, context); + } + /** * Count the data elements in this container. This is slow as no internal * counter is maintained. diff --git a/vtm/src/org/oscim/utils/SpatialIndex.java b/vtm/src/org/oscim/utils/SpatialIndex.java index d3180a68..8baf40ed 100644 --- a/vtm/src/org/oscim/utils/SpatialIndex.java +++ b/vtm/src/org/oscim/utils/SpatialIndex.java @@ -1,6 +1,7 @@ package org.oscim.utils; import org.oscim.core.Box; +import org.oscim.core.Point; import java.util.List; @@ -24,6 +25,10 @@ public interface SpatialIndex { public boolean search(Box bbox, SearchCb cb, Object context); + public List searchKNearestNeighbors(Point center, int k, double maxDistance, List results); + + public void searchKNearestNeighbors(Point center, int k, double maxDistance, SearchCb cb, Object context); + public int size(); public void clear();