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.
Files changed (153) hide show
  1. beratools/__init__.py +1 -7
  2. beratools/core/algo_centerline.py +491 -351
  3. beratools/core/algo_common.py +497 -0
  4. beratools/core/algo_cost.py +192 -0
  5. beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
  6. beratools/core/algo_footprint_rel.py +577 -0
  7. beratools/core/algo_line_grouping.py +944 -0
  8. beratools/core/algo_merge_lines.py +214 -0
  9. beratools/core/algo_split_with_lines.py +304 -0
  10. beratools/core/algo_tiler.py +428 -0
  11. beratools/core/algo_vertex_optimization.py +469 -0
  12. beratools/core/constants.py +52 -86
  13. beratools/core/logger.py +76 -85
  14. beratools/core/tool_base.py +196 -133
  15. beratools/gui/__init__.py +11 -15
  16. beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
  17. beratools/gui/batch_processing_dlg.py +513 -463
  18. beratools/gui/bt_data.py +481 -487
  19. beratools/gui/bt_gui_main.py +710 -691
  20. beratools/gui/main.py +26 -0
  21. beratools/gui/map_window.py +162 -146
  22. beratools/gui/tool_widgets.py +725 -493
  23. beratools/tools/Beratools_r_script.r +1120 -1120
  24. beratools/tools/Ht_metrics.py +116 -116
  25. beratools/tools/__init__.py +7 -7
  26. beratools/tools/batch_processing.py +136 -132
  27. beratools/tools/canopy_threshold_relative.py +672 -670
  28. beratools/tools/canopycostraster.py +222 -222
  29. beratools/tools/centerline.py +136 -176
  30. beratools/tools/common.py +857 -885
  31. beratools/tools/fl_regen_csf.py +428 -428
  32. beratools/tools/forest_line_attributes.py +408 -408
  33. beratools/tools/line_footprint_absolute.py +213 -363
  34. beratools/tools/line_footprint_fixed.py +436 -282
  35. beratools/tools/line_footprint_functions.py +733 -720
  36. beratools/tools/line_footprint_relative.py +73 -64
  37. beratools/tools/line_grouping.py +45 -0
  38. beratools/tools/ln_relative_metrics.py +615 -615
  39. beratools/tools/r_cal_lpi_elai.r +24 -24
  40. beratools/tools/r_generate_pd_focalraster.r +100 -100
  41. beratools/tools/r_interface.py +79 -79
  42. beratools/tools/r_point_density.r +8 -8
  43. beratools/tools/rpy_chm2trees.py +86 -86
  44. beratools/tools/rpy_dsm_chm_by.py +81 -81
  45. beratools/tools/rpy_dtm_by.py +63 -63
  46. beratools/tools/rpy_find_cellsize.py +43 -43
  47. beratools/tools/rpy_gnd_csf.py +74 -74
  48. beratools/tools/rpy_hummock_hollow.py +85 -85
  49. beratools/tools/rpy_hummock_hollow_raster.py +71 -71
  50. beratools/tools/rpy_las_info.py +51 -51
  51. beratools/tools/rpy_laz2las.py +40 -40
  52. beratools/tools/rpy_lpi_elai_lascat.py +466 -466
  53. beratools/tools/rpy_normalized_lidar_by.py +56 -56
  54. beratools/tools/rpy_percent_above_dbh.py +80 -80
  55. beratools/tools/rpy_points2trees.py +88 -88
  56. beratools/tools/rpy_vegcoverage.py +94 -94
  57. beratools/tools/tiler.py +48 -206
  58. beratools/tools/tool_template.py +69 -54
  59. beratools/tools/vertex_optimization.py +61 -620
  60. beratools/tools/zonal_threshold.py +144 -144
  61. beratools-0.2.2.dist-info/METADATA +108 -0
  62. beratools-0.2.2.dist-info/RECORD +74 -0
  63. {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/WHEEL +1 -1
  64. {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/licenses/LICENSE +22 -22
  65. beratools/gui/cli.py +0 -18
  66. beratools/gui/gui.json +0 -8
  67. beratools/gui_tk/ASCII Banners.txt +0 -248
  68. beratools/gui_tk/__init__.py +0 -20
  69. beratools/gui_tk/beratools_main.py +0 -515
  70. beratools/gui_tk/bt_widgets.py +0 -442
  71. beratools/gui_tk/cli.py +0 -18
  72. beratools/gui_tk/img/BERALogo.png +0 -0
  73. beratools/gui_tk/img/closed.gif +0 -0
  74. beratools/gui_tk/img/closed.png +0 -0
  75. beratools/gui_tk/img/open.gif +0 -0
  76. beratools/gui_tk/img/open.png +0 -0
  77. beratools/gui_tk/img/tool.gif +0 -0
  78. beratools/gui_tk/img/tool.png +0 -0
  79. beratools/gui_tk/main.py +0 -14
  80. beratools/gui_tk/map_window.py +0 -144
  81. beratools/gui_tk/runner.py +0 -1481
  82. beratools/gui_tk/tooltip.py +0 -55
  83. beratools/third_party/pyqtlet2/__init__.py +0 -9
  84. beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
  85. beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
  86. beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
  87. beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
  88. beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
  89. beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
  90. beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
  91. beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
  92. beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
  93. beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
  94. beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
  95. beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
  96. beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
  97. beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
  98. beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
  99. beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
  100. beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
  101. beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
  102. beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
  103. beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
  104. beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
  105. beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
  106. beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
  107. beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
  108. beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
  109. beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
  110. beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
  111. beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
  112. beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
  113. beratools/third_party/pyqtlet2/mapwidget.py +0 -45
  114. beratools/third_party/pyqtlet2/web/custom.js +0 -43
  115. beratools/third_party/pyqtlet2/web/map.html +0 -23
  116. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
  117. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
  118. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
  119. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
  120. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
  121. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
  122. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
  123. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
  124. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
  125. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
  126. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
  127. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
  128. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
  129. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
  130. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
  131. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
  132. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
  133. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
  134. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
  135. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
  136. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
  137. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
  138. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
  139. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
  140. beratools/tools/forest_line_ecosite.py +0 -216
  141. beratools/tools/lapis_all.py +0 -103
  142. beratools/tools/least_cost_path_from_chm.py +0 -152
  143. beratools-0.2.0.dist-info/METADATA +0 -63
  144. beratools-0.2.0.dist-info/RECORD +0 -142
  145. /beratools/gui/{img → assets}/BERALogo.png +0 -0
  146. /beratools/gui/{img → assets}/closed.gif +0 -0
  147. /beratools/gui/{img → assets}/closed.png +0 -0
  148. /beratools/{gui_tk → gui/assets}/gui.json +0 -0
  149. /beratools/gui/{img → assets}/open.gif +0 -0
  150. /beratools/gui/{img → assets}/open.png +0 -0
  151. /beratools/gui/{img → assets}/tool.gif +0 -0
  152. /beratools/gui/{img → assets}/tool.png +0 -0
  153. {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -1,282 +1,436 @@
1
- import time
2
- from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString
3
- from beratools.tools.common import *
4
-
5
-
6
- def prepare_line_args(shp_line, shp_poly, n_samples, offset):
7
- """
8
- Parameters
9
- ----------
10
- shp_line
11
- shp_poly
12
- n_samples
13
- offset
14
-
15
- Returns
16
- -------
17
- line_args : list
18
- row :
19
- inter_poly :
20
- n_samples :
21
- offset :
22
- i : line ID
23
-
24
- """
25
- line_gdf = gpd.read_file(shp_line)
26
- poly_gdf = gpd.read_file(shp_poly)
27
- spatial_index = poly_gdf.sindex
28
- line_args = []
29
-
30
- i = 0
31
- for i, row in line_gdf.iterrows():
32
- line = row.geometry
33
-
34
- # Skip rows where geometry is None
35
- if line is None:
36
- print(row)
37
- continue
38
-
39
- inter_poly = poly_gdf.iloc[spatial_index.query(line)]
40
- line_args.append([line_gdf.iloc[[i]], inter_poly, n_samples, offset, i])
41
-
42
- return line_args
43
-
44
-
45
- # Calculating Line Widths
46
- def generate_sample_points(line, n_samples=10):
47
- """
48
- Generate evenly spaced points along a line.
49
-
50
- Parameters
51
- ----------
52
- line : shapely LineString
53
- The line along which to generate points.
54
- n_samples : int, optional
55
- The number of points to generate (default is 10).
56
-
57
- Returns
58
- -------
59
- list
60
- List of shapely Point objects.
61
- """
62
- # return [line.interpolate(i / n_samples, normalized=True) for i in range(n_samples)]
63
- return [Point(item) for item in list(line.coords)]
64
-
65
-
66
- def generate_perpendicular_line(point, line, offset=FP_PERP_LINE_OFFSET):
67
- """
68
- Generate a perpendicular line to the input line at the given point.
69
-
70
- Parameters
71
- ----------
72
- point : shapely.geometry.Point
73
- The point on the line where the perpendicular should be generated.
74
- line : shapely.geometry.LineString
75
- The line to which the perpendicular line will be generated.
76
- offset : float, optional
77
- The length of the perpendicular line.
78
-
79
- Returns
80
- -------
81
- shapely.geometry.LineString
82
- The generated perpendicular line.
83
- """
84
- # Compute the angle of the line
85
- p1, p2 = line.coords[0], line.coords[-1] # Modify this line
86
- angle = np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
87
-
88
- # Compute the angle of the perpendicular line
89
- angle_perp = angle + np.pi / 2.0 # Perpendicular angle
90
-
91
- # Generate the perpendicular line
92
- perp_line = LineString([(point.x - offset * np.cos(angle_perp), point.y - offset * np.sin(angle_perp)),
93
- (point.x + offset * np.cos(angle_perp), point.y + offset * np.sin(angle_perp))])
94
-
95
- return perp_line
96
-
97
-
98
- def process_single_line(line_arg):
99
- row = line_arg[0]
100
- inter_poly = line_arg[1]
101
- n_samples = line_arg[2]
102
- offset = line_arg[3]
103
- line_id = line_arg[4]
104
-
105
- widths, line, perp_lines = calculate_average_width(row.iloc[0].geometry, inter_poly, offset, n_samples)
106
-
107
- # Calculate the 75th percentile width
108
- # filter zeros in width array
109
- arr_filter = [False if math.isclose(i, 0.0) else True for i in widths]
110
- widths = widths[arr_filter]
111
-
112
- q3_width = FP_FIXED_WIDTH_DEFAULT
113
- q4_width = FP_FIXED_WIDTH_DEFAULT
114
- try:
115
- q3_width = np.percentile(widths, 40)
116
- q4_width = np.percentile(widths, 90)
117
- except Exception as e:
118
- print(e)
119
-
120
- # Store the 75th percentile width as a new attribute
121
- row['avg_width'] = q3_width
122
- row['max_width'] = q4_width
123
- hist, bins = np.histogram(widths)
124
- bins = pd.Series(bins).rolling(2).mean()[1:].to_numpy() # mid-points of bins
125
- row['width_hist'] = str(hist)
126
- row['width_bins'] = str(bins)
127
-
128
- row['geometry'] = line
129
- try:
130
- row['perp_lines'] = perp_lines
131
- except Exception as e:
132
- print(e)
133
-
134
- print('line processed: {}'.format(line_id))
135
-
136
- return row
137
-
138
-
139
- def generate_fixed_width_footprint(line_gdf, shp_footprint, max_width=False):
140
- """
141
- Creates a buffer around each line in the GeoDataFrame using its 'max_width' attribute and
142
- saves the resulting polygons in a new shapefile.
143
-
144
- Parameters:
145
- - line_gdf: A GeoDataFrame containing LineString geometries with 'max_width' attribute.
146
- - output_file_path: The path where the output shapefile will be stored.
147
- """
148
- # Create a new GeoDataFrame with the buffer polygons
149
- buffer_gdf = line_gdf.copy(deep=True)
150
-
151
- mean_avg_width = line_gdf['avg_width'].mean()
152
- mean_max_width = line_gdf['max_width'].mean()
153
-
154
- line_gdf['avg_width'].fillna(mean_avg_width, inplace=True)
155
- line_gdf['max_width'].fillna(mean_max_width, inplace=True)
156
-
157
- line_gdf['avg_width'].replace(0.0, mean_avg_width, inplace=True)
158
- line_gdf['max_width'].replace(0.0, mean_max_width, inplace=True)
159
-
160
- if not max_width:
161
- print('Using quantile 75% width')
162
- buffer_gdf['geometry'] = line_gdf.apply(
163
- lambda row: row.geometry.buffer(row.avg_width / 2) if row.geometry is not None else None, axis=1)
164
- else:
165
- print('Using quantile 90% + 20% width')
166
- buffer_gdf['geometry'] = line_gdf.apply(
167
- lambda row: row.geometry.buffer(row.max_width * 1.2 / 2) if row.geometry is not None else None, axis=1)
168
-
169
- return buffer_gdf
170
-
171
-
172
- def smooth_linestring(line, tolerance=1.0):
173
- """
174
- Smooths a LineString geometry using the Ramer-Douglas-Peucker algorithm.
175
-
176
- Parameters:
177
- - line: The LineString geometry to smooth.
178
- - tolerance: The maximum distance from a point to a line for the point to be considered part of the line.
179
-
180
- Returns:
181
- The smoothed LineString geometry.
182
- """
183
- # simplified_line = line.simplify(tolerance)
184
- simplified_line = line
185
- return simplified_line
186
-
187
-
188
- def calculate_average_width(line, polygon, offset, n_samples):
189
- """
190
- Calculates the average width of a polygon perpendicular to the given line.
191
- """
192
- # Smooth the line
193
- line = smooth_linestring(line, tolerance=1.0)
194
-
195
- valid_widths = 0
196
- sample_points = generate_sample_points(line, n_samples=n_samples)
197
- sample_points_pairs = list(zip(sample_points[:-2], sample_points[1:-1], sample_points[2:]))
198
- widths = np.zeros(len(sample_points_pairs))
199
- perp_lines = []
200
-
201
- # remove polygon holes
202
- poly_list = []
203
- for geom in polygon.geometry:
204
- if type(geom) is MultiPolygon:
205
- for item in geom.geoms:
206
- poly_list.append(Polygon(list(item.exterior.coords)))
207
- else:
208
- poly_list.append(Polygon(list(geom.exterior.coords)))
209
-
210
- polygon_no_holes = gpd.GeoDataFrame(geometry=poly_list, crs=polygon.crs)
211
-
212
- for i, points in enumerate(sample_points_pairs):
213
- perp_line = generate_perpendicular_line_precise(points, offset=offset)
214
-
215
- polygon_intersect = polygon_no_holes.iloc[polygon_no_holes.sindex.query(perp_line)]
216
- intersections = polygon_intersect.intersection(perp_line)
217
-
218
- line_list = []
219
- try:
220
- for inter in intersections:
221
- if not inter.is_empty:
222
- if type(inter) is MultiLineString:
223
- line_list += list(inter.geoms)
224
- else:
225
- line_list.append(inter)
226
-
227
- perp_lines += line_list
228
- except Exception as e:
229
- print(e)
230
-
231
- try:
232
- for item in line_list:
233
- widths[i] = max(widths[i], item.length)
234
- valid_widths += 1
235
- except Exception as e:
236
- print(e)
237
-
238
- return widths, line, MultiLineString(perp_lines)
239
-
240
-
241
- def line_footprint_fixed(callback, in_line, in_footprint, n_samples, offset, max_width,
242
- out_footprint, processes, verbose):
243
- n_samples = int(n_samples)
244
- offset = float(offset)
245
- line_args = prepare_line_args(in_line, in_footprint, n_samples, offset)
246
-
247
- out_lines = execute_multiprocessing(process_single_line, line_args, 'Fixed footprint',
248
- processes, 1, verbose=verbose)
249
- line_attr = pd.concat(out_lines)
250
-
251
- # create fixed width footprint
252
- buffer_gdf = generate_fixed_width_footprint(line_attr, in_footprint, max_width=max_width)
253
-
254
- # Save the lines with attributes and polygons to a new shapefile
255
- perp_lines_gdf = buffer_gdf.copy(deep=True)
256
- buffer_gdf = buffer_gdf.drop(columns=['perp_lines'])
257
- buffer_gdf.crs = perp_lines_gdf.crs
258
- buffer_gdf.to_file(out_footprint)
259
-
260
- # perpendicular lines
261
- perp_lines_gdf = perp_lines_gdf.set_geometry('perp_lines')
262
- perp_lines_gdf = perp_lines_gdf.drop(columns=['geometry'])
263
- perp_lines_gdf.crs = buffer_gdf.crs
264
- perp_lines_path = Path(out_footprint).with_stem(Path(out_footprint).stem + '_perp_lines')
265
- perp_lines_gdf.to_file(perp_lines_path)
266
-
267
- geojson_path = Path(out_footprint).with_suffix('.geojson')
268
- buffer_gpd_4326 = buffer_gdf.to_crs('EPSG:4326')
269
- buffer_gpd_4326.to_file(geojson_path.as_posix(), driver='GeoJSON')
270
-
271
- gdf_simplified_path = Path(in_line).with_stem(Path(in_line).stem + "_simplified")
272
- line_attr = line_attr.drop(columns='perp_lines')
273
- line_attr.to_file(gdf_simplified_path)
274
-
275
- callback('Fixed width footprint tool finished.')
276
-
277
-
278
- if __name__ == '__main__':
279
- in_args, in_verbose = check_arguments()
280
- start_time = time.time()
281
- line_footprint_fixed(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
282
- print('Elapsed time: {}'.format(time.time() - start_time))
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))