BERATools 0.2.2__py3-none-any.whl → 0.2.4__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 (78) hide show
  1. beratools/__init__.py +8 -3
  2. beratools/core/{algo_footprint_rel.py → algo_canopy_footprint_exp.py} +176 -139
  3. beratools/core/algo_centerline.py +61 -77
  4. beratools/core/algo_common.py +48 -57
  5. beratools/core/algo_cost.py +18 -25
  6. beratools/core/algo_dijkstra.py +37 -45
  7. beratools/core/algo_line_grouping.py +100 -100
  8. beratools/core/algo_merge_lines.py +40 -8
  9. beratools/core/algo_split_with_lines.py +289 -304
  10. beratools/core/algo_vertex_optimization.py +25 -46
  11. beratools/core/canopy_threshold_relative.py +755 -0
  12. beratools/core/constants.py +8 -9
  13. beratools/{tools → core}/line_footprint_functions.py +411 -258
  14. beratools/core/logger.py +18 -2
  15. beratools/core/tool_base.py +17 -75
  16. beratools/gui/assets/BERALogo.ico +0 -0
  17. beratools/gui/assets/BERA_Splash.gif +0 -0
  18. beratools/gui/assets/BERA_WizardImage.png +0 -0
  19. beratools/gui/assets/beratools.json +475 -2171
  20. beratools/gui/bt_data.py +585 -234
  21. beratools/gui/bt_gui_main.py +129 -91
  22. beratools/gui/main.py +4 -7
  23. beratools/gui/tool_widgets.py +530 -354
  24. beratools/tools/__init__.py +0 -7
  25. beratools/tools/{line_footprint_absolute.py → canopy_footprint_absolute.py} +81 -56
  26. beratools/tools/canopy_footprint_exp.py +113 -0
  27. beratools/tools/centerline.py +30 -37
  28. beratools/tools/check_seed_line.py +127 -0
  29. beratools/tools/common.py +65 -586
  30. beratools/tools/{line_footprint_fixed.py → ground_footprint.py} +140 -117
  31. beratools/tools/line_footprint_relative.py +64 -35
  32. beratools/tools/tool_template.py +48 -40
  33. beratools/tools/vertex_optimization.py +20 -34
  34. beratools/utility/env_checks.py +53 -0
  35. beratools/utility/spatial_common.py +210 -0
  36. beratools/utility/tool_args.py +138 -0
  37. beratools-0.2.4.dist-info/METADATA +134 -0
  38. beratools-0.2.4.dist-info/RECORD +50 -0
  39. {beratools-0.2.2.dist-info → beratools-0.2.4.dist-info}/WHEEL +1 -1
  40. beratools-0.2.4.dist-info/entry_points.txt +3 -0
  41. beratools-0.2.4.dist-info/licenses/LICENSE +674 -0
  42. beratools/core/algo_tiler.py +0 -428
  43. beratools/gui/__init__.py +0 -11
  44. beratools/gui/batch_processing_dlg.py +0 -513
  45. beratools/gui/map_window.py +0 -162
  46. beratools/tools/Beratools_r_script.r +0 -1120
  47. beratools/tools/Ht_metrics.py +0 -116
  48. beratools/tools/batch_processing.py +0 -136
  49. beratools/tools/canopy_threshold_relative.py +0 -672
  50. beratools/tools/canopycostraster.py +0 -222
  51. beratools/tools/fl_regen_csf.py +0 -428
  52. beratools/tools/forest_line_attributes.py +0 -408
  53. beratools/tools/line_grouping.py +0 -45
  54. beratools/tools/ln_relative_metrics.py +0 -615
  55. beratools/tools/r_cal_lpi_elai.r +0 -25
  56. beratools/tools/r_generate_pd_focalraster.r +0 -101
  57. beratools/tools/r_interface.py +0 -80
  58. beratools/tools/r_point_density.r +0 -9
  59. beratools/tools/rpy_chm2trees.py +0 -86
  60. beratools/tools/rpy_dsm_chm_by.py +0 -81
  61. beratools/tools/rpy_dtm_by.py +0 -63
  62. beratools/tools/rpy_find_cellsize.py +0 -43
  63. beratools/tools/rpy_gnd_csf.py +0 -74
  64. beratools/tools/rpy_hummock_hollow.py +0 -85
  65. beratools/tools/rpy_hummock_hollow_raster.py +0 -71
  66. beratools/tools/rpy_las_info.py +0 -51
  67. beratools/tools/rpy_laz2las.py +0 -40
  68. beratools/tools/rpy_lpi_elai_lascat.py +0 -466
  69. beratools/tools/rpy_normalized_lidar_by.py +0 -56
  70. beratools/tools/rpy_percent_above_dbh.py +0 -80
  71. beratools/tools/rpy_points2trees.py +0 -88
  72. beratools/tools/rpy_vegcoverage.py +0 -94
  73. beratools/tools/tiler.py +0 -48
  74. beratools/tools/zonal_threshold.py +0 -144
  75. beratools-0.2.2.dist-info/METADATA +0 -108
  76. beratools-0.2.2.dist-info/RECORD +0 -74
  77. beratools-0.2.2.dist-info/entry_points.txt +0 -2
  78. beratools-0.2.2.dist-info/licenses/LICENSE +0 -22
