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
A general purpose STRtree which can store any Python object #1094
Changes from 11 commits
ac1d050
6885217
e8fc9ae
4f14f1b
8905a00
e6fda8d
703a5a3
9758069
0af0ab8
297ae43
8cbdc72
eca5abd
ae6f2b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,11 +19,38 @@ | |
""" | ||
|
||
import ctypes | ||
from functools import wraps | ||
import logging | ||
from warnings import warn | ||
|
||
from shapely.errors import ShapelyDeprecationWarning | ||
from shapely.geos import lgeos | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def nearest_callback(func): | ||
@wraps(func) | ||
def wrapper(arg1, arg2, arg3, arg4): | ||
value = ctypes.cast(arg1, ctypes.py_object).value | ||
geom = ctypes.cast(arg2, ctypes.py_object).value | ||
dist = ctypes.cast(arg3, ctypes.POINTER(ctypes.c_double)) | ||
try: | ||
dist.contents.value = func(value, geom) | ||
return 1 | ||
except Exception: | ||
log.exception() | ||
return 0 | ||
return wrapper | ||
|
||
|
||
def query_callback(func): | ||
@wraps(func) | ||
def wrapper(arg1, arg2): | ||
value = ctypes.cast(arg1, ctypes.py_object).value | ||
func(value) | ||
return wrapper | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These decorators need docs if we're going to go in this direction. I assume we could write these in C for 2.0. Something like https://code.activestate.com/recipes/576731-c-function-decorator/. |
||
|
||
|
||
class STRtree: | ||
""" | ||
|
@@ -75,49 +102,70 @@ class STRtree: | |
None | ||
""" | ||
|
||
def __init__(self, geoms): | ||
def __init__(self, initdata=None): | ||
sgillies marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Create a new STRtree. | ||
|
||
Parameters | ||
---------- | ||
initdata : iterable | ||
An iterable sequence of single geometry objects or (geom, | ||
value) tuples. | ||
|
||
Notes | ||
----- | ||
Items from initdata will be stored in two lists. | ||
|
||
""" | ||
warn( | ||
"STRtree will be completely changed in 2.0.0. The exact API is not yet decided, but will be documented before 1.8.0", | ||
ShapelyDeprecationWarning, | ||
stacklevel=2, | ||
) | ||
# filter empty geometries out of the input | ||
geoms = [geom for geom in geoms if not geom.is_empty] | ||
self._n_geoms = len(geoms) | ||
|
||
self._init_tree_handle(geoms) | ||
|
||
# Keep references to geoms. | ||
self._geoms = list(geoms) | ||
|
||
def _init_tree_handle(self, geoms): | ||
node_capacity = 10 | ||
self._tree_handle = lgeos.GEOSSTRtree_create(node_capacity) | ||
for geom in geoms: | ||
lgeos.GEOSSTRtree_insert(self._tree_handle, geom._geom, ctypes.py_object(geom)) | ||
self._initdata = None | ||
self._tree = None | ||
if initdata: | ||
self._initdata = list(self._iteritems(initdata)) | ||
self._init_tree(self._initdata) | ||
|
||
def _iteritems(self, initdata): | ||
for obj in initdata: | ||
if not isinstance(obj, tuple): | ||
geom = obj | ||
value = obj | ||
else: | ||
value, geom = obj | ||
if not geom.is_empty: | ||
yield value, geom | ||
|
||
def _init_tree(self, initdata): | ||
if initdata: | ||
node_capacity = 10 | ||
self._tree = lgeos.GEOSSTRtree_create(node_capacity) | ||
for value, geom in self._iteritems(initdata): | ||
lgeos.GEOSSTRtree_insert( | ||
self._tree, geom._geom, ctypes.py_object(value) | ||
) | ||
|
||
def __getstate__(self): | ||
state = self.__dict__.copy() | ||
del state["_tree_handle"] | ||
del state["_tree"] | ||
return state | ||
|
||
def __setstate__(self, state): | ||
self.__dict__.update(state) | ||
self._init_tree_handle(self._geoms) | ||
self._init_tree(self._initdata) | ||
|
||
def __del__(self): | ||
if self._tree_handle is not None: | ||
if self._tree is not None: | ||
try: | ||
lgeos.GEOSSTRtree_destroy(self._tree_handle) | ||
except AttributeError: | ||
pass # lgeos might be empty on shutdown. | ||
|
||
self._tree_handle = None | ||
lgeos.GEOSSTRtree_destroy(self._tree) | ||
except AttributeError: # lgeos might be empty on shutdown. | ||
pass | ||
self._tree = None | ||
|
||
def query(self, geom): | ||
""" | ||
Search the index for geometry objects whose extents | ||
intersect the extent of the given object. | ||
Search the tree for nodes which intersect geom's envelope | ||
|
||
Parameters | ||
---------- | ||
|
@@ -126,18 +174,17 @@ def query(self, geom): | |
|
||
Returns | ||
------- | ||
list of geometry objects | ||
All the geometry objects in the index whose extents | ||
intersect the extent of `geom`. | ||
list | ||
A list of the values stored at the found nodes. The list | ||
will be empty if the tree is empty. | ||
|
||
Note | ||
---- | ||
A geometry object's "extent" is its the minimum xy bounding | ||
A geometry object's envelope is its the minimum xy bounding | ||
rectangle. | ||
|
||
Examples | ||
-------- | ||
|
||
A buffer around a point can be used to control the extent | ||
of the query. | ||
|
||
|
@@ -160,45 +207,53 @@ def query(self, geom): | |
>>> [o.wkt for o in tree.query(query_geom) if o.intersects(query_geom)] | ||
['POINT (2 2)'] | ||
|
||
To get the original indices of the returned objects, create an | ||
auxiliary dictionary. But use the geometry *ids* as keys since | ||
the shapely geometry objects themselves are not hashable. | ||
|
||
>>> index_by_id = dict((id(pt), i) for i, pt in enumerate(points)) | ||
>>> [(index_by_id[id(pt)], pt.wkt) for pt in tree.query(Point(2,2).buffer(1.0))] | ||
[(1, 'POINT (1 1)'), (2, 'POINT (2 2)'), (3, 'POINT (3 3)')] | ||
""" | ||
if self._n_geoms == 0: | ||
return [] | ||
|
||
result = [] | ||
|
||
def callback(item, userdata): | ||
geom = ctypes.cast(item, ctypes.py_object).value | ||
result.append(geom) | ||
|
||
lgeos.GEOSSTRtree_query(self._tree_handle, geom._geom, lgeos.GEOSQueryCallback(callback), None) | ||
@query_callback | ||
def callback(value): | ||
result.append(value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python callback function with our special decorator. |
||
|
||
self.query_cb(geom, callback) | ||
return result | ||
|
||
def nearest(self, geom): | ||
""" | ||
Get the nearest object in the index to a geometry object. | ||
def query_cb(self, geom, callback): | ||
sgillies marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Searches the tree for nodes and applies a callback function. | ||
|
||
Parameters | ||
---------- | ||
geom : geometry object | ||
geom : Geometry | ||
The query geometry | ||
callback : callable | ||
This is a function which takes a node value as argument and | ||
which is decorated by shapely.strtree.query_callback. See | ||
STRtree.query() for an example. | ||
|
||
Returns | ||
------- | ||
geometry object | ||
The nearest geometry object in the index to `geom`. | ||
None | ||
|
||
""" | ||
if self._tree is None or not self._initdata: | ||
return | ||
|
||
lgeos.GEOSSTRtree_query( | ||
self._tree, geom._geom, lgeos.GEOSQueryCallback(callback), None | ||
) | ||
|
||
def nearest(self, geom): | ||
"""Finds the tree node nearest to a given geometry object. | ||
|
||
Will always only return *one* object even if several | ||
in the index are the minimum distance away. | ||
Parameters | ||
---------- | ||
geom : geometry object | ||
The query geometry | ||
|
||
`None` if the index is empty. | ||
Returns | ||
------- | ||
object or None | ||
The value of the tree node nearest to geom or None if the | ||
index is empty. | ||
|
||
Examples | ||
-------- | ||
|
@@ -210,27 +265,31 @@ def nearest(self, geom): | |
|
||
Will only return one object: | ||
|
||
>>> tree = STRtree ([Point(0, 0), Point(0, 0)]) | ||
>>> tree = STRtree([Point(0, 0), Point(0, 0)]) | ||
>>> tree.nearest(Point(0, 0)).wkt | ||
'POINT (0 0)' | ||
|
||
""" | ||
if self._n_geoms == 0: | ||
if self._tree is None or not self._initdata: | ||
return None | ||
|
||
# In a future version of shapely, geometries will be hashable | ||
# and we won't need to reindex like this. | ||
geoms = {id(v): g for g, v in self._initdata} | ||
|
||
@nearest_callback | ||
def callback(value, geom): | ||
value_geom = geoms[id(value)] | ||
return geom.distance(value_geom) | ||
|
||
envelope = geom.envelope | ||
|
||
def callback(item1, item2, distance, userdata): | ||
try: | ||
geom1 = ctypes.cast(item1, ctypes.py_object).value | ||
geom2 = ctypes.cast(item2, ctypes.py_object).value | ||
dist = ctypes.cast(distance, ctypes.POINTER(ctypes.c_double)) | ||
lgeos.GEOSDistance(geom1._geom, geom2._geom, dist) | ||
return 1 | ||
except Exception: | ||
return 0 | ||
|
||
item = lgeos.GEOSSTRtree_nearest_generic(self._tree_handle, ctypes.py_object(geom), envelope._geom, \ | ||
lgeos.GEOSDistanceCallback(callback), None) | ||
result = ctypes.cast(item, ctypes.py_object).value | ||
item = lgeos.GEOSSTRtree_nearest_generic( | ||
self._tree, | ||
ctypes.py_object(geom), | ||
envelope._geom, | ||
lgeos.GEOSDistanceCallback(callback), | ||
None, | ||
) | ||
|
||
return result | ||
return ctypes.cast(item, ctypes.py_object).value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a use for the userdata argument right now. If one came up, it could be added as a keyword arg.