fastquadtree 1.5.0__cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastquadtree/__init__.py +5 -0
- fastquadtree/_base_quadtree.py +543 -0
- fastquadtree/_item.py +91 -0
- fastquadtree/_native.abi3.so +0 -0
- fastquadtree/_obj_store.py +194 -0
- fastquadtree/point_quadtree.py +206 -0
- fastquadtree/py.typed +0 -0
- fastquadtree/pyqtree.py +176 -0
- fastquadtree/rect_quadtree.py +230 -0
- fastquadtree-1.5.0.dist-info/METADATA +206 -0
- fastquadtree-1.5.0.dist-info/RECORD +13 -0
- fastquadtree-1.5.0.dist-info/WHEEL +5 -0
- fastquadtree-1.5.0.dist-info/licenses/LICENSE +21 -0
fastquadtree/__init__.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# _abc_quadtree.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pickle
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
9
|
+
Generic,
|
|
10
|
+
Iterable,
|
|
11
|
+
Sequence,
|
|
12
|
+
SupportsFloat,
|
|
13
|
+
Tuple,
|
|
14
|
+
TypeVar,
|
|
15
|
+
overload,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ._item import Item # base class for PointItem and RectItem
|
|
19
|
+
from ._obj_store import ObjStore
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from typing import Self # Only in Python 3.11+
|
|
23
|
+
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
Bounds = Tuple[SupportsFloat, SupportsFloat, SupportsFloat, SupportsFloat]
|
|
27
|
+
|
|
28
|
+
# Generic parameters
|
|
29
|
+
G = TypeVar("G") # geometry type, e.g. Point or Bounds
|
|
30
|
+
HitT = TypeVar("HitT") # raw native tuple, e.g. (id,x,y) or (id,x0,y0,x1,y1)
|
|
31
|
+
ItemType = TypeVar("ItemType", bound=Item) # e.g. PointItem or RectItem
|
|
32
|
+
|
|
33
|
+
# Quadtree dtype to numpy dtype mapping
|
|
34
|
+
QUADTREE_DTYPE_TO_NP_DTYPE = {
|
|
35
|
+
"f32": "float32",
|
|
36
|
+
"f64": "float64",
|
|
37
|
+
"i32": "int32",
|
|
38
|
+
"i64": "int64",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_np_array(x: Any) -> bool:
|
|
43
|
+
mod = getattr(x.__class__, "__module__", "")
|
|
44
|
+
return mod.startswith("numpy") and hasattr(x, "ndim") and hasattr(x, "shape")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
|
48
|
+
"""
|
|
49
|
+
Shared logic for Python QuadTree wrappers over native Rust engines.
|
|
50
|
+
|
|
51
|
+
Concrete subclasses must implement:
|
|
52
|
+
- _new_native(bounds, capacity, max_depth)
|
|
53
|
+
- _make_item(id_, geom, obj)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__slots__ = (
|
|
57
|
+
"_bounds",
|
|
58
|
+
"_capacity",
|
|
59
|
+
"_count",
|
|
60
|
+
"_dtype",
|
|
61
|
+
"_max_depth",
|
|
62
|
+
"_native",
|
|
63
|
+
"_next_id",
|
|
64
|
+
"_store",
|
|
65
|
+
"_track_objects",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ---- required native hooks ----
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
|
|
72
|
+
"""Create the native engine instance."""
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _new_native_from_bytes(cls, data: bytes, dtype: str) -> Any:
|
|
76
|
+
"""Create the native engine instance from serialized bytes."""
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def _make_item(id_: int, geom: G, obj: Any | None) -> ItemType:
|
|
81
|
+
"""Build an ItemType from id, geometry, and optional object."""
|
|
82
|
+
|
|
83
|
+
# ---- ctor ----
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
bounds: Bounds,
|
|
88
|
+
capacity: int,
|
|
89
|
+
*,
|
|
90
|
+
max_depth: int | None = None,
|
|
91
|
+
track_objects: bool = False,
|
|
92
|
+
dtype: str = "f32",
|
|
93
|
+
):
|
|
94
|
+
# Handle some bounds validation and list --> tuple conversion
|
|
95
|
+
if type(bounds) is not tuple:
|
|
96
|
+
bounds = tuple(bounds) # pyright: ignore[reportAssignmentType]
|
|
97
|
+
if len(bounds) != 4:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"bounds must be a tuple of four numeric values (x min, y min, x max, y max)"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self._bounds = bounds
|
|
103
|
+
|
|
104
|
+
self._max_depth = max_depth
|
|
105
|
+
self._capacity = capacity
|
|
106
|
+
self._dtype = dtype
|
|
107
|
+
self._native = self._new_native(self._bounds, self._capacity, self._max_depth)
|
|
108
|
+
|
|
109
|
+
self._track_objects = bool(track_objects)
|
|
110
|
+
self._store: ObjStore[ItemType] | None = ObjStore() if track_objects else None
|
|
111
|
+
|
|
112
|
+
# Auto ids when not using ObjStore.free slots
|
|
113
|
+
self._next_id = 0
|
|
114
|
+
self._count = 0
|
|
115
|
+
|
|
116
|
+
# ---- serialization ----
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict[str, Any]:
|
|
119
|
+
"""
|
|
120
|
+
Serialize the quadtree to a dict suitable for JSON or other serialization.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Includes a binary 'core' key for the native engine state, plus other metadata such as bounds and capacity and the obj store if tracking is enabled.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
```python
|
|
127
|
+
state = qt.to_dict()
|
|
128
|
+
assert "core" in state and "bounds" in state
|
|
129
|
+
```
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
core_bytes = self._native.to_bytes()
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"core": core_bytes,
|
|
136
|
+
"store": self._store.to_dict() if self._store is not None else None,
|
|
137
|
+
"bounds": self._bounds,
|
|
138
|
+
"capacity": self._capacity,
|
|
139
|
+
"max_depth": self._max_depth,
|
|
140
|
+
"track_objects": self._track_objects,
|
|
141
|
+
"next_id": self._next_id,
|
|
142
|
+
"count": self._count,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def to_bytes(self) -> bytes:
|
|
146
|
+
"""
|
|
147
|
+
Serialize the quadtree to bytes.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Bytes representing the serialized quadtree. Can be saved as a file or loaded with `from_bytes()`.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
```python
|
|
154
|
+
blob = qt.to_bytes()
|
|
155
|
+
with open("tree.fqt", "wb") as f:
|
|
156
|
+
f.write(blob)
|
|
157
|
+
```
|
|
158
|
+
"""
|
|
159
|
+
return pickle.dumps(self.to_dict())
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_bytes(cls, data: bytes, dtype: str = "f32") -> Self:
|
|
163
|
+
"""
|
|
164
|
+
Deserialize a quadtree from bytes. Specifiy the dtype if the original tree that was serialized used a non-default dtype.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
data: Bytes representing the serialized quadtree from `to_bytes()`.
|
|
168
|
+
dtype: The data type used in the native engine ('f32', 'f64', 'i32', 'i64') when saved to bytes.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A new quadtree instance with the same state as when serialized.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
```python
|
|
175
|
+
blob = qt.to_bytes()
|
|
176
|
+
qt2 = type(qt).from_bytes(blob)
|
|
177
|
+
assert qt2.count_items() == qt.count_items()
|
|
178
|
+
```
|
|
179
|
+
"""
|
|
180
|
+
in_dict = pickle.loads(data)
|
|
181
|
+
core_bytes = in_dict["core"]
|
|
182
|
+
store_dict = in_dict["store"]
|
|
183
|
+
|
|
184
|
+
qt = cls.__new__(cls) # type: ignore[call-arg]
|
|
185
|
+
try:
|
|
186
|
+
qt._native = cls._new_native_from_bytes(core_bytes, dtype=dtype)
|
|
187
|
+
except ValueError as ve:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"Failed to deserialize quadtree native core. "
|
|
190
|
+
"This may be due to a dtype mismatch. "
|
|
191
|
+
"Ensure the dtype used in from_bytes() matches the original tree. "
|
|
192
|
+
"Error details: " + str(ve)
|
|
193
|
+
) from ve
|
|
194
|
+
|
|
195
|
+
if store_dict is not None:
|
|
196
|
+
qt._store = ObjStore.from_dict(store_dict, qt._make_item)
|
|
197
|
+
else:
|
|
198
|
+
qt._store = None
|
|
199
|
+
|
|
200
|
+
# Extract bounds, capacity, max_depth from native
|
|
201
|
+
qt._bounds = in_dict["bounds"]
|
|
202
|
+
qt._capacity = in_dict["capacity"]
|
|
203
|
+
qt._max_depth = in_dict["max_depth"]
|
|
204
|
+
qt._next_id = in_dict["next_id"]
|
|
205
|
+
qt._count = in_dict["count"]
|
|
206
|
+
qt._track_objects = in_dict["track_objects"]
|
|
207
|
+
|
|
208
|
+
return qt
|
|
209
|
+
|
|
210
|
+
# ---- internal helper ----
|
|
211
|
+
|
|
212
|
+
def _ids_to_objects(self, ids: Iterable[int]) -> list[Any]:
|
|
213
|
+
"""Map ids -> Python objects via ObjStore in a batched way."""
|
|
214
|
+
if self._store is None:
|
|
215
|
+
raise ValueError("Cannot map ids to objects when track_objects=False")
|
|
216
|
+
return self._store.get_many_objects(list(ids))
|
|
217
|
+
|
|
218
|
+
# ---- shared API ----
|
|
219
|
+
|
|
220
|
+
def insert(self, geom: G, *, obj: Any | None = None) -> int:
|
|
221
|
+
"""
|
|
222
|
+
Insert a single item.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
geom: Point (x, y) or Rect (x0, y0, x1, y1) depending on quadtree type.
|
|
226
|
+
obj: Optional Python object to associate with id if tracking is enabled.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The id used for this insert.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ValueError: If geometry is outside the tree bounds.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
```python
|
|
236
|
+
id0 = point_qt.insert((10.0, 20.0)) # for point trees
|
|
237
|
+
id1 = rect_qt.insert((0.0, 0.0, 5.0, 5.0), obj="box") # for rect trees
|
|
238
|
+
assert isinstance(id0, int) and isinstance(id1, int)
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
if self._store is not None:
|
|
242
|
+
# Reuse a dense free slot if available, else append
|
|
243
|
+
rid = self._store.alloc_id()
|
|
244
|
+
else:
|
|
245
|
+
rid = self._next_id
|
|
246
|
+
self._next_id += 1
|
|
247
|
+
|
|
248
|
+
if not self._native.insert(rid, geom):
|
|
249
|
+
bx0, by0, bx1, by1 = self._bounds
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Geometry {geom!r} is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if self._store is not None:
|
|
255
|
+
self._store.add(self._make_item(rid, geom, obj))
|
|
256
|
+
|
|
257
|
+
self._count += 1
|
|
258
|
+
return rid
|
|
259
|
+
|
|
260
|
+
@overload
|
|
261
|
+
def insert_many(self, geoms: Sequence[G], objs: list[Any] | None = None) -> int: ...
|
|
262
|
+
@overload
|
|
263
|
+
def insert_many(
|
|
264
|
+
self, geoms: NDArray[Any], objs: list[Any] | None = None
|
|
265
|
+
) -> int: ...
|
|
266
|
+
def insert_many(
|
|
267
|
+
self, geoms: NDArray[Any] | Sequence[G], objs: list[Any] | None = None
|
|
268
|
+
) -> int:
|
|
269
|
+
"""
|
|
270
|
+
Bulk insert with auto-assigned contiguous ids. Faster than inserting one-by-one.<br>
|
|
271
|
+
Can accept either a Python sequence of geometries or a NumPy array of shape (N,2) or (N,4) with a dtype that matches the quadtree's dtype.
|
|
272
|
+
|
|
273
|
+
If tracking is enabled, the objects will be bulk stored internally.
|
|
274
|
+
If no objects are provided, the items will have obj=None (if tracking).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
geoms: List of geometries.
|
|
278
|
+
objs: Optional list of Python objects aligned with geoms.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Number of items inserted.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
ValueError: If any geometry is outside bounds.
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
```python
|
|
288
|
+
n = qt.insert_many([(1.0, 1.0), (2.0, 2.0)])
|
|
289
|
+
assert n == 2
|
|
290
|
+
|
|
291
|
+
import numpy as np
|
|
292
|
+
arr = np.array([[3.0, 3.0], [4.0, 4.0]], dtype=np.float32)
|
|
293
|
+
n2 = qt.insert_many(arr)
|
|
294
|
+
assert n2 == 2
|
|
295
|
+
```
|
|
296
|
+
"""
|
|
297
|
+
if type(geoms) is list and len(geoms) == 0:
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
if _is_np_array(geoms):
|
|
301
|
+
import numpy as _np
|
|
302
|
+
else:
|
|
303
|
+
_np = None
|
|
304
|
+
|
|
305
|
+
# Early return if the numpy array is empty
|
|
306
|
+
if _np is not None and isinstance(geoms, _np.ndarray):
|
|
307
|
+
if geoms.size == 0:
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
# Check if dtype matches quadtree dtype
|
|
311
|
+
expected_np_dtype = QUADTREE_DTYPE_TO_NP_DTYPE.get(self._dtype)
|
|
312
|
+
if geoms.dtype != expected_np_dtype:
|
|
313
|
+
raise TypeError(
|
|
314
|
+
f"Numpy array dtype {geoms.dtype} does not match quadtree dtype {self._dtype}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if self._store is None:
|
|
318
|
+
# Simple contiguous path with native bulk insert
|
|
319
|
+
start_id = self._next_id
|
|
320
|
+
|
|
321
|
+
if _np is not None:
|
|
322
|
+
last_id = self._native.insert_many_np(start_id, geoms)
|
|
323
|
+
else:
|
|
324
|
+
last_id = self._native.insert_many(start_id, geoms)
|
|
325
|
+
num = last_id - start_id + 1
|
|
326
|
+
if num < len(geoms):
|
|
327
|
+
raise ValueError("One or more items are outside tree bounds")
|
|
328
|
+
self._next_id = last_id + 1
|
|
329
|
+
self._count += num
|
|
330
|
+
return num
|
|
331
|
+
|
|
332
|
+
# With tracking enabled:
|
|
333
|
+
start_id = len(self._store._arr) # contiguous tail position
|
|
334
|
+
if _np is not None:
|
|
335
|
+
last_id = self._native.insert_many_np(start_id, geoms)
|
|
336
|
+
else:
|
|
337
|
+
last_id = self._native.insert_many(start_id, geoms)
|
|
338
|
+
num = last_id - start_id + 1
|
|
339
|
+
if num < len(geoms):
|
|
340
|
+
raise ValueError("One or more items are outside tree bounds")
|
|
341
|
+
|
|
342
|
+
# For object tracking, we need the geoms to be a Python list
|
|
343
|
+
if _np is not None:
|
|
344
|
+
geoms = geoms.tolist() # pyright: ignore[reportAttributeAccessIssue]
|
|
345
|
+
|
|
346
|
+
# Function bindings to avoid repeated attribute lookups
|
|
347
|
+
add = self._store.add
|
|
348
|
+
mk = self._make_item
|
|
349
|
+
|
|
350
|
+
# Add items to the store in one pass
|
|
351
|
+
if objs is None:
|
|
352
|
+
for off, geom in enumerate(geoms):
|
|
353
|
+
add(mk(start_id + off, geom, None))
|
|
354
|
+
else:
|
|
355
|
+
if len(objs) != len(geoms):
|
|
356
|
+
raise ValueError("objs length must match geoms length")
|
|
357
|
+
for off, (geom, o) in enumerate(zip(geoms, objs)):
|
|
358
|
+
add(mk(start_id + off, geom, o))
|
|
359
|
+
|
|
360
|
+
# Keep _next_id monotonic for the non-tracking path
|
|
361
|
+
self._next_id = max(self._next_id, last_id + 1)
|
|
362
|
+
|
|
363
|
+
self._count += num
|
|
364
|
+
return num
|
|
365
|
+
|
|
366
|
+
def delete(self, id_: int, geom: G) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Delete an item by id and exact geometry.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
id_: The id of the item to delete.
|
|
372
|
+
geom: The geometry of the item to delete.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
True if the item was found and deleted.
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
```python
|
|
379
|
+
i = qt.insert((1.0, 2.0))
|
|
380
|
+
ok = qt.delete(i, (1.0, 2.0))
|
|
381
|
+
assert ok is True
|
|
382
|
+
```
|
|
383
|
+
"""
|
|
384
|
+
deleted = self._native.delete(id_, geom)
|
|
385
|
+
if deleted:
|
|
386
|
+
self._count -= 1
|
|
387
|
+
if self._store is not None:
|
|
388
|
+
self._store.pop_id(id_)
|
|
389
|
+
return deleted
|
|
390
|
+
|
|
391
|
+
def attach(self, id_: int, obj: Any) -> None:
|
|
392
|
+
"""
|
|
393
|
+
Attach or replace the Python object for an existing id.
|
|
394
|
+
Tracking must be enabled.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
id_: The id of the item to attach the object to.
|
|
398
|
+
obj: The Python object to attach.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
```python
|
|
402
|
+
i = qt.insert((2.0, 3.0), obj=None)
|
|
403
|
+
qt.attach(i, {"meta": 123})
|
|
404
|
+
assert qt.get(i) == {"meta": 123}
|
|
405
|
+
```
|
|
406
|
+
"""
|
|
407
|
+
if self._store is None:
|
|
408
|
+
raise ValueError("Cannot attach objects when track_objects=False")
|
|
409
|
+
it = self._store.by_id(id_)
|
|
410
|
+
if it is None:
|
|
411
|
+
raise KeyError(f"Id {id_} not found in quadtree")
|
|
412
|
+
# Preserve geometry from existing item
|
|
413
|
+
self._store.add(self._make_item(id_, it.geom, obj)) # type: ignore[attr-defined]
|
|
414
|
+
|
|
415
|
+
def delete_by_object(self, obj: Any) -> bool:
|
|
416
|
+
"""
|
|
417
|
+
Delete an item by Python object identity. Tracking must be enabled.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
obj: The Python object to delete.
|
|
421
|
+
|
|
422
|
+
Example:
|
|
423
|
+
```python
|
|
424
|
+
i = qt.insert((3.0, 4.0), obj="tag")
|
|
425
|
+
ok = qt.delete_by_object("tag")
|
|
426
|
+
assert ok is True
|
|
427
|
+
```
|
|
428
|
+
"""
|
|
429
|
+
if self._store is None:
|
|
430
|
+
raise ValueError("Cannot delete by object when track_objects=False")
|
|
431
|
+
it = self._store.by_obj(obj)
|
|
432
|
+
if it is None:
|
|
433
|
+
return False
|
|
434
|
+
return self.delete(it.id_, it.geom) # type: ignore[arg-type]
|
|
435
|
+
|
|
436
|
+
def clear(self) -> None:
|
|
437
|
+
"""
|
|
438
|
+
Empty the tree in place, preserving bounds, capacity, and max_depth.
|
|
439
|
+
|
|
440
|
+
If tracking is enabled, the id -> object mapping is also cleared.
|
|
441
|
+
The ids are reset to start at zero again.
|
|
442
|
+
|
|
443
|
+
Example:
|
|
444
|
+
```python
|
|
445
|
+
_ = qt.insert((5.0, 6.0))
|
|
446
|
+
qt.clear()
|
|
447
|
+
assert qt.count_items() == 0 and len(qt) == 0
|
|
448
|
+
```
|
|
449
|
+
"""
|
|
450
|
+
self._native = self._new_native(self._bounds, self._capacity, self._max_depth)
|
|
451
|
+
self._count = 0
|
|
452
|
+
if self._store is not None:
|
|
453
|
+
self._store.clear()
|
|
454
|
+
self._next_id = 0
|
|
455
|
+
|
|
456
|
+
def get_all_objects(self) -> list[Any]:
|
|
457
|
+
"""
|
|
458
|
+
Return all tracked Python objects in the tree.
|
|
459
|
+
|
|
460
|
+
Example:
|
|
461
|
+
```python
|
|
462
|
+
_ = qt.insert((7.0, 8.0), obj="a")
|
|
463
|
+
_ = qt.insert((9.0, 1.0), obj="b")
|
|
464
|
+
objs = qt.get_all_objects()
|
|
465
|
+
assert set(objs) == {"a", "b"}
|
|
466
|
+
```
|
|
467
|
+
"""
|
|
468
|
+
if self._store is None:
|
|
469
|
+
raise ValueError("Cannot get objects when track_objects=False")
|
|
470
|
+
return [t.obj for t in self._store.items() if t.obj is not None]
|
|
471
|
+
|
|
472
|
+
def get_all_items(self) -> list[ItemType]:
|
|
473
|
+
"""
|
|
474
|
+
Return all Item wrappers in the tree.
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
```python
|
|
478
|
+
_ = qt.insert((1.0, 1.0), obj=None)
|
|
479
|
+
items = qt.get_all_items()
|
|
480
|
+
assert hasattr(items[0], "id_") and hasattr(items[0], "geom")
|
|
481
|
+
```
|
|
482
|
+
"""
|
|
483
|
+
if self._store is None:
|
|
484
|
+
raise ValueError("Cannot get items when track_objects=False")
|
|
485
|
+
return list(self._store.items())
|
|
486
|
+
|
|
487
|
+
def get_all_node_boundaries(self) -> list[Bounds]:
|
|
488
|
+
"""
|
|
489
|
+
Return all node boundaries in the tree. Useful for visualization.
|
|
490
|
+
|
|
491
|
+
Example:
|
|
492
|
+
```python
|
|
493
|
+
bounds = qt.get_all_node_boundaries()
|
|
494
|
+
assert isinstance(bounds, list)
|
|
495
|
+
```
|
|
496
|
+
"""
|
|
497
|
+
return self._native.get_all_node_boundaries()
|
|
498
|
+
|
|
499
|
+
def get(self, id_: int) -> Any | None:
|
|
500
|
+
"""
|
|
501
|
+
Return the object associated with id, if tracking is enabled.
|
|
502
|
+
|
|
503
|
+
Example:
|
|
504
|
+
```python
|
|
505
|
+
i = qt.insert((1.0, 2.0), obj={"k": "v"})
|
|
506
|
+
obj = qt.get(i)
|
|
507
|
+
assert obj == {"k": "v"}
|
|
508
|
+
```
|
|
509
|
+
"""
|
|
510
|
+
if self._store is None:
|
|
511
|
+
raise ValueError("Cannot get objects when track_objects=False")
|
|
512
|
+
item = self._store.by_id(id_)
|
|
513
|
+
return None if item is None else item.obj
|
|
514
|
+
|
|
515
|
+
def count_items(self) -> int:
|
|
516
|
+
"""
|
|
517
|
+
Return the number of items currently in the tree (native count).
|
|
518
|
+
|
|
519
|
+
Example:
|
|
520
|
+
```python
|
|
521
|
+
before = qt.count_items()
|
|
522
|
+
_ = qt.insert((2.0, 2.0))
|
|
523
|
+
assert qt.count_items() == before + 1
|
|
524
|
+
```
|
|
525
|
+
"""
|
|
526
|
+
return self._native.count_items()
|
|
527
|
+
|
|
528
|
+
def get_inner_max_depth(self) -> int:
|
|
529
|
+
"""
|
|
530
|
+
Return the maximum depth of the quadtree core.
|
|
531
|
+
Useful if you let the core chose the default max depth based on dtype
|
|
532
|
+
by constructing with max_depth=None.
|
|
533
|
+
|
|
534
|
+
Example:
|
|
535
|
+
```python
|
|
536
|
+
depth = qt.get_inner_max_depth()
|
|
537
|
+
assert isinstance(depth, int)
|
|
538
|
+
```
|
|
539
|
+
"""
|
|
540
|
+
return self._native.get_max_depth()
|
|
541
|
+
|
|
542
|
+
def __len__(self) -> int:
|
|
543
|
+
return self._count
|
fastquadtree/_item.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# item.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, SupportsFloat, Tuple
|
|
5
|
+
|
|
6
|
+
Bounds = Tuple[SupportsFloat, SupportsFloat, SupportsFloat, SupportsFloat]
|
|
7
|
+
"""Axis-aligned rectangle as (min_x, min_y, max_x, max_y)."""
|
|
8
|
+
|
|
9
|
+
Point = Tuple[SupportsFloat, SupportsFloat]
|
|
10
|
+
"""2D point as (x, y)."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Item:
|
|
14
|
+
"""
|
|
15
|
+
Lightweight view of an index entry.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
id_: Integer identifier.
|
|
19
|
+
geom: The geometry, either a Point or Rectangle Bounds.
|
|
20
|
+
obj: The attached Python object if available, else None.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__slots__ = ("geom", "id_", "obj")
|
|
24
|
+
|
|
25
|
+
def __init__(self, id_: int, geom: Point | Bounds, obj: Any | None = None):
|
|
26
|
+
self.id_: int = id_
|
|
27
|
+
self.geom: Point | Bounds = geom
|
|
28
|
+
self.obj: Any | None = obj
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Serialize the item to a dictionary.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A dictionary with 'id', 'geom', and 'obj' keys.
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
"id": self.id_,
|
|
39
|
+
"geom": self.geom,
|
|
40
|
+
"obj": self.obj,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: dict[str, Any]) -> Item:
|
|
45
|
+
"""
|
|
46
|
+
Deserialize an item from a dictionary.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data: A dictionary with 'id', 'geom', and 'obj' keys.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
An Item instance populated with the deserialized data.
|
|
53
|
+
"""
|
|
54
|
+
id_ = data["id"]
|
|
55
|
+
geom = data["geom"]
|
|
56
|
+
obj = data["obj"]
|
|
57
|
+
return cls(id_, geom, obj)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PointItem(Item):
|
|
61
|
+
"""
|
|
62
|
+
Lightweight point item wrapper for tracking and as_items results.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
id_: Integer identifier.
|
|
66
|
+
geom: The point geometry as (x, y).
|
|
67
|
+
obj: The attached Python object if available, else None.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = ("x", "y")
|
|
71
|
+
|
|
72
|
+
def __init__(self, id_: int, geom: Point, obj: Any | None = None):
|
|
73
|
+
super().__init__(id_, geom, obj)
|
|
74
|
+
self.x, self.y = geom
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RectItem(Item):
|
|
78
|
+
"""
|
|
79
|
+
Lightweight rectangle item wrapper for tracking and as_items results.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
id_: Integer identifier.
|
|
83
|
+
geom: The rectangle geometry as (min_x, min_y, max_x, max_y
|
|
84
|
+
obj: The attached Python object if available, else None.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
__slots__ = ("max_x", "max_y", "min_x", "min_y")
|
|
88
|
+
|
|
89
|
+
def __init__(self, id_: int, geom: Bounds, obj: Any | None = None):
|
|
90
|
+
super().__init__(id_, geom, obj)
|
|
91
|
+
self.min_x, self.min_y, self.max_x, self.max_y = geom
|
|
Binary file
|