huff 1.2.0__py3-none-any.whl → 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- huff/gistools.py +123 -3
- huff/models.py +134 -30
- huff/ors.py +23 -18
- huff/osm.py +250 -0
- huff/tests/data/Haslach.qmd +43 -43
- huff/tests/data/Haslach_supermarkets.qmd +43 -43
- huff/tests/tests_huff.py +126 -19
- huff-1.3.1.dist-info/METADATA +78 -0
- {huff-1.2.0.dist-info → huff-1.3.1.dist-info}/RECORD +11 -10
- huff-1.2.0.dist-info/METADATA +0 -59
- {huff-1.2.0.dist-info → huff-1.3.1.dist-info}/WHEEL +0 -0
- {huff-1.2.0.dist-info → huff-1.3.1.dist-info}/top_level.txt +0 -0
huff/gistools.py
CHANGED
@@ -4,13 +4,15 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.
|
8
|
-
# Last update: 2025-05-
|
7
|
+
# Version: 1.3.1
|
8
|
+
# Last update: 2025-05-28 18:01
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
12
12
|
|
13
13
|
import geopandas as gp
|
14
|
+
import pandas as pd
|
15
|
+
from pandas.api.types import is_numeric_dtype
|
14
16
|
from math import pi, sin, cos, acos
|
15
17
|
|
16
18
|
|
@@ -60,6 +62,65 @@ def distance_matrix(
|
|
60
62
|
return matrix
|
61
63
|
|
62
64
|
|
65
|
+
def buffers(
|
66
|
+
point_gdf: gp.GeoDataFrame,
|
67
|
+
unique_id_col: str,
|
68
|
+
distances: list,
|
69
|
+
donut: bool = True,
|
70
|
+
save_output: bool = True,
|
71
|
+
output_filepath: str = "buffers.shp",
|
72
|
+
output_crs: str = "EPSG:4326"
|
73
|
+
):
|
74
|
+
|
75
|
+
all_buffers_gdf = gp.GeoDataFrame(columns=[unique_id_col, "segment", "geometry"])
|
76
|
+
|
77
|
+
for idx, row in point_gdf.iterrows():
|
78
|
+
|
79
|
+
point_buffers = []
|
80
|
+
|
81
|
+
for distance in distances:
|
82
|
+
|
83
|
+
point = row["geometry"]
|
84
|
+
point_buffer = point.buffer(distance)
|
85
|
+
|
86
|
+
point_buffer_gdf = gp.GeoDataFrame(
|
87
|
+
{
|
88
|
+
unique_id_col: row[unique_id_col],
|
89
|
+
"geometry": [point_buffer],
|
90
|
+
"segment": [distance]
|
91
|
+
},
|
92
|
+
crs=point_gdf.crs
|
93
|
+
)
|
94
|
+
|
95
|
+
point_buffers.append(point_buffer_gdf)
|
96
|
+
|
97
|
+
point_buffers_gdf = pd.concat(
|
98
|
+
point_buffers,
|
99
|
+
ignore_index = True
|
100
|
+
)
|
101
|
+
|
102
|
+
if donut:
|
103
|
+
point_buffers_gdf = overlay_difference(
|
104
|
+
polygon_gdf = point_buffers_gdf,
|
105
|
+
sort_col = "segment"
|
106
|
+
)
|
107
|
+
|
108
|
+
all_buffers_gdf = pd.concat(
|
109
|
+
[
|
110
|
+
all_buffers_gdf,
|
111
|
+
point_buffers_gdf
|
112
|
+
],
|
113
|
+
ignore_index = True)
|
114
|
+
|
115
|
+
all_buffers_gdf = all_buffers_gdf.to_crs(output_crs)
|
116
|
+
|
117
|
+
if save_output:
|
118
|
+
all_buffers_gdf.to_file(output_filepath)
|
119
|
+
print ("Saved as", output_filepath)
|
120
|
+
|
121
|
+
return all_buffers_gdf
|
122
|
+
|
123
|
+
|
63
124
|
def overlay_difference(
|
64
125
|
polygon_gdf: gp.GeoDataFrame,
|
65
126
|
sort_col: str = None,
|
@@ -74,6 +135,7 @@ def overlay_difference(
|
|
74
135
|
new_data = []
|
75
136
|
|
76
137
|
for i in range(len(polygon_gdf) - 1, 0, -1):
|
138
|
+
|
77
139
|
current_polygon = polygon_gdf.iloc[i].geometry
|
78
140
|
previous_polygon = polygon_gdf.iloc[i - 1].geometry
|
79
141
|
difference_polygon = current_polygon.difference(previous_polygon)
|
@@ -85,7 +147,9 @@ def overlay_difference(
|
|
85
147
|
new_data.append(polygon_gdf.iloc[i].drop("geometry"))
|
86
148
|
|
87
149
|
inner_most_polygon = polygon_gdf.iloc[0].geometry
|
150
|
+
|
88
151
|
if inner_most_polygon.is_valid:
|
152
|
+
|
89
153
|
new_geometries.append(inner_most_polygon)
|
90
154
|
new_data.append(polygon_gdf.iloc[0].drop("geometry"))
|
91
155
|
|
@@ -93,4 +157,60 @@ def overlay_difference(
|
|
93
157
|
new_data, geometry=new_geometries, crs=polygon_gdf.crs
|
94
158
|
)
|
95
159
|
|
96
|
-
return polygon_gdf_difference
|
160
|
+
return polygon_gdf_difference
|
161
|
+
|
162
|
+
|
163
|
+
def point_spatial_join(
|
164
|
+
polygon_gdf: gp.GeoDataFrame,
|
165
|
+
point_gdf: gp.GeoDataFrame,
|
166
|
+
join_type: str = "inner",
|
167
|
+
polygon_ref_cols: list = [],
|
168
|
+
point_stat_col: str = None
|
169
|
+
):
|
170
|
+
|
171
|
+
if polygon_gdf.crs != point_gdf.crs:
|
172
|
+
raise ValueError (f"Coordinate reference systems of polygon and point data do not match. Polygons: {str(polygon_gdf.crs)}, points: {str(point_gdf.crs)}")
|
173
|
+
|
174
|
+
if polygon_ref_cols != []:
|
175
|
+
for polygon_ref_col in polygon_ref_cols:
|
176
|
+
if polygon_ref_col not in polygon_gdf.columns:
|
177
|
+
raise KeyError (f"Column {polygon_ref_col} not in polygon data")
|
178
|
+
|
179
|
+
if point_stat_col is not None:
|
180
|
+
if point_stat_col not in point_gdf.columns:
|
181
|
+
raise KeyError (f"Column {point_stat_col} not in point data")
|
182
|
+
if not is_numeric_dtype(point_gdf[point_stat_col]):
|
183
|
+
raise TypeError (f"Column {point_stat_col} is not numeric")
|
184
|
+
|
185
|
+
shp_points_gdf_join = point_gdf.sjoin(
|
186
|
+
polygon_gdf,
|
187
|
+
how=join_type
|
188
|
+
)
|
189
|
+
|
190
|
+
spatial_join_stat = None
|
191
|
+
|
192
|
+
if polygon_ref_cols != [] and point_stat_col is not None:
|
193
|
+
shp_points_gdf_join_count = shp_points_gdf_join.groupby(polygon_ref_cols)[point_stat_col].count()
|
194
|
+
shp_points_gdf_join_sum = shp_points_gdf_join.groupby(polygon_ref_cols)[point_stat_col].sum()
|
195
|
+
shp_points_gdf_join_min = shp_points_gdf_join.groupby(polygon_ref_cols)[point_stat_col].min()
|
196
|
+
shp_points_gdf_join_max = shp_points_gdf_join.groupby(polygon_ref_cols)[point_stat_col].max()
|
197
|
+
shp_points_gdf_join_mean = shp_points_gdf_join.groupby(polygon_ref_cols)[point_stat_col].mean()
|
198
|
+
|
199
|
+
shp_points_gdf_join_count = shp_points_gdf_join_count.rename("count").to_frame()
|
200
|
+
shp_points_gdf_join_sum = shp_points_gdf_join_sum.rename("sum").to_frame()
|
201
|
+
shp_points_gdf_join_min = shp_points_gdf_join_min.rename("min").to_frame()
|
202
|
+
shp_points_gdf_join_max = shp_points_gdf_join_max.rename("max").to_frame()
|
203
|
+
shp_points_gdf_join_mean = shp_points_gdf_join_mean.rename("mean").to_frame()
|
204
|
+
spatial_join_stat = shp_points_gdf_join_count.join(
|
205
|
+
[
|
206
|
+
shp_points_gdf_join_sum,
|
207
|
+
shp_points_gdf_join_min,
|
208
|
+
shp_points_gdf_join_max,
|
209
|
+
shp_points_gdf_join_mean
|
210
|
+
]
|
211
|
+
)
|
212
|
+
|
213
|
+
return [
|
214
|
+
shp_points_gdf_join,
|
215
|
+
spatial_join_stat
|
216
|
+
]
|
huff/models.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.
|
8
|
-
# Last update: 2025-05-
|
7
|
+
# Version: 1.3.1
|
8
|
+
# Last update: 2025-05-28 18:01
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -18,7 +18,7 @@ from statsmodels.formula.api import ols
|
|
18
18
|
from shapely.geometry import Point
|
19
19
|
from shapely import wkt
|
20
20
|
from huff.ors import Client, TimeDistanceMatrix, Isochrone
|
21
|
-
from huff.gistools import overlay_difference, distance_matrix
|
21
|
+
from huff.gistools import overlay_difference, distance_matrix, buffers
|
22
22
|
|
23
23
|
|
24
24
|
class CustomerOrigins:
|
@@ -28,13 +28,15 @@ class CustomerOrigins:
|
|
28
28
|
geodata_gpd,
|
29
29
|
geodata_gpd_original,
|
30
30
|
metadata,
|
31
|
-
isochrones_gdf
|
31
|
+
isochrones_gdf,
|
32
|
+
buffers_gdf
|
32
33
|
):
|
33
34
|
|
34
35
|
self.geodata_gpd = geodata_gpd
|
35
36
|
self.geodata_gpd_original = geodata_gpd_original
|
36
37
|
self.metadata = metadata
|
37
38
|
self.isochrones_gdf = isochrones_gdf
|
39
|
+
self.buffers_gdf = buffers_gdf
|
38
40
|
|
39
41
|
def get_geodata_gpd(self):
|
40
42
|
|
@@ -48,10 +50,14 @@ class CustomerOrigins:
|
|
48
50
|
|
49
51
|
return self.metadata
|
50
52
|
|
51
|
-
def
|
53
|
+
def get_isochrones_gdf(self):
|
52
54
|
|
53
55
|
return self.isochrones_gdf
|
54
56
|
|
57
|
+
def get_buffers_gdf(self):
|
58
|
+
|
59
|
+
return self.buffers_gdf
|
60
|
+
|
55
61
|
def summary(self):
|
56
62
|
|
57
63
|
metadata = self.metadata
|
@@ -77,6 +83,11 @@ class CustomerOrigins:
|
|
77
83
|
else:
|
78
84
|
print("Including isochrones YES")
|
79
85
|
|
86
|
+
if self.buffers_gdf is None:
|
87
|
+
print("Including buffers NO")
|
88
|
+
else:
|
89
|
+
print("Including buffers YES")
|
90
|
+
|
80
91
|
return metadata
|
81
92
|
|
82
93
|
def define_marketsize(
|
@@ -151,6 +162,32 @@ class CustomerOrigins:
|
|
151
162
|
|
152
163
|
return self
|
153
164
|
|
165
|
+
def buffers(
|
166
|
+
self,
|
167
|
+
segments_distance: list = [500, 1000],
|
168
|
+
donut: bool = True,
|
169
|
+
save_output: bool = True,
|
170
|
+
output_filepath: str = "customer_origins_buffers.shp",
|
171
|
+
output_crs: str = "EPSG:4326"
|
172
|
+
):
|
173
|
+
|
174
|
+
geodata_gpd_original = self.get_geodata_gpd_original()
|
175
|
+
metadata = self.metadata
|
176
|
+
|
177
|
+
buffers_gdf = buffers(
|
178
|
+
point_gdf = geodata_gpd_original,
|
179
|
+
unique_id_col = metadata["unique_id"],
|
180
|
+
distances = segments_distance,
|
181
|
+
donut = donut,
|
182
|
+
save_output = save_output,
|
183
|
+
output_filepath = output_filepath,
|
184
|
+
output_crs = output_crs
|
185
|
+
)
|
186
|
+
|
187
|
+
self.buffers_gdf = buffers_gdf
|
188
|
+
|
189
|
+
return self
|
190
|
+
|
154
191
|
|
155
192
|
class SupplyLocations:
|
156
193
|
|
@@ -159,13 +196,15 @@ class SupplyLocations:
|
|
159
196
|
geodata_gpd,
|
160
197
|
geodata_gpd_original,
|
161
198
|
metadata,
|
162
|
-
isochrones_gdf
|
199
|
+
isochrones_gdf,
|
200
|
+
buffers_gdf
|
163
201
|
):
|
164
202
|
|
165
203
|
self.geodata_gpd = geodata_gpd
|
166
204
|
self.geodata_gpd_original = geodata_gpd_original
|
167
205
|
self.metadata = metadata
|
168
206
|
self.isochrones_gdf = isochrones_gdf
|
207
|
+
self.buffers_gdf = buffers_gdf
|
169
208
|
|
170
209
|
def get_geodata_gpd(self):
|
171
210
|
|
@@ -182,6 +221,10 @@ class SupplyLocations:
|
|
182
221
|
def get_isochrones_gdf(self):
|
183
222
|
|
184
223
|
return self.isochrones_gdf
|
224
|
+
|
225
|
+
def get_buffers_gdf(self):
|
226
|
+
|
227
|
+
return self.buffers_gdf
|
185
228
|
|
186
229
|
def summary(self):
|
187
230
|
|
@@ -346,6 +389,32 @@ class SupplyLocations:
|
|
346
389
|
|
347
390
|
return self
|
348
391
|
|
392
|
+
def buffers(
|
393
|
+
self,
|
394
|
+
segments_distance: list = [500, 1000],
|
395
|
+
donut: bool = True,
|
396
|
+
save_output: bool = True,
|
397
|
+
output_filepath: str = "supply_locations_buffers.shp",
|
398
|
+
output_crs: str = "EPSG:4326"
|
399
|
+
):
|
400
|
+
|
401
|
+
geodata_gpd_original = self.get_geodata_gpd_original()
|
402
|
+
metadata = self.metadata
|
403
|
+
|
404
|
+
buffers_gdf = buffers(
|
405
|
+
point_gdf = geodata_gpd_original,
|
406
|
+
unique_id_col = metadata["unique_id"],
|
407
|
+
distances = segments_distance,
|
408
|
+
donut = donut,
|
409
|
+
save_output = save_output,
|
410
|
+
output_filepath = output_filepath,
|
411
|
+
output_crs = output_crs
|
412
|
+
)
|
413
|
+
|
414
|
+
self.buffers_gdf = buffers_gdf
|
415
|
+
|
416
|
+
return self
|
417
|
+
|
349
418
|
|
350
419
|
class InteractionMatrix:
|
351
420
|
|
@@ -459,14 +528,10 @@ class InteractionMatrix:
|
|
459
528
|
range_type = transport_costs_matrix_config["range_type"]
|
460
529
|
|
461
530
|
transport_costs_matrix["source"] = transport_costs_matrix["source"].astype(int)
|
462
|
-
transport_costs_matrix["source"] = transport_costs_matrix["source"].map(
|
463
|
-
dict(enumerate(customer_origins_ids))
|
464
|
-
)
|
531
|
+
transport_costs_matrix["source"] = transport_costs_matrix["source"].map(dict(enumerate(customer_origins_ids)))
|
465
532
|
|
466
533
|
transport_costs_matrix["destination"] = transport_costs_matrix["destination"].astype(int)
|
467
|
-
transport_costs_matrix["destination"] = transport_costs_matrix["destination"].map(
|
468
|
-
dict(enumerate(supply_locations_ids))
|
469
|
-
)
|
534
|
+
transport_costs_matrix["destination"] = transport_costs_matrix["destination"].map(dict(enumerate(supply_locations_ids)))
|
470
535
|
|
471
536
|
transport_costs_matrix["source_destination"] = transport_costs_matrix["source"].astype(str)+"_"+transport_costs_matrix["destination"].astype(str)
|
472
537
|
transport_costs_matrix = transport_costs_matrix[["source_destination", range_type]]
|
@@ -952,8 +1017,7 @@ class MCIModel:
|
|
952
1017
|
transformation = "LCT"
|
953
1018
|
):
|
954
1019
|
|
955
|
-
interaction_matrix = self.interaction_matrix
|
956
|
-
|
1020
|
+
interaction_matrix = self.interaction_matrix
|
957
1021
|
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
958
1022
|
|
959
1023
|
if interaction_matrix_df["t_ij"].isna().all():
|
@@ -966,7 +1030,8 @@ class MCIModel:
|
|
966
1030
|
cols = ["A_j", "t_ij"]
|
967
1031
|
)
|
968
1032
|
|
969
|
-
|
1033
|
+
customer_origins = interaction_matrix.get_customer_origins()
|
1034
|
+
customer_origins_metadata = customer_origins.get_metadata()
|
970
1035
|
|
971
1036
|
t_ij_weighting = customer_origins_metadata["weighting"][0]["param"]
|
972
1037
|
|
@@ -975,7 +1040,8 @@ class MCIModel:
|
|
975
1040
|
else:
|
976
1041
|
mci_formula = f"t_ij**{t_ij_weighting}"
|
977
1042
|
|
978
|
-
|
1043
|
+
supply_locations = interaction_matrix.get_supply_locations()
|
1044
|
+
supply_locations_metadata = supply_locations.get_metadata()
|
979
1045
|
attraction_col = supply_locations_metadata["attraction_col"]
|
980
1046
|
attraction_weighting = supply_locations_metadata["weighting"]
|
981
1047
|
|
@@ -991,17 +1057,27 @@ class MCIModel:
|
|
991
1057
|
if transformation == "ILCT":
|
992
1058
|
interaction_matrix_df["U_ij"] = np.exp(interaction_matrix_df["U_ij"])
|
993
1059
|
|
994
|
-
|
1060
|
+
interaction_matrix = InteractionMatrix(
|
1061
|
+
interaction_matrix_df,
|
1062
|
+
customer_origins,
|
1063
|
+
supply_locations
|
1064
|
+
)
|
1065
|
+
self.interaction_matrix = interaction_matrix
|
995
1066
|
|
996
1067
|
return self
|
997
1068
|
|
998
|
-
def probabilities (
|
1069
|
+
def probabilities (
|
1070
|
+
self,
|
1071
|
+
transformation = "LCT"
|
1072
|
+
):
|
999
1073
|
|
1000
|
-
|
1074
|
+
interaction_matrix = self.interaction_matrix
|
1075
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1001
1076
|
|
1002
1077
|
if interaction_matrix_df["U_ij"].isna().all():
|
1003
|
-
self.utility()
|
1004
|
-
|
1078
|
+
self.utility(transformation = transformation)
|
1079
|
+
interaction_matrix = self.interaction_matrix
|
1080
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1005
1081
|
|
1006
1082
|
utility_i = pd.DataFrame(interaction_matrix_df.groupby("i")["U_ij"].sum())
|
1007
1083
|
utility_i = utility_i.rename(columns = {"U_ij": "U_i"})
|
@@ -1017,14 +1093,22 @@ class MCIModel:
|
|
1017
1093
|
|
1018
1094
|
interaction_matrix_df = interaction_matrix_df.drop(columns=["U_i"])
|
1019
1095
|
|
1020
|
-
|
1096
|
+
interaction_matrix.interaction_matrix_df = interaction_matrix_df
|
1097
|
+
self.interaction_matrix = interaction_matrix
|
1021
1098
|
|
1022
1099
|
return self
|
1023
1100
|
|
1024
|
-
def flows (
|
1101
|
+
def flows (
|
1102
|
+
self,
|
1103
|
+
transformation = "LCT"
|
1104
|
+
):
|
1025
1105
|
|
1026
|
-
|
1106
|
+
interaction_matrix = self.interaction_matrix
|
1107
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1027
1108
|
|
1109
|
+
if "C_i" not in interaction_matrix_df.columns:
|
1110
|
+
raise ValueError ("No market size column defined in interaction matrix.")
|
1111
|
+
|
1028
1112
|
if interaction_matrix_df["C_i"].isna().all():
|
1029
1113
|
raise ValueError ("Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
|
1030
1114
|
|
@@ -1034,8 +1118,9 @@ class MCIModel:
|
|
1034
1118
|
)
|
1035
1119
|
|
1036
1120
|
if interaction_matrix_df["p_ij"].isna().all():
|
1037
|
-
self.probabilities()
|
1038
|
-
|
1121
|
+
self.probabilities(transformation=transformation)
|
1122
|
+
interaction_matrix = self.interaction_matrix
|
1123
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1039
1124
|
|
1040
1125
|
interaction_matrix_df["E_ij"] = interaction_matrix_df["p_ij"] * interaction_matrix_df["C_i"]
|
1041
1126
|
|
@@ -1080,16 +1165,20 @@ def load_geodata (
|
|
1080
1165
|
):
|
1081
1166
|
|
1082
1167
|
if location_type is None or (location_type != "origins" and location_type != "destinations"):
|
1083
|
-
raise ValueError ("location_type must be either 'origins' or 'destinations'")
|
1168
|
+
raise ValueError ("Argument location_type must be either 'origins' or 'destinations'")
|
1084
1169
|
|
1085
1170
|
if isinstance(data, gp.GeoDataFrame):
|
1086
1171
|
geodata_gpd_original = data
|
1172
|
+
if not all(geodata_gpd_original.geometry.geom_type == "Point"):
|
1173
|
+
raise ValueError ("Input geopandas.GeoDataFrame must be of type 'Point'")
|
1087
1174
|
crs_input = geodata_gpd_original.crs
|
1088
1175
|
elif isinstance(data, pd.DataFrame):
|
1089
1176
|
geodata_tab = data
|
1090
1177
|
elif isinstance(data, str):
|
1091
1178
|
if data_type == "shp":
|
1092
1179
|
geodata_gpd_original = gp.read_file(data)
|
1180
|
+
if not all(geodata_gpd_original.geometry.geom_type == "Point"):
|
1181
|
+
raise ValueError ("Input shapefile must be of type 'Point'")
|
1093
1182
|
crs_input = geodata_gpd_original.crs
|
1094
1183
|
elif data_type == "csv" or data_type == "xlsx":
|
1095
1184
|
if x_col is None:
|
@@ -1111,6 +1200,12 @@ def load_geodata (
|
|
1111
1200
|
raise TypeError("data must be pandas.DataFrame, geopandas.GeoDataFrame or file (.csv, .xlsx, .shp)")
|
1112
1201
|
|
1113
1202
|
if data_type == "csv" or data_type == "xlsx" or (isinstance(data, pd.DataFrame) and not isinstance(data, gp.GeoDataFrame)):
|
1203
|
+
|
1204
|
+
check_vars(
|
1205
|
+
df = geodata_tab,
|
1206
|
+
cols = [x_col, y_col]
|
1207
|
+
)
|
1208
|
+
|
1114
1209
|
geodata_gpd_original = gp.GeoDataFrame(
|
1115
1210
|
geodata_tab,
|
1116
1211
|
geometry = gp.points_from_xy(
|
@@ -1146,6 +1241,7 @@ def load_geodata (
|
|
1146
1241
|
geodata_gpd,
|
1147
1242
|
geodata_gpd_original,
|
1148
1243
|
metadata,
|
1244
|
+
None,
|
1149
1245
|
None
|
1150
1246
|
)
|
1151
1247
|
elif location_type == "destinations":
|
@@ -1153,6 +1249,7 @@ def load_geodata (
|
|
1153
1249
|
geodata_gpd,
|
1154
1250
|
geodata_gpd_original,
|
1155
1251
|
metadata,
|
1252
|
+
None,
|
1156
1253
|
None
|
1157
1254
|
)
|
1158
1255
|
|
@@ -1349,7 +1446,8 @@ def load_interaction_matrix(
|
|
1349
1446
|
geodata_gpd = customer_origins_geodata_gpd,
|
1350
1447
|
geodata_gpd_original = customer_origins_geodata_original_tab,
|
1351
1448
|
metadata = customer_origins_metadata,
|
1352
|
-
isochrones_gdf = None
|
1449
|
+
isochrones_gdf = None,
|
1450
|
+
buffers_gdf = None
|
1353
1451
|
)
|
1354
1452
|
|
1355
1453
|
if supply_locations_coords_col is not None:
|
@@ -1415,7 +1513,8 @@ def load_interaction_matrix(
|
|
1415
1513
|
geodata_gpd = supply_locations_geodata_gpd,
|
1416
1514
|
geodata_gpd_original = supply_locations_geodata_original_tab,
|
1417
1515
|
metadata = supply_locations_metadata,
|
1418
|
-
isochrones_gdf = None
|
1516
|
+
isochrones_gdf = None,
|
1517
|
+
buffers_gdf = None
|
1419
1518
|
)
|
1420
1519
|
|
1421
1520
|
interaction_matrix_df = interaction_matrix_df.rename(
|
@@ -1494,7 +1593,7 @@ def get_isochrones(
|
|
1494
1593
|
profile: str = "driving-car",
|
1495
1594
|
donut: bool = True,
|
1496
1595
|
ors_server: str = "https://api.openrouteservice.org/v2/",
|
1497
|
-
ors_auth: str = None,
|
1596
|
+
ors_auth: str = None,
|
1498
1597
|
timeout = 10,
|
1499
1598
|
delay = 1,
|
1500
1599
|
save_output: bool = True,
|
@@ -1544,6 +1643,8 @@ def get_isochrones(
|
|
1544
1643
|
time.sleep(delay)
|
1545
1644
|
|
1546
1645
|
isochrone_gdf[unique_id_col] = unique_id_values[i]
|
1646
|
+
|
1647
|
+
isochrone_gdf["segment_minutes"] = isochrone_gdf["segment"]/60
|
1547
1648
|
|
1548
1649
|
isochrones_gdf = pd.concat(
|
1549
1650
|
[
|
@@ -1555,6 +1656,9 @@ def get_isochrones(
|
|
1555
1656
|
|
1556
1657
|
i = i+1
|
1557
1658
|
|
1659
|
+
isochrones_gdf["segment"] = isochrones_gdf["segment"].astype(int)
|
1660
|
+
isochrones_gdf["segment_minutes"] = isochrones_gdf["segment_minutes"].astype(int)
|
1661
|
+
|
1558
1662
|
isochrones_gdf.set_crs(
|
1559
1663
|
output_crs,
|
1560
1664
|
allow_override=True,
|
huff/ors.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.
|
8
|
-
# Last update: 2025-05-
|
7
|
+
# Version: 1.3.1
|
8
|
+
# Last update: 2025-05-28 18:01
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -119,7 +119,6 @@ class Client:
|
|
119
119
|
def isochrone(
|
120
120
|
self,
|
121
121
|
locations: list,
|
122
|
-
id: list = [],
|
123
122
|
segments: list = [900, 600, 300],
|
124
123
|
range_type: str = "time",
|
125
124
|
intersections: str = "true",
|
@@ -130,6 +129,12 @@ class Client:
|
|
130
129
|
output_crs: str = "EPSG:4326"
|
131
130
|
):
|
132
131
|
|
132
|
+
if len(segments) > 10:
|
133
|
+
raise ValueError ("ORS client does not allow >10 intervals in an Isochrones query. See https://openrouteservice.org/restrictions/.")
|
134
|
+
|
135
|
+
if len(locations) > 5:
|
136
|
+
raise ValueError ("ORS client does not allow >5 locations in an Isochrones query. See https://openrouteservice.org/restrictions/.")
|
137
|
+
|
133
138
|
ors_url = self.server + "isochrones/" + profile
|
134
139
|
auth = self.auth
|
135
140
|
|
@@ -162,9 +167,9 @@ class Client:
|
|
162
167
|
timeout=timeout
|
163
168
|
)
|
164
169
|
|
165
|
-
except:
|
170
|
+
except Exception as e:
|
166
171
|
|
167
|
-
print ("
|
172
|
+
print ("Error while accessing ORS server: ", str(e))
|
168
173
|
|
169
174
|
status_code = 99999
|
170
175
|
isochrones_gdf = None
|
@@ -200,7 +205,7 @@ class Client:
|
|
200
205
|
|
201
206
|
isochrones_gdf["segment"] = 0
|
202
207
|
isochrones_gdf_properties_dict = dict(isochrones_gdf["properties"])
|
203
|
-
|
208
|
+
|
204
209
|
for i in range(len(isochrones_gdf_properties_dict)):
|
205
210
|
isochrones_gdf.iloc[i,3] = isochrones_gdf_properties_dict[i]["value"]
|
206
211
|
|
@@ -212,9 +217,9 @@ class Client:
|
|
212
217
|
print ("Saved as", output_filepath)
|
213
218
|
|
214
219
|
else:
|
215
|
-
|
216
|
-
print
|
217
|
-
|
220
|
+
|
221
|
+
print(f"Error while accessing ORS server. Status code: {status_code} - {response.reason}")
|
222
|
+
|
218
223
|
isochrones_gdf = None
|
219
224
|
metadata = None
|
220
225
|
|
@@ -233,14 +238,14 @@ class Client:
|
|
233
238
|
sources: list = [],
|
234
239
|
destinations: list = [],
|
235
240
|
id: str = None,
|
236
|
-
range_type = "time",
|
237
|
-
profile = "driving-car",
|
241
|
+
range_type: str = "time",
|
242
|
+
profile: str = "driving-car",
|
238
243
|
metrics: list = [],
|
239
244
|
resolve_locations: bool = False,
|
240
245
|
units: str = "mi",
|
241
|
-
timeout = 10,
|
242
|
-
save_output = False,
|
243
|
-
output_filepath = "matrix.csv",
|
246
|
+
timeout: int = 10,
|
247
|
+
save_output: bool = False,
|
248
|
+
output_filepath: str = "matrix.csv",
|
244
249
|
csv_sep = ";",
|
245
250
|
csv_decimal = ",",
|
246
251
|
csv_encoding = None
|
@@ -285,9 +290,9 @@ class Client:
|
|
285
290
|
timeout=timeout
|
286
291
|
)
|
287
292
|
|
288
|
-
except:
|
289
|
-
|
290
|
-
print ("
|
293
|
+
except Exception as e:
|
294
|
+
|
295
|
+
print ("Error while accessing ORS server: ", str(e))
|
291
296
|
|
292
297
|
status_code = 99999
|
293
298
|
matrix_df = None
|
@@ -368,7 +373,7 @@ class Client:
|
|
368
373
|
|
369
374
|
else:
|
370
375
|
|
371
|
-
print
|
376
|
+
print(f"Error while accessing ORS server. Status code: {status_code} - {response.reason}")
|
372
377
|
|
373
378
|
matrix_df = None
|
374
379
|
metadata = None
|