@@ -10,28 +10,38 @@ Description:
10
10
  This script is part of the BERA Tools.
11
11
  Webpage: https://github.com/appliedgrg/beratools
12
12
 
13
- This file hosts the line_footprint_fixed tool.
13
+ This file hosts the ground_footprint tool.
14
14
  """
15
+
16
+ import logging
15
17
  import math
16
- import time
17
18
  from itertools import chain
18
19
  from pathlib import Path
19
20
 
20
21
  import geopandas as gpd
21
22
  import numpy as np
22
23
  import pandas as pd
24
+ import pyogrio.errors
23
25
  import shapely.geometry as sh_geom
24
26
  import shapely.ops as sh_ops
25
27
 
26
28
  import beratools.core.algo_common as algo_common
27
29
  import beratools.core.constants as bt_const
28
- import beratools.tools.common as bt_common
30
+ import beratools.utility.spatial_common as sp_common
29
31
  from beratools.core.algo_line_grouping import LineGrouping
32
+ from beratools.core.algo_merge_lines import custom_line_merge
30
33
  from beratools.core.algo_split_with_lines import LineSplitter
34
+ from beratools.core.logger import Logger
31
35
  from beratools.core.tool_base import execute_multiprocessing
36
+ from beratools.utility.tool_args import CallMode
37
+
38
+ log = Logger("ground_footprint", file_level=logging.INFO)
39
+ logger = log.get_logger()
40
+ print = log.print
32
41
 
33
42
  FP_FIXED_WIDTH_DEFAULT = 5.0
34
43
 
44
+
35
45
  def prepare_line_args(line_gdf, poly_gdf, n_samples, offset, width_percentile):
36
46
  """
37
47
  Generate arguments for each line in the GeoDataFrame.
@@ -65,14 +75,10 @@ def prepare_line_args(line_gdf, poly_gdf, n_samples, offset, width_percentile):
65
75
 
66
76
  inter_poly = poly_gdf.loc[spatial_index.query(line)]
67
77
  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
- ]
78
+ inter_poly = inter_poly[inter_poly[bt_const.BT_GROUP] == row[bt_const.BT_GROUP].values[0]]
71
79
 
72
80
  try:
73
- line_args.append(
74
- [row, inter_poly, n_samples, offset, width_percentile]
75
- )
81
+ line_args.append([row, inter_poly, n_samples, offset, width_percentile])
76
82
  except Exception as e:
77
83
  print(e)
78
84
 
@@ -152,7 +158,7 @@ def process_single_line(line_arg):
152
158
  def generate_fixed_width_footprint(line_gdf, max_width=False):
