@@ -91,6 +91,12 @@ def total_bounds(self) -> np.ndarray | None:
91
91
"""
92
92
Return the bounds of the GeoSpace in [min_x, min_y, max_x, max_y] format.
93
93
"""
94
+ if self ._total_bounds is None :
95
+ if len (self .agents ) > 0 :
96
+ self ._update_bounds (self ._agent_layer .total_bounds )
97
+ if len (self .layers ) > 0 :
98
+ for layer in self .layers :
99
+ self ._update_bounds (layer .total_bounds )
94
100
return self ._total_bounds
95
101
96
102
def _update_bounds (self , new_bounds : np .ndarray ) -> None :
@@ -128,7 +134,7 @@ def add_layer(self, layer: ImageLayer | RasterLayer | gpd.GeoDataFrame) -> None:
128
134
"to `False` to suppress this warning message."
129
135
)
130
136
layer .to_crs (self .crs , inplace = True )
131
- self ._update_bounds ( layer . total_bounds )
137
+ self ._total_bounds = None
132
138
self ._static_layers .append (layer )
133
139
134
140
def _check_agent (self , agent ):
@@ -161,7 +167,7 @@ def add_agents(self, agents):
161
167
for agent in agents :
162
168
self ._check_agent (agent )
163
169
self ._agent_layer .add_agents (agents )
164
- self ._update_bounds ( new_bounds = self . _agent_layer . total_bounds )
170
+ self ._total_bounds = None
165
171
166
172
def _recreate_rtree (self , new_agents = None ):
167
173
"""Create a new rtree index from agents geometries."""
@@ -170,6 +176,7 @@ def _recreate_rtree(self, new_agents=None):
170
176
def remove_agent (self , agent ):
171
177
"""Remove an agent from the GeoSpace."""
172
178
self ._agent_layer .remove_agent (agent )
179
+ self ._total_bounds = None
173
180
174
181
def get_relation (self , agent , relation ):
175
182
"""Return a list of related agents.
@@ -231,34 +238,50 @@ class _AgentLayer:
231
238
"""
232
239
233
240
def __init__ (self ):
241
+ # neighborhood graph for touching neighbors
234
242
self ._neighborhood = None
235
-
236
- # Set up rtree index
237
- self .idx = index .Index ()
238
- self .idx .agents = {}
243
+ # rtree index for spatial indexing (e.g., neighbors within distance, agents at pos, etc.)
244
+ self ._idx = None
245
+ self ._id_to_agent = {}
246
+ # bounds of the layer in [min_x, min_y, max_x, max_y] format
247
+ # While it is possible to calculate the bounds from rtree index,
248
+ # total_bounds is almost always needed (e.g., for plotting), while rtree index is not.
249
+ # Hence we compute total_bounds separately from rtree index.
250
+ self ._total_bounds = None
239
251
240
252
@property
241
253
def agents (self ):
242
254
"""
243
255
Return a list of all agents in the layer.
244
256
"""
245
257
246
- return list (self .idx . agents .values ())
258
+ return list (self ._id_to_agent .values ())
247
259
248
260
@property
249
261
def total_bounds (self ):
250
262
"""
251
263
Return the bounds of the layer in [min_x, min_y, max_x, max_y] format.
252
264
"""
253
265
254
- return self .idx .get_bounds (coordinate_interleaved = True )
266
+ if self ._total_bounds is None and len (self .agents ) > 0 :
267
+ bounds = np .array ([agent .geometry .bounds for agent in self .agents ])
268
+ min_x , min_y = np .min (bounds [:, :2 ], axis = 0 )
269
+ max_x , max_y = np .max (bounds [:, 2 :], axis = 0 )
270
+ self ._total_bounds = np .array ([min_x , min_y , max_x , max_y ])
271
+ return self ._total_bounds
255
272
256
273
def _get_rtree_intersections (self , geometry ):
257
274
"""
258
275
Calculate rtree intersections for candidate agents.
259
276
"""
260
277
261
- return (self .idx .agents [i ] for i in self .idx .intersection (geometry .bounds ))
278
+ self ._ensure_index ()
279
+ if self ._idx is None :
280
+ return []
281
+ else :
282
+ return [
283
+ self ._id_to_agent [i ] for i in self ._idx .intersection (geometry .bounds )
284
+ ]
262
285
263
286
def _create_neighborhood (self ):
264
287
"""
@@ -273,21 +296,27 @@ def _create_neighborhood(self):
273
296
for agent , key in zip (agents , self ._neighborhood .neighbors .keys ()):
274
297
self ._neighborhood .idx [agent ] = key
275
298
299
+ def _ensure_index (self ):
300
+ """
301
+ Ensure that the rtree index is created.
302
+ """
303
+
304
+ if self ._idx is None :
305
+ self ._recreate_rtree ()
306
+
276
307
def _recreate_rtree (self , new_agents = None ):
277
308
"""
278
309
Create a new rtree index from agents geometries.
279
310
"""
280
311
281
312
if new_agents is None :
282
313
new_agents = []
283
- old_agents = list (self .agents )
284
- agents = old_agents + new_agents
285
-
286
- # Bulk insert agents
287
- index_data = ((id (agent ), agent .geometry .bounds , None ) for agent in agents )
314
+ agents = list (self .agents ) + new_agents
288
315
289
- self .idx = index .Index (index_data )
290
- self .idx .agents = {id (agent ): agent for agent in agents }
316
+ if len (agents ) > 0 :
317
+ # Bulk insert agents
318
+ index_data = ((id (agent ), agent .geometry .bounds , None ) for agent in agents )
319
+ self ._idx = index .Index (index_data )
291
320
292
321
def add_agents (self , agents ):
293
322
"""
@@ -303,18 +332,25 @@ def add_agents(self, agents):
303
332
304
333
if isinstance (agents , GeoAgent ):
305
334
agent = agents
306
- self .idx .insert (id (agent ), agent .geometry .bounds , None )
307
- self .idx .agents [id (agent )] = agent
335
+ self ._id_to_agent [id (agent )] = agent
336
+ if self ._idx :
337
+ self ._idx .insert (id (agent ), agent .geometry .bounds , None )
308
338
else :
309
- self ._recreate_rtree (agents )
339
+ for agent in agents :
340
+ self ._id_to_agent [id (agent )] = agent
341
+ if self ._idx :
342
+ self ._recreate_rtree (agents )
343
+ self ._total_bounds = None
310
344
311
345
def remove_agent (self , agent ):
312
346
"""
313
347
Remove an agent from the layer.
314
348
"""
315
349
316
- self .idx .delete (id (agent ), agent .geometry .bounds )
317
- del self .idx .agents [id (agent )]
350
+ del self ._id_to_agent [id (agent )]
351
+ if self ._idx :
352
+ self ._idx .delete (id (agent ), agent .geometry .bounds )
353
+ self ._total_bounds = None
318
354
319
355
def get_relation (self , agent , relation ):
320
356
"""Return a list of related agents.
@@ -327,6 +363,7 @@ def get_relation(self, agent, relation):
327
363
Omit to compare against all other agents of the layer.
328
364
"""
329
365
366
+ self ._ensure_index ()
330
367
possible_agents = self ._get_rtree_intersections (agent .geometry )
331
368
for other_agent in possible_agents :
332
369
if (
@@ -336,6 +373,7 @@ def get_relation(self, agent, relation):
336
373
yield other_agent
337
374
338
375
def get_intersecting_agents (self , agent ):
376
+ self ._ensure_index ()
339
377
intersecting_agents = self .get_relation (agent , "intersects" )
340
378
return intersecting_agents
341
379
@@ -347,7 +385,7 @@ def get_neighbors_within_distance(
347
385
Distance is measured as a buffer around the agent's geometry,
348
386
set center=True to calculate distance from center.
349
387
"""
350
-
388
+ self . _ensure_index ()
351
389
if center :
352
390
geometry = agent .geometry .centroid .buffer (distance )
353
391
else :
@@ -363,6 +401,7 @@ def agents_at(self, pos):
363
401
Return a generator of agents at given pos.
364
402
"""
365
403
404
+ self ._ensure_index ()
366
405
if not isinstance (pos , Point ):
367
406
pos = Point (pos )
368
407
@@ -386,10 +425,13 @@ def get_neighbors(self, agent):
386
425
if not self ._neighborhood or self ._neighborhood .agents != self .agents :
387
426
self ._create_neighborhood ()
388
427
389
- idx = self ._neighborhood .idx [agent ]
390
- neighbors_idx = self ._neighborhood .neighbors [idx ]
391
- neighbors = [self .agents [i ] for i in neighbors_idx ]
392
- return neighbors
428
+ if self ._neighborhood is None :
429
+ return []
430
+ else :
431
+ idx = self ._neighborhood .idx [agent ]
432
+ neighbors_idx = self ._neighborhood .neighbors [idx ]
433
+ neighbors = [self .agents [i ] for i in neighbors_idx ]
434
+ return neighbors
393
435
394
436
def get_agents_as_GeoDataFrame (self , agent_cls = GeoAgent ) -> gpd .GeoDataFrame :
395
437
"""
0 commit comments