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.

Files changed (36) hide show
  1. objectnat/_api.py +6 -8
  2. objectnat/_config.py +0 -24
  3. objectnat/_version.py +1 -1
  4. objectnat/methods/coverage_zones/__init__.py +2 -0
  5. objectnat/methods/coverage_zones/graph_coverage.py +118 -0
  6. objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
  7. objectnat/methods/isochrones/__init__.py +1 -0
  8. objectnat/methods/isochrones/isochrone_utils.py +130 -0
  9. objectnat/methods/isochrones/isochrones.py +325 -0
  10. objectnat/methods/noise/__init__.py +3 -0
  11. objectnat/methods/noise/noise_exceptions.py +14 -0
  12. objectnat/methods/noise/noise_init_data.py +10 -0
  13. objectnat/methods/noise/noise_reduce.py +155 -0
  14. objectnat/methods/noise/noise_sim.py +423 -0
  15. objectnat/methods/point_clustering/__init__.py +1 -0
  16. objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
  17. objectnat/methods/provision/__init__.py +1 -0
  18. objectnat/methods/provision/provision.py +10 -7
  19. objectnat/methods/provision/provision_exceptions.py +4 -4
  20. objectnat/methods/provision/provision_model.py +21 -20
  21. objectnat/methods/utils/__init__.py +0 -0
  22. objectnat/methods/utils/geom_utils.py +130 -0
  23. objectnat/methods/utils/graph_utils.py +127 -0
  24. objectnat/methods/utils/math_utils.py +32 -0
  25. objectnat/methods/visibility/__init__.py +6 -0
  26. objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +222 -243
  27. objectnat-1.0.0.dist-info/METADATA +143 -0
  28. objectnat-1.0.0.dist-info/RECORD +32 -0
  29. objectnat/methods/balanced_buildings.py +0 -69
  30. objectnat/methods/coverage_zones.py +0 -90
  31. objectnat/methods/isochrones.py +0 -143
  32. objectnat/methods/living_buildings_osm.py +0 -168
  33. objectnat-0.2.6.dist-info/METADATA +0 -113
  34. objectnat-0.2.6.dist-info/RECORD +0 -19
  35. {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/LICENSE.txt +0 -0
  36. {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 polygonize, unary_union
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(point_from: Point, obstacles: gpd.GeoDataFrame, view_distance) -> Polygon:
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 `get_visibility_result()` function instead.
38
- However, please note that `get_visibility_result()` may provide less accurate results.
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
- >>> point_from = Point(1, 1)
43
- >>> buildings = gpd.read_file('buildings.shp')
44
- >>> view_distance = 1000
45
- >>> visibility = get_visibility_accurate(point_from, obstacles, view_distance)
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 get_point_from_a_thorough_b(a: Point, b: Point, dist):
49
- """
50
- Func to get Point from point a thorough point b on dist
51
- """
52
- direction = math.atan2(b.y - a.y, b.x - a.x)
53
- c_x = a.x + dist * math.cos(direction)
54
- c_y = a.y + dist * math.sin(direction)
55
- return Point(c_x, c_y)
56
-
57
- def polygon_to_linestring(geometry: Polygon):
58
- """A function to return all segments of a polygon as a list of linestrings"""
59
- coords_ext = geometry.exterior.coords # Create a list of all line node coordinates
60
- polygons_inter = [Polygon(x) for x in geometry.interiors]
61
- result = [LineString(part) for part in zip(coords_ext, coords_ext[1:])]
62
- for poly in polygons_inter:
63
- poly_coords = poly.exterior.coords
64
- result.extend([LineString(part) for part in zip(poly_coords, poly_coords[1:])])
65
- return result
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
- buildings_in_buffer = obstacles.loc[s[s].index]
70
- # TODO kick all geoms except Polygons/MultiPolygons
71
- buildings_in_buffer = buildings_in_buffer.geometry.apply(
72
- lambda x: list(x.geoms) if isinstance(x, MultiPolygon) else x
73
- ).explode()
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
- drop=True
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 > math.pi:
104
- delta_angle = 2 * math.pi - delta_angle
105
- a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
106
- p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], a)
107
- p2 = get_point_from_a_thorough_b(point_from, points_with_angle[1][0], a)
108
- polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
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
- res = point_buffer.difference(unary_union(polygons))
118
- if isinstance(res, Polygon):
119
- return res
120
- res = list(res.geoms)
121
- polygon_containing_point = None
122
- for polygon in res:
123
- if polygon.contains(point_from):
124
- polygon_containing_point = polygon
125
- break
126
- return polygon_containing_point
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
- def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32) -> Polygon:
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
- point : Point
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 estimated area of visibility from the given point.
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
- >>> point = Point(1, 1)
157
- >>> buildings = gpd.read_file('buildings.shp')
158
- >>> view_distance = 1000
159
- >>> visibility = get_visibility(point, obstacles, view_distance)
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
- point_buffer = point.buffer(view_distance, resolution=resolution)
163
- s = obstacles.within(point_buffer)
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([point, ext]) for ext in buffer_exterior_]
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.unary_union
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
- Parameters
271
- ----------
272
- data: numpy.ndarray
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).unary_union
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
- assert points.crs == obstacles.crs
463
- crs = obstacles.crs
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=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.unary_union
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"] = _min_max_normalization(
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 = _combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=crs))
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
- _min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
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