fastquadtree 0.6.1__cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl → 0.8.0__cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.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 +4 -380
- fastquadtree/_base_quadtree.py +263 -0
- fastquadtree/_bimap.py +22 -22
- fastquadtree/_item.py +35 -7
- fastquadtree/_native.abi3.so +0 -0
- fastquadtree/point_quadtree.py +161 -0
- fastquadtree/rect_quadtree.py +98 -0
- {fastquadtree-0.6.1.dist-info → fastquadtree-0.8.0.dist-info}/METADATA +32 -141
- fastquadtree-0.8.0.dist-info/RECORD +12 -0
- {fastquadtree-0.6.1.dist-info → fastquadtree-0.8.0.dist-info}/WHEEL +1 -1
- fastquadtree/__init__.pyi +0 -70
- fastquadtree-0.6.1.dist-info/RECORD +0 -10
- {fastquadtree-0.6.1.dist-info → fastquadtree-0.8.0.dist-info}/licenses/LICENSE +0 -0
fastquadtree/__init__.py
CHANGED
@@ -1,381 +1,5 @@
|
|
1
|
-
from
|
1
|
+
from ._item import Item, PointItem, RectItem
|
2
|
+
from .point_quadtree import QuadTree
|
3
|
+
from .rect_quadtree import RectQuadTree
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
from ._bimap import BiMap # type: ignore[attr-defined]
|
6
|
-
from ._item import Item
|
7
|
-
|
8
|
-
# Compiled Rust module is provided by maturin (tool.maturin.module-name)
|
9
|
-
from ._native import QuadTree as _RustQuadTree
|
10
|
-
|
11
|
-
Bounds = Tuple[float, float, float, float]
|
12
|
-
"""Axis-aligned rectangle as (min_x, min_y, max_x, max_y)."""
|
13
|
-
|
14
|
-
Point = Tuple[float, float]
|
15
|
-
"""2D point as (x, y)."""
|
16
|
-
|
17
|
-
_IdCoord = Tuple[int, float, float]
|
18
|
-
"""Result tuple as (id, x, y)."""
|
19
|
-
|
20
|
-
|
21
|
-
class QuadTree:
|
22
|
-
"""
|
23
|
-
High-level Python wrapper over the Rust quadtree engine.
|
24
|
-
|
25
|
-
The quadtree stores points with integer IDs. You may attach an arbitrary
|
26
|
-
Python object per ID when object tracking is enabled.
|
27
|
-
|
28
|
-
Performance characteristics:
|
29
|
-
Inserts: average O(log n)
|
30
|
-
Rect queries: average O(log n + k) where k is matches returned
|
31
|
-
Nearest neighbor: average O(log n)
|
32
|
-
|
33
|
-
Thread-safety:
|
34
|
-
Instances are not thread-safe. Use external synchronization if you
|
35
|
-
mutate the same tree from multiple threads.
|
36
|
-
|
37
|
-
Args:
|
38
|
-
bounds: World bounds as (min_x, min_y, max_x, max_y).
|
39
|
-
capacity: Max number of points per node before splitting.
|
40
|
-
max_depth: Optional max tree depth. If omitted, engine decides.
|
41
|
-
track_objects: Enable id <-> object mapping inside Python.
|
42
|
-
start_id: Starting auto-assigned id when you omit id on insert.
|
43
|
-
|
44
|
-
Raises:
|
45
|
-
ValueError: If parameters are invalid or inserts are out of bounds.
|
46
|
-
"""
|
47
|
-
|
48
|
-
__slots__ = ("_bounds", "_count", "_items", "_native", "_next_id")
|
49
|
-
|
50
|
-
def __init__(
|
51
|
-
self,
|
52
|
-
bounds: Bounds,
|
53
|
-
capacity: int,
|
54
|
-
*,
|
55
|
-
max_depth: int | None = None,
|
56
|
-
track_objects: bool = False,
|
57
|
-
start_id: int = 1,
|
58
|
-
):
|
59
|
-
if max_depth is None:
|
60
|
-
self._native = _RustQuadTree(bounds, capacity)
|
61
|
-
else:
|
62
|
-
self._native = _RustQuadTree(bounds, capacity, max_depth=max_depth)
|
63
|
-
self._items: BiMap | None = BiMap() if track_objects else None
|
64
|
-
self._next_id: int = int(start_id)
|
65
|
-
self._count: int = 0
|
66
|
-
self._bounds = bounds
|
67
|
-
|
68
|
-
# ---------- inserts ----------
|
69
|
-
|
70
|
-
def insert(self, xy: Point, *, id_: int | None = None, obj: Any = None) -> int:
|
71
|
-
"""
|
72
|
-
Insert a single point.
|
73
|
-
|
74
|
-
Args:
|
75
|
-
xy: Point (x, y).
|
76
|
-
id: Optional integer id. If None, an auto id is assigned.
|
77
|
-
obj: Optional Python object to associate with id. Stored only if
|
78
|
-
object tracking is enabled.
|
79
|
-
|
80
|
-
Returns:
|
81
|
-
The id used for this insert.
|
82
|
-
|
83
|
-
Raises:
|
84
|
-
ValueError: If the point is outside tree bounds.
|
85
|
-
"""
|
86
|
-
if id_ is None:
|
87
|
-
id_ = self._next_id
|
88
|
-
self._next_id += 1
|
89
|
-
# ensure future auto-ids do not collide
|
90
|
-
elif id_ >= self._next_id:
|
91
|
-
self._next_id = id_ + 1
|
92
|
-
|
93
|
-
if not self._native.insert(id_, xy):
|
94
|
-
x, y = xy
|
95
|
-
bx0, by0, bx1, by1 = self._bounds
|
96
|
-
raise ValueError(
|
97
|
-
f"Point ({x}, {y}) is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
|
98
|
-
)
|
99
|
-
|
100
|
-
if self._items is not None:
|
101
|
-
self._items.add(Item(id_, xy[0], xy[1], obj))
|
102
|
-
|
103
|
-
self._count += 1
|
104
|
-
return id_
|
105
|
-
|
106
|
-
def insert_many_points(self, points: list[Point]) -> int:
|
107
|
-
"""
|
108
|
-
Bulk insert points with auto-assigned ids.
|
109
|
-
|
110
|
-
Args:
|
111
|
-
points: List of (x, y) points.
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
The number of points inserted
|
115
|
-
"""
|
116
|
-
start_id = self._next_id
|
117
|
-
last_id = self._native.insert_many_points(start_id, points)
|
118
|
-
|
119
|
-
num_inserted = last_id - start_id + 1
|
120
|
-
|
121
|
-
if num_inserted < len(points):
|
122
|
-
raise ValueError("One or more points are outside tree bounds")
|
123
|
-
|
124
|
-
self._next_id = last_id + 1
|
125
|
-
|
126
|
-
# Update the item tracker if needed
|
127
|
-
if self._items is not None:
|
128
|
-
for i, id_ in enumerate(range(start_id, last_id + 1)):
|
129
|
-
x, y = points[i]
|
130
|
-
self._items.add(Item(id_, x, y, None))
|
131
|
-
|
132
|
-
return num_inserted
|
133
|
-
|
134
|
-
def attach(self, id_: int, obj: Any) -> None:
|
135
|
-
"""
|
136
|
-
Attach or replace the Python object for an existing id.
|
137
|
-
Tracking must be enabled.
|
138
|
-
|
139
|
-
Args:
|
140
|
-
id_: Target id.
|
141
|
-
obj: Object to associate with id.
|
142
|
-
"""
|
143
|
-
if self._items is None:
|
144
|
-
raise ValueError("Cannot attach objects when track_objects=False")
|
145
|
-
|
146
|
-
item = self._items.by_id(id_)
|
147
|
-
if item is None:
|
148
|
-
raise KeyError(f"Id {id_} not found in quadtree")
|
149
|
-
self._items.add(Item(id_, item.x, item.y, obj))
|
150
|
-
|
151
|
-
def delete(self, id_: int, xy: Point) -> bool:
|
152
|
-
"""
|
153
|
-
Delete an item by id and exact coordinates.
|
154
|
-
|
155
|
-
Args:
|
156
|
-
id_: Integer id to remove.
|
157
|
-
xy: Coordinates (x, y) of the item.
|
158
|
-
|
159
|
-
Returns:
|
160
|
-
True if the item was found and deleted, else False.
|
161
|
-
"""
|
162
|
-
deleted = self._native.delete(id_, xy)
|
163
|
-
if deleted:
|
164
|
-
self._count -= 1
|
165
|
-
if self._items is not None:
|
166
|
-
self._items.pop_id(id_) # ignore result
|
167
|
-
return deleted
|
168
|
-
|
169
|
-
def delete_by_object(self, obj: Any) -> bool:
|
170
|
-
"""
|
171
|
-
Delete an item by Python object.
|
172
|
-
|
173
|
-
Requires object tracking to be enabled. Performs an O(1) reverse
|
174
|
-
lookup to get the id, then deletes that entry at the given location.
|
175
|
-
|
176
|
-
Args:
|
177
|
-
obj: The tracked Python object to remove.
|
178
|
-
|
179
|
-
Returns:
|
180
|
-
True if the item was found and deleted, else False.
|
181
|
-
|
182
|
-
Raises:
|
183
|
-
ValueError: If object tracking is disabled.
|
184
|
-
"""
|
185
|
-
if self._items is None:
|
186
|
-
raise ValueError(
|
187
|
-
"Cannot delete by object when track_objects=False. Use delete(id, xy) instead."
|
188
|
-
)
|
189
|
-
|
190
|
-
item = self._items.by_obj(obj)
|
191
|
-
if item is None:
|
192
|
-
return False
|
193
|
-
|
194
|
-
return self.delete(item.id_, (item.x, item.y))
|
195
|
-
|
196
|
-
# ---------- queries ----------
|
197
|
-
|
198
|
-
@overload
|
199
|
-
def query(
|
200
|
-
self, rect: Bounds, *, as_items: Literal[False] = ...
|
201
|
-
) -> list[_IdCoord]: ...
|
202
|
-
|
203
|
-
@overload
|
204
|
-
def query(self, rect: Bounds, *, as_items: Literal[True]) -> list[Item]: ...
|
205
|
-
|
206
|
-
def query(
|
207
|
-
self, rect: Bounds, *, as_items: bool = False
|
208
|
-
) -> list[_IdCoord] | list[Item]:
|
209
|
-
"""
|
210
|
-
Return all points inside an axis-aligned rectangle.
|
211
|
-
|
212
|
-
Args:
|
213
|
-
rect: Query rectangle as (min_x, min_y, max_x, max_y).
|
214
|
-
as_items: If True, return Item wrappers. If False, return raw tuples.
|
215
|
-
|
216
|
-
Returns:
|
217
|
-
If as_items is False: list of (id, x, y) tuples.
|
218
|
-
If as_items is True: list of Item objects.
|
219
|
-
"""
|
220
|
-
raw = self._native.query(rect)
|
221
|
-
if not as_items:
|
222
|
-
return raw
|
223
|
-
|
224
|
-
if self._items is None:
|
225
|
-
raise ValueError("Cannot return results as items with track_objects=False")
|
226
|
-
out: list[Item] = []
|
227
|
-
for id_, _, _ in raw:
|
228
|
-
item = self._items.by_id(id_)
|
229
|
-
if item is None:
|
230
|
-
raise RuntimeError(
|
231
|
-
f"Internal error: id {id_} found in native tree but missing from object tracker. "
|
232
|
-
f"Ensure all inserts/deletes are done via this wrapper."
|
233
|
-
)
|
234
|
-
out.append(item)
|
235
|
-
return out
|
236
|
-
|
237
|
-
@overload
|
238
|
-
def nearest_neighbor(
|
239
|
-
self, xy: Point, *, as_item: Literal[False] = ...
|
240
|
-
) -> _IdCoord | None: ...
|
241
|
-
|
242
|
-
@overload
|
243
|
-
def nearest_neighbor(self, xy: Point, *, as_item: Literal[True]) -> Item | None: ...
|
244
|
-
|
245
|
-
def nearest_neighbor(self, xy: Point, *, as_item: bool = False):
|
246
|
-
"""
|
247
|
-
Return the single nearest neighbor to the query point.
|
248
|
-
|
249
|
-
Args:
|
250
|
-
xy: Query point (x, y).
|
251
|
-
as_item: If True, return Item. If False, return (id, x, y).
|
252
|
-
|
253
|
-
Returns:
|
254
|
-
The nearest neighbor or None if the tree is empty.
|
255
|
-
"""
|
256
|
-
t = self._native.nearest_neighbor(xy)
|
257
|
-
if t is None or not as_item:
|
258
|
-
return t
|
259
|
-
|
260
|
-
if self._items is None:
|
261
|
-
raise ValueError("Cannot return result as item with track_objects=False")
|
262
|
-
id_, _x, _y = t
|
263
|
-
item = self._items.by_id(id_)
|
264
|
-
if item is None:
|
265
|
-
raise RuntimeError(
|
266
|
-
f"Internal error: id {id_} found in native tree but missing from object tracker. "
|
267
|
-
f"Ensure all inserts/deletes are done via this wrapper."
|
268
|
-
)
|
269
|
-
return item
|
270
|
-
|
271
|
-
@overload
|
272
|
-
def nearest_neighbors(
|
273
|
-
self, xy: Point, k: int, *, as_items: Literal[False] = ...
|
274
|
-
) -> list[_IdCoord]: ...
|
275
|
-
|
276
|
-
@overload
|
277
|
-
def nearest_neighbors(
|
278
|
-
self, xy: Point, k: int, *, as_items: Literal[True]
|
279
|
-
) -> list[Item]: ...
|
280
|
-
|
281
|
-
def nearest_neighbors(self, xy: Point, k: int, *, as_items: bool = False):
|
282
|
-
"""
|
283
|
-
Return the k nearest neighbors to the query point.
|
284
|
-
|
285
|
-
Args:
|
286
|
-
xy: Query point (x, y).
|
287
|
-
k: Number of neighbors to return.
|
288
|
-
as_items: If True, return Item wrappers. If False, return raw tuples.
|
289
|
-
|
290
|
-
Returns:
|
291
|
-
List of results in ascending distance order.
|
292
|
-
"""
|
293
|
-
raw = self._native.nearest_neighbors(xy, k)
|
294
|
-
if not as_items:
|
295
|
-
return raw
|
296
|
-
if self._items is None:
|
297
|
-
raise ValueError("Cannot return results as items with track_objects=False")
|
298
|
-
|
299
|
-
out: list[Item] = []
|
300
|
-
for id_, _, _ in raw:
|
301
|
-
item = self._items.by_id(id_)
|
302
|
-
if item is None:
|
303
|
-
raise RuntimeError(
|
304
|
-
f"Internal error: id {id_} found in native tree but missing from object tracker. "
|
305
|
-
f"Ensure all inserts/deletes are done via this wrapper."
|
306
|
-
)
|
307
|
-
out.append(item)
|
308
|
-
return out
|
309
|
-
|
310
|
-
# ---------- misc ----------
|
311
|
-
|
312
|
-
def get(self, id_: int) -> Any | None:
|
313
|
-
"""
|
314
|
-
Return the object associated with id.
|
315
|
-
|
316
|
-
Returns:
|
317
|
-
The tracked object if present and tracking is enabled, else None.
|
318
|
-
"""
|
319
|
-
if self._items is None:
|
320
|
-
raise ValueError("Cannot get objects when track_objects=False")
|
321
|
-
item = self._items.by_id(id_)
|
322
|
-
if item is None:
|
323
|
-
return None
|
324
|
-
return item.obj
|
325
|
-
|
326
|
-
def get_all_rectangles(self) -> list[Bounds]:
|
327
|
-
"""
|
328
|
-
Return all node rectangles in the current quadtree.
|
329
|
-
|
330
|
-
Returns:
|
331
|
-
List of (min_x, min_y, max_x, max_y) for each node in the tree.
|
332
|
-
"""
|
333
|
-
return self._native.get_all_rectangles()
|
334
|
-
|
335
|
-
def get_all_objects(self) -> list[Any]:
|
336
|
-
"""
|
337
|
-
Return all tracked objects.
|
338
|
-
|
339
|
-
Returns:
|
340
|
-
List of objects if tracking is enabled, else an empty list.
|
341
|
-
"""
|
342
|
-
if self._items is None:
|
343
|
-
raise ValueError("Cannot get objects when track_objects=False")
|
344
|
-
return [t.obj for t in self._items.items() if t.obj is not None]
|
345
|
-
|
346
|
-
def get_all_items(self) -> list[Item]:
|
347
|
-
"""
|
348
|
-
Return all tracked items.
|
349
|
-
|
350
|
-
Returns:
|
351
|
-
List of Item if tracking is enabled, else an empty list.
|
352
|
-
"""
|
353
|
-
if self._items is None:
|
354
|
-
raise ValueError("Cannot get items when track_objects=False")
|
355
|
-
return list(self._items.items())
|
356
|
-
|
357
|
-
def count_items(self) -> int:
|
358
|
-
"""
|
359
|
-
Return the number of items stored in the native tree.
|
360
|
-
|
361
|
-
Notes:
|
362
|
-
This calls the native engine and may differ from len(self) if
|
363
|
-
you create multiple wrappers around the same native structure.
|
364
|
-
"""
|
365
|
-
return self._native.count_items()
|
366
|
-
|
367
|
-
def __len__(self) -> int:
|
368
|
-
"""
|
369
|
-
Return the number of successful inserts done via this wrapper.
|
370
|
-
|
371
|
-
Notes:
|
372
|
-
This is the Python-side counter that tracks calls that returned True.
|
373
|
-
use count_items() to get the authoritative native-side count.
|
374
|
-
"""
|
375
|
-
return self._count
|
376
|
-
|
377
|
-
# Power users can access the raw class
|
378
|
-
NativeQuadTree = _RustQuadTree
|
379
|
-
|
380
|
-
|
381
|
-
__all__ = ["Bounds", "Item", "Point", "QuadTree"]
|
5
|
+
__all__ = ["Item", "PointItem", "QuadTree", "RectItem", "RectQuadTree"]
|
@@ -0,0 +1,263 @@
|
|
1
|
+
# _abc_quadtree.py
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from typing import Any, Generic, Tuple, TypeVar
|
6
|
+
|
7
|
+
from ._bimap import BiMap
|
8
|
+
from ._item import Item # base class for PointItem and RectItem
|
9
|
+
|
10
|
+
Bounds = Tuple[float, float, float, float]
|
11
|
+
|
12
|
+
# Generic parameters
|
13
|
+
G = TypeVar("G") # geometry type, e.g. Point or Bounds
|
14
|
+
HitT = TypeVar("HitT") # raw native tuple, e.g. (id,x,y) or (id,x0,y0,x1,y1)
|
15
|
+
ItemType = TypeVar(
|
16
|
+
"ItemType", bound=Item
|
17
|
+
) # Python Item subtype, e.g. PointItem or RectItem
|
18
|
+
|
19
|
+
|
20
|
+
class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
21
|
+
"""
|
22
|
+
Shared logic for Python QuadTree wrappers over native Rust engines.
|
23
|
+
|
24
|
+
Concrete subclasses must implement the few native hooks and item builders.
|
25
|
+
"""
|
26
|
+
|
27
|
+
__slots__ = (
|
28
|
+
"_bounds",
|
29
|
+
"_capacity",
|
30
|
+
"_count",
|
31
|
+
"_items",
|
32
|
+
"_max_depth",
|
33
|
+
"_native",
|
34
|
+
"_next_id",
|
35
|
+
"_track_objects",
|
36
|
+
)
|
37
|
+
|
38
|
+
# ---- required native hooks ----
|
39
|
+
|
40
|
+
@abstractmethod
|
41
|
+
def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
|
42
|
+
"""Create the native engine instance."""
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
def _make_item(self, id_: int, geom: G, obj: Any | None) -> ItemType:
|
46
|
+
"""Build an ItemType from id, geometry, and optional object."""
|
47
|
+
|
48
|
+
# ---- ctor ----
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
bounds: Bounds,
|
53
|
+
capacity: int,
|
54
|
+
*,
|
55
|
+
max_depth: int | None = None,
|
56
|
+
track_objects: bool = False,
|
57
|
+
start_id: int = 1,
|
58
|
+
):
|
59
|
+
self._bounds = bounds
|
60
|
+
self._max_depth = max_depth
|
61
|
+
self._capacity = capacity
|
62
|
+
self._native = self._new_native(bounds, capacity, max_depth)
|
63
|
+
# typed item map if tracking enabled
|
64
|
+
self._track_objects = bool(track_objects)
|
65
|
+
self._items: BiMap[ItemType] | None = BiMap() if track_objects else None
|
66
|
+
self._next_id = int(start_id)
|
67
|
+
self._count = 0
|
68
|
+
|
69
|
+
# ---- shared helpers ----
|
70
|
+
|
71
|
+
def _alloc_id(self, id_: int | None) -> int:
|
72
|
+
if id_ is None:
|
73
|
+
nid = self._next_id
|
74
|
+
self._next_id += 1
|
75
|
+
return nid
|
76
|
+
if id_ >= self._next_id:
|
77
|
+
self._next_id = id_ + 1
|
78
|
+
return id_
|
79
|
+
|
80
|
+
def insert(self, geom: G, *, id_: int | None = None, obj: Any = None) -> int:
|
81
|
+
"""
|
82
|
+
Insert a single item
|
83
|
+
|
84
|
+
Args:
|
85
|
+
geom: Point (x, y) or Rect (x0, y0, x1, y1) depending on quadtree type.
|
86
|
+
id_: Optional integer id. If None, an auto id is assigned.
|
87
|
+
obj: Optional Python object to associate with id. Stored only if
|
88
|
+
object tracking is enabled.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
The id used for this insert.
|
92
|
+
|
93
|
+
Raises:
|
94
|
+
ValueError: If the point is outside tree bounds.
|
95
|
+
"""
|
96
|
+
use_id = self._alloc_id(id_)
|
97
|
+
if not self._native.insert(use_id, geom):
|
98
|
+
bx0, by0, bx1, by1 = self._bounds
|
99
|
+
raise ValueError(
|
100
|
+
f"Geometry {geom!r} is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
|
101
|
+
)
|
102
|
+
|
103
|
+
if self._items is not None:
|
104
|
+
self._items.add(self._make_item(use_id, geom, obj))
|
105
|
+
|
106
|
+
self._count += 1
|
107
|
+
return use_id
|
108
|
+
|
109
|
+
def insert_many(self, geoms: list[G]) -> int:
|
110
|
+
"""
|
111
|
+
Bulk insert items with auto-assigned ids. Faster than inserting one at a time.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
geoms: List of geometries. Either Points (x, y) or Rects (x0, y0, x1, y1) depending on quadtree type.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
The number of items inserted
|
118
|
+
"""
|
119
|
+
start_id = self._next_id
|
120
|
+
last_id = self._native.insert_many(start_id, geoms)
|
121
|
+
num = last_id - start_id + 1
|
122
|
+
if num < len(geoms):
|
123
|
+
raise ValueError("One or more items are outside tree bounds")
|
124
|
+
|
125
|
+
self._next_id = last_id + 1
|
126
|
+
if self._items is not None:
|
127
|
+
for i, id_ in enumerate(range(start_id, last_id + 1)):
|
128
|
+
self._items.add(self._make_item(id_, geoms[i], None))
|
129
|
+
self._count += num
|
130
|
+
return num
|
131
|
+
|
132
|
+
def delete(self, id_: int, geom: G) -> bool:
|
133
|
+
"""
|
134
|
+
Delete an item by id and exact geometry.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
id_: Integer id to remove.
|
138
|
+
geom: Exact geometry to remove. Either Point (x, y) or Rect (x0, y0, x1, y1) depending on quadtree type.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
True if the item was found and deleted, else False.
|
142
|
+
"""
|
143
|
+
deleted = self._native.delete(id_, geom)
|
144
|
+
if deleted:
|
145
|
+
self._count -= 1
|
146
|
+
if self._items is not None:
|
147
|
+
self._items.pop_id(id_)
|
148
|
+
return deleted
|
149
|
+
|
150
|
+
def attach(self, id_: int, obj: Any) -> None:
|
151
|
+
"""
|
152
|
+
Attach or replace the Python object for an existing id.
|
153
|
+
Tracking must be enabled.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
id_: Target id.
|
157
|
+
obj: Object to associate with id.
|
158
|
+
"""
|
159
|
+
if self._items is None:
|
160
|
+
raise ValueError("Cannot attach objects when track_objects=False")
|
161
|
+
it = self._items.by_id(id_)
|
162
|
+
if it is None:
|
163
|
+
raise KeyError(f"Id {id_} not found in quadtree")
|
164
|
+
# Preserve geometry from existing item
|
165
|
+
self._items.add(self._make_item(id_, it.geom, obj)) # type: ignore[attr-defined]
|
166
|
+
|
167
|
+
def delete_by_object(self, obj: Any) -> bool:
|
168
|
+
"""
|
169
|
+
Delete an item by Python object.
|
170
|
+
|
171
|
+
Requires object tracking to be enabled. Performs an O(1) reverse
|
172
|
+
lookup to get the id, then deletes that entry at the given location.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
obj: The tracked Python object to remove.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
True if the item was found and deleted, else False.
|
179
|
+
|
180
|
+
Raises:
|
181
|
+
ValueError: If object tracking is disabled.
|
182
|
+
"""
|
183
|
+
if self._items is None:
|
184
|
+
raise ValueError("Cannot delete by object when track_objects=False")
|
185
|
+
it = self._items.by_obj(obj)
|
186
|
+
if it is None:
|
187
|
+
return False
|
188
|
+
# type of geom is determined by concrete Item subtype
|
189
|
+
return self.delete(it.id_, it.geom) # type: ignore[arg-type]
|
190
|
+
|
191
|
+
def clear(self, *, reset_ids: bool = False) -> None:
|
192
|
+
"""
|
193
|
+
Empty the tree in place, preserving bounds/capacity/max_depth.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
reset_ids: If True, restart auto-assigned ids from 1.
|
197
|
+
"""
|
198
|
+
self._native = self._new_native(self._bounds, self._capacity, self._max_depth)
|
199
|
+
self._count = 0
|
200
|
+
if self._items is not None:
|
201
|
+
self._items.clear()
|
202
|
+
if reset_ids:
|
203
|
+
self._next_id = 1
|
204
|
+
|
205
|
+
def get_all_objects(self) -> list[Any]:
|
206
|
+
"""
|
207
|
+
Return all tracked Python objects in the tree.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
List of objects.
|
211
|
+
Raises:
|
212
|
+
ValueError: If object tracking is disabled.
|
213
|
+
"""
|
214
|
+
if self._items is None:
|
215
|
+
raise ValueError("Cannot get objects when track_objects=False")
|
216
|
+
return [t.obj for t in self._items.items() if t.obj is not None]
|
217
|
+
|
218
|
+
def get_all_items(self) -> list[ItemType]:
|
219
|
+
"""
|
220
|
+
Return all Item wrappers in the tree.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
List of Item objects.
|
224
|
+
Raises:
|
225
|
+
ValueError: If object tracking is disabled.
|
226
|
+
"""
|
227
|
+
if self._items is None:
|
228
|
+
raise ValueError("Cannot get items when track_objects=False")
|
229
|
+
return list(self._items.items())
|
230
|
+
|
231
|
+
def get_all_node_boundaries(self) -> list[Bounds]:
|
232
|
+
"""
|
233
|
+
Return all node boundaries in the tree. Great for visualizing the tree structure.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
List of (min_x, min_y, max_x, max_y) for each node in the tree.
|
237
|
+
"""
|
238
|
+
return self._native.get_all_node_boundaries()
|
239
|
+
|
240
|
+
def get(self, id_: int) -> Any | None:
|
241
|
+
"""
|
242
|
+
Return the object associated with id, if tracking is enabled.
|
243
|
+
"""
|
244
|
+
if self._items is None:
|
245
|
+
raise ValueError("Cannot get objects when track_objects=False")
|
246
|
+
item = self._items.by_id(id_)
|
247
|
+
return None if item is None else item.obj
|
248
|
+
|
249
|
+
def count_items(self) -> int:
|
250
|
+
"""
|
251
|
+
Return the number of items currently in the tree.
|
252
|
+
|
253
|
+
Note:
|
254
|
+
Performs a full scan of tree to count up every item.
|
255
|
+
Use the `len()` function or `len(tree)` for O(1) access.
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
Number of items in the tree.
|
259
|
+
"""
|
260
|
+
return self._native.count_items()
|
261
|
+
|
262
|
+
def __len__(self) -> int:
|
263
|
+
return self._count
|