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.
- objectnat/_api.py +1 -0
- objectnat/_version.py +1 -1
- objectnat/methods/isochrones.py +1 -1
- objectnat/methods/living_buildings_osm.py +4 -6
- 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 +418 -0
- objectnat/methods/provision/provision.py +15 -8
- objectnat/methods/provision/provision_exceptions.py +4 -4
- objectnat/methods/provision/provision_model.py +106 -88
- objectnat/methods/utils/__init__.py +0 -0
- objectnat/methods/utils/geom_utils.py +79 -0
- objectnat/methods/visibility_analysis.py +63 -43
- {objectnat-0.2.5.dist-info → objectnat-0.2.7.dist-info}/METADATA +31 -28
- objectnat-0.2.7.dist-info/RECORD +26 -0
- objectnat/utils/__init__.py +0 -1
- objectnat/utils/utils.py +0 -19
- objectnat-0.2.5.dist-info/RECORD +0 -21
- {objectnat-0.2.5.dist-info → objectnat-0.2.7.dist-info}/LICENSE.txt +0 -0
- {objectnat-0.2.5.dist-info → objectnat-0.2.7.dist-info}/WHEEL +0 -0
|
@@ -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
|
-
).
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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 (
|
|
23
|
-
demanded_buildings (
|
|
24
|
-
adjacency_matrix (
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
136
|
+
logger.debug(
|
|
137
|
+
f"Calculating provision from {len(self.services)} services to {len(self.demanded_buildings)} buildings."
|
|
182
138
|
)
|
|
183
139
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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(
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|