ObjectNat 1.2.0__py3-none-any.whl → 1.2.1__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.
Potentially problematic release.
This version of ObjectNat might be problematic. Click here for more details.
- objectnat/__init__.py +9 -13
- objectnat/_api.py +14 -14
- objectnat/_config.py +47 -47
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +3 -3
- objectnat/methods/coverage_zones/graph_coverage.py +98 -108
- objectnat/methods/coverage_zones/radius_voronoi_coverage.py +37 -45
- objectnat/methods/coverage_zones/stepped_coverage.py +126 -142
- objectnat/methods/isochrones/__init__.py +1 -1
- objectnat/methods/isochrones/isochrone_utils.py +167 -167
- objectnat/methods/isochrones/isochrones.py +262 -299
- objectnat/methods/noise/__init__.py +3 -4
- objectnat/methods/noise/noise_init_data.py +10 -10
- objectnat/methods/noise/noise_reduce.py +155 -155
- objectnat/methods/noise/noise_simulation.py +452 -440
- objectnat/methods/noise/noise_simulation_simplified.py +209 -135
- objectnat/methods/point_clustering/__init__.py +1 -1
- objectnat/methods/point_clustering/cluster_points_in_polygons.py +115 -116
- objectnat/methods/provision/__init__.py +1 -1
- objectnat/methods/provision/provision.py +117 -110
- objectnat/methods/provision/provision_exceptions.py +59 -59
- objectnat/methods/provision/provision_model.py +337 -337
- objectnat/methods/utils/__init__.py +1 -1
- objectnat/methods/utils/geom_utils.py +173 -173
- objectnat/methods/utils/graph_utils.py +306 -320
- objectnat/methods/utils/math_utils.py +32 -32
- objectnat/methods/visibility/__init__.py +6 -6
- objectnat/methods/visibility/visibility_analysis.py +470 -511
- {objectnat-1.2.0.dist-info → objectnat-1.2.1.dist-info}/LICENSE.txt +28 -28
- objectnat-1.2.1.dist-info/METADATA +115 -0
- objectnat-1.2.1.dist-info/RECORD +33 -0
- objectnat/methods/noise/noise_exceptions.py +0 -14
- objectnat-1.2.0.dist-info/METADATA +0 -148
- objectnat-1.2.0.dist-info/RECORD +0 -34
- {objectnat-1.2.0.dist-info → objectnat-1.2.1.dist-info}/WHEEL +0 -0
|
@@ -1,511 +1,470 @@
|
|
|
1
|
-
import math
|
|
2
|
-
from multiprocessing import cpu_count
|
|
3
|
-
|
|
4
|
-
import geopandas as gpd
|
|
5
|
-
import numpy as np
|
|
6
|
-
import pandas as pd
|
|
7
|
-
from pandarallel import pandarallel
|
|
8
|
-
from shapely import LineString, MultiPolygon, Point, Polygon
|
|
9
|
-
from shapely.ops import unary_union
|
|
10
|
-
from tqdm.contrib.concurrent import process_map
|
|
11
|
-
|
|
12
|
-
from objectnat import config
|
|
13
|
-
from objectnat.methods.utils.geom_utils import (
|
|
14
|
-
combine_geometry,
|
|
15
|
-
explode_linestring,
|
|
16
|
-
get_point_from_a_thorough_b,
|
|
17
|
-
point_side_of_line,
|
|
18
|
-
polygons_to_multilinestring,
|
|
19
|
-
)
|
|
20
|
-
from objectnat.methods.utils.math_utils import min_max_normalization
|
|
21
|
-
|
|
22
|
-
logger = config.logger
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_visibility_accurate(
|
|
26
|
-
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
|
|
27
|
-
) -> Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]:
|
|
28
|
-
"""
|
|
29
|
-
Function to get accurate visibility from a given point to buildings within a given distance.
|
|
30
|
-
|
|
31
|
-
Parameters
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0],
|
|
131
|
-
p2 = get_point_from_a_thorough_b(point_from, points_with_angle[1][0],
|
|
132
|
-
polygon =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if
|
|
158
|
-
res
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
points
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
result = sectors
|
|
472
|
-
return result
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
def _process_group(group): # pragma: no cover
|
|
476
|
-
geom = group
|
|
477
|
-
combined_geometry = combine_geometry(geom)
|
|
478
|
-
combined_geometry.drop(columns=["index", "index_right"], inplace=True)
|
|
479
|
-
combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
|
|
480
|
-
combined_geometry["new_ratio"] = combined_geometry.apply(
|
|
481
|
-
lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
threshold = combined_geometry["new_ratio"].quantile(0.25)
|
|
485
|
-
combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
|
|
486
|
-
|
|
487
|
-
combined_geometry["new_ratio_normalized"] = min_max_normalization(
|
|
488
|
-
combined_geometry["new_ratio"].values, new_min=1, new_max=10
|
|
489
|
-
)
|
|
490
|
-
|
|
491
|
-
combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
|
|
492
|
-
|
|
493
|
-
result_union = (
|
|
494
|
-
combined_geometry.groupby("new_ratio_normalized")
|
|
495
|
-
.agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
|
|
496
|
-
.reset_index(drop=True)
|
|
497
|
-
)
|
|
498
|
-
result_union.set_geometry("geometry", inplace=True)
|
|
499
|
-
result_union.set_crs(geom.crs, inplace=True)
|
|
500
|
-
|
|
501
|
-
result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
|
|
502
|
-
|
|
503
|
-
representative_points = combined_geometry.copy()
|
|
504
|
-
representative_points["geometry"] = representative_points["geometry"].representative_point()
|
|
505
|
-
|
|
506
|
-
joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
|
|
507
|
-
joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
|
|
508
|
-
|
|
509
|
-
joined.set_geometry("geometry", inplace=True)
|
|
510
|
-
joined.set_crs(geom.crs, inplace=True)
|
|
511
|
-
return joined
|
|
1
|
+
import math
|
|
2
|
+
from multiprocessing import cpu_count
|
|
3
|
+
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pandarallel import pandarallel
|
|
8
|
+
from shapely import LineString, MultiPolygon, Point, Polygon
|
|
9
|
+
from shapely.ops import unary_union
|
|
10
|
+
from tqdm.contrib.concurrent import process_map
|
|
11
|
+
|
|
12
|
+
from objectnat import config
|
|
13
|
+
from objectnat.methods.utils.geom_utils import (
|
|
14
|
+
combine_geometry,
|
|
15
|
+
explode_linestring,
|
|
16
|
+
get_point_from_a_thorough_b,
|
|
17
|
+
point_side_of_line,
|
|
18
|
+
polygons_to_multilinestring,
|
|
19
|
+
)
|
|
20
|
+
from objectnat.methods.utils.math_utils import min_max_normalization
|
|
21
|
+
|
|
22
|
+
logger = config.logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_visibility_accurate(
|
|
26
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
|
|
27
|
+
) -> Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]:
|
|
28
|
+
"""
|
|
29
|
+
Function to get accurate visibility from a given point to buildings within a given distance.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
point_from (Point | gpd.GeoDataFrame):
|
|
33
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
34
|
+
If Point is provided it should be in the same crs as obstacles.
|
|
35
|
+
obstacles (gpd.GeoDataFrame):
|
|
36
|
+
A GeoDataFrame containing the geometry of the obstacles.
|
|
37
|
+
view_distance (float):
|
|
38
|
+
The distance of view from the point.
|
|
39
|
+
return_max_view_dist (bool):
|
|
40
|
+
If True, the max view distance is returned with view polygon in tuple.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
(Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]):
|
|
44
|
+
A polygon representing the area of visibility from the given point or polygon with max view distance.
|
|
45
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
If a quick result is important, consider using the `get_visibility()` function instead.
|
|
49
|
+
However, please note that `get_visibility()` may provide less accurate results.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def find_furthest_point(point_from, view_polygon):
|
|
53
|
+
try:
|
|
54
|
+
res = round(max(Point(coords).distance(point_from) for coords in view_polygon.exterior.coords), 1)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(view_polygon)
|
|
57
|
+
raise e
|
|
58
|
+
return res
|
|
59
|
+
|
|
60
|
+
local_crs = None
|
|
61
|
+
original_crs = None
|
|
62
|
+
return_gdf = False
|
|
63
|
+
if isinstance(point_from, gpd.GeoDataFrame):
|
|
64
|
+
original_crs = point_from.crs
|
|
65
|
+
return_gdf = True
|
|
66
|
+
if len(obstacles) > 0:
|
|
67
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
68
|
+
else:
|
|
69
|
+
local_crs = point_from.estimate_utm_crs()
|
|
70
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
71
|
+
point_from = point_from.to_crs(local_crs)
|
|
72
|
+
if len(point_from) > 1:
|
|
73
|
+
logger.warning(
|
|
74
|
+
f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
|
|
75
|
+
"only the first geometry will be used for isochrone calculation. "
|
|
76
|
+
)
|
|
77
|
+
point_from = point_from.iloc[0].geometry
|
|
78
|
+
else:
|
|
79
|
+
obstacles = obstacles.copy()
|
|
80
|
+
if obstacles.contains(point_from).any():
|
|
81
|
+
return Polygon()
|
|
82
|
+
obstacles.reset_index(inplace=True, drop=True)
|
|
83
|
+
point_buffer = point_from.buffer(view_distance, resolution=32)
|
|
84
|
+
allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
|
|
85
|
+
obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
|
|
86
|
+
s = obstacles.intersects(point_buffer)
|
|
87
|
+
obstacles_in_buffer = obstacles.loc[s[s].index].geometry
|
|
88
|
+
|
|
89
|
+
buildings_lines_in_buffer = gpd.GeoSeries(
|
|
90
|
+
pd.Series(
|
|
91
|
+
obstacles_in_buffer.apply(polygons_to_multilinestring).explode(index_parts=False).apply(explode_linestring)
|
|
92
|
+
).explode()
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[buildings_lines_in_buffer.intersects(point_buffer)]
|
|
96
|
+
|
|
97
|
+
buildings_in_buffer_points = gpd.GeoSeries(
|
|
98
|
+
[Point(line.coords[0]) for line in buildings_lines_in_buffer.geometry]
|
|
99
|
+
+ [Point(line.coords[-1]) for line in buildings_lines_in_buffer.geometry]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
max_dist = max(view_distance, buildings_in_buffer_points.distance(point_from).max())
|
|
103
|
+
polygons = []
|
|
104
|
+
buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index()
|
|
105
|
+
logger.debug("Calculation vis polygon")
|
|
106
|
+
while not buildings_lines_in_buffer.empty:
|
|
107
|
+
gdf_sindex = buildings_lines_in_buffer.sindex
|
|
108
|
+
# TODO check if 2 walls are nearest and use the widest angle between points
|
|
109
|
+
nearest_wall_sind = gdf_sindex.nearest(point_from, return_all=False, max_distance=max_dist)
|
|
110
|
+
nearest_wall = buildings_lines_in_buffer.loc[nearest_wall_sind[1]].iloc[0]
|
|
111
|
+
wall_points = [Point(coords) for coords in nearest_wall.geometry.coords]
|
|
112
|
+
|
|
113
|
+
# Calculate angles and sort by angle
|
|
114
|
+
points_with_angle = sorted(
|
|
115
|
+
[(pt, math.atan2(pt.y - point_from.y, pt.x - point_from.x)) for pt in wall_points], key=lambda x: x[1]
|
|
116
|
+
)
|
|
117
|
+
delta_angle = 2 * math.pi + points_with_angle[0][1] - points_with_angle[-1][1]
|
|
118
|
+
if round(delta_angle, 10) == round(math.pi, 10):
|
|
119
|
+
wall_b_centroid = obstacles_in_buffer.loc[nearest_wall["index"]].centroid
|
|
120
|
+
p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], max_dist)
|
|
121
|
+
p2 = get_point_from_a_thorough_b(point_from, points_with_angle[1][0], max_dist)
|
|
122
|
+
polygon = LineString([p1, p2])
|
|
123
|
+
polygon = polygon.buffer(
|
|
124
|
+
distance=max_dist * point_side_of_line(polygon, wall_b_centroid), single_sided=True
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
if delta_angle > math.pi:
|
|
128
|
+
delta_angle = 2 * math.pi - delta_angle
|
|
129
|
+
a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
|
|
130
|
+
p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], a)
|
|
131
|
+
p2 = get_point_from_a_thorough_b(point_from, points_with_angle[-1][0], a)
|
|
132
|
+
polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
|
|
133
|
+
|
|
134
|
+
polygons.append(polygon)
|
|
135
|
+
buildings_lines_in_buffer.drop(nearest_wall_sind[1], inplace=True)
|
|
136
|
+
|
|
137
|
+
if not polygon.is_valid or polygon.area < 1:
|
|
138
|
+
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
lines_to_kick = buildings_lines_in_buffer.within(polygon)
|
|
142
|
+
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[~lines_to_kick]
|
|
143
|
+
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
144
|
+
logger.debug("Done calculating!")
|
|
145
|
+
res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
|
|
146
|
+
|
|
147
|
+
if isinstance(res, MultiPolygon):
|
|
148
|
+
res = list(res.geoms)
|
|
149
|
+
for polygon in res:
|
|
150
|
+
if polygon.intersects(point_from):
|
|
151
|
+
res = polygon
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if return_gdf:
|
|
155
|
+
res = gpd.GeoDataFrame(geometry=[res], crs=local_crs).to_crs(original_crs)
|
|
156
|
+
|
|
157
|
+
if return_max_view_dist:
|
|
158
|
+
return res, find_furthest_point(point_from, res)
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_visibility(
|
|
163
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
|
|
164
|
+
) -> Polygon | gpd.GeoDataFrame:
|
|
165
|
+
"""
|
|
166
|
+
Function to get a quick estimate of visibility from a given point to buildings within a given distance.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
point_from (Point | gpd.GeoDataFrame):
|
|
170
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
171
|
+
If Point is provided it should be in the same crs as obstacles.
|
|
172
|
+
obstacles (gpd.GeoDataFrame):
|
|
173
|
+
A GeoDataFrame containing the geometry of the buildings.
|
|
174
|
+
view_distance (float):
|
|
175
|
+
The distance of view from the point.
|
|
176
|
+
resolution (int) :
|
|
177
|
+
Buffer resolution for more accuracy (may give result slower)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
(Polygon | gpd.GeoDataFrame):
|
|
181
|
+
A polygon representing the area of visibility from the given point.
|
|
182
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
183
|
+
|
|
184
|
+
Notes:
|
|
185
|
+
This function provides a quicker but less accurate result compared to `get_visibility_accurate()`.
|
|
186
|
+
If accuracy is important, consider using `get_visibility_accurate()` instead.
|
|
187
|
+
"""
|
|
188
|
+
return_gdf = False
|
|
189
|
+
if isinstance(point_from, gpd.GeoDataFrame):
|
|
190
|
+
original_crs = point_from.crs
|
|
191
|
+
return_gdf = True
|
|
192
|
+
if len(obstacles) > 0:
|
|
193
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
194
|
+
else:
|
|
195
|
+
local_crs = point_from.estimate_utm_crs()
|
|
196
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
197
|
+
point_from = point_from.to_crs(local_crs)
|
|
198
|
+
if len(point_from) > 1:
|
|
199
|
+
logger.warning(
|
|
200
|
+
f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
|
|
201
|
+
"only the first geometry will be used for isochrone calculation. "
|
|
202
|
+
)
|
|
203
|
+
point_from = point_from.iloc[0].geometry
|
|
204
|
+
else:
|
|
205
|
+
obstacles = obstacles.copy()
|
|
206
|
+
point_buffer = point_from.buffer(view_distance, resolution=resolution)
|
|
207
|
+
s = obstacles.intersects(point_buffer)
|
|
208
|
+
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
209
|
+
buffer_exterior_ = list(point_buffer.exterior.coords)
|
|
210
|
+
line_geometry = [LineString([point_from, ext]) for ext in buffer_exterior_]
|
|
211
|
+
buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
|
|
212
|
+
united_buildings = buildings_in_buffer.union_all()
|
|
213
|
+
if united_buildings:
|
|
214
|
+
splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
|
|
215
|
+
else:
|
|
216
|
+
splited_lines = buffer_lines_gdf["geometry"]
|
|
217
|
+
|
|
218
|
+
splited_lines_gdf = gpd.GeoDataFrame(geometry=splited_lines).explode(index_parts=True)
|
|
219
|
+
splited_lines_list = []
|
|
220
|
+
|
|
221
|
+
for _, v in splited_lines_gdf.groupby(level=0):
|
|
222
|
+
splited_lines_list.append(v.iloc[0]["geometry"].coords[-1])
|
|
223
|
+
circuit = Polygon(splited_lines_list)
|
|
224
|
+
if united_buildings:
|
|
225
|
+
circuit = circuit.difference(united_buildings)
|
|
226
|
+
|
|
227
|
+
if return_gdf:
|
|
228
|
+
circuit = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
|
|
229
|
+
return circuit
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_visibilities_from_points(
|
|
233
|
+
points: gpd.GeoDataFrame,
|
|
234
|
+
obstacles: gpd.GeoDataFrame,
|
|
235
|
+
view_distance: int,
|
|
236
|
+
sectors_n=None,
|
|
237
|
+
max_workers: int = cpu_count(),
|
|
238
|
+
) -> list[Polygon]:
|
|
239
|
+
"""
|
|
240
|
+
Calculate visibility polygons from a set of points considering obstacles within a specified view distance.
|
|
241
|
+
|
|
242
|
+
Parameters:
|
|
243
|
+
points (gpd.GeoDataFrame):
|
|
244
|
+
GeoDataFrame containing the points from which visibility is calculated.
|
|
245
|
+
obstacles (gpd.GeoDataFrame):
|
|
246
|
+
GeoDataFrame containing the obstacles that block visibility.
|
|
247
|
+
view_distance (int):
|
|
248
|
+
The maximum distance from each point within which visibility is calculated.
|
|
249
|
+
sectors_n (int, optional):
|
|
250
|
+
Number of sectors to divide the view into for more detailed visibility calculations. Defaults to None.
|
|
251
|
+
max_workers (int, optional):
|
|
252
|
+
Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
(list[Polygon]):
|
|
256
|
+
A list of visibility polygons for each input point.
|
|
257
|
+
|
|
258
|
+
Notes:
|
|
259
|
+
This function uses `get_visibility_accurate()` in multiprocessing way.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
if points.crs != obstacles.crs:
|
|
263
|
+
raise ValueError(f"CRS mismatch, points crs:{points.crs} != obstacles crs:{obstacles.crs}")
|
|
264
|
+
if points.crs.is_geographic:
|
|
265
|
+
logger.warning("Points crs is geographic, it may produce invalid results")
|
|
266
|
+
# remove points inside polygons
|
|
267
|
+
joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
|
|
268
|
+
points = joined[joined.index_right.isnull()]
|
|
269
|
+
|
|
270
|
+
# remove unused obstacles
|
|
271
|
+
points_view = points.geometry.buffer(view_distance).union_all()
|
|
272
|
+
s = obstacles.intersects(points_view)
|
|
273
|
+
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
274
|
+
|
|
275
|
+
buildings_in_buffer.geometry = buildings_in_buffer.geometry.apply(
|
|
276
|
+
lambda geom: MultiPolygon([geom]) if isinstance(geom, Polygon) else geom
|
|
277
|
+
)
|
|
278
|
+
args = [(point, buildings_in_buffer, view_distance, sectors_n) for point in points.geometry]
|
|
279
|
+
all_visions = process_map(
|
|
280
|
+
_multiprocess_get_vis,
|
|
281
|
+
args,
|
|
282
|
+
chunksize=5,
|
|
283
|
+
desc="Calculating Visibility Catchment Area from each Point, it might take a while for a "
|
|
284
|
+
"big amount of points",
|
|
285
|
+
max_workers=max_workers,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# could return sectorized visions if sectors_n is set
|
|
289
|
+
return all_visions
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def calculate_visibility_catchment_area(
|
|
293
|
+
points: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: int | float, max_workers: int = cpu_count()
|
|
294
|
+
) -> gpd.GeoDataFrame: # pragma: no cover
|
|
295
|
+
"""
|
|
296
|
+
Calculate visibility catchment areas for a large urban area based on given points and obstacles.
|
|
297
|
+
This function is designed to work with at least 1000 points spaced 10-20 meters apart for optimal results.
|
|
298
|
+
Points can be generated using a road graph.
|
|
299
|
+
|
|
300
|
+
Parameters:
|
|
301
|
+
points (gpd.GeoDataFrame): GeoDataFrame containing the points from which visibility is calculated.
|
|
302
|
+
obstacles (gpd.GeoDataFrame): GeoDataFrame containing the obstacles that block visibility.
|
|
303
|
+
view_distance (int | float): The maximum distance from each point within which visibility is calculated.
|
|
304
|
+
max_workers (int): Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
(gpd.GeoDataFrame): GeoDataFrame containing the calculated visibility catchment areas.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def filter_geoms(x):
|
|
311
|
+
if x.geom_type == "GeometryCollection":
|
|
312
|
+
return MultiPolygon([y for y in x.geoms if y.geom_type in ["Polygon", "MultiPolygon"]])
|
|
313
|
+
return x
|
|
314
|
+
|
|
315
|
+
def calc_group_factor(x):
|
|
316
|
+
# pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
|
|
317
|
+
import numpy as np
|
|
318
|
+
|
|
319
|
+
return np.mean(x.new_ratio) * x.count_n
|
|
320
|
+
|
|
321
|
+
def unary_union_groups(x):
|
|
322
|
+
# pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
|
|
323
|
+
from shapely import MultiPolygon
|
|
324
|
+
|
|
325
|
+
# pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
|
|
326
|
+
from shapely.ops import unary_union
|
|
327
|
+
|
|
328
|
+
return unary_union(MultiPolygon(list(x["geometry"])).buffer(0))
|
|
329
|
+
|
|
330
|
+
pandarallel.initialize(progress_bar=True, verbose=0)
|
|
331
|
+
|
|
332
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
333
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
334
|
+
points = points.to_crs(local_crs)
|
|
335
|
+
|
|
336
|
+
sectors_n = 12
|
|
337
|
+
logger.info("Calculating Visibility Catchment Area from each point")
|
|
338
|
+
all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
|
|
339
|
+
all_visions_sectorized = gpd.GeoDataFrame(
|
|
340
|
+
geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=local_crs
|
|
341
|
+
)
|
|
342
|
+
logger.info("Calculating non-vision part...")
|
|
343
|
+
all_visions_unary = all_visions_sectorized.union_all()
|
|
344
|
+
convex = all_visions_unary.convex_hull
|
|
345
|
+
dif = convex.difference(all_visions_unary)
|
|
346
|
+
|
|
347
|
+
del convex, all_visions_unary
|
|
348
|
+
|
|
349
|
+
buf_area = (math.pi * view_distance**2) / sectors_n
|
|
350
|
+
all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
|
|
351
|
+
all_visions_sectorized["ratio"] = min_max_normalization(
|
|
352
|
+
all_visions_sectorized["ratio"].values, new_min=1, new_max=10
|
|
353
|
+
)
|
|
354
|
+
groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
|
|
355
|
+
groups = [group for _, group in groups]
|
|
356
|
+
|
|
357
|
+
del all_visions_sectorized
|
|
358
|
+
|
|
359
|
+
groups_result = process_map(
|
|
360
|
+
_process_group,
|
|
361
|
+
groups,
|
|
362
|
+
desc="Counting intersections in each group...",
|
|
363
|
+
max_workers=max_workers,
|
|
364
|
+
)
|
|
365
|
+
logger.info("Calculating all groups intersection...")
|
|
366
|
+
all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
|
|
367
|
+
|
|
368
|
+
del groups_result
|
|
369
|
+
|
|
370
|
+
all_in["count_n"] = all_in["index_right"].apply(len)
|
|
371
|
+
|
|
372
|
+
logger.info("Calculating intersection's parameters")
|
|
373
|
+
all_in["factor"] = all_in.parallel_apply(calc_group_factor, axis=1)
|
|
374
|
+
threshold = all_in["factor"].quantile(0.3)
|
|
375
|
+
all_in = all_in[all_in["factor"] > threshold]
|
|
376
|
+
|
|
377
|
+
all_in["factor_normalized"] = np.round(
|
|
378
|
+
min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
|
|
379
|
+
).astype(int)
|
|
380
|
+
logger.info("Calculating normalized groups geometry...")
|
|
381
|
+
all_in = all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
|
|
382
|
+
all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
|
|
383
|
+
|
|
384
|
+
all_in = all_in.explode(index_parts=True).reset_index(drop=True)
|
|
385
|
+
all_in["area"] = all_in.area
|
|
386
|
+
threshold = all_in["area"].quantile(0.9)
|
|
387
|
+
all_in = all_in[all_in["area"] > threshold]
|
|
388
|
+
all_in = all_in.groupby("factor_normalized").apply(unary_union_groups).reset_index()
|
|
389
|
+
all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
|
|
390
|
+
|
|
391
|
+
all_in.geometry = all_in.geometry.buffer(20).buffer(-20).difference(dif)
|
|
392
|
+
|
|
393
|
+
all_in.sort_values(by="factor_normalized", ascending=False, inplace=True)
|
|
394
|
+
all_in.reset_index(drop=True, inplace=True)
|
|
395
|
+
logger.info("Smoothing normalized groups geometry...")
|
|
396
|
+
for ind, row in all_in.iloc[:-1].iterrows():
|
|
397
|
+
for ind2 in range(ind + 1, len(all_in)):
|
|
398
|
+
current_geometry = all_in.at[ind2, "geometry"]
|
|
399
|
+
all_in.at[ind2, "geometry"] = current_geometry.difference(row.geometry)
|
|
400
|
+
all_in["geometry"] = all_in["geometry"].apply(filter_geoms)
|
|
401
|
+
|
|
402
|
+
all_in = all_in.explode(index_parts=True)
|
|
403
|
+
logger.info("Done!")
|
|
404
|
+
return all_in
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _multiprocess_get_vis(args): # pragma: no cover
|
|
408
|
+
point, buildings, view_distance, sectors_n = args
|
|
409
|
+
result = get_visibility_accurate(point, buildings, view_distance)
|
|
410
|
+
|
|
411
|
+
if sectors_n is not None:
|
|
412
|
+
sectors = []
|
|
413
|
+
|
|
414
|
+
cx, cy = point.x, point.y
|
|
415
|
+
|
|
416
|
+
angle_increment = 2 * math.pi / sectors_n
|
|
417
|
+
view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
|
|
418
|
+
for i in range(sectors_n):
|
|
419
|
+
angle1 = i * angle_increment
|
|
420
|
+
angle2 = (i + 1) * angle_increment
|
|
421
|
+
|
|
422
|
+
x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
|
|
423
|
+
x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
|
|
424
|
+
|
|
425
|
+
sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
|
|
426
|
+
sector = result.intersection(sector_triangle)
|
|
427
|
+
|
|
428
|
+
if not sector.is_empty:
|
|
429
|
+
sectors.append(sector)
|
|
430
|
+
result = sectors
|
|
431
|
+
return result
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _process_group(group): # pragma: no cover
|
|
435
|
+
geom = group
|
|
436
|
+
combined_geometry = combine_geometry(geom)
|
|
437
|
+
combined_geometry.drop(columns=["index", "index_right"], inplace=True)
|
|
438
|
+
combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
|
|
439
|
+
combined_geometry["new_ratio"] = combined_geometry.apply(
|
|
440
|
+
lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
threshold = combined_geometry["new_ratio"].quantile(0.25)
|
|
444
|
+
combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
|
|
445
|
+
|
|
446
|
+
combined_geometry["new_ratio_normalized"] = min_max_normalization(
|
|
447
|
+
combined_geometry["new_ratio"].values, new_min=1, new_max=10
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
|
|
451
|
+
|
|
452
|
+
result_union = (
|
|
453
|
+
combined_geometry.groupby("new_ratio_normalized")
|
|
454
|
+
.agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
|
|
455
|
+
.reset_index(drop=True)
|
|
456
|
+
)
|
|
457
|
+
result_union.set_geometry("geometry", inplace=True)
|
|
458
|
+
result_union.set_crs(geom.crs, inplace=True)
|
|
459
|
+
|
|
460
|
+
result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
|
|
461
|
+
|
|
462
|
+
representative_points = combined_geometry.copy()
|
|
463
|
+
representative_points["geometry"] = representative_points["geometry"].representative_point()
|
|
464
|
+
|
|
465
|
+
joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
|
|
466
|
+
joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
|
|
467
|
+
|
|
468
|
+
joined.set_geometry("geometry", inplace=True)
|
|
469
|
+
joined.set_crs(geom.crs, inplace=True)
|
|
470
|
+
return joined
|