153
159
  """
154
160
  Create a buffer around each line.
155
-
161
+
156
162
  In the GeoDataFrame using its 'max_width' attribute and
157
163
  saves the resulting polygons in a new shapefile.
158
164
 
@@ -177,67 +183,50 @@ def generate_fixed_width_footprint(line_gdf, max_width=False):
177
183
  if not max_width:
178
184
  print("Using quantile 75% width")
179
185
  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,
186
+ lambda row: row.geometry.buffer(row.avg_width / 2) if row.geometry is not None else None,
183
187
  axis=1,
184
188
  )
185
189
  else:
186
190
  print("Using quantile 90% + 20% width")
187
191
  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,
192
+ lambda row: row.geometry.buffer(row.max_width * 1.2 / 2) if row.geometry is not None else None,
191
193
  axis=1,
192
194
  )
193
195
 
194
196
  return buffer_gdf
195
197
 
196
198
 
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
199
  def calculate_average_width(line, in_poly, offset, n_samples):
216
200
  """Calculate the average width of a polygon perpendicular to the given line."""
217
201
  # Smooth the line
218
202
  try:
219
- line = smooth_linestring(line, tolerance=0.1)
203
+ line = line.simplify(0.1)
220
204
 
221
205
  valid_widths = 0
222
206
  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
- )
207
+ sample_points_pairs = list(zip(sample_points[:-2], sample_points[1:-1], sample_points[2:]))
226
208
  widths = np.zeros(len(sample_points_pairs))
227
209
  perp_lines = []
228
210
  perp_lines_original = []
229
211
  except Exception as e:
230
212
  print(e)
231
-
213
+
232
214
  try:
233
215
  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)
216
+ try:
217
+ perp_line = algo_common.generate_perpendicular_line_precise(points, offset=offset)
218
+ perp_lines_original.append(perp_line)
219
+ except Exception as e:
220
+ print(f"Failed to generate perpendicular at index {i}: {e}")
221
+ perp_lines_original.append(None)
222
+ continue
223
+
224
+ try:
225
+ polygon_intersect = in_poly.iloc[in_poly.sindex.query(perp_line)]
226
+ intersections = polygon_intersect.intersection(perp_line)
227
+ except Exception as e:
228
+ print(f"Failed intersection at index {i}: {e}")
229
+ intersections = []
241
230
 
242
231
  line_list = []
243
232
  for inter in intersections:
@@ -262,6 +251,10 @@ def calculate_average_width(line, in_poly, offset, n_samples):
262
251
  widths[i] = max(widths[i], item.length)
263
252
  valid_widths += 1
264
253
 
254
+ # Todo: check missing perpendicular lines
255
+ # if len(perp_lines_original) < len(sample_points_pairs):
256
+ # print(f"Missing perpendicular at index {i}")
257
+
265
258
  except Exception as e:
266
259
  print(f"loop: {e}")
267
260
 
@@ -273,48 +266,57 @@ def calculate_average_width(line, in_poly, offset, n_samples):
273
266
  )
274
267
 
275
268
 
276
- def line_footprint_fixed(
269
+ def ground_footprint(
277
270
  in_line,
278
271
  in_footprint,
279
272
  n_samples,
280
273
  offset,
281
274
  max_width,
282
275
  out_footprint,
283
- processes,
284
- verbose,
285
- in_layer=None,
286
276
  in_layer_lc_path="least_cost_path",
287
- in_layer_fp=None,
288
- out_layer=None,
289
277
  merge_group=True,
290
278
  width_percentile=75,
291
- parallel_mode=bt_const.ParallelMode.MULTIPROCESSING
279
+ trim_output=True,
280
+ processes=0, call_mode=CallMode.CLI, log_level="INFO"
292
281
  ):
282
+ in_file, in_layer = sp_common.decode_file_layer(in_line)
283
+ in_fp_file, in_layer_fp = sp_common.decode_file_layer(in_footprint)
284
+ out_file, out_layer = sp_common.decode_file_layer(out_footprint)
285
+
293
286
  n_samples = int(n_samples)
294
287
  offset = float(offset)
295
- width_percentile=int(width_percentile)
288
+ width_percentile = int(width_percentile)
296
289
 
297
290
  # 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)
291
+ line_gdf = gpd.read_file(in_file, layer=in_layer)
292
+ if bt_const.BT_GROUP not in line_gdf.columns:
293
+ line_gdf[bt_const.BT_GROUP] = range(1, len(line_gdf) + 1)
294
+
295
+ use_least_cost_path = True
296
+ try:
297
+ lc_path_gdf = gpd.read_file(in_file, layer=in_layer_lc_path)
298
+ except (ValueError, OSError, pyogrio.errors.DataLayerError):
299
+ print(f"Layer '{in_layer_lc_path}' not found in {in_file}, skipping least cost path logic.")
300
+ use_least_cost_path = False
301
+
300
302
  if not merge_group:
301
- line_gdf.geometry = line_gdf.line_merge()
302
- lc_path_gdf.geometry = lc_path_gdf.line_merge()
303
-
303
+ line_gdf["geometry"] = line_gdf.geometry.apply(custom_line_merge)
304
+ if use_least_cost_path:
305
+ lc_path_gdf["geometry"] = lc_path_gdf.geometry.apply(custom_line_merge)
306
+
304
307
  line_gdf = algo_common.clean_line_geometries(line_gdf)
305
308
 
306
309
  # read footprints and remove holes
307
- poly_gdf = gpd.read_file(in_footprint, layer=in_layer_fp)
310
+ poly_gdf = gpd.read_file(in_fp_file, layer=in_layer_fp)
308
311
  poly_gdf["geometry"] = poly_gdf["geometry"].apply(algo_common.remove_holes)
309
-
310
- # split lines
312
+
313
+ # merge group and/or split lines at intersections
311
314
  merged_line_gdf = line_gdf.copy(deep=True)
312
315
  if merge_group:
313
316
  lg = LineGrouping(line_gdf, merge_group)
314
317
  lg.run_grouping()
315
318
  merged_line_gdf = lg.run_line_merge()
316
319
  else:
317
- # merge group first, then not merge after splitting
318
320
  try:
319
321
  lg = LineGrouping(line_gdf, not merge_group)
320
322
  lg.run_grouping()
@@ -329,78 +331,99 @@ def line_footprint_fixed(
329
331
  )
330
332
 
331
333
  # 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()
334
+ if use_least_cost_path:
335
+ lg_leastcost = LineGrouping(lc_path_gdf, not merge_group)
336
+ lg_leastcost.run_grouping()
337
+ merged_lc_path_gdf = lg_leastcost.run_line_merge()
338
+ splitter_leastcost = LineSplitter(merged_lc_path_gdf)
339
+ splitter_leastcost.process(splitter.intersection_gdf)
340
+
341
+ splitter_leastcost.save_to_geopackage(
342
+ out_footprint,
343
+ line_layer="split_leastcost",
344
+ )
345
+
346
+ lg = LineGrouping(splitter.split_lines_gdf, merge_group)
347
+ lg.run_grouping()
348
+ merged_line_gdf = lg.run_line_merge()
346
349
  except ValueError as e:
347
- print(f"Exception: line_footprint_fixed: {e}")
350
+ print(f"Exception: ground_footprint: {e}")
348
351
 
349
352
  # save original merged lines
350
- merged_line_gdf.to_file(out_footprint, layer="merged_lines_original")
353
+ merged_line_gdf.to_file(out_file, layer="merged_lines_original")
351
354
 
352
355
  # prepare line arguments
353
- line_args = prepare_line_args(
354
- merged_line_gdf, poly_gdf, n_samples, offset, width_percentile
355
- )
356
+ line_args = prepare_line_args(merged_line_gdf, poly_gdf, n_samples, offset, width_percentile)
356
357
 
357
358
  out_lines = execute_multiprocessing(
358
- process_single_line, line_args, "Fixed footprint", processes, mode=parallel_mode
359
+ process_single_line, line_args, "Fixed footprint", processes, call_mode
359
360
  )
360
361
  line_attr = pd.concat(out_lines)
361
362
 
362
- ############################################
363
+ # Ensure BT_GROUP is present in line_attr
364
+ if bt_const.BT_GROUP not in line_attr.columns:
365
+ raise ValueError("BT_GROUP column is required in line_attr but is missing.")
366
+
363
367
  # 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
+ if not merge_group:
369
+ group_max = (
370
+ line_attr.groupby(bt_const.BT_GROUP).agg({"avg_width": "max", "max_width": "max"}).reset_index()
371
+ )
368
372
 
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'))
373
+ # Merge the result back to the original dataframe based on 'group'
374
+ line_attr = line_attr.merge(group_max, on=bt_const.BT_GROUP, suffixes=("", "_max"))
371
375
 
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']
376
+ # Overwrite the original columns directly with the max values
377
+ line_attr["avg_width"] = line_attr["avg_width_max"]
378
+ line_attr["max_width"] = line_attr["max_width_max"]
375
379
 
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
+ # Drop the temporary max columns
381
+ line_attr.drop(columns=["avg_width_max", "max_width_max"], inplace=True)
380
382
 
381
- # create fixed width footprint
383
+ # create fixed width footprint (always assign buffer_gdf)
384
+ print("Step: Generating fixed width footprints")
382
385
  buffer_gdf = generate_fixed_width_footprint(line_attr, max_width=max_width)
383
386
 
384
- # Save the lines with attributes and polygons to a new file
387
+ # reserve all layers for output
385
388
  perp_lines_gdf = buffer_gdf.copy(deep=True)
386
389
  perp_lines_original_gdf = buffer_gdf.copy(deep=True)
387
390
 
388
- # save fixed width footprint
391
+ # Save untrimmed fixed width footprint
389
392
  buffer_gdf = buffer_gdf.drop(columns=["perp_lines"])
390
393
  buffer_gdf = buffer_gdf.drop(columns=["perp_lines_original"])
391
394
  buffer_gdf = buffer_gdf.set_crs(perp_lines_gdf.crs, allow_override=True)
392
395
  buffer_gdf.reset_index(inplace=True, drop=True)
393
396
 
397
+ untrimmed_footprint = "untrimmed_footprint"
398
+ buffer_gdf.to_file(out_file, layer=untrimmed_footprint)
399
+ print(f"Untrimmed fixed width footprint saved as '{untrimmed_footprint}'")
400
+
394
401
  # trim lines and footprints
395
- lg.run_cleanup(buffer_gdf)
396
- lg.save_file(out_footprint)
402
+ if trim_output:
403
+ lg.run_cleanup(buffer_gdf)
404
+
405
+ # Ensure only polygons are saved in clean_footprint
406
+ def ensure_polygons(gdf, buffer_width=0.01):
407
+ gdf["geometry"] = gdf["geometry"].apply(
408
+ lambda geom: geom.buffer(buffer_width)
409
+ if geom.geom_type in ["LineString", "MultiLineString"]
410
+ else geom
411
+ )
412
+ gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])]
413
+ return gdf
414
+
415
+ # Patch: after trimming, ensure polygons in clean_footprint layer
416
+ if hasattr(lg, "merged_lines_trimmed") and lg.merged_lines_trimmed is not None:
417
+ lg.merged_lines_trimmed = ensure_polygons(lg.merged_lines_trimmed)
418
+ print("Step: Saving trimmed outputs")
419
+ lg.save_file(out_file, out_layer)
420
+ else:
421
+ print("Skipping line and footprint trimming per user option.")
397
422
 
398
423
  # perpendicular lines
399
424
  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
- )
425
+ out_footprint_path = Path(out_file)
426
+ out_aux_gpkg = out_footprint_path.with_stem(out_footprint_path.stem + "_aux").with_suffix(".gpkg")
404
427
  perp_lines_gdf = perp_lines_gdf.set_geometry("perp_lines")
405
428
  perp_lines_gdf = perp_lines_gdf.drop(columns=["perp_lines_original"])
406
429
  perp_lines_gdf = perp_lines_gdf.drop(columns=["geometry"])
@@ -408,29 +431,29 @@ def line_footprint_fixed(
408
431
  perp_lines_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
409
432
 
410
433
  layer = "perp_lines_original"
411
- perp_lines_original_gdf = perp_lines_original_gdf.set_geometry(
412
- "perp_lines_original"
413
- )
434
+ perp_lines_original_gdf = perp_lines_original_gdf.set_geometry("perp_lines_original")
414
435
  perp_lines_original_gdf = perp_lines_original_gdf.drop(columns=["perp_lines"])
415
436
  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
- )
437
+ perp_lines_original_gdf = perp_lines_original_gdf.set_crs(buffer_gdf.crs, allow_override=True)
419
438
  perp_lines_original_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
420
439
 
421
440
  layer = "centerline_simplified"
441
+ # Drop perp_lines_original column if present to avoid export warnings
442
+ if "perp_lines_original" in line_attr.columns:
443
+ line_attr = line_attr.drop(columns=["perp_lines_original"])
422
444
  line_attr = line_attr.drop(columns="perp_lines")
423
445
  line_attr.to_file(out_aux_gpkg.as_posix(), layer=layer)
424
446
 
425
447
  # save footprints without holes
426
448
  poly_gdf.to_file(out_aux_gpkg.as_posix(), layer="footprint_no_holes")
449
+ print("Step: Finished fixed width footprint tool")
427
450
 
428
- print("Fixed width footprint tool finished.")
429
451
 
430
452
  if __name__ == "__main__":
431
- in_args, in_verbose = bt_common.check_arguments()
453
+ import time
454
+
455
+ from beratools.utility.tool_args import compose_tool_kwargs
432
456
  start_time = time.time()
433
- line_footprint_fixed(
434
- **in_args.input, processes=int(in_args.processes), verbose=in_verbose
435
- )
457
+ kwargs = compose_tool_kwargs("ground_footprint")
458
+ ground_footprint(**kwargs)
436
459
  print("Elapsed time: {}".format(time.time() - start_time))
@@ -29,45 +29,74 @@
29
29
  # cost corridor method and individual line thresholds.
30
30
  #
31
31
  # ---------------------------------------------------------------------------
32
- import sys
33
- from pathlib import Path
34
- from inspect import getsourcefile
35
32
 
36
- if __name__ == "__main__":
37
- current_file = Path(getsourcefile(lambda: 0)).resolve()
38
- btool_dir = current_file.parents[2]
39
- sys.path.insert(0, btool_dir.as_posix())
33
+ from beratools.core.line_footprint_functions import *
34
+ from beratools.core.canopy_threshold_relative import *
35
+
36
+
37
+ def line_footprint_relative(
38
+ in_line,
39
+ in_chm,
40
+ max_ln_width,
41
+ exp_shk_cell,
42
+ out_footprint,
43
+ out_centerline,
44
+ off_ln_dist,
45
+ canopy_percentile,
46
+ canopy_thresh_percentage,
47
+ tree_radius,
48
+ max_line_dist,
49
+ canopy_avoidance,
50
+ exponent,
51
+ processes,
52
+ call_mode,
53
+ log_level,
54
+ ):
55
+
56
+ verbose = True if call_mode == CallMode.GUI.value else False
40
57
 
41
- from beratools.tools.line_footprint_functions import *
42
- from beratools.tools.canopy_threshold_relative import *
58
+ dy_cl_line = main_canopy_threshold_relative(
59
+ in_line=in_line,
60
+ in_chm=in_chm,
61
+ canopy_percentile=int(float(canopy_percentile)),
62
+ canopy_thresh_percentage=int(float(canopy_thresh_percentage)),
63
+ full_step=bool(True),
64
+ processes=int(float(processes)),
65
+ verbose=bool(verbose),
66
+ )
43
67
 
44
- if __name__ == '__main__':
68
+ if not dy_cl_line:
69
+ print("[error]: main_canopy_threshold_relative did not return a valid path. Aborting footprint step.")
70
+ return
71
+
72
+ main_line_footprint_relative(
73
+ in_line=dy_cl_line,
74
+ in_chm=in_chm,
75
+ max_ln_width=float(max_ln_width),
76
+ out_footprint=out_footprint or "",
77
+ out_centerline=out_centerline or "",
78
+ exp_shk_cell=int(float(exp_shk_cell)),
79
+ tree_radius=float(tree_radius),
80
+ max_line_dist=float(max_line_dist),
81
+ canopy_avoidance=float(canopy_avoidance),
82
+ exponent=float(exponent),
83
+ full_step=bool(True),
84
+ canopy_thresh_percentage=int(float(canopy_thresh_percentage)),
85
+ processes=int(float(processes)),
86
+ verbose=bool(verbose),
87
+ )
88
+
89
+ if __name__ == "__main__":
90
+ from beratools.utility.tool_args import CallMode, compose_tool_kwargs
45
91
  start_time = time.time()
46
- print('Dynamic CC and Footprint processing started')
47
- print('Current time: {}'.format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
92
+ print("[info]: Dynamic CC and Footprint processing started")
93
+ print("[info]: Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
48
94
 
49
- parser = argparse.ArgumentParser()
50
- parser.add_argument('-i', '--input', type=json.loads)
51
- parser.add_argument('-p', '--processes')
52
- parser.add_argument('-v', '--verbose')
53
- args = parser.parse_args()
54
- args.input['full_step'] = True
55
- del args.input['out_footprint']
56
- del args.input['out_centerline']
57
- del args.input['exp_shk_cell']
58
- del args.input['max_ln_width']
95
+ args=compose_tool_kwargs("line_footprint_relative")
59
96
 
60
- verbose = True if args.verbose == 'True' else False
61
- dy_cl_line = main_canopy_threshold_relative(print, **args.input, processes=int(args.processes), verbose=verbose)
62
- args = parser.parse_args()
63
- args.input['full_step'] = True
64
- args.input["in_line"] = dy_cl_line
65
- del args.input['off_ln_dist']
66
- del args.input['canopy_percentile']
67
- verbose = True if args.verbose == 'True' else False
68
- main_line_footprint_relative(print, **args.input, processes=int(args.processes), verbose=verbose)
97
+ line_footprint_relative(**args)
69
98
 
70
- print('%{}'.format(100))
71
- print('Dynamic CC and Footprint processes finished')
72
- print('Current time: {}'.format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
73
- print('Total processing time (seconds): {}'.format(round(time.time() - start_time, 3)))
99
+ print("{}%".format(100))
100
+ print("[info]: Dynamic CC and Footprint processes finished")
101
+ print("[info]: Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
102
+ print("[info]: Total processing time (seconds): {}".format(round(time.time() - start_time, 3)))
@@ -7,63 +7,71 @@ See <https://gnu.org/licenses/gpl-3.0> for full license details.
7
7
  Author: AUTHOR NAME
8
8
 
9
9
  Description:
10
- This script is part of the BERA Tools.
10
+ This script is tool template for the BERA Tools. The tool showcases
11
+ how to create a new tool for the BERA Tools framework. It is a
12
+ starting point for developers to implement their own tools.
13
+ It uses the GeoPandas to read and write geospatial data,
14
+ and the multiprocessing library to process the geospatial data.
15
+
16
+ To integrate with GUI, work in the gui/assets/beratools.json is needed.
17
+ Please see developer's guide for more details.
18
+
11
19
  Webpage: https://github.com/appliedgrg/beratools
12
20
 
13
21
  The purpose of this script is to provide template for tool.
14
22
  """
