geo-adjacency 1.1.1__tar.gz → 1.2.0__tar.gz
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-1.1.1 → geo_adjacency-1.2.0}/PKG-INFO +1 -1
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/geo_adjacency/adjacency.py +206 -85
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/geo_adjacency/utils.py +38 -14
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/pyproject.toml +2 -1
- geo_adjacency-1.1.1/geo_adjacency/file.log +0 -10
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/LICENSE +0 -0
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/README.md +0 -0
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/geo_adjacency/__init__.py +0 -0
- {geo_adjacency-1.1.1 → geo_adjacency-1.2.0}/geo_adjacency/exception.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geo-adjacency
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A package to determine which geometries are adjacent to each other, accounting for obstacles and gaps between features.
|
|
5
5
|
Home-page: https://asmyth01.github.io/geo-adjacency/
|
|
6
6
|
License: MIT
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
The `adjacency` module implements the AdjacencyEngine class,
|
|
3
|
-
which allows us to
|
|
4
|
-
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import math
|
|
16
16
|
from collections import defaultdict
|
|
17
|
-
from typing import List, Union, Dict, Tuple
|
|
17
|
+
from typing import List, Union, Dict, Tuple, Generator
|
|
18
|
+
|
|
18
19
|
try:
|
|
19
20
|
from typing_extensions import Self # Python < 3.11
|
|
20
21
|
except ImportError:
|
|
@@ -26,7 +27,7 @@ import numpy as np
|
|
|
26
27
|
import shapely.ops
|
|
27
28
|
from scipy.spatial import distance
|
|
28
29
|
from scipy.spatial import Voronoi
|
|
29
|
-
from shapely import LineString, Point, Polygon, MultiPolygon
|
|
30
|
+
from shapely import LineString, Point, Polygon, MultiPolygon, box
|
|
30
31
|
from shapely.geometry.base import BaseGeometry
|
|
31
32
|
|
|
32
33
|
from geo_adjacency.exception import ImmutablePropertyError
|
|
@@ -39,34 +40,54 @@ from geo_adjacency.utils import (
|
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
# ToDo: Support geometries with Z-coordinates
|
|
42
|
-
|
|
43
43
|
# Create a custom logger
|
|
44
|
-
|
|
44
|
+
log: logging.Logger = logging.getLogger(__name__)
|
|
45
45
|
|
|
46
46
|
# Create handlers
|
|
47
|
-
c_handler = logging.StreamHandler()
|
|
47
|
+
c_handler: logging.StreamHandler = logging.StreamHandler()
|
|
48
48
|
c_handler.setLevel(logging.WARNING)
|
|
49
49
|
|
|
50
50
|
# Create formatters and add it to handlers
|
|
51
|
-
c_format = logging.Formatter(
|
|
52
|
-
|
|
51
|
+
c_format: logging.Formatter = logging.Formatter(
|
|
52
|
+
"%(name)s - %(levelname)s - %(message)s"
|
|
53
|
+
)
|
|
54
|
+
f_format: logging.Formatter = logging.Formatter(
|
|
55
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
56
|
+
)
|
|
53
57
|
c_handler.setFormatter(c_format)
|
|
54
58
|
|
|
55
59
|
# Add handlers to the logger
|
|
56
|
-
|
|
60
|
+
log.addHandler(c_handler)
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
class _Feature:
|
|
64
|
+
"""
|
|
65
|
+
A _Feature is a wrapper around a Shapely geometry that allows us to easily determine if two
|
|
66
|
+
geometries are adjacent.
|
|
67
|
+
"""
|
|
68
|
+
|
|
60
69
|
__slots__ = ("_geometry", "_coords", "voronoi_points")
|
|
61
70
|
|
|
62
71
|
def __init__(self, geometry: BaseGeometry):
|
|
72
|
+
"""
|
|
73
|
+
Create a _Feature from a Shapely geometry.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
geometry (BaseGeometry): A valid Shapely Geometry, either a Point, LineString, Polygon, or
|
|
77
|
+
MultiPolygon.
|
|
78
|
+
"""
|
|
79
|
+
|
|
63
80
|
if not isinstance(geometry, (Point, Polygon, MultiPolygon, LineString)):
|
|
64
81
|
raise TypeError(
|
|
65
|
-
"Cannot create _Feature for geometry type '%s'." % type(
|
|
82
|
+
"Cannot create _Feature for geometry type '%s'." % type(geometry)
|
|
66
83
|
)
|
|
67
84
|
|
|
85
|
+
assert geometry.is_valid, (
|
|
86
|
+
"Could not process invalid geometry: %s" % geometry.wkt
|
|
87
|
+
)
|
|
88
|
+
|
|
68
89
|
self._geometry: BaseGeometry = geometry
|
|
69
|
-
self._coords = None
|
|
90
|
+
self._coords: Union[List[Tuple[float, float]], None] = None
|
|
70
91
|
self.voronoi_points: set = set()
|
|
71
92
|
|
|
72
93
|
def __str__(self):
|
|
@@ -75,32 +96,30 @@ class _Feature:
|
|
|
75
96
|
def __repr__(self):
|
|
76
97
|
return f"<_Feature: {str(self.geometry)}>"
|
|
77
98
|
|
|
78
|
-
def
|
|
79
|
-
if not isinstance(other, type(self)):
|
|
80
|
-
return False
|
|
81
|
-
return self.geometry.equals_exact(other.geometry, 1e-8)
|
|
82
|
-
|
|
83
|
-
def __ne__(self, other: Self):
|
|
84
|
-
return not self.__eq__(other)
|
|
85
|
-
|
|
86
|
-
def is_adjacent(
|
|
99
|
+
def _is_adjacent(
|
|
87
100
|
self, other: Self, min_overlapping_voronoi_vertices: int = 2
|
|
88
101
|
) -> bool:
|
|
89
102
|
"""
|
|
90
103
|
Determine if two features are adjacent based on how many Voronoi vertices they share. Note:
|
|
91
104
|
the Voronoi analysis must have been run, or this will always return False.
|
|
92
|
-
:
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
Args:
|
|
106
|
+
other (_Feature): Another _Feature to compare to.
|
|
107
|
+
min_overlapping_voronoi_vertices (int): The minimum number of Voronoi vertices that
|
|
108
|
+
must be shared to be considered adjacent.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
bool: True if the two features are adjacent.
|
|
112
|
+
|
|
95
113
|
"""
|
|
96
114
|
assert isinstance(other, type(self)), "Cannot compare '%s' with '%s'." % (
|
|
97
115
|
type(self),
|
|
98
116
|
type(other),
|
|
99
117
|
)
|
|
100
118
|
if len(self.voronoi_points) == 0 and len(other.voronoi_points) == 0:
|
|
101
|
-
|
|
119
|
+
log.warning(
|
|
102
120
|
"No Voronoi vertices found for either feature. Did you run the analysis yet?"
|
|
103
121
|
)
|
|
122
|
+
return False
|
|
104
123
|
return (
|
|
105
124
|
len(self.voronoi_points & other.voronoi_points)
|
|
106
125
|
>= min_overlapping_voronoi_vertices
|
|
@@ -110,7 +129,10 @@ class _Feature:
|
|
|
110
129
|
def geometry(self):
|
|
111
130
|
"""
|
|
112
131
|
Access the Shapely geometry of the feature.
|
|
113
|
-
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
BaseGeometry: The Shapely geometry of the feature.
|
|
135
|
+
|
|
114
136
|
"""
|
|
115
137
|
return self._geometry
|
|
116
138
|
|
|
@@ -124,7 +146,9 @@ class _Feature:
|
|
|
124
146
|
"""
|
|
125
147
|
Convenience property for accessing the coordinates of the geometry as a list of 2-tuples.
|
|
126
148
|
|
|
127
|
-
:
|
|
149
|
+
Returns:
|
|
150
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
151
|
+
|
|
128
152
|
"""
|
|
129
153
|
|
|
130
154
|
if not self._coords:
|
|
@@ -140,6 +164,10 @@ class _Feature:
|
|
|
140
164
|
raise TypeError(f"Unknown geometry type '{type(self.geometry)}'")
|
|
141
165
|
return self._coords
|
|
142
166
|
|
|
167
|
+
@coords.setter
|
|
168
|
+
def coords(self, coords):
|
|
169
|
+
raise ImmutablePropertyError("Property coords is immutable.")
|
|
170
|
+
|
|
143
171
|
|
|
144
172
|
class AdjacencyEngine:
|
|
145
173
|
"""
|
|
@@ -149,9 +177,6 @@ class AdjacencyEngine:
|
|
|
149
177
|
First, the Voronoi diagram is generated for each geometry and obstacle. Then, we check which
|
|
150
178
|
voronoi shapes intersect one another. If they do, then the two underlying geometries are
|
|
151
179
|
adjacent.
|
|
152
|
-
|
|
153
|
-
:ivar all_features: List of all features in order of source, target, and obstacle.
|
|
154
|
-
:ivar all_coordinates: List of all coordinates in the same order as all_features.
|
|
155
180
|
"""
|
|
156
181
|
|
|
157
182
|
__slots__ = (
|
|
@@ -161,8 +186,10 @@ class AdjacencyEngine:
|
|
|
161
186
|
"_adjacency_dict",
|
|
162
187
|
"_feature_indices",
|
|
163
188
|
"_vor",
|
|
164
|
-
"
|
|
189
|
+
"_all_features",
|
|
165
190
|
"_all_coordinates",
|
|
191
|
+
"_max_distance",
|
|
192
|
+
"_bounding_rectangle",
|
|
166
193
|
)
|
|
167
194
|
|
|
168
195
|
def __init__(
|
|
@@ -170,30 +197,51 @@ class AdjacencyEngine:
|
|
|
170
197
|
source_geoms: List[BaseGeometry],
|
|
171
198
|
target_geoms: Union[List[BaseGeometry], None] = None,
|
|
172
199
|
obstacle_geoms: Union[List[BaseGeometry], None] = None,
|
|
173
|
-
|
|
174
|
-
max_segment_length: Union[float, None] = None,
|
|
200
|
+
**kwargs,
|
|
175
201
|
):
|
|
176
202
|
"""
|
|
177
|
-
|
|
203
|
+
Note: only Multipolygons, Polygons, LineStrings and Points are supported. It is assumed all
|
|
178
204
|
features are in the same projection.
|
|
179
205
|
|
|
180
|
-
:
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
Args:
|
|
207
|
+
source_geoms (List[BaseGeometry]): List of Shapely geometries. We will which ones are adjacent to
|
|
208
|
+
which others, unless target_geoms is specified.
|
|
209
|
+
target_geoms (Union[List[BaseGeometry], None]), optional): list of Shapley geometries. if not None, We will
|
|
183
210
|
test if these features are adjacent to the source features.
|
|
184
|
-
|
|
211
|
+
obstacle_geoms (Union[List[BaseGeometry], None]), optional): List
|
|
185
212
|
of Shapely geometries. These features will not be tested for adjacency, but they can
|
|
186
213
|
prevent a source and target feature from being adjacent.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
|
|
215
|
+
Keyword Args:
|
|
216
|
+
densify_features (bool, optional): If True, we will add additional points to the
|
|
217
|
+
features to improve accuracy of the voronoi diagram. If densify_features is True and
|
|
218
|
+
max_segment_length is false, then the max_segment_length will be calculated based on
|
|
219
|
+
the average segment length of all features, divided by 5.
|
|
220
|
+
max_segment_length (Union[float, None], optional): The maximum distance between vertices
|
|
221
|
+
that we want iIn projection units. densify_features must be True, or an error will be thrown.
|
|
222
|
+
max_distance (Union[float, None], optional): The maximum distance between two features
|
|
223
|
+
for them to be candidates for adjacency. Units are same as geometry coordinate system.
|
|
224
|
+
bounding_box (Union[float, float, float, float, None], optional): Set a bounding box
|
|
225
|
+
for the analysis. Only include features that intersect the box in the output.
|
|
226
|
+
This is useful for removing data from the edges from the final analysis, as these
|
|
227
|
+
are often not accurate. This is particularly helpful when analyzing a large data set
|
|
228
|
+
in a windowed fashion. Expected format is (minx, miny, maxx, maxy).
|
|
229
|
+
|
|
195
230
|
"""
|
|
196
231
|
|
|
232
|
+
densify_features = kwargs.get("densify_features", False)
|
|
233
|
+
max_segment_length = kwargs.get("max_segment_length", None)
|
|
234
|
+
self._max_distance = kwargs.get("max_distance", None)
|
|
235
|
+
|
|
236
|
+
if kwargs.get("bounding_box", None):
|
|
237
|
+
minx, miny, maxx, maxy = kwargs.get("bounding_box")
|
|
238
|
+
assert (
|
|
239
|
+
minx < maxx and miny < maxy
|
|
240
|
+
), "Bounding box must have minx < maxx and miny < maxy"
|
|
241
|
+
self._bounding_rectangle: Polygon = box(minx, miny, maxx, maxy)
|
|
242
|
+
else:
|
|
243
|
+
self._bounding_rectangle = None
|
|
244
|
+
|
|
197
245
|
if max_segment_length and not densify_features:
|
|
198
246
|
raise ValueError(
|
|
199
247
|
"interpolate_points must be True if interpolation_distance is not None"
|
|
@@ -212,21 +260,21 @@ class AdjacencyEngine:
|
|
|
212
260
|
if obstacle_geoms
|
|
213
261
|
else tuple()
|
|
214
262
|
)
|
|
215
|
-
self._adjacency_dict = None
|
|
216
|
-
self._feature_indices = None
|
|
263
|
+
self._adjacency_dict: Union[Dict[int, List[int]], None] = None
|
|
264
|
+
self._feature_indices: Union[Dict[int, int], None] = None
|
|
217
265
|
self._vor = None
|
|
218
266
|
self._all_coordinates = None
|
|
219
267
|
|
|
220
268
|
"""All source, target, and obstacle features in a single list. The order of this list must
|
|
221
269
|
not be changed."""
|
|
222
|
-
self.
|
|
270
|
+
self._all_features: Tuple[_Feature, ...] = tuple(
|
|
223
271
|
[*self.source_features, *self.target_features, *self.obstacle_features]
|
|
224
272
|
)
|
|
225
273
|
|
|
226
274
|
if densify_features:
|
|
227
275
|
if max_segment_length is None:
|
|
228
|
-
max_segment_length = self.
|
|
229
|
-
|
|
276
|
+
max_segment_length = self._calc_segmentation_dist()
|
|
277
|
+
log.info("Calculated max_segment_length of %s" % max_segment_length)
|
|
230
278
|
|
|
231
279
|
for feature in self.all_features:
|
|
232
280
|
if not isinstance(feature.geometry, Point):
|
|
@@ -234,8 +282,32 @@ class AdjacencyEngine:
|
|
|
234
282
|
# Reset all coordinates
|
|
235
283
|
self._all_coordinates = None
|
|
236
284
|
|
|
285
|
+
@property
|
|
286
|
+
def all_features(self):
|
|
287
|
+
"""
|
|
288
|
+
All source, target, and obstacle features in a single list. The order of this list must
|
|
289
|
+
not be changed. This property cannot be set manually.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List[_Feature]: A list of _Features.
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
return self._all_features
|
|
296
|
+
|
|
297
|
+
@all_features.setter
|
|
298
|
+
def all_features(self, value):
|
|
299
|
+
raise ImmutablePropertyError("Property all_features is immutable.")
|
|
300
|
+
|
|
237
301
|
@property
|
|
238
302
|
def all_coordinates(self):
|
|
303
|
+
"""
|
|
304
|
+
All source, target, and obstacle coordinates in a single list. The order of this list must
|
|
305
|
+
not be changed. This property cannot be set manually.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
List[tuple[float, float]]: A list of coordinate tuples.
|
|
309
|
+
"""
|
|
310
|
+
|
|
239
311
|
if not self._all_coordinates:
|
|
240
312
|
self._all_coordinates = []
|
|
241
313
|
for feature in self.all_features:
|
|
@@ -248,7 +320,7 @@ class AdjacencyEngine:
|
|
|
248
320
|
def all_coordinates(self, value):
|
|
249
321
|
raise ImmutablePropertyError("Property all_coordinates is immutable.")
|
|
250
322
|
|
|
251
|
-
def
|
|
323
|
+
def _calc_segmentation_dist(self, divisor=5):
|
|
252
324
|
"""
|
|
253
325
|
Try to create a well-fitting maximum length for all line segments in all features. Take
|
|
254
326
|
the average distance between all coordinate pairs and divide by 5. This means that the
|
|
@@ -258,10 +330,12 @@ class AdjacencyEngine:
|
|
|
258
330
|
average segment lengths. In that case, it is advisable to prepare the data appropriately
|
|
259
331
|
beforehand.
|
|
260
332
|
|
|
261
|
-
:
|
|
333
|
+
Args:
|
|
334
|
+
divisor (int, optional): Divide the average segment length by this number to get the new desired
|
|
262
335
|
segment length.
|
|
263
336
|
|
|
264
|
-
:
|
|
337
|
+
Returns:
|
|
338
|
+
float: Average segment length divided by divisor.
|
|
265
339
|
"""
|
|
266
340
|
|
|
267
341
|
return float(
|
|
@@ -276,7 +350,10 @@ class AdjacencyEngine:
|
|
|
276
350
|
def source_features(self) -> Tuple[_Feature]:
|
|
277
351
|
"""
|
|
278
352
|
Features which will be the keys in the adjacency_dict.
|
|
279
|
-
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List[_Feature]: A list of _Features.
|
|
356
|
+
|
|
280
357
|
"""
|
|
281
358
|
return self._source_features
|
|
282
359
|
|
|
@@ -288,7 +365,8 @@ class AdjacencyEngine:
|
|
|
288
365
|
def target_features(self) -> Tuple[_Feature]:
|
|
289
366
|
"""
|
|
290
367
|
Features which will be the values in the adjacency_dict.
|
|
291
|
-
:
|
|
368
|
+
Returns:
|
|
369
|
+
List[_Feature]: A list of _Features.
|
|
292
370
|
"""
|
|
293
371
|
return self._target_features
|
|
294
372
|
|
|
@@ -301,7 +379,9 @@ class AdjacencyEngine:
|
|
|
301
379
|
"""
|
|
302
380
|
Features which can prevent source and target features from being adjacent. They
|
|
303
381
|
Do not participate in the adjacency_dict.
|
|
304
|
-
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
List[_Feature]: A list of _Features.
|
|
305
385
|
"""
|
|
306
386
|
return self._obstacle_features
|
|
307
387
|
|
|
@@ -316,8 +396,11 @@ class AdjacencyEngine:
|
|
|
316
396
|
determine which coordinate belongs to which feature after we calculate the voronoi
|
|
317
397
|
diagram.
|
|
318
398
|
|
|
319
|
-
:
|
|
399
|
+
Args:
|
|
400
|
+
coord_index (int): The index of the coordinate in self._all_coordinates
|
|
320
401
|
|
|
402
|
+
Returns:
|
|
403
|
+
_Feature: A _Feature at the given index.
|
|
321
404
|
"""
|
|
322
405
|
if not self._feature_indices:
|
|
323
406
|
self._feature_indices = {}
|
|
@@ -335,9 +418,9 @@ class AdjacencyEngine:
|
|
|
335
418
|
The Voronoi diagram object returned by Scipy. Useful primarily for debugging an
|
|
336
419
|
adjacency analysis.
|
|
337
420
|
|
|
338
|
-
:
|
|
421
|
+
Returns:
|
|
422
|
+
scipy.spatial.Voronoi: The Scipy Voronoi object.
|
|
339
423
|
"""
|
|
340
|
-
|
|
341
424
|
if not self._vor:
|
|
342
425
|
self._vor = Voronoi(np.array(self.all_coordinates))
|
|
343
426
|
return self._vor
|
|
@@ -346,12 +429,33 @@ class AdjacencyEngine:
|
|
|
346
429
|
def vor(self, _):
|
|
347
430
|
raise ImmutablePropertyError("Property vor is immutable.")
|
|
348
431
|
|
|
432
|
+
def _get_voronoi_vertex_idx_for_coord_idx(
|
|
433
|
+
self, feature_coord_index: int
|
|
434
|
+
) -> Generator[int, None, None]:
|
|
435
|
+
"""
|
|
436
|
+
For a given feature coordinate index, return the indices of the voronoi vertices. Ignore
|
|
437
|
+
any "-1"s, which indicate vertices at infinity; these provide no adjacency information.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
feature_coord_index (int): The index of the coordinate in self.all_coordinates
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Generator[int, None, None]: A generator of the indices of the voronoi vertices.
|
|
444
|
+
"""
|
|
445
|
+
return (
|
|
446
|
+
i
|
|
447
|
+
for i in self.vor.regions[self.vor.point_region[feature_coord_index]]
|
|
448
|
+
if i != -1
|
|
449
|
+
)
|
|
450
|
+
|
|
349
451
|
def _tag_feature_with_voronoi_vertices(self):
|
|
350
452
|
"""
|
|
351
453
|
Tag each feature with the vertices of the voronoi region it belongs to. Runs the
|
|
352
454
|
voronoi analysis if it has not been done already. This is broken out mostly for testing.
|
|
353
455
|
Do not call this function directly.
|
|
354
|
-
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
None
|
|
355
459
|
"""
|
|
356
460
|
# We don't need to tag obstacles with their voronoi vertices
|
|
357
461
|
obstacle_coord_len = sum(len(feat.coords) for feat in self.obstacle_features)
|
|
@@ -362,32 +466,45 @@ class AdjacencyEngine:
|
|
|
362
466
|
len(self.all_coordinates) - obstacle_coord_len
|
|
363
467
|
):
|
|
364
468
|
feature = self.get_feature_from_coord_index(feature_coord_index)
|
|
365
|
-
for
|
|
366
|
-
|
|
367
|
-
]:
|
|
368
|
-
# "-1" indices indicate the vertex goes to infinity. These don't provide us
|
|
369
|
-
# with adjacency information, so we ignore them.
|
|
370
|
-
if voronoi_vertex_index != -1:
|
|
371
|
-
feature.voronoi_points.add(voronoi_vertex_index)
|
|
469
|
+
for i in self._get_voronoi_vertex_idx_for_coord_idx(feature_coord_index):
|
|
470
|
+
feature.voronoi_points.add(i)
|
|
372
471
|
|
|
373
472
|
def _determine_adjacency(
|
|
374
473
|
self, source_set: Tuple[_Feature], target_set: Tuple[_Feature]
|
|
375
474
|
):
|
|
376
475
|
"""
|
|
377
476
|
Determines the adjacency relationship between two sets of features.
|
|
378
|
-
|
|
477
|
+
Args:
|
|
379
478
|
source_set (Tuple[_Feature]): The set of source features.
|
|
380
479
|
target_set (Tuple[_Feature]): The set of target features.
|
|
480
|
+
|
|
381
481
|
Returns:
|
|
382
482
|
None
|
|
383
483
|
"""
|
|
384
484
|
min_overlapping_voronoi_vertices = 2
|
|
385
485
|
for source_index, source_feature in enumerate(source_set):
|
|
486
|
+
if (
|
|
487
|
+
self._bounding_rectangle is not None
|
|
488
|
+
and not self._bounding_rectangle.intersects(source_feature.geometry)
|
|
489
|
+
):
|
|
490
|
+
continue
|
|
386
491
|
for target_index, target_feature in enumerate(target_set):
|
|
387
|
-
if source_feature != target_feature
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
492
|
+
if source_feature != target_feature:
|
|
493
|
+
if (
|
|
494
|
+
self._max_distance is not None
|
|
495
|
+
and source_feature.geometry.distance(target_feature.geometry)
|
|
496
|
+
> self._max_distance
|
|
497
|
+
) or (
|
|
498
|
+
self._bounding_rectangle is not None
|
|
499
|
+
and not self._bounding_rectangle.intersects(
|
|
500
|
+
target_feature.geometry
|
|
501
|
+
)
|
|
502
|
+
):
|
|
503
|
+
continue
|
|
504
|
+
if source_feature._is_adjacent(
|
|
505
|
+
target_feature, min_overlapping_voronoi_vertices
|
|
506
|
+
):
|
|
507
|
+
self._adjacency_dict[source_index].append(target_index)
|
|
391
508
|
|
|
392
509
|
def get_adjacency_dict(self) -> Dict[int, List[int]]:
|
|
393
510
|
"""
|
|
@@ -397,12 +514,14 @@ class AdjacencyEngine:
|
|
|
397
514
|
If no targets were specified, then calculate adjacency between source features and other
|
|
398
515
|
source features.
|
|
399
516
|
|
|
400
|
-
:
|
|
401
|
-
|
|
517
|
+
Returns:
|
|
518
|
+
dict: A dictionary of indices. The keys are the indices of feature_geoms. The
|
|
519
|
+
values are the indices of any adjacent features.
|
|
520
|
+
|
|
402
521
|
"""
|
|
403
522
|
|
|
404
|
-
|
|
405
|
-
|
|
523
|
+
"""Note: We want adjacent features to have at least two overlapping vertices, otherwise we
|
|
524
|
+
might call the features adjacent when their Voronoi regions don't share any edges."""
|
|
406
525
|
|
|
407
526
|
if self._adjacency_dict is None:
|
|
408
527
|
self._tag_feature_with_voronoi_vertices()
|
|
@@ -424,7 +543,9 @@ class AdjacencyEngine:
|
|
|
424
543
|
"""
|
|
425
544
|
Plot the adjacency linkages between the source and target with pyplot. Runs the analysis if
|
|
426
545
|
it has not already been run.
|
|
427
|
-
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
None
|
|
428
549
|
"""
|
|
429
550
|
# Plot the adjacency linkages between the source and target
|
|
430
551
|
if len(self.target_features) > 0:
|
|
@@ -445,7 +566,7 @@ class AdjacencyEngine:
|
|
|
445
566
|
)
|
|
446
567
|
)
|
|
447
568
|
except ValueError:
|
|
448
|
-
|
|
569
|
+
log.error(
|
|
449
570
|
f"Error creating link between '{target_poly}' and '{source_poly}'"
|
|
450
571
|
)
|
|
451
572
|
add_geometry_to_plot(links, "green")
|
|
@@ -13,8 +13,13 @@ def coords_from_point(point: Point) -> List[Tuple[float, float]]:
|
|
|
13
13
|
"""
|
|
14
14
|
Convert a Point into a tuple of (x, y). We put this inside a list for consistency with other
|
|
15
15
|
coordinate methods to allow us to seamlessly merge them later.
|
|
16
|
-
|
|
17
|
-
:
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
point (Point): A Shapely Point.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
22
|
+
|
|
18
23
|
"""
|
|
19
24
|
assert isinstance(point, Point), "Geometry must be a Point, not '%s'." % type(point)
|
|
20
25
|
return [(float(point.x), float(point.y))]
|
|
@@ -22,9 +27,13 @@ def coords_from_point(point: Point) -> List[Tuple[float, float]]:
|
|
|
22
27
|
|
|
23
28
|
def coords_from_ring(ring: LineString) -> List[Tuple[float, float]]:
|
|
24
29
|
"""
|
|
25
|
-
Convert a LinearRing into a list of (x, y) tuples
|
|
26
|
-
|
|
27
|
-
:
|
|
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.
|
|
28
37
|
"""
|
|
29
38
|
assert isinstance(
|
|
30
39
|
ring, LineString
|
|
@@ -36,8 +45,12 @@ def coords_from_polygon(polygon: Polygon) -> List[Tuple[float, float]]:
|
|
|
36
45
|
"""
|
|
37
46
|
Convert a Polygon into a list of (x, y) tuples. Does not repeat the first coordinate to close
|
|
38
47
|
the ring.
|
|
39
|
-
|
|
40
|
-
:
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
polygon (Polygon): A Shapely Polygon.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
41
54
|
"""
|
|
42
55
|
assert isinstance(polygon, Polygon), "Geometry must be a Polygon, not '%s'." % type(
|
|
43
56
|
polygon
|
|
@@ -53,8 +66,12 @@ def coords_from_multipolygon(multipolygon: MultiPolygon) -> List[Tuple[float, fl
|
|
|
53
66
|
"""
|
|
54
67
|
Convert a MultiPolygon into a list of (x, y) tuples. Does not repeat the first coordinate to
|
|
55
68
|
close the ring.
|
|
56
|
-
|
|
57
|
-
:
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
multipolygon (MultiPolygon): A Shapely MultiPolygon.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List[Tuple[float, float]]: A list of coordinate tuples.
|
|
58
75
|
"""
|
|
59
76
|
assert isinstance(
|
|
60
77
|
multipolygon, MultiPolygon
|
|
@@ -68,8 +85,11 @@ def coords_from_multipolygon(multipolygon: MultiPolygon) -> List[Tuple[float, fl
|
|
|
68
85
|
def flatten_list(nested_list) -> List:
|
|
69
86
|
"""
|
|
70
87
|
Flatten a list of lists.
|
|
71
|
-
:
|
|
72
|
-
|
|
88
|
+
Args:
|
|
89
|
+
nested_list (List): A list of lists.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
|
|
73
93
|
"""
|
|
74
94
|
# check if list is empty
|
|
75
95
|
if not bool(nested_list):
|
|
@@ -88,9 +108,13 @@ def add_geometry_to_plot(geoms, color="black"):
|
|
|
88
108
|
"""
|
|
89
109
|
When updating the test data, it may be useful to visualize it. Add a geometry to the global
|
|
90
110
|
maplotlib plt object. The next time we call plt.show(), this geometry will be plotted.
|
|
91
|
-
|
|
92
|
-
:
|
|
93
|
-
|
|
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
|
|
94
118
|
"""
|
|
95
119
|
for geom in geoms:
|
|
96
120
|
if isinstance(geom, Point):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "geo-adjacency"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "A package to determine which geometries are adjacent to each other, accounting for obstacles and gaps between features."
|
|
5
5
|
authors = ["Andrew Smyth <andrew.j.smyth.89@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.9",
|
|
21
21
|
"Programming Language :: Python :: 3.10",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
23
24
|
"Topic :: Scientific/Engineering",
|
|
24
25
|
"Topic :: Scientific/Engineering :: GIS",
|
|
25
26
|
]
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|