BERATools 0.2.0__py3-none-any.whl → 0.2.2__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.
- beratools/__init__.py +1 -7
- beratools/core/algo_centerline.py +491 -351
- beratools/core/algo_common.py +497 -0
- beratools/core/algo_cost.py +192 -0
- beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
- beratools/core/algo_footprint_rel.py +577 -0
- beratools/core/algo_line_grouping.py +944 -0
- beratools/core/algo_merge_lines.py +214 -0
- beratools/core/algo_split_with_lines.py +304 -0
- beratools/core/algo_tiler.py +428 -0
- beratools/core/algo_vertex_optimization.py +469 -0
- beratools/core/constants.py +52 -86
- beratools/core/logger.py +76 -85
- beratools/core/tool_base.py +196 -133
- beratools/gui/__init__.py +11 -15
- beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
- beratools/gui/batch_processing_dlg.py +513 -463
- beratools/gui/bt_data.py +481 -487
- beratools/gui/bt_gui_main.py +710 -691
- beratools/gui/main.py +26 -0
- beratools/gui/map_window.py +162 -146
- beratools/gui/tool_widgets.py +725 -493
- beratools/tools/Beratools_r_script.r +1120 -1120
- beratools/tools/Ht_metrics.py +116 -116
- beratools/tools/__init__.py +7 -7
- beratools/tools/batch_processing.py +136 -132
- beratools/tools/canopy_threshold_relative.py +672 -670
- beratools/tools/canopycostraster.py +222 -222
- beratools/tools/centerline.py +136 -176
- beratools/tools/common.py +857 -885
- beratools/tools/fl_regen_csf.py +428 -428
- beratools/tools/forest_line_attributes.py +408 -408
- beratools/tools/line_footprint_absolute.py +213 -363
- beratools/tools/line_footprint_fixed.py +436 -282
- beratools/tools/line_footprint_functions.py +733 -720
- beratools/tools/line_footprint_relative.py +73 -64
- beratools/tools/line_grouping.py +45 -0
- beratools/tools/ln_relative_metrics.py +615 -615
- beratools/tools/r_cal_lpi_elai.r +24 -24
- beratools/tools/r_generate_pd_focalraster.r +100 -100
- beratools/tools/r_interface.py +79 -79
- beratools/tools/r_point_density.r +8 -8
- beratools/tools/rpy_chm2trees.py +86 -86
- beratools/tools/rpy_dsm_chm_by.py +81 -81
- beratools/tools/rpy_dtm_by.py +63 -63
- beratools/tools/rpy_find_cellsize.py +43 -43
- beratools/tools/rpy_gnd_csf.py +74 -74
- beratools/tools/rpy_hummock_hollow.py +85 -85
- beratools/tools/rpy_hummock_hollow_raster.py +71 -71
- beratools/tools/rpy_las_info.py +51 -51
- beratools/tools/rpy_laz2las.py +40 -40
- beratools/tools/rpy_lpi_elai_lascat.py +466 -466
- beratools/tools/rpy_normalized_lidar_by.py +56 -56
- beratools/tools/rpy_percent_above_dbh.py +80 -80
- beratools/tools/rpy_points2trees.py +88 -88
- beratools/tools/rpy_vegcoverage.py +94 -94
- beratools/tools/tiler.py +48 -206
- beratools/tools/tool_template.py +69 -54
- beratools/tools/vertex_optimization.py +61 -620
- beratools/tools/zonal_threshold.py +144 -144
- beratools-0.2.2.dist-info/METADATA +108 -0
- beratools-0.2.2.dist-info/RECORD +74 -0
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/WHEEL +1 -1
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/licenses/LICENSE +22 -22
- beratools/gui/cli.py +0 -18
- beratools/gui/gui.json +0 -8
- beratools/gui_tk/ASCII Banners.txt +0 -248
- beratools/gui_tk/__init__.py +0 -20
- beratools/gui_tk/beratools_main.py +0 -515
- beratools/gui_tk/bt_widgets.py +0 -442
- beratools/gui_tk/cli.py +0 -18
- beratools/gui_tk/img/BERALogo.png +0 -0
- beratools/gui_tk/img/closed.gif +0 -0
- beratools/gui_tk/img/closed.png +0 -0
- beratools/gui_tk/img/open.gif +0 -0
- beratools/gui_tk/img/open.png +0 -0
- beratools/gui_tk/img/tool.gif +0 -0
- beratools/gui_tk/img/tool.png +0 -0
- beratools/gui_tk/main.py +0 -14
- beratools/gui_tk/map_window.py +0 -144
- beratools/gui_tk/runner.py +0 -1481
- beratools/gui_tk/tooltip.py +0 -55
- beratools/third_party/pyqtlet2/__init__.py +0 -9
- beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
- beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
- beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
- beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
- beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
- beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
- beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
- beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
- beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
- beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
- beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
- beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
- beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
- beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
- beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
- beratools/third_party/pyqtlet2/mapwidget.py +0 -45
- beratools/third_party/pyqtlet2/web/custom.js +0 -43
- beratools/third_party/pyqtlet2/web/map.html +0 -23
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
- beratools/tools/forest_line_ecosite.py +0 -216
- beratools/tools/lapis_all.py +0 -103
- beratools/tools/least_cost_path_from_chm.py +0 -152
- beratools-0.2.0.dist-info/METADATA +0 -63
- beratools-0.2.0.dist-info/RECORD +0 -142
- /beratools/gui/{img → assets}/BERALogo.png +0 -0
- /beratools/gui/{img → assets}/closed.gif +0 -0
- /beratools/gui/{img → assets}/closed.png +0 -0
- /beratools/{gui_tk → gui/assets}/gui.json +0 -0
- /beratools/gui/{img → assets}/open.gif +0 -0
- /beratools/gui/{img → assets}/open.png +0 -0
- /beratools/gui/{img → assets}/tool.gif +0 -0
- /beratools/gui/{img → assets}/tool.png +0 -0
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,282 +1,436 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
for
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2025 Applied Geospatial Research Group.
|
|
3
|
+
|
|
4
|
+
This script is licensed under the GNU General Public License v3.0.
|
|
5
|
+
See <https://gnu.org/licenses/gpl-3.0> for full license details.
|
|
6
|
+
|
|
7
|
+
Author: Richard Zeng, Maverick Fong
|
|
8
|
+
|
|
9
|
+
Description:
|
|
10
|
+
This script is part of the BERA Tools.
|
|
11
|
+
Webpage: https://github.com/appliedgrg/beratools
|
|
12
|
+
|
|
13
|
+
This file hosts the line_footprint_fixed tool.
|
|
14
|
+
"""
|
|
15
|
+
import math
|
|
16
|
+
import time
|
|
17
|
+
from itertools import chain
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import geopandas as gpd
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import shapely.geometry as sh_geom
|
|
24
|
+
import shapely.ops as sh_ops
|
|
25
|
+
|
|
26
|
+
import beratools.core.algo_common as algo_common
|
|
27
|
+
import beratools.core.constants as bt_const
|
|
28
|
+
import beratools.tools.common as bt_common
|
|
29
|
+
from beratools.core.algo_line_grouping import LineGrouping
|
|
30
|
+
from beratools.core.algo_split_with_lines import LineSplitter
|
|
31
|
+
from beratools.core.tool_base import execute_multiprocessing
|
|
32
|
+
|
|
33
|
+
FP_FIXED_WIDTH_DEFAULT = 5.0
|
|
34
|
+
|
|
35
|
+
def prepare_line_args(line_gdf, poly_gdf, n_samples, offset, width_percentile):
|
|
36
|
+
"""
|
|
37
|
+
Generate arguments for each line in the GeoDataFrame.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
line_gdf
|
|
41
|
+
poly_gdf
|
|
42
|
+
n_samples
|
|
43
|
+
offset
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
line_args : list
|
|
47
|
+
row :
|
|
48
|
+
inter_poly :
|
|
49
|
+
n_samples :
|
|
50
|
+
offset :
|
|
51
|
+
width_percentile :
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
spatial_index = poly_gdf.sindex
|
|
55
|
+
line_args = []
|
|
56
|
+
|
|
57
|
+
for idx in line_gdf.index:
|
|
58
|
+
row = line_gdf.loc[[idx]]
|
|
59
|
+
line = row.geometry.iloc[0]
|
|
60
|
+
|
|
61
|
+
# Skip rows where geometry is None
|
|
62
|
+
if line is None:
|
|
63
|
+
print(row)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
inter_poly = poly_gdf.loc[spatial_index.query(line)]
|
|
67
|
+
if bt_const.BT_GROUP in inter_poly.columns:
|
|
68
|
+
inter_poly = inter_poly[
|
|
69
|
+
inter_poly[bt_const.BT_GROUP] == row[bt_const.BT_GROUP].values[0]
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
line_args.append(
|
|
74
|
+
[row, inter_poly, n_samples, offset, width_percentile]
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(e)
|
|
78
|
+
|
|
79
|
+
return line_args
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Calculating Line Widths
|
|
83
|
+
def generate_sample_points(line, n_samples=10):
|
|
84
|
+
"""
|
|
85
|
+
Generate evenly spaced points along a line.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
line (LineString): The line along which to generate points.
|
|
89
|
+
n_samples (int): The number of points to generate (default is 10).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
list: List of shapely Point objects.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
# TODO: determine line type
|
|
96
|
+
try:
|
|
97
|
+
pts = line.coords
|
|
98
|
+
except Exception as e: # TODO: check the code
|
|
99
|
+
print(e)
|
|
100
|
+
line = sh_ops.linemerge(line)
|
|
101
|
+
tuple_coord = sh_geom.mapping(line)["coordinates"]
|
|
102
|
+
pts = list(chain(*tuple_coord))
|
|
103
|
+
|
|
104
|
+
return [sh_geom.Point(item) for item in pts]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def process_single_line(line_arg):
|
|
108
|
+
row = line_arg[0]
|
|
109
|
+
inter_poly = line_arg[1]
|
|
110
|
+
n_samples = line_arg[2]
|
|
111
|
+
offset = line_arg[3]
|
|
112
|
+
width_percentile = line_arg[4]
|
|
113
|
+
|
|
114
|
+
# TODO: deal with case when inter_poly is empty
|
|
115
|
+
try:
|
|
116
|
+
widths, line, perp_lines, perp_lines_original = calculate_average_width(
|
|
117
|
+
row.iloc[0].geometry, inter_poly, offset, n_samples
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(e)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Calculate the 75th percentile width
|
|
124
|
+
# filter zeros in width array
|
|
125
|
+
arr_filter = [False if math.isclose(i, 0.0) else True for i in widths]
|
|
126
|
+
widths = widths[arr_filter]
|
|
127
|
+
|
|
128
|
+
q3_width = FP_FIXED_WIDTH_DEFAULT
|
|
129
|
+
q4_width = FP_FIXED_WIDTH_DEFAULT
|
|
130
|
+
try:
|
|
131
|
+
# TODO: check the code. widths is empty
|
|
132
|
+
if len(widths) > 0:
|
|
133
|
+
q3_width = np.percentile(widths, width_percentile)
|
|
134
|
+
q4_width = np.percentile(widths, 90)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(e)
|
|
137
|
+
|
|
138
|
+
# Store the 75th percentile width as a new attribute
|
|
139
|
+
row["avg_width"] = q3_width
|
|
140
|
+
row["max_width"] = q4_width
|
|
141
|
+
|
|
142
|
+
row["geometry"] = line
|
|
143
|
+
try:
|
|
144
|
+
row["perp_lines"] = perp_lines
|
|
145
|
+
row["perp_lines_original"] = perp_lines_original
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(e)
|
|
148
|
+
|
|
149
|
+
return row
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def generate_fixed_width_footprint(line_gdf, max_width=False):
|
|
153
|
+
"""
|
|
154
|
+
Create a buffer around each line.
|
|
155
|
+
|
|
156
|
+
In the GeoDataFrame using its 'max_width' attribute and
|
|
157
|
+
saves the resulting polygons in a new shapefile.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
line_gdf: GeoDataFrame containing LineString with 'max_width' attribute.
|
|
161
|
+
max_width: Use max width or not to produce buffer.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
# Create a new GeoDataFrame with the buffer polygons
|
|
165
|
+
buffer_gdf = line_gdf.copy(deep=True)
|
|
166
|
+
|
|
167
|
+
mean_avg_width = line_gdf["avg_width"].mean()
|
|
168
|
+
mean_max_width = line_gdf["max_width"].mean()
|
|
169
|
+
|
|
170
|
+
# Use .loc to avoid chained assignment
|
|
171
|
+
line_gdf.loc[line_gdf["avg_width"].isna(), "avg_width"] = mean_avg_width
|
|
172
|
+
line_gdf.loc[line_gdf["max_width"].isna(), "max_width"] = mean_max_width
|
|
173
|
+
|
|
174
|
+
line_gdf.loc[line_gdf["avg_width"] == 0.0, "avg_width"] = mean_avg_width
|
|
175
|
+
line_gdf.loc[line_gdf["max_width"] == 0.0, "max_width"] = mean_max_width
|
|
176
|
+
|
|
177
|
+
if not max_width:
|
|
178
|
+
print("Using quantile 75% width")
|
|
179
|
+
buffer_gdf["geometry"] = line_gdf.apply(
|
|
180
|
+
lambda row: row.geometry.buffer(row.avg_width / 2)
|
|
181
|
+
if row.geometry is not None
|
|
182
|
+
else None,
|
|
183
|
+
axis=1,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
print("Using quantile 90% + 20% width")
|
|
187
|
+
buffer_gdf["geometry"] = line_gdf.apply(
|
|
188
|
+
lambda row: row.geometry.buffer(row.max_width * 1.2 / 2)
|
|
189
|
+
if row.geometry is not None
|
|
190
|
+
else None,
|
|
191
|
+
axis=1,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return buffer_gdf
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def smooth_linestring(line, tolerance=0.5):
|
|
198
|
+
"""
|
|
199
|
+
Smooths a LineString geometry using the Ramer-Douglas-Peucker algorithm.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
line: The LineString geometry to smooth.
|
|
203
|
+
tolerance: The maximum distance from a point to a line for the point
|
|
204
|
+
to be considered part of the line.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The smoothed LineString geometry.
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
simplified_line = line.simplify(tolerance)
|
|
211
|
+
# simplified_line = line
|
|
212
|
+
return simplified_line
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def calculate_average_width(line, in_poly, offset, n_samples):
|
|
216
|
+
"""Calculate the average width of a polygon perpendicular to the given line."""
|
|
217
|
+
# Smooth the line
|
|
218
|
+
try:
|
|
219
|
+
line = smooth_linestring(line, tolerance=0.1)
|
|
220
|
+
|
|
221
|
+
valid_widths = 0
|
|
222
|
+
sample_points = generate_sample_points(line, n_samples=n_samples)
|
|
223
|
+
sample_points_pairs = list(
|
|
224
|
+
zip(sample_points[:-2], sample_points[1:-1], sample_points[2:])
|
|
225
|
+
)
|
|
226
|
+
widths = np.zeros(len(sample_points_pairs))
|
|
227
|
+
perp_lines = []
|
|
228
|
+
perp_lines_original = []
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(e)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
for i, points in enumerate(sample_points_pairs):
|
|
234
|
+
perp_line = algo_common.generate_perpendicular_line_precise(
|
|
235
|
+
points, offset=offset
|
|
236
|
+
)
|
|
237
|
+
perp_lines_original.append(perp_line)
|
|
238
|
+
|
|
239
|
+
polygon_intersect = in_poly.iloc[in_poly.sindex.query(perp_line)]
|
|
240
|
+
intersections = polygon_intersect.intersection(perp_line)
|
|
241
|
+
|
|
242
|
+
line_list = []
|
|
243
|
+
for inter in intersections:
|
|
244
|
+
if inter.is_empty:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
if isinstance(inter, sh_geom.GeometryCollection):
|
|
248
|
+
for item in inter.geoms:
|
|
249
|
+
if isinstance(item, sh_geom.LineString):
|
|
250
|
+
line_list.append(item)
|
|
251
|
+
elif isinstance(inter, sh_geom.MultiLineString):
|
|
252
|
+
line_list += list(inter.geoms)
|
|
253
|
+
else:
|
|
254
|
+
line_list.append(inter)
|
|
255
|
+
|
|
256
|
+
perp_lines += line_list
|
|
257
|
+
|
|
258
|
+
if isinstance(line_list, sh_geom.GeometryCollection):
|
|
259
|
+
print("Found 2: GeometryCollection")
|
|
260
|
+
|
|
261
|
+
for item in line_list:
|
|
262
|
+
widths[i] = max(widths[i], item.length)
|
|
263
|
+
valid_widths += 1
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
print(f"loop: {e}")
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
widths,
|
|
270
|
+
line,
|
|
271
|
+
sh_geom.MultiLineString(perp_lines),
|
|
272
|
+
sh_geom.MultiLineString(perp_lines_original),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def line_footprint_fixed(
|
|
277
|
+
in_line,
|
|
278
|
+
in_footprint,
|
|
279
|
+
n_samples,
|
|
280
|
+
offset,
|
|
281
|
+
max_width,
|
|
282
|
+
out_footprint,
|
|
283
|
+
processes,
|
|
284
|
+
verbose,
|
|
285
|
+
in_layer=None,
|
|
286
|
+
in_layer_lc_path="least_cost_path",
|
|
287
|
+
in_layer_fp=None,
|
|
288
|
+
out_layer=None,
|
|
289
|
+
merge_group=True,
|
|
290
|
+
width_percentile=75,
|
|
291
|
+
parallel_mode=bt_const.ParallelMode.MULTIPROCESSING
|
|
292
|
+
):
|
|
293
|
+
n_samples = int(n_samples)
|
|
294
|
+
offset = float(offset)
|
|
295
|
+
width_percentile=int(width_percentile)
|
|
296
|
+
|
|
297
|
+
# TODO: refactor this code for better line quality check
|
|
298
|
+
line_gdf = gpd.read_file(in_line, layer=in_layer)
|
|
299
|
+
lc_path_gdf = gpd.read_file(in_line, layer=in_layer_lc_path)
|
|
300
|
+
if not merge_group:
|
|
301
|
+
line_gdf.geometry = line_gdf.line_merge()
|
|
302
|
+
lc_path_gdf.geometry = lc_path_gdf.line_merge()
|
|
303
|
+
|
|
304
|
+
line_gdf = algo_common.clean_line_geometries(line_gdf)
|
|
305
|
+
|
|
306
|
+
# read footprints and remove holes
|
|
307
|
+
poly_gdf = gpd.read_file(in_footprint, layer=in_layer_fp)
|
|
308
|
+
poly_gdf["geometry"] = poly_gdf["geometry"].apply(algo_common.remove_holes)
|
|
309
|
+
|
|
310
|
+
# split lines
|
|
311
|
+
merged_line_gdf = line_gdf.copy(deep=True)
|
|
312
|
+
if merge_group:
|
|
313
|
+
lg = LineGrouping(line_gdf, merge_group)
|
|
314
|
+
lg.run_grouping()
|
|
315
|
+
merged_line_gdf = lg.run_line_merge()
|
|
316
|
+
else:
|
|
317
|
+
# merge group first, then not merge after splitting
|
|
318
|
+
try:
|
|
319
|
+
lg = LineGrouping(line_gdf, not merge_group)
|
|
320
|
+
lg.run_grouping()
|
|
321
|
+
merged_line_gdf = lg.run_line_merge()
|
|
322
|
+
splitter = LineSplitter(merged_line_gdf)
|
|
323
|
+
splitter.process()
|
|
324
|
+
splitter.save_to_geopackage(
|
|
325
|
+
out_footprint,
|
|
326
|
+
line_layer="split_centerline",
|
|
327
|
+
intersection_layer="inter_points",
|
|
328
|
+
invalid_layer="invalid_splits",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# least cost path merge and split
|
|
332
|
+
lg_leastcost = LineGrouping(lc_path_gdf, not merge_group)
|
|
333
|
+
lg_leastcost.run_grouping()
|
|
334
|
+
merged_lc_path_gdf = lg_leastcost.run_line_merge()
|
|
335
|
+
splitter_leastcost = LineSplitter(merged_lc_path_gdf)
|
|
336
|
+
splitter_leastcost.process(splitter.intersection_gdf)
|
|
337
|
+
|
|
338
|
+
splitter_leastcost.save_to_geopackage(
|
|
339
|
+
out_footprint,
|
|
340
|
+
line_layer="split_leastcost",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
lg = LineGrouping(splitter.split_lines_gdf, merge_group)
|
|
344
|
+
lg.run_grouping()
|
|
345
|
+
merged_line_gdf = lg.run_line_merge()
|
|
346
|
+
except ValueError as e:
|
|
347
|
+
print(f"Exception: line_footprint_fixed: {e}")
|
|
348
|
+
|
|
349
|
+
# save original merged lines
|
|
350
|
+
merged_line_gdf.to_file(out_footprint, layer="merged_lines_original")
|
|
351
|
+
|
|
352
|
+
# prepare line arguments
|
|
353
|
+
line_args = prepare_line_args(
|
|
354
|
+
merged_line_gdf, poly_gdf, n_samples, offset, width_percentile
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
out_lines = execute_multiprocessing(
|
|
358
|
+
process_single_line, line_args, "Fixed footprint", processes, mode=parallel_mode
|
|
359
|
+
)
|
|
360
|
+
line_attr = pd.concat(out_lines)
|
|
361
|
+
|
|
362
|
+
############################################
|
|
363
|
+
# update avg_width and max_width by max value of group
|
|
364
|
+
group_max = line_attr.groupby(bt_const.BT_GROUP).agg({
|
|
365
|
+
'avg_width': 'max',
|
|
366
|
+
'max_width': 'max'
|
|
367
|
+
}).reset_index()
|
|
368
|
+
|
|
369
|
+
# Step 2: Merge the result back to the original dataframe based on 'group'
|
|
370
|
+
line_attr = line_attr.merge(group_max, on=bt_const.BT_GROUP, suffixes=('', '_max'))
|
|
371
|
+
|
|
372
|
+
# Step 3: Overwrite the original columns directly with the max values
|
|
373
|
+
line_attr['avg_width'] = line_attr['avg_width_max']
|
|
374
|
+
line_attr['max_width'] = line_attr['max_width_max']
|
|
375
|
+
|
|
376
|
+
# Drop the temporary max columns
|
|
377
|
+
line_attr.drop(columns=['avg_width_max', 'max_width_max'], inplace=True)
|
|
378
|
+
# Done: updating avg_width and max_width
|
|
379
|
+
############################################
|
|
380
|
+
|
|
381
|
+
# create fixed width footprint
|
|
382
|
+
buffer_gdf = generate_fixed_width_footprint(line_attr, max_width=max_width)
|
|
383
|
+
|
|
384
|
+
# Save the lines with attributes and polygons to a new file
|
|
385
|
+
perp_lines_gdf = buffer_gdf.copy(deep=True)
|
|
386
|
+
perp_lines_original_gdf = buffer_gdf.copy(deep=True)
|
|
387
|
+
|
|
388
|
+
# save fixed width footprint
|
|
389
|
+
buffer_gdf = buffer_gdf.drop(columns=["perp_lines"])
|
|
390
|
+
buffer_gdf = buffer_gdf.drop(columns=["perp_lines_original"])
|
|
391
|
+
buffer_gdf = buffer_gdf.set_crs(perp_lines_gdf.crs, allow_override=True)
|
|
392
|
+
buffer_gdf.reset_index(inplace=True, drop=True)
|
|
393
|
+
|
|
394
|
+
# trim lines and footprints
|
|
395
|
+
lg.run_cleanup(buffer_gdf)
|
|
396
|
+
lg.save_file(out_footprint)
|
|
397
|
+
|
|
398
|
+
# perpendicular lines
|
|
399
|
+
layer = "perp_lines"
|
|
400
|
+
out_footprint = Path(out_footprint)
|
|
401
|
+
out_aux_gpkg = out_footprint.with_stem(out_footprint.stem + "_aux").with_suffix(
|
|
402
|
+
".gpkg"
|
|
403
|
+
)
|
|
404
|
+
perp_lines_gdf = perp_lines_gdf.set_geometry("perp_lines")
|
|
405
|
+
perp_lines_gdf = perp_lines_gdf.drop(columns=["perp_lines_original"])
|
|
406
|
+
perp_lines_gdf = perp_lines_gdf.drop(columns=["geometry"])
|
|
407
|
+
perp_lines_gdf = perp_lines_gdf.set_crs(buffer_gdf.crs, allow_override=True)
|
|
408
|
+
perp_lines_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
409
|
+
|
|
410
|
+
layer = "perp_lines_original"
|
|
411
|
+
perp_lines_original_gdf = perp_lines_original_gdf.set_geometry(
|
|
412
|
+
"perp_lines_original"
|
|
413
|
+
)
|
|
414
|
+
perp_lines_original_gdf = perp_lines_original_gdf.drop(columns=["perp_lines"])
|
|
415
|
+
perp_lines_original_gdf = perp_lines_original_gdf.drop(columns=["geometry"])
|
|
416
|
+
perp_lines_original_gdf = perp_lines_original_gdf.set_crs(
|
|
417
|
+
buffer_gdf.crs, allow_override=True
|
|
418
|
+
)
|
|
419
|
+
perp_lines_original_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
420
|
+
|
|
421
|
+
layer = "centerline_simplified"
|
|
422
|
+
line_attr = line_attr.drop(columns="perp_lines")
|
|
423
|
+
line_attr.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
424
|
+
|
|
425
|
+
# save footprints without holes
|
|
426
|
+
poly_gdf.to_file(out_aux_gpkg.as_posix(), layer="footprint_no_holes")
|
|
427
|
+
|
|
428
|
+
print("Fixed width footprint tool finished.")
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
in_args, in_verbose = bt_common.check_arguments()
|
|
432
|
+
start_time = time.time()
|
|
433
|
+
line_footprint_fixed(
|
|
434
|
+
**in_args.input, processes=int(in_args.processes), verbose=in_verbose
|
|
435
|
+
)
|
|
436
|
+
print("Elapsed time: {}".format(time.time() - start_time))
|