15
- import time
16
- from multiprocessing.pool import Pool
17
- from random import random
18
23
 
19
- import numpy as np
24
+ import geopandas as gpd
25
+ import pandas as pd
20
26
 
21
- import beratools.tools.common as bt_common
27
+ import beratools.utility.spatial_common as sp_common
22
28
  from beratools.core.tool_base import execute_multiprocessing
29
+ from beratools.utility.tool_args import CallMode
23
30
 
24
- # Example task_data as a list of numpy ndarrays
25
- task_data_list = [
26
- np.array([1, 2, 3]),
27
- np.array([4, 5, 6]),
28
- np.array([7, 8, 9])
29
- ]
30
31
 
31
- def tool_name(
32
- in_line, in_cost_raster, line_radius, out_line, processes, verbose
33
- ):
32
+ def tool_template(in_feature, buffer_dist, out_feature,
33
+ processes=0, call_mode=CallMode.CLI, log_level="INFO"):
34
34
  """
35
35
  Define tool entry point.
36
-
37
- These arguments are defined in beratools.json file.
38
- execute_multiprocessing is common function to run tasks in parallel.
36
+
37
+ Args:
38
+ in_feature: input feature (encoded as "file:layer")
39
+ buffer_dist: buffer for input lines
40
+ out_feature: output feature (encoded as "file:layer")
41
+ processes: number of processes to use
42
+ verbose: verbosity level
43
+
44
+ These arguments are defined in beratools.json file. Whenever possible,
45
+ use execute_multiprocessing to run tasks in parallel to speedup.
46
+
39
47
  """
