Immutable in-memory R-tree and R*-tree implementations in Java with reactive api
Immutable in-memory R-tree and R*-tree implementations in Java with reactive api
In-memory immutable 2D R-tree implementation in java using RxJava Observables for reactive processing of search results.
Status: released to Maven Central
Note that the next version (without a reactive API and without serialization) is at rtree2.
An R-tree is a commonly used spatial index.
This was fun to make, has an elegant concise algorithm, is thread-safe, fast, and reasonably memory efficient (uses structural sharing).
The algorithm to achieve immutability is cute. For insertion/deletion it involves recursion down to the required leaf node then recursion back up to replace the parent nodes up to the root. The guts of it is in Leaf.java and NonLeaf.java.
Backpressure support required some complexity because effectively a bookmark needed to be kept for a position in the tree and returned to later to continue traversal. An immutable stack containing the node and child index of the path nodes came to the rescue here and recursion was abandoned in favour of looping to prevent stack overflow (unfortunately java doesn't support tail recursion!).
Maven site reports are here including javadoc.
Observable O(log(n)) on averageO(n) worst caseNumber of points = 1000, max children per node 8:
| Quadratic split | R*-tree split | STR bulk loaded |
|---|---|---|
Notice that there is little overlap in the R*-tree split compared to the Quadratic split. This should provide better search performance (and in general benchmarks show this).
STR bulk loaded R-tree has a bit more overlap than R*-tree, which affects the search performance at some extent.
Add this maven dependency to your pom.xml:
com.github.davidmoten
rtree
VERSION_HERE
Use the static builder methods on the RTree class:
// create an R-tree using Quadratic split with max
// children per node 4, min children 2 (the threshold
// at which members are redistributed)
RTree tree = RTree.create();
You can specify a few parameters to the builder, including minChildren, maxChildren, splitter, selector:
RTree tree = RTree.minChildren(3).maxChildren(6).create();
The following geometries are supported for insertion in an RTree:
RectanglePointCircleLineIf for instance you know that the entry geometry is always Point then create an RTree specifying that generic type to gain more type safety:
RTree tree = RTree.create();
If you'd like an R*-tree (which uses a topological splitter on minimal margin, overlap area and area and a selector combination of minimal area increase, minimal overlap, and area):
RTree tree = RTree.star().maxChildren(6).create();
See benchmarks below for some of the performance differences.
When you add an item to the R-tree you need to provide a geometry that represents the 2D physical location or
extension of the item. The Geometries builder provides these factory methods:
Geometries.rectangleGeometries.circleGeometries.pointGeometries.line (requires jts-core dependency)To add an item to an R-tree:
RTree tree = RTree.create();
tree = tree.add(item, Geometries.point(10,20));
or
tree = tree.add(Entries.entry(item, Geometries.point(10,20));
Important note: being an immutable data structure, calling tree.add(item, geometry) does nothing to tree,
it returns a new RTree containing the addition. Make sure you use the result of the add!
To remove an item from an R-tree, you need to match the item and its geometry:
tree = tree.delete(item, Geometries.point(10,20));
or
tree = tree.delete(entry);
Important note: being an immutable data structure, calling tree.delete(item, geometry) does nothing to tree,
it returns a new RTree without the deleted item. Make sure you use the result of the delete!
To handle wraparounds of longitude values on the earth (180/-180 boundary trickiness) there are special factory methods in the Geometries class. If you want to do geospatial searches then you should use these methods to build Points and Rectangles:
Point point = Geometries.pointGeographic(lon, lat);
Rectangle rectangle = Geometries.rectangleGeographic(lon1, lat1, lon2, lat2);
Under the covers these methods normalize the longitude value to be in the interval [-180, 180) and for rectangles the rightmost longitude has 360 added to it if it is less than the leftmost longitude.
You can also write your own implementation of Geometry. An implementation of Geometry needs to specify methods to:
equals and hashCode for consistent equality checkingRuntimeException.For the R-tree to be well-behaved, the distance function if implemented needs to satisfy these properties:
distance(r) >= 0 for all rectangles r tree.search(Geometries.rectangle(0,0,2,2));
or search for items within a distance from the given geometry:
```java
Observable> results =
tree.search(Geometries.rectangle(0,0,2,2),5.0);
To return all entries from an R-tree:
Observable> results = tree.entries();
Suppose you make a custom geometry like Polygon and you want to search an RTree for points inside the polygon. This is how you do it:
RTree tree = RTree.create();
Func2 pointInPolygon = ...
Polygon polygon = ...
...
entries = tree.search(polygon, pointInPolygon);
The key is that you need to supply the intersects function (pointInPolygon) to the search. It is on you to implement that for all types of geometry present in the RTree. This is one reason that the generic Geometry type was added in rtree 0.5 (so the type system could tell you what geometry types you needed to calculate intersection for) .
As per the example above to do a proximity search you need to specify how to calculate distance between the geometry you are searching and the entry geometries:
RTree tree = RTree.create();
Func2 distancePointToPolygon = ...
Polygon polygon = ...
...
entries = tree.search(polygon, 10, distancePointToPolygon);
import com.github.davidmoten.rtree.RTree;
import static com.github.davidmoten.rtree.geometry.Geometries.*;
RTree tree = RTree.maxChildren(5).create();
tree = tree.add("DAVE", point(10, 20))
.add("FRED", point(12, 25))
.add("MARY", point(97, 125));
Observable> entries =
tree.search(Geometries.rectangle(8, 15, 30, 35));
See LatLongExampleTest.java for an example. The example depends on grumpy-core artifact which is also on Maven Central.
See LatLongExampleTest.testSearchLatLongCircles() for an example of searching circles around geographic points (using great circle distance).
Very useful, see RxJava.
As an example, suppose you want to filter the search results then apply a function on each and reduce to some best answer:
…
java // create geometry using double precision Rectangle r = Geometries.rectangle(1.0, 2.0, 3.0, 4.0);
// create geometry using single precision Rectangle r = Geometries.rectangle(1.0f, 2.0f, 3.0f, 4.0f);
The same creation methods exist for `Circle` and `Line`.
How do I just get an Iterable back from a search?
---------------------------------------------------------
If you are not familiar with the Observable API and want to skip the reactive stuff then here's how to get an ```Iterable``` from a search:
```java
Iterable it = tree.search(Geometries.point(4,5))
.toBlocking().toIterable();
The backpressure slow path may be enabled by some RxJava operators. This may slow search performance by a factor of 3 but avoids possible out of memory errors and thread starvation due to asynchronous buffering. Backpressure is benchmarked below.
To visualize the R-tree in a PNG file of size 600 by 600 pixels just call:
tree.visualize(600,600)
.save("target/mytree.png");
The result is like the images in the Features section above.
The RTree.asString() method returns output like this:
mbr=Rectangle [x1=10.0, y1=4.0, x2=62.0, y2=85.0]
mbr=Rectangle [x1=28.0, y1=4.0, x2=34.0, y2=85.0]
entry=Entry [value=2,
No open issues yet, or sync has not completed.