ObjectNat 0.2.5__py3-none-any.whl → 0.2.7__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.

@@ -4,8 +4,12 @@ import geopandas as gpd
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
 
7
+ from objectnat import config
8
+
7
9
  from .provision_model import Provision
8
10
 
11
+ logger = config.logger
12
+
9
13
 
10
14
  def get_service_provision(
11
15
  buildings: gpd.GeoDataFrame,
@@ -39,7 +43,7 @@ def get_service_provision(
39
43
  demanded_buildings=buildings,
40
44
  adjacency_matrix=adjacency_matrix,
41
45
  threshold=threshold,
42
- ).get_provisions()
46
+ ).run()
43
47
  return provision_buildings, provision_services, provision_links
44
48
 
45
49
 
@@ -47,9 +51,12 @@ def clip_provision(
47
51
  buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, selection_zone: gpd.GeoDataFrame
48
52
  ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
49
53
 
50
- assert (
51
- selection_zone.crs == buildings.crs == services.crs == links.crs
52
- ), f"CRS mismatch: buildings_crs:{buildings.crs}, links_crs:{links.crs} , services_crs:{services.crs}, selection_zone_crs:{selection_zone.crs}"
54
+ assert selection_zone.crs == buildings.crs == services.crs == links.crs, (
55
+ f"CRS mismatch: buildings_crs:{buildings.crs}, "
56
+ f"links_crs:{links.crs} , "
57
+ f"services_crs:{services.crs}, "
58
+ f"selection_zone_crs:{selection_zone.crs}"
59
+ )
53
60
  buildings = buildings.copy()
54
61
  links = links.copy()
55
62
  services = services.copy()
@@ -69,12 +76,12 @@ def recalculate_links(
69
76
  services = services.copy()
70
77
  links = links.copy()
71
78
 
72
- max_dist = links["distance"].max()
73
- assert new_max_dist <= max_dist, "New distance exceeds max links distance"
74
-
75
79
  links_to_recalculate = links[links["distance"] > new_max_dist]
76
- links_to_keep = links[links["distance"] <= new_max_dist]
80
+ if len(links_to_recalculate) == 0:
81
+ logger.warning("To clip distance exceeds max links distance, returning full provision")
82
+ return buildings, services, links
77
83
 
84
+ links_to_keep = links[links["distance"] <= new_max_dist]
78
85
  free_demand = links_to_recalculate.groupby("building_index").agg({"demand": list, "distance": list})
79
86
  free_demand["distance"] = free_demand.apply(
80
87
  lambda x: sum((x1 * x2) for x1, x2 in zip(x.demand, x.distance)), axis=1
@@ -7,7 +7,7 @@ class CapacityKeyError(KeyError):
7
7
 
8
8
  def __str__(self):
9
9
  if self.message:
10
- return "CapacityKeyError, {0} ".format(self.message)
10
+ return f"CapacityKeyError, {self.message} "
11
11
 
12
12
  return (
13
13
  "Column 'capacity' was not found in provided 'services' GeoDataFrame. This attribute "
@@ -24,7 +24,7 @@ class CapacityValueError(ValueError):
24
24
 
25
25
  def __str__(self):
26
26
  if self.message:
27
- return "CapacityValueError, {0} ".format(self.message)
27
+ return f"CapacityValueError, {self.message} "
28
28
 
29
29
  return "Column 'capacity' in 'services' GeoDataFrame has no valid value."
30
30
 
@@ -38,7 +38,7 @@ class DemandKeyError(KeyError):
38
38
 
39
39
  def __str__(self):
40
40
  if self.message:
41
- return "DemandKeyError, {0} ".format(self.message)
41
+ return f"DemandKeyError, {self.message} "
42
42
 
43
43
  return (
44
44
  "The column 'demand' was not found in the provided 'demanded_buildings' GeoDataFrame. "
@@ -55,5 +55,5 @@ class DemandValueError(ValueError):
55
55
 
56
56
  def __str__(self):
57
57
  if self.message:
58
- return "DemandValueError, {0} ".format(self.message)
58
+ return f"DemandValueError, {self.message} "
59
59
  return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."
@@ -19,9 +19,9 @@ class Provision:
19
19
  Represents the logic for city provision calculations using a gravity or linear model.
20
20
 
21
21
  Args:
22
- services (InstanceOf[gpd.GeoDataFrame]): GeoDataFrame representing the services available in the city.
23
- demanded_buildings (InstanceOf[gpd.GeoDataFrame]): GeoDataFrame representing the buildings with demands for services.
24
- adjacency_matrix (InstanceOf[pd.DataFrame]): DataFrame representing the adjacency matrix between buildings.
22
+ services (gpd.GeoDataFrame): GeoDataFrame representing the services available in the city.
23
+ demanded_buildings (gpd.GeoDataFrame): GeoDataFrame representing the buildings with demands for services.
24
+ adjacency_matrix (pd.DataFrame): DataFrame representing the adjacency matrix between buildings.
25
25
  threshold (int): Threshold value for the provision calculations.
26
26
  calculation_type (str, optional): Type of calculation ("gravity" or "linear"). Defaults to "gravity".
27
27
 
@@ -33,7 +33,7 @@ class Provision:
33
33
  column in 'services' or 'demand' column 'demanded_buildings' GeoDataFrame has no valid value.
34
34
  """
35
35
 
36
- _destination_matrix = None
36
+ destination_matrix = None
37
37
 
38
38
  def __init__(
39
39
  self,
@@ -42,12 +42,13 @@ class Provision:
42
42
  adjacency_matrix: pd.DataFrame,
43
43
  threshold: int,
44
44
  ):
45
- self.services = self.ensure_services(services)
46
- self.demanded_buildings = self.ensure_buildings(demanded_buildings)
47
- self.adjacency_matrix = self.delete_useless_matrix_rows_columns(adjacency_matrix, demanded_buildings, services)
45
+ self.services = self.ensure_services(services.copy())
46
+ self.demanded_buildings = self.ensure_buildings(demanded_buildings.copy())
47
+ self.adjacency_matrix = self.delete_useless_matrix_rows_columns(
48
+ adjacency_matrix.copy(), demanded_buildings, services
49
+ ).copy()
48
50
  self.threshold = threshold
49
51
  self.check_crs(self.demanded_buildings, self.services)
50
- print(config.pandarallel_use_file_system)
51
52
  pandarallel.initialize(progress_bar=False, verbose=0, use_memory_fs=config.pandarallel_use_file_system)
52
53
 
53
54
  @staticmethod
@@ -66,9 +67,11 @@ class Provision:
66
67
 
67
68
  @staticmethod
68
69
  def check_crs(demanded_buildings, services):
69
- assert (
70
- demanded_buildings.crs == services.crs
71
- ), f"\nThe CRS in the provided geodataframes are different.\nBuildings CRS:{demanded_buildings.crs}\nServices CRS:{services.crs} \n"
70
+ assert demanded_buildings.crs == services.crs, (
71
+ f"\nThe CRS in the provided geodataframes are different."
72
+ f"\nBuildings CRS:{demanded_buildings.crs}"
73
+ f"\nServices CRS:{services.crs}"
74
+ )
72
75
 
73
76
  @staticmethod
74
77
  def delete_useless_matrix_rows_columns(adjacency_matrix, demanded_buildings, services):
@@ -83,63 +86,16 @@ class Provision:
83
86
  columns = set(adjacency_matrix.columns.astype(int).tolist())
84
87
  dif = columns ^ service_indexes
85
88
  adjacency_matrix.drop(columns=(list(dif)), axis=0, inplace=True)
86
- return adjacency_matrix
87
-
88
- def get_provisions(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
89
- self._destination_matrix = pd.DataFrame(
90
- 0,
91
- index=self.adjacency_matrix.columns,
92
- columns=self.adjacency_matrix.index,
93
- )
94
- self.adjacency_matrix = self.adjacency_matrix.transpose()
95
- logger.debug(
96
- "Calculating provision from {} services to {} buildings.",
97
- len(self.services),
98
- len(self.demanded_buildings),
99
- )
100
- self.adjacency_matrix = self.adjacency_matrix.where(self.adjacency_matrix <= self.threshold * 3, np.inf)
101
-
102
- self._destination_matrix = self._provision_loop_gravity(
103
- self.demanded_buildings.copy(),
104
- self.services.copy(),
105
- self.adjacency_matrix.copy() + 1,
106
- self.threshold,
107
- self._destination_matrix.copy(),
108
- )
109
- _additional_options(
110
- self.demanded_buildings,
111
- self.services,
112
- self.adjacency_matrix,
113
- self._destination_matrix,
114
- self.threshold,
115
- )
89
+ return adjacency_matrix.transpose()
116
90
 
117
- return (
118
- self.demanded_buildings,
119
- self.services,
120
- _calc_links(
121
- self._destination_matrix,
122
- self.services,
123
- self.demanded_buildings,
124
- self.adjacency_matrix,
125
- ),
126
- )
91
+ def run(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
127
92
 
128
- def _provision_loop_gravity(
129
- self,
130
- houses_table: gpd.GeoDataFrame,
131
- services_table: gpd.GeoDataFrame,
132
- distance_matrix: pd.DataFrame,
133
- selection_range,
134
- destination_matrix: pd.DataFrame,
135
- best_houses=0.9,
136
- ):
137
- def apply_function_based_on_size(df, func, axis, threshold=500):
93
+ def apply_function_based_on_size(df, func, axis, threshold=100):
138
94
  if len(df) > threshold:
139
95
  return df.parallel_apply(func, axis=axis)
140
96
  return df.apply(func, axis=axis)
141
97
 
142
- def _calculate_flows_y(loc):
98
+ def calculate_flows_y(loc):
143
99
  import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
144
100
  import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
145
101
 
@@ -158,7 +114,7 @@ class Provision:
158
114
 
159
115
  return choice
160
116
 
161
- def _balance_flows_to_demands(loc):
117
+ def balance_flows_to_demands(loc):
162
118
  import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
163
119
  import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
164
120
 
@@ -177,43 +133,104 @@ class Provision:
177
133
  return choice
178
134
  return loc
179
135
 
180
- temp_destination_matrix = apply_function_based_on_size(
181
- distance_matrix, lambda x: _calculate_flows_y(x[x <= selection_range]), 1
136
+ logger.debug(
137
+ f"Calculating provision from {len(self.services)} services to {len(self.demanded_buildings)} buildings."
182
138
  )
183
139
 
184
- temp_destination_matrix = temp_destination_matrix.fillna(0)
185
-
186
- temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, _balance_flows_to_demands, 0)
187
-
188
- temp_destination_matrix = temp_destination_matrix.fillna(0)
189
- destination_matrix = destination_matrix.add(temp_destination_matrix, fill_value=0)
190
-
191
- axis_1 = destination_matrix.sum(axis=1)
192
- axis_0 = destination_matrix.sum(axis=0)
193
-
194
- services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
195
- houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
140
+ distance_matrix = self.adjacency_matrix
141
+ destination_matrix = pd.DataFrame(
142
+ 0,
143
+ index=distance_matrix.index,
144
+ columns=distance_matrix.columns,
145
+ dtype=int,
146
+ )
147
+ distance_matrix = distance_matrix.where(distance_matrix <= self.threshold * 3, np.inf)
196
148
 
149
+ houses_table = self.demanded_buildings[["demand", "demand_left"]].copy()
150
+ services_table = self.services[["capacity", "capacity_left"]].copy()
197
151
  distance_matrix = distance_matrix.drop(
198
152
  index=services_table[services_table["capacity_left"] == 0].index.values,
199
153
  columns=houses_table[houses_table["demand_left"] == 0].index.values,
200
154
  errors="ignore",
201
155
  )
202
-
203
156
  distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
204
157
  distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
205
158
 
206
- selection_range += selection_range
159
+ distance_matrix = distance_matrix + 1
160
+ selection_range = (self.threshold + 1) / 2
161
+ best_houses = 0.9
162
+ while len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
163
+ objects_n = sum(distance_matrix.shape)
164
+ logger.debug(
165
+ f"Matrix shape: {distance_matrix.shape},"
166
+ f" Total objects: {objects_n},"
167
+ f" Selection range: {selection_range},"
168
+ f" Best houses: {best_houses}"
169
+ )
170
+
171
+ temp_destination_matrix = apply_function_based_on_size(
172
+ distance_matrix, lambda x: calculate_flows_y(x[x <= selection_range]), 1
173
+ )
207
174
 
208
- if best_houses > 0.1:
209
- best_houses -= 0.1
175
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
176
+ temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, balance_flows_to_demands, 0)
177
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
178
+ temp_destination_matrix_aligned = temp_destination_matrix.reindex(
179
+ index=destination_matrix.index, columns=destination_matrix.columns, fill_value=0
180
+ )
181
+ del temp_destination_matrix
182
+ destination_matrix_np = destination_matrix.to_numpy()
183
+ temp_destination_matrix_np = temp_destination_matrix_aligned.to_numpy()
184
+ del temp_destination_matrix_aligned
185
+ destination_matrix = pd.DataFrame(
186
+ destination_matrix_np + temp_destination_matrix_np,
187
+ index=destination_matrix.index,
188
+ columns=destination_matrix.columns,
189
+ )
190
+ del destination_matrix_np, temp_destination_matrix_np
191
+ axis_1 = destination_matrix.sum(axis=1).astype(int)
192
+ axis_0 = destination_matrix.sum(axis=0).astype(int)
193
+
194
+ services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
195
+ houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
196
+ del axis_1, axis_0
197
+ distance_matrix = distance_matrix.drop(
198
+ index=services_table[services_table["capacity_left"] == 0].index.values,
199
+ columns=houses_table[houses_table["demand_left"] == 0].index.values,
200
+ errors="ignore",
201
+ )
202
+ distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
203
+ distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
204
+
205
+ selection_range *= 1.5
210
206
  if best_houses <= 0.1:
211
207
  best_houses = 0
212
- if len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
213
- return self._provision_loop_gravity(
214
- houses_table, services_table, distance_matrix, selection_range, destination_matrix, best_houses
215
- )
216
- return destination_matrix
208
+ else:
209
+ objects_n_new = sum(distance_matrix.shape)
210
+ best_houses = objects_n_new / (objects_n / best_houses)
211
+
212
+ logger.debug("Done!")
213
+ del distance_matrix, houses_table, services_table
214
+ self.destination_matrix = destination_matrix
215
+
216
+ _additional_options(
217
+ self.demanded_buildings,
218
+ self.services,
219
+ self.adjacency_matrix,
220
+ self.destination_matrix,
221
+ self.threshold,
222
+ )
223
+
224
+ return (
225
+ self.demanded_buildings,
226
+ self.services,
227
+ _calc_links(
228
+ self.destination_matrix,
229
+ self.services,
230
+ self.demanded_buildings,
231
+ self.adjacency_matrix,
232
+ ),
233
+ )
217
234
 
218
235
 
219
236
  def _calc_links(
@@ -279,8 +296,7 @@ def _additional_options(
279
296
  buildings["supplyed_demands_without"] = 0
280
297
  services["carried_capacity_within"] = 0
281
298
  services["carried_capacity_without"] = 0
282
- for i in range(len(destination_matrix)):
283
- loc = destination_matrix.iloc[i]
299
+ for _, loc in destination_matrix.iterrows():
284
300
  distances_all = matrix.loc[loc.name]
285
301
  distances = distances_all[distances_all <= normative_distance]
286
302
  s = matrix.loc[loc.name] <= normative_distance
@@ -306,6 +322,7 @@ def _additional_options(
306
322
  services.at[loc.name, "carried_capacity_without"] = (
307
323
  services.at[loc.name, "carried_capacity_without"] + without.sum()
308
324
  )
325
+ buildings["min_dist"] = matrix.min(axis=0).replace(np.inf, None)
309
326
  buildings["avg_dist"] = (buildings["avg_dist"] / (buildings["demand"] - buildings["demand_left"])).astype(
310
327
  np.float32
311
328
  )
@@ -318,3 +335,4 @@ def _additional_options(
318
335
  buildings["supplyed_demands_without"] = buildings["supplyed_demands_without"].astype(np.uint16)
319
336
  services["carried_capacity_within"] = services["carried_capacity_within"].astype(np.uint16)
320
337
  services["carried_capacity_without"] = services["carried_capacity_without"].astype(np.uint16)
338
+ logger.debug("Done adding additional options")
File without changes
@@ -0,0 +1,79 @@
1
+ import math
2
+
3
+ import geopandas as gpd
4
+ from shapely import LineString, MultiLineString, MultiPolygon, Point, Polygon
5
+ from shapely.ops import polygonize, unary_union
6
+
7
+ from objectnat import config
8
+
9
+ logger = config.logger
10
+
11
+
12
+ def polygons_to_multilinestring(geom: Polygon | MultiPolygon):
13
+ def convert_polygon(polygon: Polygon):
14
+ lines = []
15
+ exterior = LineString(polygon.exterior.coords)
16
+ lines.append(exterior)
17
+ interior = [LineString(p.coords) for p in polygon.interiors]
18
+ lines = lines + interior
19
+ return lines
20
+
21
+ def convert_multipolygon(polygon: MultiPolygon):
22
+ return MultiLineString(sum([convert_polygon(p) for p in polygon.geoms], []))
23
+
24
+ if geom.geom_type == "Polygon":
25
+ return MultiLineString(convert_polygon(geom))
26
+ return convert_multipolygon(geom)
27
+
28
+
29
+ def explode_linestring(geometry: LineString) -> list[LineString]:
30
+ """A function to return all segments of a linestring as a list of linestrings"""
31
+ coords_ext = geometry.coords # Create a list of all line node coordinates
32
+ result = [LineString(part) for part in zip(coords_ext, coords_ext[1:])]
33
+ return result
34
+
35
+
36
+ def point_side_of_line(line: LineString, point: Point) -> int:
37
+ """A positive indicates the left-hand side a negative indicates the right-hand side"""
38
+ x1, y1 = line.coords[0]
39
+ x2, y2 = line.coords[-1]
40
+ x, y = point.coords[0]
41
+ cross_product = (x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)
42
+ if cross_product > 0:
43
+ return 1
44
+ return -1
45
+
46
+
47
+ def get_point_from_a_thorough_b(a: Point, b: Point, dist):
48
+ """
49
+ Func to get Point from point a thorough point b on dist
50
+ """
51
+ direction = math.atan2(b.y - a.y, b.x - a.x)
52
+ c_x = a.x + dist * math.cos(direction)
53
+ c_y = a.y + dist * math.sin(direction)
54
+ return Point(c_x, c_y)
55
+
56
+
57
+ def gdf_to_circle_zones_from_point(
58
+ gdf: gpd.GeoDataFrame, point_from: Point, zone_radius, resolution=4, explode_multigeom=True
59
+ ) -> gpd.GeoDataFrame:
60
+ """n_segments = 4*resolution,e.g. if resolution = 4 that means there will be 16 segments"""
61
+ crs = gdf.crs
62
+ buffer = point_from.buffer(zone_radius, resolution=resolution)
63
+ gdf_unary = gdf.clip(buffer, keep_geom_type=True).unary_union
64
+ gdf_geometry = (
65
+ gpd.GeoDataFrame(geometry=[gdf_unary], crs=crs)
66
+ .explode(index_parts=True)
67
+ .geometry.apply(polygons_to_multilinestring)
68
+ .unary_union
69
+ )
70
+ zones_lines = [LineString([Point(coords1), Point(point_from)]) for coords1 in buffer.exterior.coords[:-1]]
71
+ if explode_multigeom:
72
+ return (
73
+ gpd.GeoDataFrame(geometry=list(polygonize(unary_union([gdf_geometry] + zones_lines))), crs=crs)
74
+ .clip(gdf_unary, keep_geom_type=True)
75
+ .explode(index_parts=False)
76
+ )
77
+ return gpd.GeoDataFrame(geometry=list(polygonize(unary_union([gdf_geometry] + zones_lines))), crs=crs).clip(
78
+ gdf_unary, keep_geom_type=True
79
+ )
@@ -10,11 +10,19 @@ from shapely.ops import polygonize, 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
+ explode_linestring,
15
+ get_point_from_a_thorough_b,
16
+ point_side_of_line,
17
+ polygons_to_multilinestring,
18
+ )
13
19
 
14
20
  logger = config.logger
15
21
 
16
22
 
17
- def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view_distance) -> Polygon:
23
+ def get_visibility_accurate(
24
+ point_from: Point, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
25
+ ) -> Polygon | tuple[Polygon, float]:
18
26
  """
19
27
  Function to get accurate visibility from a given point to buildings within a given distance.
20
28
 
@@ -26,11 +34,13 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
26
34
  A GeoDataFrame containing the geometry of the obstacles.
27
35
  view_distance : float
28
36
  The distance of view from the point.
37
+ return_max_view_dist
38
+ If True, the max view distance is returned with view polygon in tuple.
29
39
 
30
40
  Returns
31
41
  -------
32
- Polygon
33
- A polygon representing the area of visibility from the given point.
42
+ Polygon | tuple[Polygon, float]
43
+ A polygon representing the area of visibility from the given point or polygon with max view distance.
34
44
 
35
45
  Notes
36
46
  -----
@@ -45,34 +55,29 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
45
55
  >>> visibility = get_visibility_accurate(point_from, obstacles, view_distance)
46
56
  """
47
57
 
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
58
+ def find_furthest_point(point_from, view_polygon):
59
+ try:
60
+ res = round(max(Point(coords).distance(point_from) for coords in view_polygon.exterior.coords), 1)
61
+ except Exception as e:
62
+ print(view_polygon)
63
+ raise e
64
+ return res
65
+
66
+ obstacles = obstacles.copy()
67
+ obstacles.reset_index(inplace=True, drop=True)
66
68
 
67
69
  point_buffer = point_from.buffer(view_distance, resolution=32)
70
+ allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
71
+ obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
68
72
  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()
73
+ obstacles_in_buffer = obstacles.loc[s[s].index].geometry
74
+
75
+ buildings_lines_in_buffer = gpd.GeoSeries(
76
+ pd.Series(
77
+ obstacles_in_buffer.apply(polygons_to_multilinestring).explode(index_parts=False).apply(explode_linestring)
78
+ ).explode()
79
+ )
74
80
 
75
- buildings_lines_in_buffer = gpd.GeoSeries(buildings_in_buffer.apply(polygon_to_linestring).explode())
76
81
  buildings_lines_in_buffer = buildings_lines_in_buffer.loc[buildings_lines_in_buffer.intersects(point_buffer)]
77
82
 
78
83
  buildings_in_buffer_points = gpd.GeoSeries(
@@ -82,15 +87,12 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
82
87
 
83
88
  max_dist = max(view_distance, buildings_in_buffer_points.distance(point_from).max())
84
89
  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
90
+ buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index()
91
+ logger.debug("Calculation vis polygon")
89
92
  while not buildings_lines_in_buffer.empty:
90
- iteration += 1
91
93
  gdf_sindex = buildings_lines_in_buffer.sindex
92
94
  # 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)
95
+ nearest_wall_sind = gdf_sindex.nearest(point_from, return_all=False, max_distance=max_dist)
94
96
  nearest_wall = buildings_lines_in_buffer.loc[nearest_wall_sind[1]].iloc[0]
95
97
  wall_points = [Point(coords) for coords in nearest_wall.geometry.coords]
96
98
 
@@ -98,31 +100,49 @@ def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view
98
100
  points_with_angle = sorted(
99
101
  [(pt, math.atan2(pt.y - point_from.y, pt.x - point_from.x)) for pt in wall_points], key=lambda x: x[1]
100
102
  )
101
-
102
103
  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]])
104
+ if round(delta_angle, 10) == round(math.pi, 10):
105
+ wall_b_centroid = obstacles_in_buffer.loc[nearest_wall["index"]].centroid
106
+ p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], max_dist)
107
+ p2 = get_point_from_a_thorough_b(point_from, points_with_angle[1][0], max_dist)
108
+ polygon = LineString([p1, p2])
109
+ polygon = polygon.buffer(
110
+ distance=max_dist * point_side_of_line(polygon, wall_b_centroid), single_sided=True
111
+ )
112
+ else:
113
+ if delta_angle > math.pi:
114
+ delta_angle = 2 * math.pi - delta_angle
115
+ a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
116
+ p1 = get_point_from_a_thorough_b(point_from, points_with_angle[0][0], a)
117
+ p2 = get_point_from_a_thorough_b(point_from, points_with_angle[-1][0], a)
118
+ polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
109
119
 
110
120
  polygons.append(polygon)
111
-
112
121
  buildings_lines_in_buffer.drop(nearest_wall_sind[1], inplace=True)
113
122
 
123
+ if not polygon.is_valid or polygon.area < 1:
124
+ buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
125
+ continue
126
+
114
127
  lines_to_kick = buildings_lines_in_buffer.within(polygon)
115
128
  buildings_lines_in_buffer = buildings_lines_in_buffer.loc[~lines_to_kick]
116
129
  buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
117
- res = point_buffer.difference(unary_union(polygons))
130
+ logger.debug("Done calculating!")
131
+ res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
132
+
118
133
  if isinstance(res, Polygon):
134
+ if return_max_view_dist:
135
+ return res, find_furthest_point(point_from, res)
119
136
  return res
120
137
  res = list(res.geoms)
121
138
  polygon_containing_point = None
139
+
122
140
  for polygon in res:
123
- if polygon.contains(point_from):
141
+ if polygon.intersects(point_from):
124
142
  polygon_containing_point = polygon
125
143
  break
144
+ if return_max_view_dist:
145
+ return polygon_containing_point, find_furthest_point(point_from, polygon_containing_point)
126
146
  return polygon_containing_point
127
147
 
128
148