diff --git a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java index f596673a8b..f6f166712a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java +++ b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java @@ -12,16 +12,14 @@ package org.locationtech.jts.index.hprtree; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.index.ArrayListVisitor; import org.locationtech.jts.index.ItemVisitor; import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.IntArrayList; /** * A Hilbert-Packed R-tree. This is a static R-tree @@ -59,28 +57,32 @@ * @author Martin Davis * */ -public class HPRtree +public class HPRtree implements SpatialIndex { private static final int ENV_SIZE = 4; private static final int HILBERT_LEVEL = 12; - private static int DEFAULT_NODE_CAPACITY = 16; + private static final int DEFAULT_NODE_CAPACITY = 16; - private List items = new ArrayList(); - - private int nodeCapacity = DEFAULT_NODE_CAPACITY; + private List itemsToLoad = new ArrayList<>(); + + private final int nodeCapacity; + + private int numItems = 0; - private Envelope totalExtent = new Envelope(); + private final Envelope totalExtent = new Envelope(); private int[] layerStartIndex; private double[] nodeBounds; - private boolean isBuilt = false; + private double[] itemBounds; - //public int nodeIntersectsCount; + private Object[] itemValues; + + private volatile boolean isBuilt = false; /** * Creates a new index with the default node capacity. @@ -104,7 +106,7 @@ public HPRtree(int nodeCapacity) { * @return the number of items */ public int size() { - return items.size(); + return numItems; } @Override @@ -112,7 +114,8 @@ public void insert(Envelope itemEnv, Object item) { if (isBuilt) { throw new IllegalStateException("Cannot insert items after tree is built."); } - items.add( new Item(itemEnv, item) ); + numItems++; + itemsToLoad.add( new Item(itemEnv, item) ); totalExtent.expandToInclude(itemEnv); } @@ -153,7 +156,7 @@ private void queryTopLayer(Envelope searchEnv, ItemVisitor visitor) { private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemVisitor visitor) { int layerStart = layerStartIndex[layerIndex]; int nodeIndex = layerStart + nodeOffset; - if (! intersects(nodeIndex, searchEnv)) return; + if (! intersects(nodeBounds, nodeIndex, searchEnv)) return; if (layerIndex == 0) { int childNodesOffset = nodeOffset / ENV_SIZE * nodeCapacity; queryItems(childNodesOffset, searchEnv, visitor); @@ -164,12 +167,11 @@ private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemV } } - private boolean intersects(int nodeIndex, Envelope env) { - //nodeIntersectsCount++; - boolean isBeyond = (env.getMaxX() < nodeBounds[nodeIndex]) - || (env.getMaxY() < nodeBounds[nodeIndex+1]) - || (env.getMinX() > nodeBounds[nodeIndex+2]) - || (env.getMinY() > nodeBounds[nodeIndex+3]); + private static boolean intersects(double[] bounds, int nodeIndex, Envelope env) { + boolean isBeyond = (env.getMaxX() < bounds[nodeIndex]) + || (env.getMaxY() < bounds[nodeIndex+1]) + || (env.getMinX() > bounds[nodeIndex+2]) + || (env.getMinY() > bounds[nodeIndex+3]); return ! isBeyond; } @@ -187,34 +189,14 @@ private void queryNodeChildren(int layerIndex, int blockOffset, Envelope searchE private void queryItems(int blockStart, Envelope searchEnv, ItemVisitor visitor) { for (int i = 0; i < nodeCapacity; i++) { - int itemIndex = blockStart + i; + int itemIndex = blockStart + i; // don't query past end of items - if (itemIndex >= items.size()) break; - - // visit the item if its envelope intersects search env - Item item = items.get(itemIndex); - //nodeIntersectsCount++; - if (intersects( item.getEnvelope(), searchEnv) ) { - //if (item.getEnvelope().intersects(searchEnv)) { - visitor.visitItem(item.getItem()); + if (itemIndex >= numItems) break; + if (intersects(itemBounds, itemIndex * ENV_SIZE, searchEnv)) { + visitor.visitItem(itemValues[itemIndex]); } } } - - /** - * Tests whether two envelopes intersect. - * Avoids the null check in {@link Envelope#intersects(Envelope)}. - * - * @param env1 an envelope - * @param env2 an envelope - * @return true if the envelopes intersect - */ - private static boolean intersects(Envelope env1, Envelope env2) { - return !(env2.getMinX() > env1.getMaxX() || - env2.getMaxX() < env1.getMinX() || - env2.getMinY() > env1.getMaxY() || - env2.getMaxY() < env1.getMinY()); - } private int layerSize(int layerIndex) { int layerStart = layerStartIndex[layerIndex]; @@ -231,47 +213,55 @@ public boolean remove(Envelope itemEnv, Object item) { /** * Builds the index, if not already built. */ - public synchronized void build() { + public void build() { // skip if already built - if (isBuilt) return; - isBuilt = true; + if (!isBuilt) { + synchronized (this) { + if (!isBuilt) { + prepareIndex(); + prepareItems(); + this.isBuilt = true; + } + } + } + } + + private void prepareIndex() { // don't need to build an empty or very small tree - if (items.size() <= nodeCapacity) return; + if (itemsToLoad.size() <= nodeCapacity) return; sortItems(); - //dumpItems(items); - - layerStartIndex = computeLayerIndices(items.size(), nodeCapacity); + + layerStartIndex = computeLayerIndices(numItems, nodeCapacity); // allocate storage int nodeCount = layerStartIndex[ layerStartIndex.length - 1 ] / 4; nodeBounds = createBoundsArray(nodeCount); - + // compute tree nodes computeLeafNodes(layerStartIndex[1]); for (int i = 1; i < layerStartIndex.length - 1; i++) { computeLayerNodes(i); } - //dumpNodes(); } - /* - private void dumpNodes() { - GeometryFactory fact = new GeometryFactory(); - for (int i = 0; i < nodeMinX.length; i++) { - Envelope env = new Envelope(nodeMinX[i], nodeMaxX[i], nodeMinY[i], nodeMaxY[i]);; - System.out.println(fact.toGeometry(env)); + private void prepareItems() { + // copy item contents out to arrays for querying + int boundsIndex = 0; + int valueIndex = 0; + itemBounds = new double[itemsToLoad.size() * 4]; + itemValues = new Object[itemsToLoad.size()]; + for (Item item : itemsToLoad) { + Envelope envelope = item.getEnvelope(); + itemBounds[boundsIndex++] = envelope.getMinX(); + itemBounds[boundsIndex++] = envelope.getMinY(); + itemBounds[boundsIndex++] = envelope.getMaxX(); + itemBounds[boundsIndex++] = envelope.getMaxY(); + itemValues[valueIndex++] = item.getItem(); } + // and let GC free the original list + itemsToLoad = null; } - private static void dumpItems(List items) { - GeometryFactory fact = new GeometryFactory(); - for (Item item : items) { - Envelope env = item.getEnvelope(); - System.out.println(fact.toGeometry(env)); - } - } - */ - private static double[] createBoundsArray(int size) { double[] a = new double[4*size]; for (int i = 0; i < size; i++) { @@ -292,7 +282,6 @@ private void computeLayerNodes(int layerIndex) { for (int i = 0; i < layerSize; i += ENV_SIZE) { int childStart = childLayerStart + nodeCapacity * i; computeNodeBounds(layerStart + i, childStart, childLayerEnd); - //System.out.println("Layer: " + layerIndex + " node: " + i + " - " + getNodeEnvelope(layerStart + i)); } } @@ -313,8 +302,8 @@ private void computeLeafNodes(int layerSize) { private void computeLeafNodeBounds(int nodeIndex, int blockStart) { for (int i = 0; i <= nodeCapacity; i++ ) { int itemIndex = blockStart + i; - if (itemIndex >= items.size()) break; - Envelope env = items.get(itemIndex).getEnvelope(); + if (itemIndex >= itemsToLoad.size()) break; + Envelope env = itemsToLoad.get(itemIndex).getEnvelope(); updateNodeBounds(nodeIndex, env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()); } } @@ -325,13 +314,9 @@ private void updateNodeBounds(int nodeIndex, double minX, double minY, double ma if (maxX > nodeBounds[nodeIndex+2]) nodeBounds[nodeIndex+2] = maxX; if (maxY > nodeBounds[nodeIndex+3]) nodeBounds[nodeIndex+3] = maxY; } - - private Envelope getNodeEnvelope(int i) { - return new Envelope(nodeBounds[i], nodeBounds[i+1], nodeBounds[i+2], nodeBounds[i+3]); - } private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { - List layerIndexList = new ArrayList(); + IntArrayList layerIndexList = new IntArrayList(); int layerSize = itemSize; int index = 0; do { @@ -339,7 +324,7 @@ private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { layerSize = numNodesToCover(layerSize, nodeCapacity); index += ENV_SIZE * layerSize; } while (layerSize > 1); - return toIntArray(layerIndexList); + return layerIndexList.toArray(); } /** @@ -356,14 +341,6 @@ private static int numNodesToCover(int nChild, int nodeCapacity) { if (total == nChild) return mult; return mult + 1; } - - private static int[] toIntArray(List list) { - int[] array = new int[list.size()]; - for (int i = 0; i < array.length; i++) { - array[i] = list.get(i); - } - return array; - } /** * Gets the extents of the internal index nodes @@ -383,24 +360,46 @@ public Envelope[] getBounds() { } private void sortItems() { - ItemComparator comp = new ItemComparator(new HilbertEncoder(HILBERT_LEVEL, totalExtent)); - Collections.sort(items, comp); + HilbertEncoder encoder = new HilbertEncoder(HILBERT_LEVEL, totalExtent); + int[] hilbertValues = new int[itemsToLoad.size()]; + int pos = 0; + for (Item item : itemsToLoad) { + hilbertValues[pos++] = encoder.encode(item.getEnvelope()); + } + quickSortItemsIntoNodes(hilbertValues, 0, itemsToLoad.size() - 1); } - - static class ItemComparator implements Comparator { - private HilbertEncoder encoder; - - public ItemComparator(HilbertEncoder encoder) { - this.encoder = encoder; + private void quickSortItemsIntoNodes(int[] values, int lo, int hi) { + // stop sorting when left/right pointers are within the same node + // because queryItems just searches through them all sequentially + if (lo / nodeCapacity < hi / nodeCapacity) { + int pivot = hoarePartition(values, lo, hi); + quickSortItemsIntoNodes(values, lo, pivot); + quickSortItemsIntoNodes(values, pivot + 1, hi); } + } - @Override - public int compare(Item item1, Item item2) { - int hcode1 = encoder.encode(item1.getEnvelope()); - int hcode2 = encoder.encode(item2.getEnvelope()); - return Integer.compare(hcode1, hcode2); + private int hoarePartition(int[] values, int lo, int hi) { + int pivot = values[(lo + hi) >> 1]; + int i = lo - 1; + int j = hi + 1; + + while (true) { + do i++; while (values[i] < pivot); + do j--; while (values[j] > pivot); + if (i >= j) return j; + swapItems(values, i, j); } } + private void swapItems(int[] values, int i, int j) { + Item tmpItemp = itemsToLoad.get(i); + itemsToLoad.set(i, itemsToLoad.get(j)); + itemsToLoad.set(j, tmpItemp); + + int tmpValue = values[i]; + values[i] = values[j]; + values[j] = tmpValue; + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java index 9536abc3d5..399efda0d9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java @@ -21,14 +21,14 @@ import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainBuilder; import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; -import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.index.hprtree.HPRtree; /** * Nodes a set of {@link SegmentString}s using a index based * on {@link MonotoneChain}s and a {@link SpatialIndex}. * The {@link SpatialIndex} used should be something that supports * envelope (range) queries efficiently (such as a Quadtree} - * or {@link STRtree} (which is the default index provided). + * or {@link HPRtree} (which is the default index provided). *

* The noder supports using an overlap tolerance distance . * This allows determining segment intersection using a buffer for uses @@ -40,7 +40,7 @@ public class MCIndexNoder extends SinglePassNoder { private List monoChains = new ArrayList(); - private SpatialIndex index= new STRtree(); + private SpatialIndex index= new HPRtree(); private int idCounter = 0; private Collection nodedSegStrings; // statistics diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java index 043ac92640..e4c6f95cdb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java @@ -18,7 +18,6 @@ import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainSelectAction; -import org.locationtech.jts.index.strtree.STRtree; import org.locationtech.jts.noding.NodedSegmentString; import org.locationtech.jts.noding.SegmentString; @@ -32,10 +31,10 @@ public class MCIndexPointSnapper { //public static final int nSnaps = 0; - private STRtree index; + private SpatialIndex index; public MCIndexPointSnapper(SpatialIndex index) { - this.index = (STRtree) index; + this.index = index; } /** diff --git a/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java new file mode 100644 index 0000000000..989208119f --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.index; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.index.hprtree.HPRtree; +import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.Stopwatch; +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Reproduce the performance benchmark scenario that + * Flatbush + * uses, and run against spatial indexes. + */ +public class FlatbushPerfTest extends PerformanceTestCase { + private static final int NUM_ITEMS = 1_000_000; + private static final int NUM_QUERIES = 1_000; + private Envelope[] items; + private Envelope[] queries; + private HPRtree hprtree; + private STRtree strtree; + + public static void main(String[] args) { + PerformanceTestRunner.run(FlatbushPerfTest.class); + } + + public FlatbushPerfTest(String name) { + super(name); + setRunSize(new int[] { 1, 10, (int) (100 * Math.sqrt(0.1))}); + setRunIterations(1); + } + + private static Envelope randomBox(Random random, double boxSize) { + double x = random.nextDouble() * (100d - boxSize); + double y = random.nextDouble() * (100d - boxSize); + double x2 = x + random.nextDouble() * boxSize; + double y2 = y + random.nextDouble() * boxSize; + return new Envelope(x, x2, y, y2); + } + + public void setUp() + { + Random random = new Random(0); + items = new Envelope[NUM_ITEMS]; + + for (int i = 0; i < NUM_ITEMS; i++) { + items[i] = randomBox(random, 1); + } + + // warmup the jvm by building once and running queries + warmupQueries(createIndex(HPRtree::new, HPRtree::build)); + warmupQueries(createIndex(STRtree::new, STRtree::build)); + + Stopwatch sw = new Stopwatch(); + hprtree = createIndex(HPRtree::new, HPRtree::build); + System.out.println("HPRTree Build time = " + sw.getTimeString()); + + sw = new Stopwatch(); + strtree = createIndex(STRtree::new, STRtree::build); + System.out.println("STRTree Build time = " + sw.getTimeString()); + } + + private T createIndex(Supplier supplier, Consumer builder) { + T index = supplier.get(); + for (Envelope env : items) { + index.insert(env, env); + } + builder.accept(index); + return index; + } + + private void warmupQueries(SpatialIndex index) { + Random random = new Random(0); + CountItemVisitor visitor = new CountItemVisitor(); + for (int i = 0; i < NUM_QUERIES; i++) { + index.query(randomBox(random, 1), visitor); + } + } + + public void startRun(int size) + { + System.out.println("----- Query size: " + size); + Random random = new Random(0); + queries = new Envelope[NUM_QUERIES]; + for (int i = 0; i < NUM_QUERIES; i++) { + queries[i] = randomBox(random, size); + } + } + + public void runQueriesHPR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + hprtree.query(box, visitor); + } + System.out.println("HPRTree query result items = " + visitor.count); + } + + public void runQueriesSTR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + strtree.query(box, visitor); + } + System.out.println("STRTree query result items = " + visitor.count); + } +}