ObjectNat 1.3.3__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.
- objectnat/__init__.py +9 -0
- objectnat/_api.py +14 -0
- objectnat/_config.py +43 -0
- objectnat/_version.py +1 -0
- objectnat/methods/__init__.py +0 -0
- objectnat/methods/coverage_zones/__init__.py +3 -0
- objectnat/methods/coverage_zones/graph_coverage.py +105 -0
- objectnat/methods/coverage_zones/radius_voronoi_coverage.py +39 -0
- objectnat/methods/coverage_zones/stepped_coverage.py +136 -0
- objectnat/methods/isochrones/__init__.py +1 -0
- objectnat/methods/isochrones/isochrone_utils.py +167 -0
- objectnat/methods/isochrones/isochrones.py +282 -0
- objectnat/methods/noise/__init__.py +3 -0
- objectnat/methods/noise/noise_init_data.py +10 -0
- objectnat/methods/noise/noise_reduce.py +155 -0
- objectnat/methods/noise/noise_simulation.py +453 -0
- objectnat/methods/noise/noise_simulation_simplified.py +222 -0
- objectnat/methods/point_clustering/__init__.py +1 -0
- objectnat/methods/point_clustering/cluster_points_in_polygons.py +115 -0
- objectnat/methods/provision/__init__.py +1 -0
- objectnat/methods/provision/provision.py +213 -0
- objectnat/methods/provision/provision_exceptions.py +59 -0
- objectnat/methods/provision/provision_model.py +323 -0
- objectnat/methods/utils/__init__.py +1 -0
- objectnat/methods/utils/geom_utils.py +173 -0
- objectnat/methods/utils/graph_utils.py +306 -0
- objectnat/methods/utils/math_utils.py +32 -0
- objectnat/methods/visibility/__init__.py +6 -0
- objectnat/methods/visibility/visibility_analysis.py +485 -0
- objectnat-1.3.3.dist-info/METADATA +202 -0
- objectnat-1.3.3.dist-info/RECORD +33 -0
- objectnat-1.3.3.dist-info/WHEEL +4 -0
- objectnat-1.3.3.dist-info/licenses/LICENSE.txt +28 -0
|
@@ -0,0 +1,485 @@
|
|
|
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 shapely import LineString, MultiPolygon, Point, Polygon
|
|
8
|
+
from shapely.ops import unary_union
|
|
9
|
+
from tqdm.contrib.concurrent import process_map
|
|
10
|
+
|
|
11
|
+
from objectnat import config
|
|
12
|
+
from objectnat.methods.utils.geom_utils import (
|
|
13
|
+
combine_geometry,
|
|
14
|
+
explode_linestring,
|
|
15
|
+
get_point_from_a_thorough_b,
|
|
16
|
+
point_side_of_line,
|
|
17
|
+
polygons_to_multilinestring,
|
|
18
|
+
)
|
|
19
|
+
from objectnat.methods.utils.math_utils import min_max_normalization
|
|
20
|
+
|
|
21
|
+
logger = config.logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_crs(point_from, obstacles):
|
|
25
|
+
if isinstance(point_from, gpd.GeoDataFrame):
|
|
26
|
+
if point_from.empty:
|
|
27
|
+
raise ValueError("GeoDataFrame 'point_from' is empty.")
|
|
28
|
+
return_gdf = True
|
|
29
|
+
original_crs = point_from.crs
|
|
30
|
+
local_crs = point_from.estimate_utm_crs()
|
|
31
|
+
point_from = point_from.to_crs(local_crs)
|
|
32
|
+
if len(point_from) > 1:
|
|
33
|
+
logger.warning(
|
|
34
|
+
"This method processes only a single point. "
|
|
35
|
+
f"The GeoDataFrame contains {len(point_from)} points – "
|
|
36
|
+
"only the first geometry will be used for visibility calculation."
|
|
37
|
+
)
|
|
38
|
+
point_geom = point_from.iloc[0].geometry
|
|
39
|
+
if len(obstacles) > 0:
|
|
40
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
41
|
+
|
|
42
|
+
else:
|
|
43
|
+
return_gdf = False
|
|
44
|
+
|
|
45
|
+
if len(obstacles) > 0:
|
|
46
|
+
original_crs = obstacles.crs
|
|
47
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
48
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
49
|
+
point_geom = gpd.GeoDataFrame(geometry=[point_from], crs=original_crs).to_crs(local_crs).iloc[0].geometry
|
|
50
|
+
else:
|
|
51
|
+
original_crs = 4326
|
|
52
|
+
logger.warning("No information about CRS was found, accepting everything in 4326")
|
|
53
|
+
point_gdf = gpd.GeoDataFrame(geometry=[point_from], crs=original_crs)
|
|
54
|
+
local_crs = point_gdf.estimate_utm_crs()
|
|
55
|
+
|
|
56
|
+
point_gdf = point_gdf.to_crs(local_crs)
|
|
57
|
+
point_geom = point_gdf.geometry.iloc[0]
|
|
58
|
+
|
|
59
|
+
return point_geom, obstacles, original_crs, local_crs, return_gdf
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_visibility_accurate(
|
|
63
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
|
|
64
|
+
) -> Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]:
|
|
65
|
+
"""
|
|
66
|
+
Function to get accurate visibility from a given point to buildings within a given distance.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
point_from (Point | gpd.GeoDataFrame):
|
|
70
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
71
|
+
If Point is provided it should be in the same crs as obstacles.
|
|
72
|
+
obstacles (gpd.GeoDataFrame):
|
|
73
|
+
A GeoDataFrame containing the geometry of the obstacles.
|
|
74
|
+
view_distance (float):
|
|
75
|
+
The distance of view from the point.
|
|
76
|
+
return_max_view_dist (bool):
|
|
77
|
+
If True, the max view distance is returned with view polygon in tuple.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
(Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]):
|
|
81
|
+
A polygon representing the area of visibility from the given point or polygon with max view distance.
|
|
82
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
If a quick result is important, consider using the `get_visibility()` function instead.
|
|
86
|
+
However, please note that `get_visibility()` may provide less accurate results.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def find_furthest_point(point_from, view_polygon):
|
|
90
|
+
try:
|
|
91
|
+
res = round(max(Point(coords).distance(point_from) for coords in view_polygon.exterior.coords), 1)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(view_polygon)
|
|
94
|
+
raise e
|
|
95
|
+
return res
|
|
96
|
+
|
|
97
|
+
point_geom, obstacles, original_crs, local_crs, return_gdf = _ensure_crs(point_from, obstacles)
|
|
98
|
+
|
|
99
|
+
if len(obstacles) > 0 and obstacles.contains(point_geom).any():
|
|
100
|
+
return gpd.GeoDataFrame() if return_gdf else Point()
|
|
101
|
+
|
|
102
|
+
point_buffer = point_geom.buffer(view_distance, resolution=32)
|
|
103
|
+
|
|
104
|
+
if len(obstacles) == 0:
|
|
105
|
+
full_vision_gdf = gpd.GeoDataFrame(geometry=[point_buffer], crs=local_crs).to_crs(original_crs)
|
|
106
|
+
return full_vision_gdf if return_gdf else full_vision_gdf.iloc[0].geometry
|
|
107
|
+
|
|
108
|
+
obstacles.reset_index(inplace=True, drop=True)
|
|
109
|
+
|
|
110
|
+
allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
|
|
111
|
+
|
|
112
|
+
obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
|
|
113
|
+
s = obstacles.intersects(point_buffer)
|
|
114
|
+
obstacles_in_buffer = obstacles.loc[s[s].index].geometry
|
|
115
|
+
|
|
116
|
+
buildings_lines_in_buffer = gpd.GeoSeries(
|
|
117
|
+
pd.Series(
|
|
118
|
+
obstacles_in_buffer.apply(polygons_to_multilinestring).explode(index_parts=False).apply(explode_linestring)
|
|
119
|
+
).explode()
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[buildings_lines_in_buffer.intersects(point_buffer)]
|
|
123
|
+
|
|
124
|
+
buildings_in_buffer_points = gpd.GeoSeries(
|
|
125
|
+
[Point(line.coords[0]) for line in buildings_lines_in_buffer.geometry]
|
|
126
|
+
+ [Point(line.coords[-1]) for line in buildings_lines_in_buffer.geometry]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
max_dist = max(view_distance, buildings_in_buffer_points.distance(point_geom).max())
|
|
130
|
+
polygons = []
|
|
131
|
+
buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index()
|
|
132
|
+
logger.debug("Calculation vis polygon")
|
|
133
|
+
while not buildings_lines_in_buffer.empty:
|
|
134
|
+
gdf_sindex = buildings_lines_in_buffer.sindex
|
|
135
|
+
# TODO check if 2 walls are nearest and use the widest angle between points
|
|
136
|
+
nearest_wall_sind = gdf_sindex.nearest(point_geom, return_all=False, max_distance=max_dist)
|
|
137
|
+
nearest_wall = buildings_lines_in_buffer.loc[nearest_wall_sind[1]].iloc[0]
|
|
138
|
+
wall_points = [Point(coords) for coords in nearest_wall.geometry.coords]
|
|
139
|
+
|
|
140
|
+
# Calculate angles and sort by angle
|
|
141
|
+
points_with_angle = sorted(
|
|
142
|
+
[(pt, math.atan2(pt.y - point_geom.y, pt.x - point_geom.x)) for pt in wall_points], key=lambda x: x[1]
|
|
143
|
+
)
|
|
144
|
+
delta_angle = 2 * math.pi + points_with_angle[0][1] - points_with_angle[-1][1]
|
|
145
|
+
if round(delta_angle, 10) == round(math.pi, 10):
|
|
146
|
+
wall_b_centroid = obstacles_in_buffer.loc[nearest_wall["index"]].centroid
|
|
147
|
+
p1 = get_point_from_a_thorough_b(point_geom, points_with_angle[0][0], max_dist)
|
|
148
|
+
p2 = get_point_from_a_thorough_b(point_geom, points_with_angle[1][0], max_dist)
|
|
149
|
+
polygon = LineString([p1, p2])
|
|
150
|
+
polygon = polygon.buffer(
|
|
151
|
+
distance=max_dist * point_side_of_line(polygon, wall_b_centroid), single_sided=True
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
if delta_angle > math.pi:
|
|
155
|
+
delta_angle = 2 * math.pi - delta_angle
|
|
156
|
+
a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
|
|
157
|
+
p1 = get_point_from_a_thorough_b(point_geom, points_with_angle[0][0], a)
|
|
158
|
+
p2 = get_point_from_a_thorough_b(point_geom, points_with_angle[-1][0], a)
|
|
159
|
+
polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
|
|
160
|
+
|
|
161
|
+
polygons.append(polygon)
|
|
162
|
+
buildings_lines_in_buffer.drop(nearest_wall_sind[1], inplace=True)
|
|
163
|
+
|
|
164
|
+
if not polygon.is_valid or polygon.area < 1:
|
|
165
|
+
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
lines_to_kick = buildings_lines_in_buffer.within(polygon)
|
|
169
|
+
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[~lines_to_kick]
|
|
170
|
+
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
171
|
+
logger.debug("Done calculating!")
|
|
172
|
+
res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
|
|
173
|
+
|
|
174
|
+
if isinstance(res, MultiPolygon):
|
|
175
|
+
res = list(res.geoms)
|
|
176
|
+
for polygon in res:
|
|
177
|
+
if polygon.intersects(point_geom):
|
|
178
|
+
res = polygon
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
result_gdf = gpd.GeoDataFrame(geometry=[res], crs=local_crs).to_crs(original_crs)
|
|
182
|
+
if return_gdf:
|
|
183
|
+
return result_gdf
|
|
184
|
+
res = result_gdf.to_crs(original_crs).iloc[0].geometry
|
|
185
|
+
if return_max_view_dist:
|
|
186
|
+
return res, find_furthest_point(point_geom, res)
|
|
187
|
+
return res
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_visibility(
|
|
191
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
|
|
192
|
+
) -> Polygon | gpd.GeoDataFrame:
|
|
193
|
+
"""
|
|
194
|
+
Function to get a quick estimate of visibility from a given point to buildings within a given distance.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
point_from (Point | gpd.GeoDataFrame):
|
|
198
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
199
|
+
If Point is provided it should be in the same crs as obstacles.
|
|
200
|
+
obstacles (gpd.GeoDataFrame):
|
|
201
|
+
A GeoDataFrame containing the geometry of the buildings.
|
|
202
|
+
view_distance (float):
|
|
203
|
+
The distance of view from the point.
|
|
204
|
+
resolution (int) :
|
|
205
|
+
Buffer resolution for more accuracy (may give result slower)
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
(Polygon | gpd.GeoDataFrame):
|
|
209
|
+
A polygon representing the area of visibility from the given point.
|
|
210
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
211
|
+
|
|
212
|
+
Notes:
|
|
213
|
+
This function provides a quicker but less accurate result compared to `get_visibility_accurate()`.
|
|
214
|
+
If accuracy is important, consider using `get_visibility_accurate()` instead.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
obstacles = obstacles.copy()
|
|
218
|
+
|
|
219
|
+
point_geom, obstacles, original_crs, local_crs, return_gdf = _ensure_crs(point_from, obstacles)
|
|
220
|
+
print(return_gdf)
|
|
221
|
+
if len(obstacles) > 0 and obstacles.contains(point_geom).any():
|
|
222
|
+
return Polygon()
|
|
223
|
+
|
|
224
|
+
point_buffer = point_geom.buffer(view_distance, resolution=resolution)
|
|
225
|
+
|
|
226
|
+
if len(obstacles) == 0:
|
|
227
|
+
full_vision_gdf = gpd.GeoDataFrame(geometry=[point_buffer], crs=local_crs).to_crs(original_crs)
|
|
228
|
+
return full_vision_gdf if return_gdf else full_vision_gdf.iloc[0].geometry
|
|
229
|
+
|
|
230
|
+
s = obstacles.intersects(point_buffer)
|
|
231
|
+
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
232
|
+
buffer_exterior_ = list(point_buffer.exterior.coords)
|
|
233
|
+
line_geometry = [LineString([point_geom, ext]) for ext in buffer_exterior_]
|
|
234
|
+
buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
|
|
235
|
+
united_buildings = buildings_in_buffer.union_all()
|
|
236
|
+
if united_buildings:
|
|
237
|
+
splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
|
|
238
|
+
else:
|
|
239
|
+
splited_lines = buffer_lines_gdf["geometry"]
|
|
240
|
+
|
|
241
|
+
splited_lines_gdf = gpd.GeoDataFrame(geometry=splited_lines).explode(index_parts=True)
|
|
242
|
+
splited_lines_list = []
|
|
243
|
+
|
|
244
|
+
for _, v in splited_lines_gdf.groupby(level=0):
|
|
245
|
+
splited_lines_list.append(v.iloc[0]["geometry"].coords[-1])
|
|
246
|
+
circuit = Polygon(splited_lines_list)
|
|
247
|
+
if united_buildings:
|
|
248
|
+
circuit = circuit.difference(united_buildings)
|
|
249
|
+
|
|
250
|
+
result_gdf = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
|
|
251
|
+
return result_gdf if return_gdf else result_gdf.iloc[0].geometry
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_visibilities_from_points(
|
|
255
|
+
points: gpd.GeoDataFrame,
|
|
256
|
+
obstacles: gpd.GeoDataFrame,
|
|
257
|
+
view_distance: int,
|
|
258
|
+
sectors_n=None,
|
|
259
|
+
max_workers: int = cpu_count(),
|
|
260
|
+
) -> list[Polygon]:
|
|
261
|
+
"""
|
|
262
|
+
Calculate visibility polygons from a set of points considering obstacles within a specified view distance.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
points (gpd.GeoDataFrame):
|
|
266
|
+
GeoDataFrame containing the points from which visibility is calculated.
|
|
267
|
+
obstacles (gpd.GeoDataFrame):
|
|
268
|
+
GeoDataFrame containing the obstacles that block visibility.
|
|
269
|
+
view_distance (int):
|
|
270
|
+
The maximum distance from each point within which visibility is calculated.
|
|
271
|
+
sectors_n (int, optional):
|
|
272
|
+
Number of sectors to divide the view into for more detailed visibility calculations. Defaults to None.
|
|
273
|
+
max_workers (int, optional):
|
|
274
|
+
Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
(list[Polygon]):
|
|
278
|
+
A list of visibility polygons for each input point.
|
|
279
|
+
|
|
280
|
+
Notes:
|
|
281
|
+
This function uses `get_visibility_accurate()` in multiprocessing way.
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
if points.crs != obstacles.crs:
|
|
285
|
+
raise ValueError(f"CRS mismatch, points crs:{points.crs} != obstacles crs:{obstacles.crs}")
|
|
286
|
+
if points.crs.is_geographic:
|
|
287
|
+
logger.warning("Points crs is geographic, it may produce invalid results")
|
|
288
|
+
# remove points inside polygons
|
|
289
|
+
joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
|
|
290
|
+
points = joined[joined.index_right.isnull()]
|
|
291
|
+
|
|
292
|
+
# remove unused obstacles
|
|
293
|
+
points_view = points.geometry.buffer(view_distance).union_all()
|
|
294
|
+
s = obstacles.intersects(points_view)
|
|
295
|
+
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
296
|
+
|
|
297
|
+
buildings_in_buffer.geometry = buildings_in_buffer.geometry.apply(
|
|
298
|
+
lambda geom: MultiPolygon([geom]) if isinstance(geom, Polygon) else geom
|
|
299
|
+
)
|
|
300
|
+
args = [(point, buildings_in_buffer, view_distance, sectors_n) for point in points.geometry]
|
|
301
|
+
all_visions = process_map(
|
|
302
|
+
_multiprocess_get_vis,
|
|
303
|
+
args,
|
|
304
|
+
chunksize=5,
|
|
305
|
+
desc="Calculating Visibility Catchment Area from each Point, it might take a while for a "
|
|
306
|
+
"big amount of points",
|
|
307
|
+
max_workers=max_workers,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# could return sectorized visions if sectors_n is set
|
|
311
|
+
return all_visions
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def calculate_visibility_catchment_area(
|
|
315
|
+
points: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: int | float, max_workers: int = cpu_count()
|
|
316
|
+
) -> gpd.GeoDataFrame: # pragma: no cover
|
|
317
|
+
"""
|
|
318
|
+
Calculate visibility catchment areas for a large urban area based on given points and obstacles.
|
|
319
|
+
This function is designed to work with at least 1000 points spaced 10-20 meters apart for optimal results.
|
|
320
|
+
Points can be generated using a road graph.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
points (gpd.GeoDataFrame): GeoDataFrame containing the points from which visibility is calculated.
|
|
324
|
+
obstacles (gpd.GeoDataFrame): GeoDataFrame containing the obstacles that block visibility.
|
|
325
|
+
view_distance (int | float): The maximum distance from each point within which visibility is calculated.
|
|
326
|
+
max_workers (int): Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
(gpd.GeoDataFrame): GeoDataFrame containing the calculated visibility catchment areas.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def filter_geoms(x):
|
|
333
|
+
if x.geom_type == "GeometryCollection":
|
|
334
|
+
return MultiPolygon([y for y in x.geoms if y.geom_type in ["Polygon", "MultiPolygon"]])
|
|
335
|
+
return x
|
|
336
|
+
|
|
337
|
+
def calc_group_factor(x):
|
|
338
|
+
return np.mean(x.new_ratio) * x.count_n
|
|
339
|
+
|
|
340
|
+
def unary_union_groups(x):
|
|
341
|
+
return unary_union(MultiPolygon(list(x["geometry"])).buffer(0))
|
|
342
|
+
|
|
343
|
+
raise NotImplementedError("This method is temporarily unsupported.")
|
|
344
|
+
|
|
345
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
346
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
347
|
+
points = points.to_crs(local_crs)
|
|
348
|
+
|
|
349
|
+
sectors_n = 12
|
|
350
|
+
logger.info("Calculating Visibility Catchment Area from each point")
|
|
351
|
+
all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
|
|
352
|
+
all_visions_sectorized = gpd.GeoDataFrame(
|
|
353
|
+
geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=local_crs
|
|
354
|
+
)
|
|
355
|
+
logger.info("Calculating non-vision part...")
|
|
356
|
+
all_visions_unary = all_visions_sectorized.union_all()
|
|
357
|
+
convex = all_visions_unary.convex_hull
|
|
358
|
+
dif = convex.difference(all_visions_unary)
|
|
359
|
+
|
|
360
|
+
del convex, all_visions_unary
|
|
361
|
+
|
|
362
|
+
buf_area = (math.pi * view_distance**2) / sectors_n
|
|
363
|
+
all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
|
|
364
|
+
all_visions_sectorized["ratio"] = min_max_normalization(
|
|
365
|
+
all_visions_sectorized["ratio"].values, new_min=1, new_max=10
|
|
366
|
+
)
|
|
367
|
+
groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
|
|
368
|
+
groups = [group for _, group in groups]
|
|
369
|
+
|
|
370
|
+
del all_visions_sectorized
|
|
371
|
+
|
|
372
|
+
groups_result = process_map(
|
|
373
|
+
_process_group,
|
|
374
|
+
groups,
|
|
375
|
+
desc="Counting intersections in each group...",
|
|
376
|
+
max_workers=max_workers,
|
|
377
|
+
)
|
|
378
|
+
logger.info("Calculating all groups intersection...")
|
|
379
|
+
all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
|
|
380
|
+
|
|
381
|
+
del groups_result
|
|
382
|
+
|
|
383
|
+
all_in["count_n"] = all_in["index_right"].apply(len)
|
|
384
|
+
|
|
385
|
+
logger.info("Calculating intersection's parameters")
|
|
386
|
+
# all_in["factor"] = all_in.parallel_apply(calc_group_factor, axis=1) # TODO replace pandarallel methods
|
|
387
|
+
threshold = all_in["factor"].quantile(0.3)
|
|
388
|
+
all_in = all_in[all_in["factor"] > threshold]
|
|
389
|
+
|
|
390
|
+
all_in["factor_normalized"] = np.round(
|
|
391
|
+
min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
|
|
392
|
+
).astype(int)
|
|
393
|
+
logger.info("Calculating normalized groups geometry...")
|
|
394
|
+
all_in = (
|
|
395
|
+
all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
|
|
396
|
+
) # TODO replace pandarallel methods
|
|
397
|
+
all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
|
|
398
|
+
|
|
399
|
+
all_in = all_in.explode(index_parts=True).reset_index(drop=True)
|
|
400
|
+
all_in["area"] = all_in.area
|
|
401
|
+
threshold = all_in["area"].quantile(0.9)
|
|
402
|
+
all_in = all_in[all_in["area"] > threshold]
|
|
403
|
+
all_in = all_in.groupby("factor_normalized").apply(unary_union_groups).reset_index()
|
|
404
|
+
all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
|
|
405
|
+
|
|
406
|
+
all_in.geometry = all_in.geometry.buffer(20).buffer(-20).difference(dif)
|
|
407
|
+
|
|
408
|
+
all_in.sort_values(by="factor_normalized", ascending=False, inplace=True)
|
|
409
|
+
all_in.reset_index(drop=True, inplace=True)
|
|
410
|
+
logger.info("Smoothing normalized groups geometry...")
|
|
411
|
+
for ind, row in all_in.iloc[:-1].iterrows():
|
|
412
|
+
for ind2 in range(ind + 1, len(all_in)):
|
|
413
|
+
current_geometry = all_in.at[ind2, "geometry"]
|
|
414
|
+
all_in.at[ind2, "geometry"] = current_geometry.difference(row.geometry)
|
|
415
|
+
all_in["geometry"] = all_in["geometry"].apply(filter_geoms)
|
|
416
|
+
|
|
417
|
+
all_in = all_in.explode(index_parts=True)
|
|
418
|
+
logger.info("Done!")
|
|
419
|
+
return all_in
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _multiprocess_get_vis(args): # pragma: no cover
|
|
423
|
+
point, buildings, view_distance, sectors_n = args
|
|
424
|
+
result = get_visibility_accurate(point, buildings, view_distance)
|
|
425
|
+
|
|
426
|
+
if sectors_n is not None:
|
|
427
|
+
sectors = []
|
|
428
|
+
|
|
429
|
+
cx, cy = point.x, point.y
|
|
430
|
+
|
|
431
|
+
angle_increment = 2 * math.pi / sectors_n
|
|
432
|
+
view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
|
|
433
|
+
for i in range(sectors_n):
|
|
434
|
+
angle1 = i * angle_increment
|
|
435
|
+
angle2 = (i + 1) * angle_increment
|
|
436
|
+
|
|
437
|
+
x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
|
|
438
|
+
x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
|
|
439
|
+
|
|
440
|
+
sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
|
|
441
|
+
sector = result.intersection(sector_triangle)
|
|
442
|
+
|
|
443
|
+
if not sector.is_empty:
|
|
444
|
+
sectors.append(sector)
|
|
445
|
+
result = sectors
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _process_group(group): # pragma: no cover
|
|
450
|
+
geom = group
|
|
451
|
+
combined_geometry = combine_geometry(geom)
|
|
452
|
+
combined_geometry.drop(columns=["index", "index_right"], inplace=True)
|
|
453
|
+
combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
|
|
454
|
+
combined_geometry["new_ratio"] = combined_geometry.apply(
|
|
455
|
+
lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
threshold = combined_geometry["new_ratio"].quantile(0.25)
|
|
459
|
+
combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
|
|
460
|
+
|
|
461
|
+
combined_geometry["new_ratio_normalized"] = min_max_normalization(
|
|
462
|
+
combined_geometry["new_ratio"].values, new_min=1, new_max=10
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
|
|
466
|
+
|
|
467
|
+
result_union = (
|
|
468
|
+
combined_geometry.groupby("new_ratio_normalized")
|
|
469
|
+
.agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
|
|
470
|
+
.reset_index(drop=True)
|
|
471
|
+
)
|
|
472
|
+
result_union.set_geometry("geometry", inplace=True)
|
|
473
|
+
result_union.set_crs(geom.crs, inplace=True)
|
|
474
|
+
|
|
475
|
+
result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
|
|
476
|
+
|
|
477
|
+
representative_points = combined_geometry.copy()
|
|
478
|
+
representative_points["geometry"] = representative_points["geometry"].representative_point()
|
|
479
|
+
|
|
480
|
+
joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
|
|
481
|
+
joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
|
|
482
|
+
|
|
483
|
+
joined.set_geometry("geometry", inplace=True)
|
|
484
|
+
joined.set_crs(geom.crs, inplace=True)
|
|
485
|
+
return joined
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ObjectNat
|
|
3
|
+
Version: 1.3.3
|
|
4
|
+
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE.txt
|
|
7
|
+
Author: DDonnyy
|
|
8
|
+
Author-email: 63115678+DDonnyy@users.noreply.github.com
|
|
9
|
+
Requires-Python: >=3.11,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: geopandas (>=1.0.1,<2.0.0)
|
|
15
|
+
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
16
|
+
Requires-Dist: networkx (>=3.4.2,<4.0.0)
|
|
17
|
+
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
18
|
+
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
19
|
+
Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
|
|
20
|
+
Requires-Dist: tqdm (>=4.66.2,<5.0.0)
|
|
21
|
+
Description-Content-Type: text/x-rst
|
|
22
|
+
|
|
23
|
+
ObjectNat
|
|
24
|
+
=========
|
|
25
|
+
|
|
26
|
+
Object-oriented Network Analysis Tools
|
|
27
|
+
--------------------------------------
|
|
28
|
+
|
|
29
|
+
.. |badge-black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
30
|
+
:target: https://github.com/psf/black
|
|
31
|
+
:alt: Code style: black
|
|
32
|
+
|
|
33
|
+
.. |badge-pypi| image:: https://img.shields.io/pypi/v/objectnat.svg
|
|
34
|
+
:target: https://pypi.org/project/objectnat/
|
|
35
|
+
:alt: PyPI version
|
|
36
|
+
|
|
37
|
+
.. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/ci_pipeline.yml/badge.svg
|
|
38
|
+
:target: https://github.com/IDUclub/ObjectNat/actions/workflows/ci_pipeline.yml
|
|
39
|
+
:alt: CI
|
|
40
|
+
|
|
41
|
+
.. |badge-codecov| image:: https://codecov.io/gh/DDonnyy/ObjectNat/graph/badge.svg?token=K6JFSJ02GU
|
|
42
|
+
:target: https://codecov.io/gh/DDonnyy/ObjectNat
|
|
43
|
+
:alt: Coverage
|
|
44
|
+
|
|
45
|
+
.. |badge-license| image:: https://img.shields.io/badge/license-BSD--3--Clause-blue.svg
|
|
46
|
+
:target: https://opensource.org/licenses/BSD-3-Clause
|
|
47
|
+
:alt: License
|
|
48
|
+
|
|
49
|
+
.. |badge-docs| image:: https://img.shields.io/badge/docs-latest-4aa0d5?logo=readthedocs
|
|
50
|
+
:target: https://iduclub.github.io/ObjectNat/
|
|
51
|
+
:alt: Docs
|
|
52
|
+
|
|
53
|
+
|badge-black| |badge-pypi| |badge-ci| |badge-codecov| |badge-license| |badge-docs|
|
|
54
|
+
|
|
55
|
+
`РИДМИ (Russian) <https://github.com/IDUclub/ObjectNat/blob/master/README_RU.rst>`__
|
|
56
|
+
|
|
57
|
+
.. image:: https://raw.githubusercontent.com/IDUclub/ObjectNat/master/docs/_static/ONlogo.svg
|
|
58
|
+
:align: center
|
|
59
|
+
:width: 400
|
|
60
|
+
:alt: ObjectNat logo
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
**ObjectNat** is an open-source library developed by the **IDU** team
|
|
64
|
+
for spatial and network analysis in urban studies.
|
|
65
|
+
The library provides tools for analyzing **accessibility**, **visibility**,
|
|
66
|
+
**noise propagation**, and **service provision**.
|
|
67
|
+
----
|
|
68
|
+
|
|
69
|
+
Key Features
|
|
70
|
+
------------
|
|
71
|
+
|
|
72
|
+
Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
73
|
+
|
|
74
|
+
1. **Isochrones and Transport Accessibility**
|
|
75
|
+
|
|
76
|
+
Isochrones represent areas reachable from an origin point within a specified time along a transport network.
|
|
77
|
+
This feature allows the analysis of transport accessibility using pedestrian, road,
|
|
78
|
+
public transport, or multimodal graphs.
|
|
79
|
+
|
|
80
|
+
The library supports several methods for building isochrones:
|
|
81
|
+
|
|
82
|
+
- **Basic isochrones**: display a single zone reachable within a specified time.
|
|
83
|
+
- **Step isochrones**: divide the accessibility area into time intervals (e.g., 3, 5, 10 minutes).
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/isochrones.html>`__
|
|
87
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/isochrones.html>`__
|
|
88
|
+
|
|
89
|
+
2. **Graph Coverage Zones from Points**
|
|
90
|
+
|
|
91
|
+
A function for generating **coverage areas** from a set of origin points using a transport network.
|
|
92
|
+
It computes the area reachable from each point by **travel time** or **distance**,
|
|
93
|
+
then builds polygons using **Voronoi diagrams** and clips them by a given boundary if specified.
|
|
94
|
+
|
|
95
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/coverage.html>`__
|
|
96
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/coverage.html>`__
|
|
97
|
+
|
|
98
|
+
3. **Service Provision Analysis**
|
|
99
|
+
|
|
100
|
+
A function to evaluate how well residential buildings and their populations are provided
|
|
101
|
+
with services (e.g., schools, clinics) that have limited **capacity**
|
|
102
|
+
and a defined **accessibility threshold** (in minutes or meters).
|
|
103
|
+
The function models the **balance between supply and demand**,
|
|
104
|
+
assessing how well services meet the needs of nearby buildings within an acceptable time.
|
|
105
|
+
|
|
106
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/provision.html>`__
|
|
107
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/provision.html>`__
|
|
108
|
+
|
|
109
|
+
4. **Visibility Analysis**
|
|
110
|
+
|
|
111
|
+
A function for evaluating visibility from a given point or set of points to nearby buildings within a given radius.
|
|
112
|
+
It is used to assess visual accessibility in urban environments.
|
|
113
|
+
A module is also implemented for computing **visibility coverage zones**
|
|
114
|
+
using a dense observer grid (recommended ~1000 points with a 10–20 m spacing).
|
|
115
|
+
Points can be generated along the transport network and distributed across its edges.
|
|
116
|
+
|
|
117
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/visibility.html>`__
|
|
118
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/visibility.html>`__
|
|
119
|
+
|
|
120
|
+
5. **Noise Simulation & Noise Frame**
|
|
121
|
+
|
|
122
|
+
Simulation of noise propagation from sources, taking into account **obstacles**, **vegetation**,
|
|
123
|
+
and **environmental factors**.
|
|
124
|
+
|
|
125
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/noise.html>`__
|
|
126
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/noise.html>`__
|
|
127
|
+
🧠 `Detailed theory <https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation>`__
|
|
128
|
+
|
|
129
|
+
6. **Point Clusterization**
|
|
130
|
+
|
|
131
|
+
A function for constructing **cluster polygons** based on a set of points using:
|
|
132
|
+
|
|
133
|
+
- Minimum **distance** between points.
|
|
134
|
+
- Minimum **number of points** in a cluster.
|
|
135
|
+
|
|
136
|
+
The function can also compute the **ratio of service types** in each cluster
|
|
137
|
+
for spatial analysis of service composition.
|
|
138
|
+
|
|
139
|
+
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/clustering.html>`__
|
|
140
|
+
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/clustering.html>`__
|
|
141
|
+
|
|
142
|
+
----
|
|
143
|
+
|
|
144
|
+
City Graphs via *IduEdu*
|
|
145
|
+
------------------------
|
|
146
|
+
|
|
147
|
+
For optimal performance, **ObjectNat** is recommended to be used with graphs
|
|
148
|
+
created by the `IduEdu <https://github.com/IDUclub/IduEdu>`_ library.
|
|
149
|
+
|
|
150
|
+
**IduEdu** is an open-source Python library designed for building and processing
|
|
151
|
+
complex urban networks based on OpenStreetMap data.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
**IduEdu** can be installed via ``pip``::
|
|
155
|
+
|
|
156
|
+
pip install IduEdu
|
|
157
|
+
|
|
158
|
+
Example usage::
|
|
159
|
+
|
|
160
|
+
from iduedu import get_4326_boundary, get_intermodal_graph
|
|
161
|
+
|
|
162
|
+
poly = get_4326_boundary(osm_id=1114252)
|
|
163
|
+
G_intermodal = get_intermodal_graph(territory=poly, clip_by_territory=True)
|
|
164
|
+
|
|
165
|
+
----
|
|
166
|
+
|
|
167
|
+
Installation
|
|
168
|
+
------------
|
|
169
|
+
|
|
170
|
+
**ObjectNat** can be installed via ``pip``::
|
|
171
|
+
|
|
172
|
+
pip install ObjectNat
|
|
173
|
+
|
|
174
|
+
----
|
|
175
|
+
|
|
176
|
+
Configuration
|
|
177
|
+
-------------
|
|
178
|
+
|
|
179
|
+
You can adjust logging and progress bar output using the config module::
|
|
180
|
+
|
|
181
|
+
from objectnat import config
|
|
182
|
+
|
|
183
|
+
config.change_logger_lvl("INFO") # mute debug logs
|
|
184
|
+
config.set_enable_tqdm(False) # disable tqdm progress bars
|
|
185
|
+
|
|
186
|
+
----
|
|
187
|
+
|
|
188
|
+
Contacts
|
|
189
|
+
--------
|
|
190
|
+
|
|
191
|
+
- `NCCR <https://actcognitive.org/>`_ — National Center for Cognitive Research
|
|
192
|
+
- `IDU <https://idu.itmo.ru/>`_ — Institute of Design and Urban Studies
|
|
193
|
+
- `Natalya Chichkova <https://t.me/nancy_nat>`_ — Project Manager
|
|
194
|
+
- `Danila Oleynikov (Donny) <https://t.me/ddonny_dd>`_ — Lead Software Engineer
|
|
195
|
+
|
|
196
|
+
----
|
|
197
|
+
|
|
198
|
+
Publications
|
|
199
|
+
------------
|
|
200
|
+
|
|
201
|
+
Coming soon.
|
|
202
|
+
|