Skip to content

Commit 8e55655

Browse files
wang-boyuCorvince
authored andcommitted
extract an _AgentLayer from GeoSpace
1 parent 7345d87 commit 8e55655

File tree

1 file changed

+168
-91
lines changed

1 file changed

+168
-91
lines changed

mesa_geo/geospace.py

Lines changed: 168 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)