ObjectNat 0.2.6__py3-none-any.whl → 1.0.0__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 +6 -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 +3 -0
- objectnat/methods/noise/noise_exceptions.py +14 -0
- objectnat/methods/noise/noise_init_data.py +10 -0
- objectnat/methods/noise/noise_reduce.py +155 -0
- objectnat/methods/noise/noise_sim.py +423 -0
- 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 +10 -7
- objectnat/methods/provision/provision_exceptions.py +4 -4
- objectnat/methods/provision/provision_model.py +21 -20
- objectnat/methods/utils/__init__.py +0 -0
- objectnat/methods/utils/geom_utils.py +130 -0
- 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} +222 -243
- objectnat-1.0.0.dist-info/METADATA +143 -0
- objectnat-1.0.0.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.6.dist-info/METADATA +0 -113
- objectnat-0.2.6.dist-info/RECORD +0 -19
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/LICENSE.txt +0 -0
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/WHEEL +0 -0
|
@@ -6,73 +6,102 @@ 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
|
+
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
|
|
13
21
|
|
|
14
22
|
logger = config.logger
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
def get_visibility_accurate(
|
|
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]:
|
|
18
28
|
"""
|
|
19
29
|
Function to get accurate visibility from a given point to buildings within a given distance.
|
|
20
30
|
|
|
21
31
|
Parameters
|
|
22
32
|
----------
|
|
23
|
-
point_from : Point
|
|
24
|
-
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
|
|
25
36
|
obstacles : gpd.GeoDataFrame
|
|
26
37
|
A GeoDataFrame containing the geometry of the obstacles.
|
|
27
38
|
view_distance : float
|
|
28
39
|
The distance of view from the point.
|
|
40
|
+
return_max_view_dist
|
|
41
|
+
If True, the max view distance is returned with view polygon in tuple.
|
|
29
42
|
|
|
30
43
|
Returns
|
|
31
44
|
-------
|
|
32
|
-
Polygon
|
|
33
|
-
A polygon representing the area of visibility from the given point.
|
|
45
|
+
Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]
|
|
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.
|
|
34
48
|
|
|
35
49
|
Notes
|
|
36
50
|
-----
|
|
37
|
-
If a quick result is important, consider using the `
|
|
38
|
-
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.
|
|
39
53
|
|
|
40
54
|
Examples
|
|
41
55
|
--------
|
|
42
|
-
>>>
|
|
43
|
-
>>>
|
|
44
|
-
>>>
|
|
45
|
-
>>>
|
|
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)
|
|
46
60
|
"""
|
|
47
61
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
def find_furthest_point(point_from, view_polygon):
|
|
63
|
+
try:
|
|
64
|
+
res = round(max(Point(coords).distance(point_from) for coords in view_polygon.exterior.coords), 1)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(view_polygon)
|
|
67
|
+
raise e
|
|
68
|
+
return res
|
|
69
|
+
|
|
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
|
+
|
|
91
|
+
obstacles.reset_index(inplace=True, drop=True)
|
|
66
92
|
|
|
67
93
|
point_buffer = point_from.buffer(view_distance, resolution=32)
|
|
94
|
+
allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
|
|
95
|
+
obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
|
|
68
96
|
s = obstacles.intersects(point_buffer)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
obstacles_in_buffer = obstacles.loc[s[s].index].geometry
|
|
98
|
+
|
|
99
|
+
buildings_lines_in_buffer = gpd.GeoSeries(
|
|
100
|
+
pd.Series(
|
|
101
|
+
obstacles_in_buffer.apply(polygons_to_multilinestring).explode(index_parts=False).apply(explode_linestring)
|
|
102
|
+
).explode()
|
|
103
|
+
)
|
|
74
104
|
|
|
75
|
-
buildings_lines_in_buffer = gpd.GeoSeries(buildings_in_buffer.apply(polygon_to_linestring).explode())
|
|
76
105
|
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[buildings_lines_in_buffer.intersects(point_buffer)]
|
|
77
106
|
|
|
78
107
|
buildings_in_buffer_points = gpd.GeoSeries(
|
|
@@ -82,15 +111,12 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
|
|
|
82
111
|
|
|
83
112
|
max_dist = max(view_distance, buildings_in_buffer_points.distance(point_from).max())
|
|
84
113
|
polygons = []
|
|
85
|
-
buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index(
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
iteration = 0
|
|
114
|
+
buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index()
|
|
115
|
+
logger.debug("Calculation vis polygon")
|
|
89
116
|
while not buildings_lines_in_buffer.empty:
|
|
90
|
-
iteration += 1
|
|
91
117
|
gdf_sindex = buildings_lines_in_buffer.sindex
|
|
92
118
|
# TODO check if 2 walls are nearest and use the widest angle between points
|
|
93
|
-
nearest_wall_sind = gdf_sindex.nearest(point_from, return_all=False)
|
|
119
|
+
nearest_wall_sind = gdf_sindex.nearest(point_from, return_all=False, max_distance=max_dist)
|
|
94
120
|
nearest_wall = buildings_lines_in_buffer.loc[nearest_wall_sind[1]].iloc[0]
|
|
95
121
|
wall_points = [Point(coords) for coords in nearest_wall.geometry.coords]
|
|
96
122
|
|
|
@@ -98,42 +124,62 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
|
|
|
98
124
|
points_with_angle = sorted(
|
|
99
125
|
[(pt, math.atan2(pt.y - point_from.y, pt.x - point_from.x)) for pt in wall_points], key=lambda x: x[1]
|
|
100
126
|
)
|
|
101
|
-
|
|
102
127
|
delta_angle = 2 * math.pi + points_with_angle[0][1] - points_with_angle[-1][1]
|
|
103
|
-
if delta_angle
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
if round(delta_angle, 10) == round(math.pi, 10):
|
|
129
|
+
wall_b_centroid = obstacles_in_buffer.loc[nearest_wall["index"]].centroid
|
|
130
|
+
p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], max_dist)
|
|
131
|
+
p2 = get_point_from_a_thorough_b(point_from, points_with_angle[1][0], max_dist)
|
|
132
|
+
polygon = LineString([p1, p2])
|
|
133
|
+
polygon = polygon.buffer(
|
|
134
|
+
distance=max_dist * point_side_of_line(polygon, wall_b_centroid), single_sided=True
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
if delta_angle > math.pi:
|
|
138
|
+
delta_angle = 2 * math.pi - delta_angle
|
|
139
|
+
a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
|
|
140
|
+
p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], a)
|
|
141
|
+
p2 = get_point_from_a_thorough_b(point_from, points_with_angle[-1][0], a)
|
|
142
|
+
polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
|
|
109
143
|
|
|
110
144
|
polygons.append(polygon)
|
|
111
|
-
|
|
112
145
|
buildings_lines_in_buffer.drop(nearest_wall_sind[1], inplace=True)
|
|
113
146
|
|
|
147
|
+
if not polygon.is_valid or polygon.area < 1:
|
|
148
|
+
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
149
|
+
continue
|
|
150
|
+
|
|
114
151
|
lines_to_kick = buildings_lines_in_buffer.within(polygon)
|
|
115
152
|
buildings_lines_in_buffer = buildings_lines_in_buffer.loc[~lines_to_kick]
|
|
116
153
|
buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
logger.debug("Done calculating!")
|
|
155
|
+
res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
|
|
156
|
+
|
|
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)
|
|
127
166
|
|
|
167
|
+
if return_max_view_dist:
|
|
168
|
+
return res, find_furthest_point(point_from, res)
|
|
169
|
+
return res
|
|
128
170
|
|
|
129
|
-
|
|
171
|
+
|
|
172
|
+
def get_visibility(
|
|
173
|
+
point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
|
|
174
|
+
) -> Polygon | gpd.GeoDataFrame:
|
|
130
175
|
"""
|
|
131
176
|
Function to get a quick estimate of visibility from a given point to buildings within a given distance.
|
|
132
177
|
|
|
133
178
|
Parameters
|
|
134
179
|
----------
|
|
135
|
-
|
|
136
|
-
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
|
|
137
183
|
obstacles : gpd.GeoDataFrame
|
|
138
184
|
A GeoDataFrame containing the geometry of the buildings.
|
|
139
185
|
view_distance : float
|
|
@@ -143,8 +189,9 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
143
189
|
|
|
144
190
|
Returns
|
|
145
191
|
-------
|
|
146
|
-
Polygon
|
|
147
|
-
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.
|
|
148
195
|
|
|
149
196
|
Notes
|
|
150
197
|
-----
|
|
@@ -153,19 +200,36 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
153
200
|
|
|
154
201
|
Examples
|
|
155
202
|
--------
|
|
156
|
-
>>>
|
|
157
|
-
>>>
|
|
158
|
-
>>>
|
|
159
|
-
>>>
|
|
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)
|
|
160
207
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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)
|
|
164
228
|
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
165
229
|
buffer_exterior_ = list(point_buffer.exterior.coords)
|
|
166
|
-
line_geometry = [LineString([
|
|
230
|
+
line_geometry = [LineString([point_from, ext]) for ext in buffer_exterior_]
|
|
167
231
|
buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
|
|
168
|
-
united_buildings = buildings_in_buffer.
|
|
232
|
+
united_buildings = buildings_in_buffer.union_all()
|
|
169
233
|
if united_buildings:
|
|
170
234
|
splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
|
|
171
235
|
else:
|
|
@@ -179,158 +243,10 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
|
|
|
179
243
|
circuit = Polygon(splited_lines_list)
|
|
180
244
|
if united_buildings:
|
|
181
245
|
circuit = circuit.difference(united_buildings)
|
|
182
|
-
return circuit
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _multiprocess_get_vis(args):
|
|
186
|
-
point, buildings, view_distance, sectors_n = args
|
|
187
|
-
result = get_visibility_accurate(point, buildings, view_distance)
|
|
188
|
-
|
|
189
|
-
if sectors_n is not None:
|
|
190
|
-
sectors = []
|
|
191
|
-
|
|
192
|
-
cx, cy = point.x, point.y
|
|
193
|
-
|
|
194
|
-
angle_increment = 2 * math.pi / sectors_n
|
|
195
|
-
view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
|
|
196
|
-
for i in range(sectors_n):
|
|
197
|
-
angle1 = i * angle_increment
|
|
198
|
-
angle2 = (i + 1) * angle_increment
|
|
199
|
-
|
|
200
|
-
x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
|
|
201
|
-
x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
|
|
202
|
-
|
|
203
|
-
sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
|
|
204
|
-
sector = result.intersection(sector_triangle)
|
|
205
|
-
|
|
206
|
-
if not sector.is_empty:
|
|
207
|
-
sectors.append(sector)
|
|
208
|
-
result = sectors
|
|
209
|
-
return result
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _polygons_to_linestring(geom):
|
|
213
|
-
# pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
|
|
214
|
-
from shapely import LineString, MultiLineString, MultiPolygon
|
|
215
|
-
|
|
216
|
-
def convert_polygon(polygon: Polygon):
|
|
217
|
-
lines = []
|
|
218
|
-
exterior = LineString(polygon.exterior.coords)
|
|
219
|
-
lines.append(exterior)
|
|
220
|
-
interior = [LineString(p.coords) for p in polygon.interiors]
|
|
221
|
-
lines = lines + interior
|
|
222
|
-
return lines
|
|
223
|
-
|
|
224
|
-
def convert_multipolygon(polygon: MultiPolygon):
|
|
225
|
-
return MultiLineString(sum([convert_polygon(p) for p in polygon.geoms], []))
|
|
226
|
-
|
|
227
|
-
if geom.geom_type == "Polygon":
|
|
228
|
-
return MultiLineString(convert_polygon(geom))
|
|
229
|
-
if geom.geom_type == "MultiPolygon":
|
|
230
|
-
return convert_multipolygon(geom)
|
|
231
|
-
return geom
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _combine_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
235
|
-
"""
|
|
236
|
-
Combine geometry of intersecting layers into a single GeoDataFrame.
|
|
237
|
-
Parameters
|
|
238
|
-
----------
|
|
239
|
-
gdf: gpd.GeoDataFrame
|
|
240
|
-
A GeoPandas GeoDataFrame
|
|
241
|
-
|
|
242
|
-
Returns
|
|
243
|
-
-------
|
|
244
|
-
gpd.GeoDataFrame
|
|
245
|
-
The combined GeoDataFrame with aggregated in lists columns.
|
|
246
|
-
|
|
247
|
-
Examples
|
|
248
|
-
--------
|
|
249
|
-
>>> gdf = gpd.read_file('path_to_your_file.geojson')
|
|
250
|
-
>>> result = _combine_geometry(gdf)
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
crs = gdf.crs
|
|
254
|
-
polygons = polygonize(gdf["geometry"].apply(_polygons_to_linestring).unary_union)
|
|
255
|
-
enclosures = gpd.GeoSeries(list(polygons), crs=crs)
|
|
256
|
-
enclosures_points = gpd.GeoDataFrame(enclosures.representative_point(), columns=["geometry"], crs=crs)
|
|
257
|
-
joined = gpd.sjoin(enclosures_points, gdf, how="inner", predicate="within").reset_index()
|
|
258
|
-
cols = joined.columns.tolist()
|
|
259
|
-
cols.remove("geometry")
|
|
260
|
-
joined = joined.groupby("index").agg({column: list for column in cols})
|
|
261
|
-
joined["geometry"] = enclosures
|
|
262
|
-
joined = gpd.GeoDataFrame(joined, geometry="geometry", crs=crs)
|
|
263
|
-
return joined
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _min_max_normalization(data, new_min=0, new_max=1):
|
|
267
|
-
"""
|
|
268
|
-
Min-max normalization for a given array of data.
|
|
269
246
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Input data to be normalized.
|
|
274
|
-
new_min: float, optional
|
|
275
|
-
New minimum value for normalization. Defaults to 0.
|
|
276
|
-
new_max: float, optional
|
|
277
|
-
New maximum value for normalization. Defaults to 1.
|
|
278
|
-
|
|
279
|
-
Returns
|
|
280
|
-
-------
|
|
281
|
-
numpy.ndarray
|
|
282
|
-
Normalized data.
|
|
283
|
-
|
|
284
|
-
Examples
|
|
285
|
-
--------
|
|
286
|
-
>>> import numpy as np
|
|
287
|
-
>>> data = np.array([1, 2, 3, 4, 5])
|
|
288
|
-
>>> normalized_data = min_max_normalization(data, new_min=0, new_max=1)
|
|
289
|
-
"""
|
|
290
|
-
|
|
291
|
-
min_value = np.min(data)
|
|
292
|
-
max_value = np.max(data)
|
|
293
|
-
normalized_data = (data - min_value) / (max_value - min_value) * (new_max - new_min) + new_min
|
|
294
|
-
return normalized_data
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def _process_group(group):
|
|
298
|
-
geom = group
|
|
299
|
-
combined_geometry = _combine_geometry(geom)
|
|
300
|
-
combined_geometry.drop(columns=["index", "index_right"], inplace=True)
|
|
301
|
-
combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
|
|
302
|
-
combined_geometry["new_ratio"] = combined_geometry.apply(
|
|
303
|
-
lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
threshold = combined_geometry["new_ratio"].quantile(0.25)
|
|
307
|
-
combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
|
|
308
|
-
|
|
309
|
-
combined_geometry["new_ratio_normalized"] = _min_max_normalization(
|
|
310
|
-
combined_geometry["new_ratio"].values, new_min=1, new_max=10
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
|
|
314
|
-
|
|
315
|
-
result_union = (
|
|
316
|
-
combined_geometry.groupby("new_ratio_normalized")
|
|
317
|
-
.agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
|
|
318
|
-
.reset_index(drop=True)
|
|
319
|
-
)
|
|
320
|
-
result_union.set_geometry("geometry", inplace=True)
|
|
321
|
-
result_union.set_crs(geom.crs, inplace=True)
|
|
322
|
-
|
|
323
|
-
result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
|
|
324
|
-
|
|
325
|
-
representative_points = combined_geometry.copy()
|
|
326
|
-
representative_points["geometry"] = representative_points["geometry"].representative_point()
|
|
327
|
-
|
|
328
|
-
joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
|
|
329
|
-
joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
|
|
330
|
-
|
|
331
|
-
joined.set_geometry("geometry", inplace=True)
|
|
332
|
-
joined.set_crs(geom.crs, inplace=True)
|
|
333
|
-
return joined
|
|
247
|
+
if return_gdf:
|
|
248
|
+
circuit = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
|
|
249
|
+
return circuit
|
|
334
250
|
|
|
335
251
|
|
|
336
252
|
def get_visibilities_from_points(
|
|
@@ -365,23 +281,17 @@ def get_visibilities_from_points(
|
|
|
365
281
|
-----
|
|
366
282
|
This function uses `get_visibility_accurate()` in multiprocessing way.
|
|
367
283
|
|
|
368
|
-
Examples
|
|
369
|
-
--------
|
|
370
|
-
>>> import geopandas as gpd
|
|
371
|
-
>>> from shapely.geometry import Point, Polygon
|
|
372
|
-
>>> points = gpd.GeoDataFrame({'geometry': [Point(0, 0), Point(1, 1)]}, crs='epsg:4326')
|
|
373
|
-
>>> obstacles = gpd.GeoDataFrame({'geometry': [Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]}, crs='epsg:4326')
|
|
374
|
-
>>> view_distance = 100
|
|
375
|
-
|
|
376
|
-
>>> visibilities = get_visibilities_from_points(points, obstacles, view_distance)
|
|
377
|
-
>>> visibilities
|
|
378
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")
|
|
379
289
|
# remove points inside polygons
|
|
380
290
|
joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
|
|
381
291
|
points = joined[joined.index_right.isnull()]
|
|
382
292
|
|
|
383
293
|
# remove unused obstacles
|
|
384
|
-
points_view = points.geometry.buffer(view_distance).
|
|
294
|
+
points_view = points.geometry.buffer(view_distance).union_all()
|
|
385
295
|
s = obstacles.intersects(points_view)
|
|
386
296
|
buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
|
|
387
297
|
|
|
@@ -397,6 +307,7 @@ def get_visibilities_from_points(
|
|
|
397
307
|
"big amount of points",
|
|
398
308
|
max_workers=max_workers,
|
|
399
309
|
)
|
|
310
|
+
|
|
400
311
|
# could return sectorized visions if sectors_n is set
|
|
401
312
|
return all_visions
|
|
402
313
|
|
|
@@ -459,16 +370,18 @@ def calculate_visibility_catchment_area(
|
|
|
459
370
|
|
|
460
371
|
pandarallel.initialize(progress_bar=True, verbose=0)
|
|
461
372
|
|
|
462
|
-
|
|
463
|
-
|
|
373
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
374
|
+
obstacles = obstacles.to_crs(local_crs)
|
|
375
|
+
points = points.to_crs(local_crs)
|
|
376
|
+
|
|
464
377
|
sectors_n = 12
|
|
465
378
|
logger.info("Calculating Visibility Catchment Area from each point")
|
|
466
379
|
all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
|
|
467
380
|
all_visions_sectorized = gpd.GeoDataFrame(
|
|
468
|
-
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
|
|
469
382
|
)
|
|
470
383
|
logger.info("Calculating non-vision part...")
|
|
471
|
-
all_visions_unary = all_visions_sectorized.
|
|
384
|
+
all_visions_unary = all_visions_sectorized.union_all()
|
|
472
385
|
convex = all_visions_unary.convex_hull
|
|
473
386
|
dif = convex.difference(all_visions_unary)
|
|
474
387
|
|
|
@@ -476,7 +389,7 @@ def calculate_visibility_catchment_area(
|
|
|
476
389
|
|
|
477
390
|
buf_area = (math.pi * view_distance**2) / sectors_n
|
|
478
391
|
all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
|
|
479
|
-
all_visions_sectorized["ratio"] =
|
|
392
|
+
all_visions_sectorized["ratio"] = min_max_normalization(
|
|
480
393
|
all_visions_sectorized["ratio"].values, new_min=1, new_max=10
|
|
481
394
|
)
|
|
482
395
|
groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
|
|
@@ -491,7 +404,7 @@ def calculate_visibility_catchment_area(
|
|
|
491
404
|
max_workers=max_workers,
|
|
492
405
|
)
|
|
493
406
|
logger.info("Calculating all groups intersection...")
|
|
494
|
-
all_in =
|
|
407
|
+
all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
|
|
495
408
|
|
|
496
409
|
del groups_result
|
|
497
410
|
|
|
@@ -503,7 +416,7 @@ def calculate_visibility_catchment_area(
|
|
|
503
416
|
all_in = all_in[all_in["factor"] > threshold]
|
|
504
417
|
|
|
505
418
|
all_in["factor_normalized"] = np.round(
|
|
506
|
-
|
|
419
|
+
min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
|
|
507
420
|
).astype(int)
|
|
508
421
|
logger.info("Calculating normalized groups geometry...")
|
|
509
422
|
all_in = all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
|
|
@@ -530,3 +443,69 @@ def calculate_visibility_catchment_area(
|
|
|
530
443
|
all_in = all_in.explode(index_parts=True)
|
|
531
444
|
logger.info("Done!")
|
|
532
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
|