Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of HPRtree #1012

Merged
merged 12 commits into from
Jan 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Item> items = new ArrayList<Item>();

private int nodeCapacity = DEFAULT_NODE_CAPACITY;
private List<Item> 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.
Expand All @@ -104,15 +106,16 @@ public HPRtree(int nodeCapacity) {
* @return the number of items
*/
public int size() {
return items.size();
return numItems;
}

@Override
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);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand All @@ -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];
Expand All @@ -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<Item> 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++) {
Expand All @@ -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));
}
}

Expand All @@ -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());
}
}
Expand All @@ -325,21 +314,17 @@ 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<Integer> layerIndexList = new ArrayList<Integer>();
IntArrayList layerIndexList = new IntArrayList();
int layerSize = itemSize;
int index = 0;
do {
layerIndexList.add(index);
layerSize = numNodesToCover(layerSize, nodeCapacity);
index += ENV_SIZE * layerSize;
} while (layerSize > 1);
return toIntArray(layerIndexList);
return layerIndexList.toArray();
}

/**
Expand All @@ -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<Integer> 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
Expand All @@ -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<Item> {

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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>Quadtree</code>}
* or {@link STRtree} (which is the default index provided).
* or {@link HPRtree} (which is the default index provided).
* <p>
* The noder supports using an overlap tolerance distance .
* This allows determining segment intersection using a buffer for uses
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}

/**
Expand Down
Loading
Loading