40
- result = execute_multiprocessing(
41
- worker,
42
- task_data_list,
43
- "tool_template",
44
- processes,
45
- verbose=verbose
46
- )
47
- print(len(result))
48
+ in_file, in_layer = sp_common.decode_file_layer(in_feature)
49
+ out_file, out_layer = sp_common.decode_file_layer(out_feature)
48
50
 
51
+ buffer_dist = float(buffer_dist)
52
+ gdf = gpd.read_file(in_file, layer=in_layer)
53
+ gdf_list = [(gdf.iloc[[i]], buffer_dist) for i in range(len(gdf))]
49
54
 
50
- # task executed in a worker process
51
- def worker(task_data):
52
- # report a message
53
- value = np.mean(task_data)
54
- print(f'Task with {value} executed', flush=True)
55
+ # Set verbose based on log_level
56
+ results = execute_multiprocessing(buffer_worker, gdf_list, "tool_template", processes, call_mode)
57
+
58
+ buffered_gdf = gpd.GeoDataFrame(pd.concat(results, ignore_index=True), crs=gdf.crs)
59
+ buffered_gdf.to_file(out_file, layer=out_layer)
55
60
 
56
- # block for a moment
57
- time.sleep(value * 10)
58
61
 
59
- # return the generated value
60
- return value
62
+ # task executed in a worker process
63
+ def buffer_worker(in_args):
64
+ buffered = in_args[0].copy()
65
+ buffer_dist = in_args[1]
66
+ buffered["geometry"] = buffered.geometry.buffer(buffer_dist)
67
+ return buffered
68
+
61
69
 
70
+ if __name__ == "__main__":
71
+ import time
62
72
 
63
- if __name__ == '__main__':
64
- in_args, in_verbose = bt_common.check_arguments()
73
+ from beratools.utility.tool_args import compose_tool_kwargs
65
74
  start_time = time.time()
66
- tool_name(
67
- print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose
68
- )
75
+ kwargs = compose_tool_kwargs("tool_template")
76
+ tool_template(**kwargs)
69
77
  print("Elapsed time: {}".format(time.time() - start_time))