@@ -21,9 +21,8 @@ def __init__(self, crs="epsg:3857"):
2121 crs: Coordinate reference system of the GeoSpace
2222 transformer: A pyproj.Transformer that transforms the GeoSpace into
2323 epsg:4326. Mainly used for GeoJSON serialization.
24- idx: R-tree index for fast spatial queries
25- bbox: Bounding box of all agents within the GeoSpace
2624 agents: List of all agents in the Geospace
25+ bounds: Bounds of the GeoSpace in [min_x, max_x, min_y, max_y] format
2726
2827 Methods:
2928 add_agents: add a list or a single GeoAgent.
@@ -32,22 +31,15 @@ def __init__(self, crs="epsg:3857"):
3231 distance: Calculate distance between two agents
3332 get_neighbors: Returns a list of (touching) neighbors
3433 get_intersecting_agents: Returns list of agents that intersect
35- get_agents_within: Returns a list of agents within
36- get_agent_contains: Returns a list of agents contained
37- get_agents_touches: Returns a list of agents that touch
38- update_bbox: Update the bounding box of the GeoSpace
34+ get_relation: Return a list of related agents
35+ get_neighbors_within_distance: Return a list of agents within `distance` of `agent`
3936 """
4037 self ._crs = pyproj .CRS .from_user_input (crs )
4138 self ._transformer = pyproj .Transformer .from_crs (
4239 crs_from = self .crs , crs_to = "epsg:4326" , always_xy = True
4340 )
4441
45- self .bbox = None
46- self ._neighborhood = None
47-
48- # Set up rtree index
49- self .idx = index .Index ()
50- self .idx .agents = {}
42+ self ._agent_layer = _AgentLayer ()
5143
5244 @property
5345 def crs (self ):
@@ -64,6 +56,38 @@ def transformer(self):
6456 """
6557 return self ._transformer
6658
59+ @property
60+ def agents (self ):
61+ """
62+ Return a list of all agents in the Geospace.
63+ """
64+ return self ._agent_layer .agents
65+
66+ @property
67+ def bounds (self ):
68+ return self ._agent_layer .bounds
69+
70+ @property
71+ def __geo_interface__ (self ):
72+ """Return a GeoJSON FeatureCollection."""
73+ features = [a .__geo_interface__ () for a in self .agents ]
74+ return {"type" : "FeatureCollection" , "features" : features }
75+
76+ def _check_agent (self , agent , auto_convert_crs ):
77+ if hasattr (agent , "geometry" ):
78+ if not self .crs .is_exact_same (agent .crs ):
79+ if auto_convert_crs :
80+ agent .to_crs (self .crs )
81+ else :
82+ raise ValueError (
83+ f"Inconsistent crs: { agent .__class__ .__name__ } is of crs { agent .crs .to_string ()} , "
84+ f"different from the crs of { self .__class__ .__name__ } - { self .crs .to_string ()} . "
85+ "Please check your crs settings, or set `auto_convert_crs` to `True` to allow "
86+ "automatic crs conversion of GeoAgents to GeoSpace."
87+ )
88+ else :
89+ raise AttributeError ("GeoAgents must have a geometry attribute" )
90+
6791 def add_agents (self , agents , auto_convert_crs = False ):
6892 """Add a list of GeoAgents to the Geospace.
6993
@@ -78,45 +102,23 @@ def add_agents(self, agents, auto_convert_crs=False):
78102 Raises:
79103 ValueError: If GeoAgent of different crs is added into the GeoSpace, while
80104 `self.auto_convert_crs` is set to False.
105+ AttributeError: If GeoAgent doesn't have a geometry attribute.
81106 """
82107 if isinstance (agents , GeoAgent ):
83108 agent = agents
84- if hasattr (agent , "geometry" ):
85- if not self .crs .is_exact_same (agent .crs ):
86- if auto_convert_crs :
87- agent .to_crs (self .crs )
88- else :
89- raise ValueError (
90- f"Inconsistent crs: { agent .__class__ .__name__ } is of crs { agent .crs .to_string ()} , "
91- f"different from the crs of { self .__class__ .__name__ } - { self .crs .to_string ()} . "
92- "Please check your crs settings, or set `auto_convert_crs` to `True` to allow "
93- "automatic crs conversion of GeoAgents to GeoSpace."
94- )
95- self .idx .insert (id (agent ), agent .geometry .bounds , None )
96- self .idx .agents [id (agent )] = agent
97- else :
98- raise AttributeError ("GeoAgents must have a geometry attribute" )
109+ self ._check_agent (agent , auto_convert_crs )
99110 else :
100111 for agent in agents :
101- if not self .crs .is_exact_same (agent .crs ):
102- if auto_convert_crs :
103- agent .to_crs (self .crs )
104- else :
105- raise ValueError (
106- f"Inconsistent crs: { agent .__class__ .__name__ } is of crs { agent .crs .to_string ()} , "
107- f"different from the crs of { self .__class__ .__name__ } - { self .crs .to_string ()} . "
108- "Please check your crs settings, or set `auto_convert_crs` to `True` to allow "
109- "automatic crs conversion of GeoAgents to GeoSpace."
110- )
111- self ._recreate_rtree (agents )
112+ self ._check_agent (agent , auto_convert_crs )
113+ self ._agent_layer .add_agents (agents )
112114
113- self .update_bbox ()
115+ def _recreate_rtree (self , new_agents = None ):
116+ """Create a new rtree index from agents geometries."""
117+ self ._agent_layer ._recreate_rtree (new_agents )
114118
115119 def remove_agent (self , agent ):
116120 """Remove an agent from the GeoSpace."""
117- self .idx .delete (id (agent ), agent .geometry .bounds )
118- del self .idx .agents [id (agent )]
119- self .update_bbox ()
121+ self ._agent_layer .remove_agent (agent )
120122
121123 def get_relation (self , agent , relation ):
122124 """Return a list of related agents.
@@ -128,18 +130,10 @@ def get_relation(self, agent, relation):
128130 other_agents: A list of agents to compare against.
129131 Omit to compare against all other agents of the GeoSpace
130132 """
131- possible_agents = self ._get_rtree_intersections (agent .geometry )
132- for other_agent in possible_agents :
133- if getattr (agent .geometry , relation )(other_agent .geometry ):
134- yield other_agent
135-
136- def _get_rtree_intersections (self , geometry ):
137- """Calculate rtree intersections for candidate agents."""
138- return (self .idx .agents [i ] for i in self .idx .intersection (geometry .bounds ))
133+ yield from self ._agent_layer .get_relation (agent , relation )
139134
140135 def get_intersecting_agents (self , agent , other_agents = None ):
141- intersecting_agents = self .get_relation (agent , "intersects" )
142- return intersecting_agents
136+ return self ._agent_layer .get_intersecting_agents (agent , other_agents )
143137
144138 def get_neighbors_within_distance (
145139 self , agent , distance , center = False , relation = "intersects"
@@ -149,25 +143,60 @@ def get_neighbors_within_distance(
149143 Distance is measured as a buffer around the agent's geometry,
150144 set center=True to calculate distance from center.
151145 """
152- if center :
153- geometry = agent .geometry .center ().buffer (distance )
154- else :
155- geometry = agent .geometry .buffer (distance )
156- possible_neighbors = self ._get_rtree_intersections (geometry )
157- prepared_geometry = prep (geometry )
158- for other_agent in possible_neighbors :
159- if getattr (prepared_geometry , relation )(other_agent .geometry ):
160- yield other_agent
146+ yield from self ._agent_layer .get_neighbors_within_distance (
147+ agent , distance , center , relation
148+ )
161149
162150 def agents_at (self , pos ):
163151 """Return a list of agents at given pos."""
164- if not isinstance (pos , Point ):
165- pos = Point (pos )
166- return self .get_relation (pos , "within" )
152+ return self ._agent_layer .agents_at (pos )
167153
168154 def distance (self , agent_a , agent_b ):
169155 """Return distance of two agents."""
170- return agent_a .geometry .distance (agent_b .geometry )
156+ return self ._agent_layer .distance (agent_a , agent_b )
157+
158+ def get_neighbors (self , agent ):
159+ """Get (touching) neighbors of an agent."""
160+ return self ._agent_layer .get_neighbors (agent )
161+
162+
163+ class _AgentLayer :
164+ """Layer that contains the GeoAgents. Mainly for internal usage within `GeoSpace`.
165+
166+ Properties:
167+ idx: R-tree index for fast spatial queries
168+ bounds: Bounds of the layer in [min_x, max_x, min_y, max_y] format
169+ agents: List of all agents in the layer
170+
171+ Methods:
172+ add_agents: add a list or a single GeoAgent.
173+ remove_agent: Remove a single agent from the layer
174+ agents_at: List all agents at a specific position
175+ distance: Calculate distance between two agents
176+ get_neighbors: Returns a list of (touching) neighbors
177+ get_intersecting_agents: Returns list of agents that intersect
178+ get_relation: Return a list of related agents
179+ get_neighbors_within_distance: Return a list of agents within `distance` of `agent`
180+ """
181+
182+ def __init__ (self ):
183+ self ._neighborhood = None
184+
185+ # Set up rtree index
186+ self .idx = index .Index ()
187+ self .idx .agents = {}
188+
189+ @property
190+ def agents (self ):
191+ return list (self .idx .agents .values ())
192+
193+ @property
194+ def bounds (self ):
195+ return self .idx .bounds
196+
197+ def _get_rtree_intersections (self , geometry ):
198+ """Calculate rtree intersections for candidate agents."""
199+ return (self .idx .agents [i ] for i in self .idx .intersection (geometry .bounds ))
171200
172201 def _create_neighborhood (self ):
173202 """Create a neighborhood graph of all agents."""
@@ -179,16 +208,6 @@ def _create_neighborhood(self):
179208 for agent , key in zip (agents , self ._neighborhood .neighbors .keys ()):
180209 self ._neighborhood .idx [agent ] = key
181210
182- def get_neighbors (self , agent ):
183- """Get (touching) neighbors of an agent."""
184- if not self ._neighborhood or self ._neighborhood .agents != self .agents :
185- self ._create_neighborhood ()
186-
187- idx = self ._neighborhood .idx [agent ]
188- neighbors_idx = self ._neighborhood .neighbors [idx ]
189- neighbors = [self .agents [i ] for i in neighbors_idx ]
190- return neighbors
191-
192211 def _recreate_rtree (self , new_agents = None ):
193212 """Create a new rtree index from agents geometries."""
194213
@@ -203,24 +222,82 @@ def _recreate_rtree(self, new_agents=None):
203222 self .idx = index .Index (index_data )
204223 self .idx .agents = {id (agent ): agent for agent in agents }
205224
206- def update_bbox (self , bbox = None ):
207- """Update bounding box of the GeoSpace."""
208- if bbox :
209- self .bbox = bbox
210- elif not self .agents :
211- self .bbox = None
225+ def add_agents (self , agents ):
226+ """Add a list of GeoAgents to the layer without checking their crs.
227+
228+ GeoAgents must have the same crs to avoid incorrect spatial indexing results.
229+ To change the crs of a GeoAgent, use `GeoAgent.to_crs()` method. Refer to
230+ `GeoSpace._check_agent()` as an example.
231+ This function may also be called with a single GeoAgent.
232+
233+ Args:
234+ agents: List of GeoAgents, or a single GeoAgent, to be added into the layer.
235+ """
236+ if isinstance (agents , GeoAgent ):
237+ agent = agents
238+ self .idx .insert (id (agent ), agent .geometry .bounds , None )
239+ self .idx .agents [id (agent )] = agent
212240 else :
213- self .bbox = self . idx . bounds
241+ self ._recreate_rtree ( agents )
214242
215- @property
216- def agents (self ):
243+ def remove_agent (self , agent ):
244+ """Remove an agent from the layer."""
245+ self .idx .delete (id (agent ), agent .geometry .bounds )
246+ del self .idx .agents [id (agent )]
247+
248+ def get_relation (self , agent , relation ):
249+ """Return a list of related agents.
250+
251+ Args:
252+ agent: the agent for which to compute the relation
253+ relation: must be one of 'intersects', 'within', 'contains',
254+ 'touches'
255+ other_agents: A list of agents to compare against.
256+ Omit to compare against all other agents of the layer.
217257 """
218- Return a list of all agents in the Geospace.
258+ possible_agents = self ._get_rtree_intersections (agent .geometry )
259+ for other_agent in possible_agents :
260+ if getattr (agent .geometry , relation )(other_agent .geometry ):
261+ yield other_agent
262+
263+ def get_intersecting_agents (self , agent , other_agents = None ):
264+ intersecting_agents = self .get_relation (agent , "intersects" )
265+ return intersecting_agents
266+
267+ def get_neighbors_within_distance (
268+ self , agent , distance , center = False , relation = "intersects"
269+ ):
270+ """Return a list of agents within `distance` of `agent`.
271+
272+ Distance is measured as a buffer around the agent's geometry,
273+ set center=True to calculate distance from center.
219274 """
220- return list (self .idx .agents .values ())
275+ if center :
276+ geometry = agent .geometry .center ().buffer (distance )
277+ else :
278+ geometry = agent .geometry .buffer (distance )
279+ possible_neighbors = self ._get_rtree_intersections (geometry )
280+ prepared_geometry = prep (geometry )
281+ for other_agent in possible_neighbors :
282+ if getattr (prepared_geometry , relation )(other_agent .geometry ):
283+ yield other_agent
221284
222- @property
223- def __geo_interface__ (self ):
224- """Return a GeoJSON FeatureCollection."""
225- features = [a .__geo_interface__ () for a in self .agents ]
226- return {"type" : "FeatureCollection" , "features" : features }
285+ def agents_at (self , pos ):
286+ """Return a list of agents at given pos."""
287+ if not isinstance (pos , Point ):
288+ pos = Point (pos )
289+ return self .get_relation (pos , "within" )
290+
291+ def distance (self , agent_a , agent_b ):
292+ """Return distance of two agents."""
293+ return agent_a .geometry .distance (agent_b .geometry )
294+
295+ def get_neighbors (self , agent ):
296+ """Get (touching) neighbors of an agent."""
297+ if not self ._neighborhood or self ._neighborhood .agents != self .agents :
298+ self ._create_neighborhood ()
299+
300+ idx = self ._neighborhood .idx [agent ]
301+ neighbors_idx = self ._neighborhood .neighbors [idx ]
302+ neighbors = [self .agents [i ] for i in neighbors_idx ]
303+ return neighbors
0 commit comments