ObjectNat 0.2.7__py3-none-any.whl → 1.0.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/_api.py +5 -8
- objectnat/_config.py +0 -24
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +2 -0
- objectnat/methods/coverage_zones/graph_coverage.py +118 -0
- objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
- objectnat/methods/isochrones/__init__.py +1 -0
- objectnat/methods/isochrones/isochrone_utils.py +130 -0
- objectnat/methods/isochrones/isochrones.py +325 -0
- objectnat/methods/noise/__init__.py +2 -2
- objectnat/methods/noise/noise_sim.py +14 -9
- objectnat/methods/point_clustering/__init__.py +1 -0
- objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
- objectnat/methods/provision/__init__.py +1 -0
- objectnat/methods/provision/provision.py +4 -4
- objectnat/methods/provision/provision_model.py +17 -18
- objectnat/methods/utils/geom_utils.py +54 -3
- objectnat/methods/utils/graph_utils.py +127 -0
- objectnat/methods/utils/math_utils.py +32 -0
- objectnat/methods/visibility/__init__.py +6 -0
- objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +167 -208
- objectnat-1.0.1.dist-info/METADATA +142 -0
- objectnat-1.0.1.dist-info/RECORD +32 -0
- objectnat/methods/balanced_buildings.py +0 -69
- objectnat/methods/coverage_zones.py +0 -90
- objectnat/methods/isochrones.py +0 -143
- objectnat/methods/living_buildings_osm.py +0 -168
- objectnat-0.2.7.dist-info/METADATA +0 -118
- objectnat-0.2.7.dist-info/RECORD +0 -26
- {objectnat-0.2.7.dist-info → objectnat-1.0.1.dist-info}/LICENSE.txt +0 -0
- {objectnat-0.2.7.dist-info → objectnat-1.0.1.dist-info}/WHEEL +0 -0
|
@@ -6,30 +6,33 @@ import numpy as np
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from pandarallel import pandarallel
|
|
8
8
|
from shapely import LineString, MultiPolygon, Point, Polygon
|
|
9
|
-
from shapely.ops import
|
|
9
|
+
from shapely.ops import unary_union
|
|
10
10
|
from tqdm.contrib.concurrent import process_map
|
|
11
11
|
|
|
12
12
|
from objectnat import config
|
|
13
13
|
from objectnat.methods.utils.geom_utils import (
|
|
14
|
+
combine_geometry,
|
|
14
15
|
explode_linestring,
|
|
15
16
|
get_point_from_a_thorough_b,
|
|
16
17
|
point_side_of_line,
|
|
17
18
|
polygons_to_multilinestring,
|
|
18
19
|
)
|
|
20
|
+
from objectnat.methods.utils.math_utils import min_max_normalization
|
|
19
21
|
|
|
20
22
|
logger = config.logger
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def get_visibility_accurate(
|
|
24
|
-
point_from: Point, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
|
|
25
|
-
) -> Polygon | tuple[Polygon, float]:
|
|
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]:
|
|
26
28
|
"""
|
|
27
29
|
Function to get accurate visibility from a given point to buildings within a given distance.
|
|
28
30
|
|
|
29
31
|
Parameters
|
|
30
32
|
----------
|
|
31
|
-
point_from : Point
|
|
32
|
-
The point from which the line of sight is drawn.
|
|
33
|
+
point_from : Point | gpd.GeoDataFrame
|
|
34
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
35
|
+
If Point is provided it should be in the same crs as obstacles
|
|
33
36
|
obstacles : gpd.GeoDataFrame
|
|
34
37
|
A GeoDataFrame containing the geometry of the obstacles.
|
|
35
38
|
view_distance : float
|
|
@@ -39,20 +42,21 @@ def get_visibility_accurate(
|
|
|
39
42
|
|
|
40
43
|
Returns
|
|
41
44
|
-------
|
|
42
|
-
Polygon | tuple[Polygon, float]
|
|
45
|
+
Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]
|
|
43
46
|
A polygon representing the area of visibility from the given point or polygon with max view distance.
|
|
47
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
44
48
|
|
|
45
49
|
Notes
|
|
46
50
|
-----
|
|
47
|
-
If a quick result is important, consider using the `
|
|
48
|
-
However, please note that `
|
|
51
|
+
If a quick result is important, consider using the `get_visibility()` function instead.
|
|
52
|
+
However, please note that `get_visibility()` may provide less accurate results.
|
|
49
53
|
|
|
50
54
|
Examples
|
|
51
55
|
--------
|
|
52
|
-
>>>
|
|
53
|
-
>>>
|
|
54
|
-
>>>
|
|
55
|
-
>>>
|
|
56
|
+
>>> from objectnat import get_visibility_accurate
|
|
57
|
+
>>> obstacles = gpd.read_parquet('examples_data/buildings.parquet')
|
|
58
|
+
>>> point_from = gpd.GeoDataFrame(geometry=[Point(30.2312112, 59.9482336)], crs=4326)
|
|
59
|
+
>>> result = get_visibility_accurate(point_from, obstacles, 500)
|
|
56
60
|
"""
|
|
57
61
|
|
|
58
62
|
def find_furthest_point(point_from, view_polygon):
|
|
@@ -63,7 +67,27 @@ def get_visibility_accurate(
|
|
|
63
67
|
raise e
|
|
64
68
|
return res
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
local_crs = None
|
|
71
|
+
original_crs = None
|
|
72
|
+
return_gdf = False
|
|
73
|
+
if isinstance(point_from, gpd.GeoDataFrame):
|
|
74
|
+
original_crs = point_from.crs
|
|
75
|
+
return_gdf = True
|
|
76
|
+
if len(obstacles) > 0:
|
|
77
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
78
|
+
else:
|
|
79
|
+
local_crs = point_from.estimate_utm_crs()
|
|
80
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
81
|
+
point_from = point_from.to_crs(local_crs)
|
|
82
|
+
if len(point_from) > 1:
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
|
|
85
|
+
"only the first geometry will be used for isochrone calculation. "
|
|
86
|
+
)
|
|
87
|
+
point_from = point_from.iloc[0].geometry
|
|
88
|
+
else:
|
|
89
|
+
obstacles = obstacles.copy()
|
|
90
|
+
|
|
67
91
|
obstacles.reset_index(inplace=True, drop=True)
|
|
68
92
|
|
|
69
93
|
point_buffer = point_from.buffer(view_distance, resolution=32)
|
|
@@ -130,30 +154,32 @@ def get_visibility_accurate(
|
|
|
130
154
|
logger.debug("Done calculating!")
|
|
131
155
|
res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
|
|
132
156
|
|
|
133
|
-
if isinstance(res,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
if isinstance(res, MultiPolygon):
|
|
158
|
+
res = list(res.geoms)
|
|
159
|
+
for polygon in res:
|
|
160
|
+
if polygon.intersects(point_from):
|
|
161
|
+
res = polygon
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if return_gdf:
|
|
165
|
+
res = gpd.GeoDataFrame(geometry=[res], crs=local_crs).to_crs(original_crs)
|
|
139
166
|
|
|
140
|
-
for polygon in res:
|
|
141
|
-
if polygon.intersects(point_from):
|
|
142
|
-
polygon_containing_point = polygon
|
|
143
|
-
break
|
|
144
167
|
if return_max_view_dist:
|
|
145
|
-
return
|
|
146
|
-
return
|
|
168
|
+
return res, find_furthest_point(point_from, res)
|
|
169
|
+
return res
|
|
147
170
|
|
|
148
171
|
|
|
149
|
-
def get_visibility(
|
|
172
|
+
def get_visibility(
|
|
173
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
|
|
174
|
+
) -> Polygon | gpd.GeoDataFrame:
|
|
150
175
|
"""
|
|
151
176
|
Function to get a quick estimate of visibility from a given point to buildings within a given distance.
|
|
152
177
|
|
|
153
178
|
Parameters
|
|
154
179
|
----------
|
|
155
|
-
|
|
156
|
-
The point from which the line of sight is drawn.
|
|
180
|
+
point_from : Point | gpd.GeoDataFrame
|
|
181
|
+
The point or GeoDataFrame with 1 point from which the line of sight is drawn.
|
|
182
|
+
If Point is provided it should be in the same crs as obstacles
|
|
157
183
|
obstacles : gpd.GeoDataFrame
|
|
158
184
|
A GeoDataFrame containing the geometry of the buildings.
|
|
159
185
|
view_distance : float
|
|
@@ -163,8 +189,9 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
163
189
|
|
|
164
190
|
Returns
|
|
165
191
|
-------
|
|
166
|
-
Polygon
|
|
167
|
-
A polygon representing the
|
|
192
|
+
Polygon | gpd.GeoDataFrame
|
|
193
|
+
A polygon representing the area of visibility from the given point.
|
|
194
|
+
if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
|
|
168
195
|
|
|
169
196
|
Notes
|
|
170
197
|
-----
|
|
@@ -173,19 +200,36 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
173
200
|
|
|
174
201
|
Examples
|
|
175
202
|
--------
|
|
176
|
-
>>>
|
|
177
|
-
>>>
|
|
178
|
-
>>>
|
|
179
|
-
>>>
|
|
203
|
+
>>> from objectnat import get_visibility
|
|
204
|
+
>>> obstacles = gpd.read_parquet('examples_data/buildings.parquet')
|
|
205
|
+
>>> point_from = gpd.GeoDataFrame(geometry=[Point(30.2312112, 59.9482336)], crs=4326)
|
|
206
|
+
>>> result = get_visibility(point_from, obstacles, 500)
|
|
180
207
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
return_gdf = False
|
|
209
|
+
if isinstance(point_from, gpd.GeoDataFrame):
|
|
210
|
+
original_crs = point_from.crs
|
|
211
|
+
return_gdf = True
|
|
212
|
+
if len(obstacles) > 0:
|
|
213
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
214
|
+
else:
|
|
215
|
+
local_crs = point_from.estimate_utm_crs()
|
|
216
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
217
|
+
point_from = point_from.to_crs(local_crs)
|
|
218
|
+
if len(point_from) > 1:
|
|
219
|
+
logger.warning(
|
|
220
|
+
f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
|
|
221
|
+
"only the first geometry will be used for isochrone calculation. "
|
|
222
|
+
)
|
|
223
|
+
point_from = point_from.iloc[0].geometry
|
|
224
|
+
else:
|
|
225
|
+
obstacles = obstacles.copy()
|
|
226
|
+
point_buffer = point_from.buffer(view_distance, resolution=resolution)
|
|
227
|
+
s = obstacles.intersects(point_buffer)
|
|
184
228
|
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
185
229
|
buffer_exterior_ = list(point_buffer.exterior.coords)
|
|
186
|
-
line_geometry = [LineString([
|
|
230
|
+
line_geometry = [LineString([point_from, ext]) for ext in buffer_exterior_]
|
|
187
231
|
buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
|
|
188
|
-
united_buildings = buildings_in_buffer.
|
|
232
|
+
united_buildings = buildings_in_buffer.union_all()
|
|
189
233
|
if united_buildings:
|
|
190
234
|
splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
|
|
191
235
|
else:
|
|
@@ -199,158 +243,10 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
199
243
|
circuit = Polygon(splited_lines_list)
|
|
200
244
|
if united_buildings:
|
|
201
245
|
circuit = circuit.difference(united_buildings)
|
|
202
|
-
return circuit
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _multiprocess_get_vis(args):
|
|
206
|
-
point, buildings, view_distance, sectors_n = args
|
|
207
|
-
result = get_visibility_accurate(point, buildings, view_distance)
|
|
208
|
-
|
|
209
|
-
if sectors_n is not None:
|
|
210
|
-
sectors = []
|
|
211
|
-
|
|
212
|
-
cx, cy = point.x, point.y
|
|
213
|
-
|
|
214
|
-
angle_increment = 2 * math.pi / sectors_n
|
|
215
|
-
view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
|
|
216
|
-
for i in range(sectors_n):
|
|
217
|
-
angle1 = i * angle_increment
|
|
218
|
-
angle2 = (i + 1) * angle_increment
|
|
219
|
-
|
|
220
|
-
x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
|
|
221
|
-
x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
|
|
222
|
-
|
|
223
|
-
sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
|
|
224
|
-
sector = result.intersection(sector_triangle)
|
|
225
|
-
|
|
226
|
-
if not sector.is_empty:
|
|
227
|
-
sectors.append(sector)
|
|
228
|
-
result = sectors
|
|
229
|
-
return result
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _polygons_to_linestring(geom):
|
|
233
|
-
# pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
|
|
234
|
-
from shapely import LineString, MultiLineString, MultiPolygon
|
|
235
|
-
|
|
236
|
-
def convert_polygon(polygon: Polygon):
|
|
237
|
-
lines = []
|
|
238
|
-
exterior = LineString(polygon.exterior.coords)
|
|
239
|
-
lines.append(exterior)
|
|
240
|
-
interior = [LineString(p.coords) for p in polygon.interiors]
|
|
241
|
-
lines = lines + interior
|
|
242
|
-
return lines
|
|
243
|
-
|
|
244
|
-
def convert_multipolygon(polygon: MultiPolygon):
|
|
245
|
-
return MultiLineString(sum([convert_polygon(p) for p in polygon.geoms], []))
|
|
246
|
-
|
|
247
|
-
if geom.geom_type == "Polygon":
|
|
248
|
-
return MultiLineString(convert_polygon(geom))
|
|
249
|
-
if geom.geom_type == "MultiPolygon":
|
|
250
|
-
return convert_multipolygon(geom)
|
|
251
|
-
return geom
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def _combine_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
255
|
-
"""
|
|
256
|
-
Combine geometry of intersecting layers into a single GeoDataFrame.
|
|
257
|
-
Parameters
|
|
258
|
-
----------
|
|
259
|
-
gdf: gpd.GeoDataFrame
|
|
260
|
-
A GeoPandas GeoDataFrame
|
|
261
|
-
|
|
262
|
-
Returns
|
|
263
|
-
-------
|
|
264
|
-
gpd.GeoDataFrame
|
|
265
|
-
The combined GeoDataFrame with aggregated in lists columns.
|
|
266
|
-
|
|
267
|
-
Examples
|
|
268
|
-
--------
|
|
269
|
-
>>> gdf = gpd.read_file('path_to_your_file.geojson')
|
|
270
|
-
>>> result = _combine_geometry(gdf)
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
crs = gdf.crs
|
|
274
|
-
polygons = polygonize(gdf["geometry"].apply(_polygons_to_linestring).unary_union)
|
|
275
|
-
enclosures = gpd.GeoSeries(list(polygons), crs=crs)
|
|
276
|
-
enclosures_points = gpd.GeoDataFrame(enclosures.representative_point(), columns=["geometry"], crs=crs)
|
|
277
|
-
joined = gpd.sjoin(enclosures_points, gdf, how="inner", predicate="within").reset_index()
|
|
278
|
-
cols = joined.columns.tolist()
|
|
279
|
-
cols.remove("geometry")
|
|
280
|
-
joined = joined.groupby("index").agg({column: list for column in cols})
|
|
281
|
-
joined["geometry"] = enclosures
|
|
282
|
-
joined = gpd.GeoDataFrame(joined, geometry="geometry", crs=crs)
|
|
283
|
-
return joined
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _min_max_normalization(data, new_min=0, new_max=1):
|
|
287
|
-
"""
|
|
288
|
-
Min-max normalization for a given array of data.
|
|
289
|
-
|
|
290
|
-
Parameters
|
|
291
|
-
----------
|
|
292
|
-
data: numpy.ndarray
|
|
293
|
-
Input data to be normalized.
|
|
294
|
-
new_min: float, optional
|
|
295
|
-
New minimum value for normalization. Defaults to 0.
|
|
296
|
-
new_max: float, optional
|
|
297
|
-
New maximum value for normalization. Defaults to 1.
|
|
298
|
-
|
|
299
|
-
Returns
|
|
300
|
-
-------
|
|
301
|
-
numpy.ndarray
|
|
302
|
-
Normalized data.
|
|
303
|
-
|
|
304
|
-
Examples
|
|
305
|
-
--------
|
|
306
|
-
>>> import numpy as np
|
|
307
|
-
>>> data = np.array([1, 2, 3, 4, 5])
|
|
308
|
-
>>> normalized_data = min_max_normalization(data, new_min=0, new_max=1)
|
|
309
|
-
"""
|
|
310
|
-
|
|
311
|
-
min_value = np.min(data)
|
|
312
|
-
max_value = np.max(data)
|
|
313
|
-
normalized_data = (data - min_value) / (max_value - min_value) * (new_max - new_min) + new_min
|
|
314
|
-
return normalized_data
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _process_group(group):
|
|
318
|
-
geom = group
|
|
319
|
-
combined_geometry = _combine_geometry(geom)
|
|
320
|
-
combined_geometry.drop(columns=["index", "index_right"], inplace=True)
|
|
321
|
-
combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
|
|
322
|
-
combined_geometry["new_ratio"] = combined_geometry.apply(
|
|
323
|
-
lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
threshold = combined_geometry["new_ratio"].quantile(0.25)
|
|
327
|
-
combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
|
|
328
|
-
|
|
329
|
-
combined_geometry["new_ratio_normalized"] = _min_max_normalization(
|
|
330
|
-
combined_geometry["new_ratio"].values, new_min=1, new_max=10
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
|
|
334
246
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.reset_index(drop=True)
|
|
339
|
-
)
|
|
340
|
-
result_union.set_geometry("geometry", inplace=True)
|
|
341
|
-
result_union.set_crs(geom.crs, inplace=True)
|
|
342
|
-
|
|
343
|
-
result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
|
|
344
|
-
|
|
345
|
-
representative_points = combined_geometry.copy()
|
|
346
|
-
representative_points["geometry"] = representative_points["geometry"].representative_point()
|
|
347
|
-
|
|
348
|
-
joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
|
|
349
|
-
joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
|
|
350
|
-
|
|
351
|
-
joined.set_geometry("geometry", inplace=True)
|
|
352
|
-
joined.set_crs(geom.crs, inplace=True)
|
|
353
|
-
return joined
|
|
247
|
+
if return_gdf:
|
|
248
|
+
circuit = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
|
|
249
|
+
return circuit
|
|
354
250
|
|
|
355
251
|
|
|
356
252
|
def get_visibilities_from_points(
|
|
@@ -385,23 +281,17 @@ def get_visibilities_from_points(
|
|
|
385
281
|
-----
|
|
386
282
|
This function uses `get_visibility_accurate()` in multiprocessing way.
|
|
387
283
|
|
|
388
|
-
Examples
|
|
389
|
-
--------
|
|
390
|
-
>>> import geopandas as gpd
|
|
391
|
-
>>> from shapely.geometry import Point, Polygon
|
|
392
|
-
>>> points = gpd.GeoDataFrame({'geometry': [Point(0, 0), Point(1, 1)]}, crs='epsg:4326')
|
|
393
|
-
>>> obstacles = gpd.GeoDataFrame({'geometry': [Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]}, crs='epsg:4326')
|
|
394
|
-
>>> view_distance = 100
|
|
395
|
-
|
|
396
|
-
>>> visibilities = get_visibilities_from_points(points, obstacles, view_distance)
|
|
397
|
-
>>> visibilities
|
|
398
284
|
"""
|
|
285
|
+
if points.crs != obstacles.crs:
|
|
286
|
+
raise ValueError(f"CRS mismatch, points crs:{points.crs} != obstacles crs:{obstacles.crs}")
|
|
287
|
+
if points.crs.is_geographic:
|
|
288
|
+
logger.warning("Points crs is geographic, it may produce invalid results")
|
|
399
289
|
# remove points inside polygons
|
|
400
290
|
joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
|
|
401
291
|
points = joined[joined.index_right.isnull()]
|
|
402
292
|
|
|
403
293
|
# remove unused obstacles
|
|
404
|
-
points_view = points.geometry.buffer(view_distance).
|
|
294
|
+
points_view = points.geometry.buffer(view_distance).union_all()
|
|
405
295
|
s = obstacles.intersects(points_view)
|
|
406
296
|
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
407
297
|
|
|
@@ -417,6 +307,7 @@ def get_visibilities_from_points(
|
|
|
417
307
|
"big amount of points",
|
|
418
308
|
max_workers=max_workers,
|
|
419
309
|
)
|
|
310
|
+
|
|
420
311
|
# could return sectorized visions if sectors_n is set
|
|
421
312
|
return all_visions
|
|
422
313
|
|
|
@@ -479,16 +370,18 @@ def calculate_visibility_catchment_area(
|
|
|
479
370
|
|
|
480
371
|
pandarallel.initialize(progress_bar=True, verbose=0)
|
|
481
372
|
|
|
482
|
-
|
|
483
|
-
|
|
373
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
374
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
375
|
+
points = points.to_crs(local_crs)
|
|
376
|
+
|
|
484
377
|
sectors_n = 12
|
|
485
378
|
logger.info("Calculating Visibility Catchment Area from each point")
|
|
486
379
|
all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
|
|
487
380
|
all_visions_sectorized = gpd.GeoDataFrame(
|
|
488
|
-
geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=
|
|
381
|
+
geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=local_crs
|
|
489
382
|
)
|
|
490
383
|
logger.info("Calculating non-vision part...")
|
|
491
|
-
all_visions_unary = all_visions_sectorized.
|
|
384
|
+
all_visions_unary = all_visions_sectorized.union_all()
|
|
492
385
|
convex = all_visions_unary.convex_hull
|
|
493
386
|
dif = convex.difference(all_visions_unary)
|
|
494
387
|
|
|
@@ -496,7 +389,7 @@ def calculate_visibility_catchment_area(
|
|
|
496
389
|
|
|
497
390
|
buf_area = (math.pi * view_distance**2) / sectors_n
|
|
498
391
|
all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
|
|
499
|
-
all_visions_sectorized["ratio"] =
|
|
392
|
+
all_visions_sectorized["ratio"] = min_max_normalization(
|
|
500
393
|
all_visions_sectorized["ratio"].values, new_min=1, new_max=10
|
|
501
394
|
)
|
|
502
395
|
groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
|
|
@@ -511,7 +404,7 @@ def calculate_visibility_catchment_area(
|
|
|
511
404
|
max_workers=max_workers,
|
|
512
405
|
)
|
|
513
406
|
logger.info("Calculating all groups intersection...")
|
|
514
|
-
all_in =
|
|
407
|
+
all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
|
|
515
408
|
|
|
516
409
|
del groups_result
|
|
517
410
|
|
|
@@ -523,7 +416,7 @@ def calculate_visibility_catchment_area(
|
|
|
523
416
|
all_in = all_in[all_in["factor"] > threshold]
|
|
524
417
|
|
|
525
418
|
all_in["factor_normalized"] = np.round(
|
|
526
|
-
|
|
419
|
+
min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
|
|
527
420
|
).astype(int)
|
|
528
421
|
logger.info("Calculating normalized groups geometry...")
|
|
529
422
|
all_in = all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
|
|
@@ -550,3 +443,69 @@ def calculate_visibility_catchment_area(
|
|
|
550
443
|
all_in = all_in.explode(index_parts=True)
|
|
551
444
|
logger.info("Done!")
|
|
552
445
|
return all_in
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _multiprocess_get_vis(args): # pragma: no cover
|
|
449
|
+
point, buildings, view_distance, sectors_n = args
|
|
450
|
+
result = get_visibility_accurate(point, buildings, view_distance)
|
|
451
|
+
|
|
452
|
+
if sectors_n is not None:
|
|
453
|
+
sectors = []
|
|
454
|
+
|
|
455
|
+
cx, cy = point.x, point.y
|
|
456
|
+
|
|
457
|
+
angle_increment = 2 * math.pi / sectors_n
|
|
458
|
+
view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
|
|
459
|
+
for i in range(sectors_n):
|
|
460
|
+
angle1 = i * angle_increment
|
|
461
|
+
angle2 = (i + 1) * angle_increment
|
|
462
|
+
|
|
463
|
+
x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
|
|
464
|
+
x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
|
|
465
|
+
|
|
466
|
+
sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
|
|
467
|
+
sector = result.intersection(sector_triangle)
|
|
468
|
+
|
|
469
|
+
if not sector.is_empty:
|
|
470
|
+
sectors.append(sector)
|
|
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
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ObjectNat
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Author: DDonnyy
|
|
7
|
+
Author-email: 63115678+DDonnyy@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.10,<3.13
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
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: networkx (>=3.4.2,<4.0.0)
|
|
16
|
+
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
17
|
+
Requires-Dist: pandarallel (>=1.6.5,<2.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/markdown
|
|
22
|
+
|
|
23
|
+
# ObjectNat
|
|
24
|
+
|
|
25
|
+
[](https://github.com/psf/black)
|
|
26
|
+
[](https://pypi.org/project/objectnat/)
|
|
27
|
+
[](https://github.com/DDonnyy/ObjecNat/actions/workflows/ci_pipeline.yml)
|
|
28
|
+
[](https://codecov.io/gh/DDonnyy/ObjectNat)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
|
|
31
|
+
- [РИДМИ (Russian)](README_ru.md)
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://github.com/user-attachments/assets/ed0f226e-1728-4659-9e21-b4d499e703cd" alt="logo" width="400">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
#### **ObjectNat** is an open-source library created for geospatial analysis created by **IDU team**
|
|
37
|
+
|
|
38
|
+
## Features and how to use
|
|
39
|
+
|
|
40
|
+
1. **[Isochrones and Transport Accessibility](./examples/isochrone_generator.ipynb)** — Isochrones represent areas reachable from a starting point within a given time limit along a transport network. This function enables analysis of transport accessibility using pedestrian, automobile, public transport graphs, or their combination.
|
|
41
|
+
|
|
42
|
+
The library offers multiple isochrone generation methods:
|
|
43
|
+
- **Baseline isochrones**: show a single area reachable within a specified time.
|
|
44
|
+
- **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="https://github.com/user-attachments/assets/b1787430-63e1-4907-9198-a6171d546599" alt="isochrone_ways_15_min" width="300">
|
|
48
|
+
<img src="https://github.com/user-attachments/assets/64fce6bf-6509-490c-928c-dbd8daf9f570" alt="isochrone_radius_15_min" width="300">
|
|
49
|
+
</p>
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="https://github.com/user-attachments/assets/ac9f8840-a867-4eb5-aec8-91a411d4e545" alt="stepped_isochrone_stepped_ways_15_min" width="300">
|
|
52
|
+
<img src="https://github.com/user-attachments/assets/b5429aa1-4625-44d1-982f-8bd4264148fb" alt="stepped_isochrone_stepped_radius_15_min" width="300">
|
|
53
|
+
<img src="https://github.com/user-attachments/assets/042c7362-70e1-45df-b2e1-02fc76bf638c" alt="stepped_isochrone_stepped_separate_15_min" width="300">
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
2. **[Coverage Zones](./examples/graph_coverage.ipynb)** — Function for generating **coverage zones** from a set of source points using a transport network. It calculates the area each point can reach based on **travel time** or **distance**, then builds polygons via **Voronoi diagrams** and clips them to a custom boundary if provided.
|
|
58
|
+
|
|
59
|
+
<p align="center">
|
|
60
|
+
<img src="https://github.com/user-attachments/assets/fa8057d7-77aa-48a2-aa10-ea3e292a918d" alt="coverage_zones_time_10min" width="350">
|
|
61
|
+
<img src="https://github.com/user-attachments/assets/44362dde-c3b0-4321-9a0a-aa547f0f2e04" alt="coverage_zones_distance_600m" width="350">
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
3. **[Service Provision Analysis](./examples/calculate_provision.ipynb)** — Function for evaluating the provision of residential buildings and their population with services (e.g., schools, clinics)
|
|
65
|
+
that have limited **capacity** and a defined **accessibility threshold** (in minutes or distance). The function models **demand-supply balance**, estimating how well services meet the needs of nearby buildings within the allowed time.
|
|
66
|
+
|
|
67
|
+
The library also supports:
|
|
68
|
+
- **Recalculation** of existing provision results using a new time threshold.
|
|
69
|
+
- **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<img src="https://github.com/user-attachments/assets/ff1ed08d-9a35-4035-9e1f-9a7fdae5b0e0" alt="service_provision_initial" width="300">
|
|
73
|
+
<img src="https://github.com/user-attachments/assets/a0c0a6b0-f83f-4982-bfb3-4a476b2153ea" alt="service_provision_recalculated" width="300">
|
|
74
|
+
<img src="https://github.com/user-attachments/assets/f57dc1c6-21a0-458d-85f4-fe1b17c77695" alt="service_provision_clipped" width="300">
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
4. **[Visibility Analysis](./examples/visibility_analysis.ipynb)** — Function for estimating visibility from a given point or multiple points to nearby buildings within a certain distance.
|
|
78
|
+
This can be used to assess visual accessibility in urban environments.
|
|
79
|
+
The library also includes a **catchment area calculator** for large-scale visibility analysis based on a dense grid of observer points (recommended: ~1000 points spaced 10–20 meters apart).
|
|
80
|
+
Points can be generated using a road network and distributed along edges.
|
|
81
|
+
|
|
82
|
+
The module includes:
|
|
83
|
+
- A **fast approximate method** for large datasets.
|
|
84
|
+
- A **accurate method** for detailed local analysis.
|
|
85
|
+
|
|
86
|
+
<p align="center">
|
|
87
|
+
<img src="https://github.com/user-attachments/assets/aa139d29-07d4-4560-b835-9646c8802fe1" alt="visibility_comparison_methods" height="250">
|
|
88
|
+
<img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
5. **[Noise Simulation](./examples/noise_simulation.ipynb)** — Simulates noise propagation from a set of source points, taking into account **obstacles**, **vegetation**, and **environmental factors**.
|
|
92
|
+
|
|
93
|
+
🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
|
|
94
|
+
|
|
95
|
+
<p align="center">
|
|
96
|
+
<img src="https://github.com/user-attachments/assets/b3a41962-6220-49c4-90d4-2e756f9706cf" alt="noise_simulation_test_result" width="400">
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
6. **[Point Clusterization](./examples/point_clusterization.ipynb)** — Function to generate **cluster polygons** from a set of input points based on:
|
|
101
|
+
- Minimum **distance** between points.
|
|
102
|
+
- Minimum **number of points** per cluster.
|
|
103
|
+
|
|
104
|
+
Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
|
|
105
|
+
|
|
106
|
+
<p align="center">
|
|
107
|
+
<img src="https://github.com/user-attachments/assets/f86aac61-497a-4330-b4cf-68f4fc47fd34" alt="building_clusters" width="400">
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
## City graphs
|
|
111
|
+
|
|
112
|
+
To ensure optimal performance of ObjectNat's geospatial analysis functions, it's recommended to utilize urban graphs sourced from the [IduEdu](https://github.com/DDonnyy/IduEdu) library.
|
|
113
|
+
**IduEdu** is an open-source Python library designed for the creation and manipulation of complex city networks derived from OpenStreetMap data.
|
|
114
|
+
|
|
115
|
+
**IduEdu** can be installed with ``pip``:
|
|
116
|
+
```
|
|
117
|
+
pip install IduEdu
|
|
118
|
+
```
|
|
119
|
+
## Installation
|
|
120
|
+
|
|
121
|
+
**ObjectNat** can be installed with ``pip``:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
pip install ObjectNat
|
|
125
|
+
```
|
|
126
|
+
### Configuration changes
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from objectnat import config
|
|
130
|
+
|
|
131
|
+
config.change_logger_lvl('INFO') # To mute all debug msgs
|
|
132
|
+
config.set_enable_tqdm(False) # To mute all tqdm's progress bars
|
|
133
|
+
```
|
|
134
|
+
## Contacts
|
|
135
|
+
|
|
136
|
+
- [NCCR](https://actcognitive.org/) - National Center for Cognitive Research
|
|
137
|
+
- [IDU](https://idu.itmo.ru/) - Institute of Design and Urban Studies
|
|
138
|
+
- [Natalya Chichkova](https://t.me/nancy_nat) - project manager
|
|
139
|
+
- [Danila Oleynikov (Donny)](https://t.me/ddonny_dd) - lead software engineer
|
|
140
|
+
|
|
141
|
+
## Publications
|
|
142
|
+
|