geo-adjacency 1.0.7__py3-none-any.whl → 1.1.2__py3-none-any.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.
- geo_adjacency/adjacency.py +332 -150
- geo_adjacency/exception.py +2 -0
- geo_adjacency/utils.py +102 -14
- {geo_adjacency-1.0.7.dist-info → geo_adjacency-1.1.2.dist-info}/METADATA +13 -3
- geo_adjacency-1.1.2.dist-info/RECORD +8 -0
- geo_adjacency/file.log +0 -10
- geo_adjacency-1.0.7.dist-info/RECORD +0 -9
- {geo_adjacency-1.0.7.dist-info → geo_adjacency-1.1.2.dist-info}/LICENSE +0 -0
- {geo_adjacency-1.0.7.dist-info → geo_adjacency-1.1.2.dist-info}/WHEEL +0 -0
geo_adjacency/adjacency.py
CHANGED
|
@@ -1,61 +1,86 @@
|
|
|
1
1
|
"""
|
|
2
|
-
The adjacency module
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
The `adjacency` module implements the AdjacencyEngine class,
|
|
3
|
+
which allows us to calculate adjacency relationships. Adjacency relationships are between a set of source geometries,
|
|
4
|
+
or between source geometries and a second set of target geometries. Obstacle geometries can be passed in to
|
|
5
|
+
stand between sources or sources and targets, but they are not included in the output.
|
|
6
|
+
|
|
7
|
+
For example, if we wanted to know what trees in a forest are adjacent to the shore of a lake, we could
|
|
8
|
+
pass in a set of Point geometries to the trees, a Polygon to represent the lake, and a LineString to represent
|
|
9
|
+
a road passing between some of the trees and the shore.
|
|
10
|
+
|
|
11
|
+
`AdjacencyEngine` utilizes a Voronoi diagram of all the vertices in all the geometries combined to determine
|
|
12
|
+
which geometries are adjacent to each other. See
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
import math
|
|
13
|
-
import os
|
|
14
|
-
import time
|
|
15
16
|
from collections import defaultdict
|
|
16
|
-
from typing import List, Union, Dict, Tuple
|
|
17
|
+
from typing import List, Union, Dict, Tuple, Generator
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from typing_extensions import Self # Python < 3.11
|
|
21
|
+
except ImportError:
|
|
22
|
+
from typing import Self # Python >= 3.11
|
|
17
23
|
import logging
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
import numpy as np
|
|
27
|
+
import shapely.ops
|
|
28
|
+
from scipy.spatial import distance
|
|
20
29
|
from scipy.spatial import Voronoi
|
|
21
|
-
from shapely import
|
|
30
|
+
from shapely import LineString, Point, Polygon, MultiPolygon
|
|
22
31
|
from shapely.geometry.base import BaseGeometry
|
|
23
|
-
from shapely.ops import nearest_points
|
|
24
|
-
from shapely.wkt import loads
|
|
25
|
-
from shapely.geometry import mapping
|
|
26
|
-
import numpy as np
|
|
27
|
-
import matplotlib.pyplot as plt
|
|
28
32
|
|
|
29
33
|
from geo_adjacency.exception import ImmutablePropertyError
|
|
30
|
-
from geo_adjacency.utils import
|
|
31
|
-
|
|
34
|
+
from geo_adjacency.utils import (
|
|
35
|
+
add_geometry_to_plot,
|
|
36
|
+
coords_from_point,
|
|
37
|
+
coords_from_ring,
|
|
38
|
+
coords_from_polygon,
|
|
39
|
+
coords_from_multipolygon,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# ToDo: Support geometries with Z-coordinates
|
|
32
43
|
# Create a custom logger
|
|
33
|
-
|
|
44
|
+
log: logging.Logger = logging.getLogger(__name__)
|
|
34
45
|
|
|
35
46
|
# Create handlers
|
|
36
|
-
c_handler = logging.StreamHandler()
|
|
37
|
-
f_handler = logging.FileHandler("file.log")
|
|
47
|
+
c_handler: logging.StreamHandler = logging.StreamHandler()
|
|
38
48
|
c_handler.setLevel(logging.WARNING)
|
|
39
|
-
f_handler.setLevel(logging.ERROR)
|
|
40
49
|
|
|
41
50
|
# Create formatters and add it to handlers
|
|
42
|
-
c_format = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
|
|
43
|
-
f_format = logging.Formatter(
|
|
44
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
51
|
+
c_format: logging.Formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
|
|
52
|
+
f_format: logging.Formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
45
53
|
c_handler.setFormatter(c_format)
|
|
46
|
-
f_handler.setFormatter(f_format)
|
|
47
54
|
|
|
48
55
|
# Add handlers to the logger
|
|
49
|
-
|
|
50
|
-
logger.addHandler(f_handler)
|
|
56
|
+
log.addHandler(c_handler)
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
class _Feature:
|
|
60
|
+
"""
|
|
61
|
+
A _Feature is a wrapper around a Shapely geometry that allows us to easily determine if two
|
|
62
|
+
geometries are adjacent.
|
|
63
|
+
"""
|
|
54
64
|
__slots__ = ("_geometry", "_coords", "voronoi_points")
|
|
55
65
|
|
|
56
66
|
def __init__(self, geometry: BaseGeometry):
|
|
67
|
+
"""
|
|
68
|
+
Create a _Feature from a Shapely geometry.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
geometry (BaseGeometry): A valid Shapely Geometry, either a Point, LineString, Polygon, or
|
|
72
|
+
MultiPolygon.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if not isinstance(geometry, (Point, Polygon, MultiPolygon, LineString)):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
"Cannot create _Feature for geometry type '%s'." % type(geometry)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert geometry.is_valid, "Could not process invalid geometry: %s" % geometry.wkt
|
|
81
|
+
|
|
57
82
|
self._geometry: BaseGeometry = geometry
|
|
58
|
-
self._coords = None
|
|
83
|
+
self._coords: Union[List[Tuple[float, float]], None] = None
|
|
59
84
|
self.voronoi_points: set = set()
|
|
60
85
|
|
|
61
86
|
def __str__(self):
|
|
@@ -64,11 +89,43 @@ class _Feature:
|
|
|
64
89
|
def __repr__(self):
|
|
65
90
|
return f"<_Feature: {str(self.geometry)}>"
|
|
66
91
|
|
|
92
|
+
def _is_adjacent(
|
|
93
|
+
self, other: Self, min_overlapping_voronoi_vertices: int = 2
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Determine if two features are adjacent based on how many Voronoi vertices they share. Note:
|
|
97
|
+
the Voronoi analysis must have been run, or this will always return False.
|
|
98
|
+
Args:
|
|
99
|
+
other (_Feature): Another _Feature to compare to.
|
|
100
|
+
min_overlapping_voronoi_vertices (int): The minimum number of Voronoi vertices that
|
|
101
|
+
must be shared to be considered adjacent.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
bool: True if the two features are adjacent.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
assert isinstance(other, type(self)), "Cannot compare '%s' with '%s'." % (
|
|
108
|
+
type(self),
|
|
109
|
+
type(other),
|
|
110
|
+
)
|
|
111
|
+
if len(self.voronoi_points) == 0 and len(other.voronoi_points) == 0:
|
|
112
|
+
log.warning(
|
|
113
|
+
"No Voronoi vertices found for either feature. Did you run the analysis yet?"
|
|
114
|
+
)
|
|
115
|
+
return False
|
|
116
|
+
return (
|
|
117
|
+
len(self.voronoi_points & other.voronoi_points)
|
|
118
|
+
>= min_overlapping_voronoi_vertices
|
|
119
|
+
)
|
|
120
|
+
|
|
67
121
|
@property
|
|
68
122
|
def geometry(self):
|
|
69
123
|
"""
|
|
70
124
|
Access the Shapely geometry of the feature.
|
|
71
|
-
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
BaseGeometry: The Shapely geometry of the feature.
|
|
128
|
+
|
|
72
129
|
"""
|
|
73
130
|
return self._geometry
|
|
74
131
|
|
|
@@ -82,23 +139,28 @@ class _Feature:
|
|
|
82
139
|
"""
|
|
83
140
|
Convenience property for accessing the coordinates of the geometry as a list of 2-tuples.
|
|
84
141
|
|
|
85
|
-
:
|
|
142
|
+
Returns:
|
|
143
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
144
|
+
|
|
86
145
|
"""
|
|
146
|
+
|
|
87
147
|
if not self._coords:
|
|
88
148
|
if isinstance(self.geometry, Point):
|
|
89
|
-
self._coords =
|
|
149
|
+
self._coords = coords_from_point(self.geometry)
|
|
150
|
+
elif isinstance(self.geometry, LineString):
|
|
151
|
+
self._coords = coords_from_ring(self.geometry)
|
|
90
152
|
elif isinstance(self.geometry, Polygon):
|
|
91
|
-
self._coords =
|
|
153
|
+
self._coords = coords_from_polygon(self.geometry)
|
|
92
154
|
elif isinstance(self.geometry, MultiPolygon):
|
|
93
|
-
self._coords =
|
|
94
|
-
mapping(self.geometry)["coordinates"][0]))
|
|
95
|
-
elif isinstance(self.geometry, LineString):
|
|
96
|
-
self._coords = tuple((x, y) for x, y in self.geometry.coords)
|
|
155
|
+
self._coords = coords_from_multipolygon(self.geometry)
|
|
97
156
|
else:
|
|
98
|
-
raise TypeError(
|
|
99
|
-
f"Unknown geometry type '{type(self.geometry)}'")
|
|
157
|
+
raise TypeError(f"Unknown geometry type '{type(self.geometry)}'")
|
|
100
158
|
return self._coords
|
|
101
159
|
|
|
160
|
+
@coords.setter
|
|
161
|
+
def coords(self, coords):
|
|
162
|
+
raise ImmutablePropertyError("Property coords is immutable.")
|
|
163
|
+
|
|
102
164
|
|
|
103
165
|
class AdjacencyEngine:
|
|
104
166
|
"""
|
|
@@ -110,36 +172,43 @@ class AdjacencyEngine:
|
|
|
110
172
|
adjacent.
|
|
111
173
|
"""
|
|
112
174
|
|
|
113
|
-
__slots__ = (
|
|
114
|
-
|
|
175
|
+
__slots__ = (
|
|
176
|
+
"_source_features",
|
|
177
|
+
"_target_features",
|
|
178
|
+
"_obstacle_features",
|
|
179
|
+
"_adjacency_dict",
|
|
180
|
+
"_feature_indices",
|
|
181
|
+
"_vor",
|
|
182
|
+
"_all_features",
|
|
183
|
+
"_all_coordinates",
|
|
184
|
+
)
|
|
115
185
|
|
|
116
186
|
def __init__(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
self,
|
|
188
|
+
source_geoms: List[BaseGeometry],
|
|
189
|
+
target_geoms: Union[List[BaseGeometry], None] = None,
|
|
190
|
+
obstacle_geoms: Union[List[BaseGeometry], None] = None,
|
|
191
|
+
densify_features: bool = False,
|
|
192
|
+
max_segment_length: Union[float, None] = None,
|
|
123
193
|
):
|
|
124
194
|
"""
|
|
125
|
-
|
|
195
|
+
Note: only Multipolygons, Polygons, LineStrings and Points are supported. It is assumed all
|
|
126
196
|
features are in the same projection.
|
|
127
197
|
|
|
128
|
-
:
|
|
129
|
-
|
|
130
|
-
|
|
198
|
+
Args:
|
|
199
|
+
source_geoms (List[BaseGeometry]): List of Shapely geometries. We will which ones are adjacent to
|
|
200
|
+
which others, unless target_geoms is specified.
|
|
201
|
+
target_geoms (Union[List[BaseGeometry], None]), optional): list of Shapley geometries. if not None, We will
|
|
131
202
|
test if these features are adjacent to the source features.
|
|
132
|
-
|
|
203
|
+
obstacle_geoms (Union[List[BaseGeometry], None]), optional): List
|
|
133
204
|
of Shapely geometries. These features will not be tested for adjacency, but they can
|
|
134
205
|
prevent a source and target feature from being adjacent.
|
|
135
|
-
|
|
206
|
+
densify_features (bool, optional): If
|
|
136
207
|
True, we will add additional points to the features to improve accuracy of the voronoi
|
|
137
|
-
diagram.
|
|
138
|
-
:param max_segment_length: The maximum distance between vertices that we want.
|
|
139
|
-
In projection units. densify_features must be True, or an error will be thrown. If
|
|
140
|
-
densify_features is True and max_segment_length is false, then the max_segment_length
|
|
208
|
+
diagram. If densify_features is True and max_segment_length is false, then the max_segment_length
|
|
141
209
|
will be calculated based on the average segment length of all features, divided by 5.
|
|
142
|
-
|
|
210
|
+
max_segment_length (Union[float, None], optional): The maximum distance between vertices that we want.
|
|
211
|
+
In projection units. densify_features must be True, or an error will be thrown.
|
|
143
212
|
"""
|
|
144
213
|
|
|
145
214
|
if max_segment_length and not densify_features:
|
|
@@ -147,38 +216,80 @@ class AdjacencyEngine:
|
|
|
147
216
|
"interpolate_points must be True if interpolation_distance is not None"
|
|
148
217
|
)
|
|
149
218
|
|
|
150
|
-
self._source_features: Tuple[_Feature] = tuple(
|
|
151
|
-
_Feature(geom) for geom in source_geoms
|
|
152
|
-
|
|
153
|
-
self._target_features: Tuple[_Feature] =
|
|
154
|
-
_Feature(geom) for geom in target_geoms
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
|
|
219
|
+
self._source_features: Tuple[_Feature] = tuple(
|
|
220
|
+
[_Feature(geom) for geom in source_geoms]
|
|
221
|
+
)
|
|
222
|
+
self._target_features: Tuple[_Feature] = (
|
|
223
|
+
tuple([_Feature(geom) for geom in target_geoms])
|
|
224
|
+
if target_geoms
|
|
225
|
+
else tuple()
|
|
226
|
+
)
|
|
227
|
+
self._obstacle_features: Union[Tuple[_Feature], None] = (
|
|
228
|
+
tuple([_Feature(geom) for geom in obstacle_geoms])
|
|
229
|
+
if obstacle_geoms
|
|
230
|
+
else tuple()
|
|
231
|
+
)
|
|
232
|
+
self._adjacency_dict: Union[Dict[int, List[int]], None] = None
|
|
233
|
+
self._feature_indices: Union[Dict[int, int], None] = None
|
|
161
234
|
self._vor = None
|
|
235
|
+
self._all_coordinates = None
|
|
162
236
|
|
|
163
237
|
"""All source, target, and obstacle features in a single list. The order of this list must
|
|
164
238
|
not be changed."""
|
|
165
|
-
self.
|
|
166
|
-
|
|
239
|
+
self._all_features: Tuple[_Feature, ...] = tuple(
|
|
240
|
+
[*self.source_features, *self.target_features, *self.obstacle_features]
|
|
241
|
+
)
|
|
167
242
|
|
|
168
243
|
if densify_features:
|
|
169
244
|
if max_segment_length is None:
|
|
170
|
-
max_segment_length = self.
|
|
171
|
-
|
|
245
|
+
max_segment_length = self._calc_segmentation_dist()
|
|
246
|
+
log.info("Calculated max_segment_length of %s" % max_segment_length)
|
|
172
247
|
|
|
173
248
|
for feature in self.all_features:
|
|
174
|
-
|
|
175
|
-
max_segment_length)
|
|
249
|
+
if not isinstance(feature.geometry, Point):
|
|
250
|
+
feature.geometry = feature.geometry.segmentize(max_segment_length)
|
|
251
|
+
# Reset all coordinates
|
|
252
|
+
self._all_coordinates = None
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def all_features(self):
|
|
256
|
+
"""
|
|
257
|
+
All source, target, and obstacle features in a single list. The order of this list must
|
|
258
|
+
not be changed. This property cannot be set manually.
|
|
176
259
|
|
|
177
|
-
|
|
178
|
-
[
|
|
179
|
-
))
|
|
260
|
+
Returns:
|
|
261
|
+
List[_Feature]: A list of _Features.
|
|
180
262
|
|
|
181
|
-
|
|
263
|
+
"""
|
|
264
|
+
return self._all_features
|
|
265
|
+
|
|
266
|
+
@all_features.setter
|
|
267
|
+
def all_features(self, value):
|
|
268
|
+
raise ImmutablePropertyError("Property all_features is immutable.")
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def all_coordinates(self):
|
|
272
|
+
"""
|
|
273
|
+
All source, target, and obstacle coordinates in a single list. The order of this list must
|
|
274
|
+
not be changed. This property cannot be set manually.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List[tuple[float, float]]: A list of coordinate tuples.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
if not self._all_coordinates:
|
|
281
|
+
self._all_coordinates = []
|
|
282
|
+
for feature in self.all_features:
|
|
283
|
+
self._all_coordinates.extend(feature.coords)
|
|
284
|
+
|
|
285
|
+
self._all_coordinates = tuple(self._all_coordinates)
|
|
286
|
+
return self._all_coordinates
|
|
287
|
+
|
|
288
|
+
@all_coordinates.setter
|
|
289
|
+
def all_coordinates(self, value):
|
|
290
|
+
raise ImmutablePropertyError("Property all_coordinates is immutable.")
|
|
291
|
+
|
|
292
|
+
def _calc_segmentation_dist(self, divisor=5):
|
|
182
293
|
"""
|
|
183
294
|
Try to create a well-fitting maximum length for all line segments in all features. Take
|
|
184
295
|
the average distance between all coordinate pairs and divide by 5. This means that the
|
|
@@ -188,19 +299,18 @@ class AdjacencyEngine:
|
|
|
188
299
|
average segment lengths. In that case, it is advisable to prepare the data appropriately
|
|
189
300
|
beforehand.
|
|
190
301
|
|
|
191
|
-
:
|
|
302
|
+
Args:
|
|
303
|
+
divisor (int, optional): Divide the average segment length by this number to get the new desired
|
|
192
304
|
segment length.
|
|
193
305
|
|
|
194
|
-
:
|
|
306
|
+
Returns:
|
|
307
|
+
float: Average segment length divided by divisor.
|
|
195
308
|
"""
|
|
196
309
|
|
|
197
|
-
all_coordinates = flatten_list(
|
|
198
|
-
[feature.coords for feature in self.all_features]
|
|
199
|
-
)
|
|
200
310
|
return float(
|
|
201
311
|
(
|
|
202
|
-
|
|
203
|
-
|
|
312
|
+
sum(distance.pdist(self.all_coordinates, "euclidean"))
|
|
313
|
+
/ math.pow(len(self.all_coordinates), 2)
|
|
204
314
|
)
|
|
205
315
|
/ divisor
|
|
206
316
|
)
|
|
@@ -209,7 +319,10 @@ class AdjacencyEngine:
|
|
|
209
319
|
def source_features(self) -> Tuple[_Feature]:
|
|
210
320
|
"""
|
|
211
321
|
Features which will be the keys in the adjacency_dict.
|
|
212
|
-
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
List[_Feature]: A list of _Features.
|
|
325
|
+
|
|
213
326
|
"""
|
|
214
327
|
return self._source_features
|
|
215
328
|
|
|
@@ -221,7 +334,8 @@ class AdjacencyEngine:
|
|
|
221
334
|
def target_features(self) -> Tuple[_Feature]:
|
|
222
335
|
"""
|
|
223
336
|
Features which will be the values in the adjacency_dict.
|
|
224
|
-
:
|
|
337
|
+
Returns:
|
|
338
|
+
List[_Feature]: A list of _Features.
|
|
225
339
|
"""
|
|
226
340
|
return self._target_features
|
|
227
341
|
|
|
@@ -234,14 +348,15 @@ class AdjacencyEngine:
|
|
|
234
348
|
"""
|
|
235
349
|
Features which can prevent source and target features from being adjacent. They
|
|
236
350
|
Do not participate in the adjacency_dict.
|
|
237
|
-
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List[_Feature]: A list of _Features.
|
|
238
354
|
"""
|
|
239
355
|
return self._obstacle_features
|
|
240
356
|
|
|
241
357
|
@obstacle_features.setter
|
|
242
358
|
def obstacle_features(self, _):
|
|
243
|
-
raise ImmutablePropertyError(
|
|
244
|
-
"Property obstacle_features is immutable.")
|
|
359
|
+
raise ImmutablePropertyError("Property obstacle_features is immutable.")
|
|
245
360
|
|
|
246
361
|
def get_feature_from_coord_index(self, coord_index: int) -> _Feature:
|
|
247
362
|
"""
|
|
@@ -250,8 +365,11 @@ class AdjacencyEngine:
|
|
|
250
365
|
determine which coordinate belongs to which feature after we calculate the voronoi
|
|
251
366
|
diagram.
|
|
252
367
|
|
|
253
|
-
:
|
|
368
|
+
Args:
|
|
369
|
+
coord_index (int): The index of the coordinate in self._all_coordinates
|
|
254
370
|
|
|
371
|
+
Returns:
|
|
372
|
+
_Feature: A _Feature at the given index.
|
|
255
373
|
"""
|
|
256
374
|
if not self._feature_indices:
|
|
257
375
|
self._feature_indices = {}
|
|
@@ -268,7 +386,9 @@ class AdjacencyEngine:
|
|
|
268
386
|
"""
|
|
269
387
|
The Voronoi diagram object returned by Scipy. Useful primarily for debugging an
|
|
270
388
|
adjacency analysis.
|
|
271
|
-
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
scipy.spatial.Voronoi: The Scipy Voronoi object.
|
|
272
392
|
"""
|
|
273
393
|
if not self._vor:
|
|
274
394
|
self._vor = Voronoi(np.array(self.all_coordinates))
|
|
@@ -278,50 +398,90 @@ class AdjacencyEngine:
|
|
|
278
398
|
def vor(self, _):
|
|
279
399
|
raise ImmutablePropertyError("Property vor is immutable.")
|
|
280
400
|
|
|
401
|
+
def _get_voronoi_vertex_idx_for_coord_idx(self, feature_coord_index: int) -> Generator[int, None, None]:
|
|
402
|
+
"""
|
|
403
|
+
For a given feature coordinate index, return the indices of the voronoi vertices. Ignore
|
|
404
|
+
any "-1"s, which indicate vertices at infinity; these provide no adjacency information.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
feature_coord_index (int): The index of the coordinate in self.all_coordinates
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Generator[int, None, None]: A generator of the indices of the voronoi vertices.
|
|
411
|
+
"""
|
|
412
|
+
return (i for i in self.vor.regions[self.vor.point_region[feature_coord_index]] if i != -1)
|
|
413
|
+
|
|
414
|
+
def _tag_feature_with_voronoi_vertices(self):
|
|
415
|
+
"""
|
|
416
|
+
Tag each feature with the vertices of the voronoi region it belongs to. Runs the
|
|
417
|
+
voronoi analysis if it has not been done already. This is broken out mostly for testing.
|
|
418
|
+
Do not call this function directly.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
None
|
|
422
|
+
"""
|
|
423
|
+
# We don't need to tag obstacles with their voronoi vertices
|
|
424
|
+
obstacle_coord_len = sum(len(feat.coords) for feat in self.obstacle_features)
|
|
425
|
+
|
|
426
|
+
# Tag each feature with the vertices of the voronoi region it
|
|
427
|
+
# belongs to
|
|
428
|
+
for feature_coord_index in range(
|
|
429
|
+
len(self.all_coordinates) - obstacle_coord_len
|
|
430
|
+
):
|
|
431
|
+
feature = self.get_feature_from_coord_index(feature_coord_index)
|
|
432
|
+
for i in self._get_voronoi_vertex_idx_for_coord_idx(feature_coord_index):
|
|
433
|
+
feature.voronoi_points.add(i)
|
|
434
|
+
|
|
435
|
+
def _determine_adjacency(
|
|
436
|
+
self, source_set: Tuple[_Feature], target_set: Tuple[_Feature]
|
|
437
|
+
):
|
|
438
|
+
"""
|
|
439
|
+
Determines the adjacency relationship between two sets of features.
|
|
440
|
+
Args:
|
|
441
|
+
source_set (Tuple[_Feature]): The set of source features.
|
|
442
|
+
target_set (Tuple[_Feature]): The set of target features.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
None
|
|
446
|
+
"""
|
|
447
|
+
min_overlapping_voronoi_vertices = 2
|
|
448
|
+
for source_index, source_feature in enumerate(source_set):
|
|
449
|
+
for target_index, target_feature in enumerate(target_set):
|
|
450
|
+
if source_feature != target_feature and source_feature._is_adjacent(
|
|
451
|
+
target_feature, min_overlapping_voronoi_vertices
|
|
452
|
+
):
|
|
453
|
+
self._adjacency_dict[source_index].append(target_index)
|
|
454
|
+
|
|
281
455
|
def get_adjacency_dict(self) -> Dict[int, List[int]]:
|
|
282
456
|
"""
|
|
283
457
|
Returns a dictionary of indices. They keys are the indices of feature_geoms. The values
|
|
284
458
|
are the indices of any target geometries which are adjacent to the feature_geoms.
|
|
285
459
|
|
|
286
|
-
|
|
287
|
-
|
|
460
|
+
If no targets were specified, then calculate adjacency between source features and other
|
|
461
|
+
source features.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
dict: A dictionary of indices. The keys are the indices of feature_geoms. The
|
|
465
|
+
values are the indices of any adjacent features.
|
|
466
|
+
|
|
288
467
|
"""
|
|
289
468
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
obstacle_coord_len = sum(
|
|
293
|
-
len(feat.coords) for feat in self.obstacle_features
|
|
294
|
-
)
|
|
469
|
+
"""Note: We want adjacent features to have at least two overlapping vertices, otherwise we
|
|
470
|
+
might call the features adjacent when their Voronoi regions don't share any edges."""
|
|
295
471
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
for coord_index in range(
|
|
299
|
-
len(self.all_coordinates) - obstacle_coord_len):
|
|
300
|
-
feature = self.get_feature_from_coord_index(coord_index)
|
|
301
|
-
for vor_vertex_index in self.vor.regions[
|
|
302
|
-
self.vor.point_region[coord_index]
|
|
303
|
-
]:
|
|
304
|
-
# "-1" indices indicate the vertex goes to infinity. These don't provide us
|
|
305
|
-
# with adjacency information, so we ignore them.
|
|
306
|
-
if vor_vertex_index != -1:
|
|
307
|
-
feature.voronoi_points.add(vor_vertex_index)
|
|
472
|
+
if self._adjacency_dict is None:
|
|
473
|
+
self._tag_feature_with_voronoi_vertices()
|
|
308
474
|
|
|
309
475
|
# If any two features have any voronoi indices in common, then their voronoi regions
|
|
310
476
|
# must intersect, therefore the input features are adjacent.
|
|
311
477
|
self._adjacency_dict = defaultdict(list)
|
|
312
478
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
& target_feature.voronoi_points
|
|
320
|
-
)
|
|
321
|
-
> 1
|
|
322
|
-
):
|
|
323
|
-
self._adjacency_dict[coord_index].append(
|
|
324
|
-
vor_region_index)
|
|
479
|
+
# Get adjacency between source and target features
|
|
480
|
+
if len(self.target_features) > 0:
|
|
481
|
+
self._determine_adjacency(self.source_features, self.target_features)
|
|
482
|
+
# If no target specified, get adjacency between source and other source features.
|
|
483
|
+
else:
|
|
484
|
+
self._determine_adjacency(self.source_features, self.source_features)
|
|
325
485
|
|
|
326
486
|
return self._adjacency_dict
|
|
327
487
|
|
|
@@ -329,32 +489,54 @@ class AdjacencyEngine:
|
|
|
329
489
|
"""
|
|
330
490
|
Plot the adjacency linkages between the source and target with pyplot. Runs the analysis if
|
|
331
491
|
it has not already been run.
|
|
332
|
-
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
None
|
|
333
495
|
"""
|
|
334
496
|
# Plot the adjacency linkages between the source and target
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
497
|
+
if len(self.target_features) > 0:
|
|
498
|
+
for source_i, target_is in self.get_adjacency_dict().items():
|
|
499
|
+
source_poly = self.source_features[source_i].geometry
|
|
500
|
+
target_polys = [
|
|
501
|
+
self.target_features[target_i].geometry for target_i in target_is
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
# Plot the linestrings between the source and target polygons
|
|
505
|
+
links = []
|
|
506
|
+
for target_poly in target_polys:
|
|
507
|
+
if target_poly:
|
|
508
|
+
try:
|
|
509
|
+
links.append(
|
|
510
|
+
LineString(
|
|
511
|
+
shapely.ops.nearest_points(target_poly, source_poly)
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
except ValueError:
|
|
515
|
+
log.error(
|
|
516
|
+
f"Error creating link between '{target_poly}' and '{source_poly}'"
|
|
517
|
+
)
|
|
518
|
+
add_geometry_to_plot(links, "green")
|
|
519
|
+
|
|
520
|
+
else:
|
|
521
|
+
for source_i, source_2_is in self.get_adjacency_dict().items():
|
|
522
|
+
source_poly = self.source_features[source_i].geometry
|
|
523
|
+
target_polys = [
|
|
524
|
+
self.source_features[source_2_i].geometry
|
|
525
|
+
for source_2_i in source_2_is
|
|
526
|
+
if source_2_i > source_i
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
# Plot the linestrings between the source and target polygons
|
|
530
|
+
links = [
|
|
531
|
+
LineString([target_poly.centroid, source_poly.centroid])
|
|
532
|
+
for target_poly in target_polys
|
|
533
|
+
if target_poly is not None
|
|
534
|
+
]
|
|
535
|
+
add_geometry_to_plot(links, "green")
|
|
536
|
+
|
|
537
|
+
add_geometry_to_plot([t.geometry for t in self.target_features], "blue")
|
|
538
|
+
add_geometry_to_plot([t.geometry for t in self.source_features], "grey")
|
|
539
|
+
add_geometry_to_plot([t.geometry for t in self.obstacle_features], "red")
|
|
358
540
|
|
|
359
541
|
plt.title("Adjacency linkages between source and target")
|
|
360
542
|
plt.xlabel("Longitude")
|
geo_adjacency/exception.py
CHANGED
geo_adjacency/utils.py
CHANGED
|
@@ -1,32 +1,120 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Utility functions. These are designed for use in the AdjacencyEngine only, and should not be called
|
|
3
|
+
by end users.
|
|
3
4
|
"""
|
|
4
5
|
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
5
8
|
from matplotlib import pyplot as plt
|
|
6
9
|
from shapely import Point, LineString, Polygon, MultiPolygon
|
|
7
10
|
|
|
8
11
|
|
|
9
|
-
def
|
|
12
|
+
def coords_from_point(point: Point) -> List[Tuple[float, float]]:
|
|
13
|
+
"""
|
|
14
|
+
Convert a Point into a tuple of (x, y). We put this inside a list for consistency with other
|
|
15
|
+
coordinate methods to allow us to seamlessly merge them later.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
point (Point): A Shapely Point.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
assert isinstance(point, Point), "Geometry must be a Point, not '%s'." % type(point)
|
|
25
|
+
return [(float(point.x), float(point.y))]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def coords_from_ring(ring: LineString) -> List[Tuple[float, float]]:
|
|
29
|
+
"""
|
|
30
|
+
Convert a LinearRing into a list of (x, y) tuples.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
ring (LineString): A Shapely LinearString.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
37
|
+
"""
|
|
38
|
+
assert isinstance(
|
|
39
|
+
ring, LineString
|
|
40
|
+
), "Geometry must be a LinearRing, not '%s'." % type(ring)
|
|
41
|
+
return [(float(coord[0]), float(coord[1])) for coord in ring.coords]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def coords_from_polygon(polygon: Polygon) -> List[Tuple[float, float]]:
|
|
45
|
+
"""
|
|
46
|
+
Convert a Polygon into a list of (x, y) tuples. Does not repeat the first coordinate to close
|
|
47
|
+
the ring.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
polygon (Polygon): A Shapely Polygon.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
54
|
+
"""
|
|
55
|
+
assert isinstance(polygon, Polygon), "Geometry must be a Polygon, not '%s'." % type(
|
|
56
|
+
polygon
|
|
57
|
+
)
|
|
58
|
+
coords = []
|
|
59
|
+
coords.extend(coords_from_ring(polygon.exterior)[:-1])
|
|
60
|
+
for ring in polygon.interiors:
|
|
61
|
+
coords.extend(coords_from_ring(ring)[:-1])
|
|
62
|
+
return coords
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def coords_from_multipolygon(multipolygon: MultiPolygon) -> List[Tuple[float, float]]:
|
|
10
66
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
67
|
+
Convert a MultiPolygon into a list of (x, y) tuples. Does not repeat the first coordinate to
|
|
68
|
+
close the ring.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
multipolygon (MultiPolygon): A Shapely MultiPolygon.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
14
75
|
"""
|
|
15
|
-
|
|
76
|
+
assert isinstance(
|
|
77
|
+
multipolygon, MultiPolygon
|
|
78
|
+
), "Geometry must be a MultiPolygon, not '%s'." % type(multipolygon)
|
|
79
|
+
coords = []
|
|
80
|
+
for polygon in multipolygon.geoms:
|
|
81
|
+
coords.extend(coords_from_polygon(polygon))
|
|
82
|
+
return coords
|
|
16
83
|
|
|
17
|
-
for sublist in list_of_lists:
|
|
18
|
-
for item in sublist:
|
|
19
|
-
flattened_list.append(item)
|
|
20
84
|
|
|
21
|
-
|
|
85
|
+
def flatten_list(nested_list) -> List:
|
|
86
|
+
"""
|
|
87
|
+
Flatten a list of lists.
|
|
88
|
+
Args:
|
|
89
|
+
nested_list (List): A list of lists.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
# check if list is empty
|
|
95
|
+
if not bool(nested_list):
|
|
96
|
+
return nested_list
|
|
97
|
+
|
|
98
|
+
# to check instance of list is empty or not
|
|
99
|
+
if isinstance(nested_list[0], list):
|
|
100
|
+
# call function with sublist as argument
|
|
101
|
+
return flatten_list(*nested_list[:1]) + flatten_list(nested_list[1:])
|
|
102
|
+
|
|
103
|
+
# Call function with sublist as argument
|
|
104
|
+
return nested_list[:1] + flatten_list(nested_list[1:])
|
|
22
105
|
|
|
23
106
|
|
|
24
107
|
def add_geometry_to_plot(geoms, color="black"):
|
|
25
108
|
"""
|
|
26
|
-
When updating the test data, it may be useful to visualize it.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
:
|
|
109
|
+
When updating the test data, it may be useful to visualize it. Add a geometry to the global
|
|
110
|
+
maplotlib plt object. The next time we call plt.show(), this geometry will be plotted.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
geoms (List[BaseGeometry]): A list of Shapely geometries.
|
|
114
|
+
color (str): The color we want the geometry to be in the plot.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
None
|
|
30
118
|
"""
|
|
31
119
|
for geom in geoms:
|
|
32
120
|
if isinstance(geom, Point):
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geo-adjacency
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.1.2
|
|
4
|
+
Summary: A package to determine which geometries are adjacent to each other, accounting for obstacles and gaps between features.
|
|
5
|
+
Home-page: https://asmyth01.github.io/geo-adjacency/
|
|
5
6
|
License: MIT
|
|
7
|
+
Keywords: voronoi,adjacency,geospatial,geometry
|
|
6
8
|
Author: Andrew Smyth
|
|
7
|
-
Author-email:
|
|
9
|
+
Author-email: andrew.j.smyth.89@gmail.com
|
|
8
10
|
Requires-Python: >=3.9,<3.13
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
10
16
|
Classifier: Programming Language :: Python :: 3
|
|
11
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
15
23
|
Requires-Dist: matplotlib (>=3.8.1,<4.0.0)
|
|
16
24
|
Requires-Dist: numpy (>=1.26.2,<2.0.0)
|
|
17
25
|
Requires-Dist: scipy (>=1.11.3,<2.0.0)
|
|
18
26
|
Requires-Dist: setuptools (>=69.0.0,<70.0.0)
|
|
19
27
|
Requires-Dist: shapely (>=2.0.2,<3.0.0)
|
|
28
|
+
Project-URL: Documentation, https://asmyth01.github.io/geo-adjacency/
|
|
29
|
+
Project-URL: Repository, https://github.com/andrewsmyth/geo-adjacency
|
|
20
30
|
Description-Content-Type: text/markdown
|
|
21
31
|
|
|
22
32
|
# Geo-Adjacency
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
geo_adjacency/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
geo_adjacency/adjacency.py,sha256=bL6ErCwgtpvNu0XfgpQwCJ6RlK2M-vi-qx_veE1_0Rk,20844
|
|
3
|
+
geo_adjacency/exception.py,sha256=zZNdBOm5LpuiCpNuqH1FNLhiPnQqyCyuhOTMBDnLSTQ,230
|
|
4
|
+
geo_adjacency/utils.py,sha256=57Q-nRZQlW1QetlLoucbDr1jm3CRHYRCVzrarm7xxZw,4188
|
|
5
|
+
geo_adjacency-1.1.2.dist-info/LICENSE,sha256=p0PMGdB2iuOndKPbBCVhTNe9TMIxZRpJ64bQ_CoUIqY,1065
|
|
6
|
+
geo_adjacency-1.1.2.dist-info/METADATA,sha256=fpHyeHH726bndsYxfE_CWAtJLmBN9MlN3lFiraRX8PA,3857
|
|
7
|
+
geo_adjacency-1.1.2.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
8
|
+
geo_adjacency-1.1.2.dist-info/RECORD,,
|
geo_adjacency/file.log
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
2023-11-20 19:19:58,755 - __main__ - ERROR - This is an error
|
|
2
|
-
2023-11-20 19:21:23,871 - __main__ - ERROR - This is an error
|
|
3
|
-
2023-11-20 19:22:38,493 - __main__ - ERROR - This is an error
|
|
4
|
-
2023-11-20 19:22:48,026 - __main__ - ERROR - This is an error
|
|
5
|
-
2023-11-20 19:23:34,369 - __main__ - ERROR - This is an error
|
|
6
|
-
2023-11-20 19:23:44,297 - __main__ - ERROR - This is an error
|
|
7
|
-
2023-11-20 19:26:28,000 - __main__ - ERROR - This is an error
|
|
8
|
-
2023-11-20 19:26:58,737 - __main__ - ERROR - This is an error
|
|
9
|
-
2023-11-20 19:28:41,686 - __main__ - ERROR - This is an error
|
|
10
|
-
2023-11-20 19:29:03,485 - __main__ - ERROR - This is an error
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
geo_adjacency/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
geo_adjacency/adjacency.py,sha256=vonZw4pZDkuAR_-zWO2zEflsvvepG9njT5APFGnMW2M,14201
|
|
3
|
-
geo_adjacency/exception.py,sha256=-1fdZVR8LayIJmu3Zj2CA-5ItuFH-mOONPCe4g5M6l8,228
|
|
4
|
-
geo_adjacency/file.log,sha256=Zc7rMTU9N_LJKx68zsOwDJcuTnD8Mxll3iHlmZWQUzg,620
|
|
5
|
-
geo_adjacency/utils.py,sha256=gqAV-0tE_P5VIAorJc92u0-jUM-POlcaJnAzFDxcbEk,1333
|
|
6
|
-
geo_adjacency-1.0.7.dist-info/LICENSE,sha256=p0PMGdB2iuOndKPbBCVhTNe9TMIxZRpJ64bQ_CoUIqY,1065
|
|
7
|
-
geo_adjacency-1.0.7.dist-info/METADATA,sha256=PEmj1BtFUJ80Ux5m0V86ayNUzqWR2rLcGJKfsVP7WuI,3215
|
|
8
|
-
geo_adjacency-1.0.7.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
9
|
-
geo_adjacency-1.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|