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
beratools/gui/bt_data.py CHANGED
@@ -12,55 +12,194 @@ Description:
12
12
 
13
13
  The purpose of this script is to provide main interface for GUI related settings.
14
14
  """
15
+
15
16
  import json
17
+ import logging
16
18
  import os
17
- import platform
18
19
  from collections import OrderedDict
19
20
  from pathlib import Path
20
21
 
21
22
  import beratools.core.constants as bt_const
23
+ from beratools.utility.spatial_common import decode_file_layer
24
+ from beratools.utility.tool_args import CallMode, determine_cpu_core_limit
22
25
 
23
- running_windows = platform.system() == 'Windows'
24
26
  BT_SHOW_ADVANCED_OPTIONS = False
25
27
 
26
28
 
27
29
  def default_callback(value):
28
30
  """
29
31
  Define default callback that outputs using the print function.
30
-
32
+
31
33
  When tools are called without providing a custom callback, this function
32
34
  will be used to print to standard output.
33
35
  """
34
36
  print(value)
35
37
 
36
38
 
39
+ class SettingsManager:
40
+ """An object for managing settings related to BERA Tools GUI."""
41
+
42
+ def __init__(self, setting_file):
43
+ self.setting_file = setting_file
44
+ self.settings = {}
45
+
46
+ def load_saved_tool_info(self):
47
+ if self.setting_file is not None:
48
+ data_path = Path(self.setting_file).parent
49
+ if not data_path.exists():
50
+ data_path.mkdir()
51
+
52
+ saved_parameters = {}
53
+ if self.setting_file is not None:
54
+ json_file = Path(self.setting_file)
55
+ if not json_file.exists():
56
+ self.settings = saved_parameters
57
+ return
58
+ with open(json_file, "r") as open_file:
59
+ try:
60
+ saved_parameters = json.load(open_file, object_pairs_hook=OrderedDict)
61
+ except json.decoder.JSONDecodeError:
62
+ logging.error("Failed to decode settings JSON file of saved tool info.", exc_info=True)
63
+
64
+ # Remove any entry with None or "null" as tool name
65
+ if isinstance(saved_parameters, dict):
66
+ saved_parameters = OrderedDict(
67
+ (k, v) for k, v in saved_parameters.items()
68
+ if k is not None and k != "null"
69
+ )
70
+
71
+ self.settings = saved_parameters
72
+ else:
73
+ self.settings = saved_parameters
74
+ return
75
+
76
+ self.settings = saved_parameters
77
+
78
+ def save_tool_info(self, recent_tool=None):
79
+ if recent_tool:
80
+ if "gui_parameters" not in self.settings:
81
+ self.settings["gui_parameters"] = {}
82
+
83
+ self.settings["gui_parameters"]["recent_tool"] = recent_tool
84
+
85
+ if self.setting_file is not None:
86
+ with open(str(self.setting_file), "w") as file_setting:
87
+ try:
88
+ json.dump(self.settings, file_setting, indent=4)
89
+ except json.decoder.JSONDecodeError:
90
+ logging.error("Failed to encode settings to JSON when writing.", exc_info=True)
91
+
92
+ def save_setting(self, key, value):
93
+ # Guard against None/null tool name
94
+ if key is None:
95
+ print("Warning: tool_name is None, not saving parameters.")
96
+ return
97
+
98
+ # check setting directory existence
99
+ if self.setting_file is not None:
100
+ data_path = Path(self.setting_file).resolve().parent
101
+ if not data_path.exists():
102
+ data_path.mkdir()
103
+
104
+ self.load_saved_tool_info()
105
+
106
+ if value is not None:
107
+ if "gui_parameters" not in self.settings.keys():
108
+ self.settings["gui_parameters"] = {}
109
+
110
+ # Fix: Ensure value is a list if existing value is a list
111
+ if (
112
+ key in self.settings["gui_parameters"]
113
+ and isinstance(self.settings["gui_parameters"][key], list)
114
+ and not isinstance(value, list)
115
+ ):
116
+ value = [value]
117
+ self.settings["gui_parameters"][key] = value
118
+
119
+ if self.setting_file is not None:
120
+ with open(str(self.setting_file), "w") as write_settings_file:
121
+ json.dump(self.settings, write_settings_file, indent=4)
122
+
123
+ def get_saved_tool_params(self, tool_api, variable=None):
124
+ self.load_saved_tool_info()
125
+
126
+ if (
127
+ isinstance(self.settings, dict)
128
+ and "tool_history" in self.settings
129
+ and self.settings["tool_history"] is not None
130
+ ):
131
+ tool_history = self.settings["tool_history"]
132
+ if tool_history is None:
133
+ return None
134
+ if tool_api in list(tool_history):
135
+ tool_params = tool_history[tool_api]
136
+ if tool_params is None:
137
+ return None
138
+ if variable:
139
+ if isinstance(tool_params, dict):
140
+ if variable in tool_params.keys():
141
+ saved_value = tool_params[variable]
142
+ return saved_value
143
+ else:
144
+ return None
145
+ else:
146
+ return None
147
+ else: # return all params
148
+ return tool_params
149
+
150
+ return None
151
+
152
+ def add_tool_history(self, tool, params):
153
+ if "tool_history" not in self.settings:
154
+ self.settings["tool_history"] = OrderedDict()
155
+
156
+ self.settings["tool_history"][tool] = params
157
+ self.settings["tool_history"].move_to_end(tool, last=False)
158
+
159
+ def remove_tool_history_item(self, index):
160
+ key = list(self.settings["tool_history"].keys())[index]
161
+ self.settings["tool_history"].pop(key)
162
+ self.save_tool_info()
163
+
164
+ def remove_tool_history_all(self):
165
+ self.settings.pop("tool_history")
166
+ self.save_tool_info()
167
+
168
+ def get_tool_history(self):
169
+ tool_history = []
170
+ self.load_saved_tool_info()
171
+ if (
172
+ isinstance(self.settings, dict)
173
+ and "tool_history" in self.settings
174
+ and self.settings["tool_history"] is not None
175
+ ):
176
+ tool_history = self.settings["tool_history"]
177
+
178
+ return tool_history
179
+
37
180
  class BTData(object):
38
181
  """An object for interfacing with the BERA Tools executable."""
39
182
 
40
183
  def __init__(self):
41
- if running_windows:
42
- self.ext = '.exe'
43
- else:
44
- self.ext = ''
45
184
  self.current_file_path = Path(__file__).resolve().parent
46
185
 
47
- self.work_dir = ""
48
- self.user_folder = Path('')
49
- self.data_folder = Path('')
186
+ self.work_dir = Path("")
187
+ self.user_folder = Path("")
188
+ self.data_folder = Path("")
50
189
  self.verbose = True
51
190
  self.show_advanced = BT_SHOW_ADVANCED_OPTIONS
52
- self.max_procs = -1
191
+ self.selected_cpu_cores = -1
53
192
  self.recent_tool = None
54
- self.ascii_art = None
193
+ self.ascii_art = ""
55
194
  self.get_working_dir()
56
195
  self.get_user_folder()
57
196
 
58
197
  # set maximum available cpu core for tools
59
- self.max_cpu_cores = os.cpu_count()
198
+ max_cores = determine_cpu_core_limit()
199
+ self.max_cpu_cores = max_cores
60
200
 
61
201
  # load bera tools
62
202
  self.tool_history = []
63
- self.settings = {}
64
203
  self.bera_tools = None
65
204
  self.tools_list = []
66
205
  self.sorted_tools = []
@@ -75,11 +214,13 @@ class BTData(object):
75
214
  self.setting_file = None
76
215
  self.get_data_folder()
77
216
  self.get_setting_file()
78
- self.gui_setting_file = Path(self.current_file_path).joinpath(
79
- bt_const.ASSETS_PATH, r"gui.json"
80
- )
217
+ self.settings_manager = SettingsManager(self.setting_file)
218
+ self.gui_setting_file = Path(self.current_file_path).joinpath(bt_const.ASSETS_PATH, r"gui.json")
81
219
 
82
- self.load_saved_tool_info()
220
+ self.settings_manager.load_saved_tool_info()
221
+ self.settings = self.settings_manager.settings
222
+ gui_settings = self.settings.get("gui_parameters", {})
223
+ self.recent_tool = gui_settings.get("recent_tool", None)
83
224
  self.load_gui_data()
84
225
  self.get_tool_history()
85
226
 
@@ -91,50 +232,42 @@ class BTData(object):
91
232
  self.current_file_path = path_str
92
233
 
93
234
  def add_tool_history(self, tool, params):
94
- if 'tool_history' not in self.settings:
95
- self.settings['tool_history'] = OrderedDict()
96
-
97
- self.settings['tool_history'][tool] = params
98
- self.settings['tool_history'].move_to_end(tool, last=False)
235
+ self.settings_manager.add_tool_history(tool, params)
99
236
 
100
237
  def remove_tool_history_item(self, index):
101
- key = list(self.settings['tool_history'].keys())[index]
102
- self.settings['tool_history'].pop(key)
103
- self.save_tool_info()
238
+ self.settings_manager.remove_tool_history_item(index)
104
239
 
105
240
  def remove_tool_history_all(self):
106
- self.settings.pop("tool_history")
107
- self.save_tool_info()
241
+ self.settings_manager.remove_tool_history_all()
108
242
 
109
243
  def save_tool_info(self):
110
- if self.recent_tool:
111
- if 'gui_parameters' not in self.settings.keys():
112
- self.settings['gui_parameters'] = {}
113
-
114
- self.settings['gui_parameters']['recent_tool'] = self.recent_tool
115
-
116
- with open(self.setting_file, 'w') as file_setting:
117
- try:
118
- json.dump(self.settings, file_setting, indent=4)
119
- except json.decoder.JSONDecodeError:
120
- pass
244
+ self.settings_manager.save_tool_info(self.recent_tool)
121
245
 
122
246
  def save_setting(self, key, value):
123
247
  # check setting directory existence
124
- data_path = Path(self.setting_file).resolve().parent
125
- if not data_path.exists():
126
- data_path.mkdir()
248
+ if self.setting_file is not None:
249
+ data_path = Path(self.setting_file).resolve().parent
250
+ if not data_path.exists():
251
+ data_path.mkdir()
127
252
 
128
253
  self.load_saved_tool_info()
129
254
 
130
255
  if value is not None:
131
- if 'gui_parameters' not in self.settings.keys():
132
- self.settings['gui_parameters'] = {}
133
-
134
- self.settings['gui_parameters'][key] = value
135
-
136
- with open(self.setting_file, 'w') as write_settings_file:
137
- json.dump(self.settings, write_settings_file, indent=4)
256
+ if "gui_parameters" not in self.settings.keys():
257
+ self.settings["gui_parameters"] = {}
258
+
259
+ # Fix: Ensure value is a list if existing value is a list
260
+ if (
261
+ key in self.settings["gui_parameters"]
262
+ and isinstance(self.settings["gui_parameters"][key], list)
263
+ and not isinstance(value, list)
264
+ ):
265
+ value = [value]
266
+ self.settings["gui_parameters"][key] = value
267
+
268
+ if self.setting_file is not None:
269
+ with open(str(self.setting_file), "w") as write_settings_file:
270
+ json.dump(self.settings, write_settings_file, indent=4)
138
271
 
139
272
  def get_working_dir(self):
140
273
  current_file = Path(__file__).resolve()
@@ -142,81 +275,85 @@ class BTData(object):
142
275
  self.work_dir = btool_dir
143
276
 
144
277
  def get_user_folder(self):
145
- self.user_folder = Path.home().joinpath('.beratools')
278
+ self.user_folder = Path.home().joinpath(".beratools")
146
279
  if not self.user_folder.exists():
147
280
  self.user_folder.mkdir()
148
281
 
149
282
  def get_data_folder(self):
150
- self.data_folder = self.user_folder.joinpath('.data')
283
+ self.data_folder = self.user_folder.joinpath(".data")
151
284
  if not self.data_folder.exists():
152
285
  self.data_folder.mkdir()
153
286
 
154
287
  def get_logger_file_name(self, name):
155
288
  if not name:
156
- name = 'beratools'
289
+ name = "beratools"
157
290
 
158
- logger_file_name = self.user_folder.joinpath(name).with_suffix('.log')
291
+ logger_file_name = self.user_folder.joinpath(name).with_suffix(".log")
159
292
  return logger_file_name.as_posix()
160
293
 
161
294
  def get_setting_file(self):
162
- self.setting_file = self.data_folder.joinpath('saved_tool_parameters.json')
295
+ self.setting_file = self.data_folder.joinpath("saved_tool_parameters.json")
163
296
 
164
- def set_max_procs(self, val=-1):
165
- """Set the maximum cores to use."""
166
- self.max_procs = val
167
- self.save_setting('max_procs', val)
297
+ def set_selected_cpu_cores(self, val=-1):
298
+ """Set the number of CPU cores to use."""
299
+ self.selected_cpu_cores = val
300
+ self.save_setting("selected_cpu_cores", val)
168
301
 
169
- def get_max_procs(self):
170
- return self.max_procs
302
+ def get_selected_cpu_cores(self):
303
+ return self.selected_cpu_cores
171
304
 
172
305
  def get_max_cpu_cores(self):
173
306
  return self.max_cpu_cores
174
307
 
175
- def run_tool(self, tool_api, args, callback=None):
176
- """
177
- Run a tool and specifies tool arguments.
178
-
179
- Returns 0 if completes without error.
180
- Returns 1 if error encountered (details are sent to callback).
181
- Returns 2 if process is cancelled by user.
182
- """
308
+ def prepare_tool_run(self, tool_api, args, callback=None):
309
+ """Prepare tool command and arguments."""
183
310
  try:
184
311
  if callback is None:
185
312
  callback = self.default_callback
186
313
  except Exception as err:
187
- callback(str(err))
314
+ if callback is not None:
315
+ callback(str(err))
316
+ else:
317
+ print(str(err))
318
+
188
319
  return 1
189
320
 
190
321
  # Call script using new process to make GUI responsive
191
322
  try:
192
323
  # convert to valid json string
193
324
  args_string = str(args).replace("'", '"')
194
- args_string = args_string.replace('True', 'true')
195
- args_string = args_string.replace('False', 'false')
325
+ args_string = args_string.replace("True", "true")
326
+ args_string = args_string.replace("False", "false")
196
327
 
197
328
  tool_name = self.get_bera_tool_name(tool_api)
198
329
  tool_type = self.get_bera_tool_type(tool_name)
199
330
  tool_args = None
200
331
 
201
- if tool_type == 'python':
202
- tool_args = [self.work_dir.joinpath(f'tools/{tool_api}.py').as_posix(),
203
- '-i', args_string, '-p', str(self.get_max_procs()),
204
- '-v', str(self.verbose)]
205
- elif tool_type == 'executable':
332
+ if tool_type == "python":
333
+ tool_args = [
334
+ self.work_dir.joinpath(f"tools/{tool_api}.py").as_posix(),
335
+ "-i", args_string,
336
+ "-p", str(self.get_selected_cpu_cores()),
337
+ "-c", CallMode.GUI,
338
+ "-l", "INFO"
339
+ ] # fmt: skip
340
+ elif tool_type == "executable":
206
341
  print(globals().get(tool_api))
207
342
  tool_args = globals()[tool_api](args_string)
208
- lapis_path = self.work_dir.joinpath('./third_party/Lapis_0_8')
343
+ lapis_path = self.work_dir.joinpath("./third_party/Lapis_0_8")
209
344
  os.chdir(lapis_path.as_posix())
210
345
  except Exception as err:
211
346
  callback(str(err))
212
347
  return 1
213
348
 
349
+ if tool_args is None:
350
+ tool_args = []
214
351
  return tool_type, tool_args
215
352
 
216
353
  def about(self):
217
354
  """Retrieve the description for BERA Tools."""
218
355
  try:
219
- about_text = 'BERA Tools provide a series of tools developed by AppliedGRG lab.\n\n'
356
+ about_text = "BERA Tools provide a series of tools developed by AppliedGRG lab.\n\n"
220
357
  about_text += self.ascii_art
221
358
  return about_text
222
359
  except (OSError, ValueError) as err:
@@ -225,7 +362,7 @@ class BTData(object):
225
362
  def license(self):
226
363
  """Retrieve the license information for BERA Tools."""
227
364
  try:
228
- with open(Path(self.current_file_path).joinpath(r'..\..\LICENSE.txt'), 'r') as f:
365
+ with open(Path(self.current_file_path).joinpath(r"..\..\LICENSE.txt"), "r") as f:
229
366
  ret = f.read()
230
367
 
231
368
  return ret
@@ -233,60 +370,67 @@ class BTData(object):
233
370
  return err
234
371
 
235
372
  def load_saved_tool_info(self):
236
- data_path = Path(self.setting_file).parent
237
- if not data_path.exists():
238
- data_path.mkdir()
373
+ if self.setting_file is not None:
374
+ data_path = Path(self.setting_file).parent
375
+ if not data_path.exists():
376
+ data_path.mkdir()
239
377
 
240
378
  saved_parameters = {}
241
- json_file = Path(self.setting_file)
242
- if not json_file.exists():
243
- return
379
+ if self.setting_file is not None:
380
+ json_file = Path(self.setting_file)
381
+ if not json_file.exists():
382
+ self.settings = saved_parameters
383
+ return
384
+ with open(json_file, "r") as open_file:
385
+ try:
386
+ saved_parameters = json.load(open_file, object_pairs_hook=OrderedDict)
387
+ except json.decoder.JSONDecodeError:
388
+ logging.error("Failed to decode settings JSON file of saved tool info.", exc_info=True)
244
389
 
245
- with open(json_file) as open_file:
246
- try:
247
- saved_parameters = json.load(open_file, object_pairs_hook=OrderedDict)
248
- except json.decoder.JSONDecodeError:
249
- pass
390
+ self.settings = saved_parameters
391
+ else:
392
+ self.settings = saved_parameters
393
+ return
250
394
 
251
395
  self.settings = saved_parameters
252
396
 
253
397
  # parse file
254
- if 'gui_parameters' in self.settings.keys():
255
- gui_settings = self.settings['gui_parameters']
256
-
257
- if 'max_procs' in gui_settings.keys():
258
- self.max_procs = gui_settings['max_procs']
259
-
260
- if 'recent_tool' in gui_settings.keys():
261
- self.recent_tool = gui_settings['recent_tool']
262
- if not self.get_bera_tool_api(self.recent_tool):
398
+ if (
399
+ isinstance(self.settings, dict)
400
+ and "gui_parameters" in self.settings
401
+ and self.settings["gui_parameters"] is not None
402
+ ):
403
+ gui_settings = self.settings["gui_parameters"]
404
+
405
+ if "selected_cpu_cores" in gui_settings and gui_settings["selected_cpu_cores"] is not None:
406
+ self.selected_cpu_cores = gui_settings["selected_cpu_cores"]
407
+
408
+ if "recent_tool" in gui_settings and gui_settings["recent_tool"] is not None:
409
+ self.recent_tool = gui_settings["recent_tool"]
410
+ api_result = self.get_bera_tool_api(self.recent_tool)
411
+ if not api_result:
263
412
  self.recent_tool = None
264
-
265
413
  def load_gui_data(self):
266
414
  gui_settings = {}
267
415
  if not self.gui_setting_file.exists():
268
416
  print("gui.json not exist.")
269
417
  else:
270
418
  # read the settings.json file if it exists
271
- with open(self.gui_setting_file, 'r') as file_gui:
419
+ with open(self.gui_setting_file, "r") as file_gui:
272
420
  try:
273
421
  gui_settings = json.load(file_gui)
274
422
  except json.decoder.JSONDecodeError:
275
423
  pass
276
424
 
277
425
  # parse file
278
- if 'ascii_art' in gui_settings.keys():
279
- bera_art = ''
280
- for line_of_art in gui_settings['ascii_art']:
426
+ if "ascii_art" in gui_settings.keys():
427
+ bera_art = ""
428
+ for line_of_art in gui_settings["ascii_art"]:
281
429
  bera_art += line_of_art
282
430
  self.ascii_art = bera_art
283
431
 
284
432
  def get_tool_history(self):
285
- tool_history = []
286
- self.load_saved_tool_info()
287
- if self.settings:
288
- if 'tool_history' in self.settings:
289
- tool_history = self.settings['tool_history']
433
+ tool_history = self.settings_manager.get_tool_history()
290
434
 
291
435
  if tool_history:
292
436
  self.tool_history = []
@@ -295,46 +439,34 @@ class BTData(object):
295
439
  self.tool_history.append(item)
296
440
 
297
441
  def get_saved_tool_params(self, tool_api, variable=None):
298
- self.load_saved_tool_info()
299
-
300
- if 'tool_history' in self.settings:
301
- if tool_api in list(self.settings['tool_history']):
302
- tool_params = self.settings['tool_history'][tool_api]
303
- if tool_params:
304
- if variable:
305
- if variable in tool_params.keys():
306
- saved_value = tool_params[variable]
307
- return saved_value
308
- else: # return all params
309
- return tool_params
310
-
311
- return None
442
+ return self.settings_manager.get_saved_tool_params(tool_api, variable)
312
443
 
313
444
  def get_bera_tools(self):
314
- tool_json = Path(self.current_file_path).joinpath(bt_const.ASSETS_PATH, r'beratools.json')
315
- if tool_json.exists():
316
- tool_json = open(Path(self.current_file_path).joinpath(bt_const.ASSETS_PATH, r'beratools.json'))
317
- self.bera_tools = json.load(tool_json)
445
+ tool_json_path = Path(self.current_file_path).joinpath(bt_const.ASSETS_PATH, r"beratools.json")
446
+ if tool_json_path.exists():
447
+ with open(tool_json_path, "r") as tool_json_file:
448
+ self.bera_tools = json.load(tool_json_file)
318
449
  else:
319
- print('Tool configuration file not exists')
450
+ print("Tool configuration file not exists")
320
451
 
321
452
  def get_bera_tool_list(self):
322
453
  self.tools_list = []
323
454
  self.sorted_tools = []
324
455
 
325
- for toolbox in self.bera_tools['toolbox']:
326
- category = []
327
- for item in toolbox['tools']:
328
- if item['name']:
329
- category.append(item['name'])
330
- self.tools_list.append(item['name']) # add tool to list
456
+ if self.bera_tools and "toolbox" in self.bera_tools:
457
+ for toolbox in self.bera_tools["toolbox"]:
458
+ category = []
459
+ for item in toolbox["tools"]:
460
+ if item["name"]:
461
+ category.append(item["name"])
462
+ self.tools_list.append(item["name"]) # add tool to list
331
463
 
332
- self.sorted_tools.append(category)
464
+ self.sorted_tools.append(category)
333
465
 
334
466
  def sort_toolboxes(self):
335
467
  for toolbox in self.toolbox_list:
336
468
  # Does not contain a sub toolbox, i.e. does not contain '/'
337
- if toolbox.find('/') == (-1):
469
+ if toolbox.find("/") == (-1):
338
470
  # add to both upper toolbox list and lower toolbox list
339
471
  self.upper_toolboxes.append(toolbox)
340
472
  self.lower_toolboxes.append(toolbox)
@@ -343,139 +475,358 @@ class BTData(object):
343
475
 
344
476
  def get_bera_toolboxes(self):
345
477
  toolboxes = []
346
- for toolbox in self.bera_tools['toolbox']:
347
- tb = toolbox['category']
348
- toolboxes.append(tb)
478
+ if self.bera_tools and "toolbox" in self.bera_tools:
479
+ for toolbox in self.bera_tools["toolbox"]:
480
+ tb = toolbox["category"]
481
+ toolboxes.append(tb)
482
+
483
+ self.toolbox_list = toolboxes
484
+ else:
485
+ self.toolbox_list = []
486
+
487
+ def _set_param_flag_and_saved_value(self, single_param, param, tool):
488
+ """Set parameter variable and load saved value if it exists in beratools.json."""
489
+ saved_value = None
490
+ if "variable" in param.keys():
491
+ single_param["variable"] = param["variable"]
492
+ # Only load saved value if tool API exists - discard if API changed
493
+ saved_value = self.get_saved_tool_params(tool["tool_api"], param["variable"])
494
+ if saved_value is not None:
495
+ single_param["saved_value"] = saved_value
496
+
497
+ def _set_param_type_for_input(self, single_param, param):
498
+ # Assume subtype is already a list
499
+ subtypes = param["subtype"]
500
+
501
+ # No mapping, use raw subtypes directly
502
+ if param["type"] == "list":
503
+ single_param["parameter_type"] = {"OptionList": param["data"]}
504
+ single_param["data_type"] = subtypes
505
+ elif param["type"] == "text":
506
+ single_param["parameter_type"] = "Text"
507
+ elif param["type"] == "number":
508
+ single_param["parameter_type"] = subtypes
509
+ elif param["type"] == "file":
510
+ single_param["parameter_type"] = {"ExistingFile": subtypes}
511
+ elif param["type"] == "directory":
512
+ single_param["parameter_type"] = {"Directory": subtypes}
513
+ else:
514
+ raise ValueError(f"Unknown parameter type: {param['type']}")
349
515
 
350
- self.toolbox_list = toolboxes
516
+ def _set_param_type_for_output(self, single_param, param):
517
+ # Assume subtype is already a list
518
+ subtypes = param["subtype"]
519
+ single_param["parameter_type"] = {"NewFile": subtypes}
351
520
 
352
- def get_bera_tool_params(self, tool_name):
353
- new_param_whole = {'parameters': []}
354
- tool = {}
355
- batch_tool_list = []
356
- for toolbox in self.bera_tools['toolbox']:
357
- for single_tool in toolbox['tools']:
358
- if single_tool['batch_processing']:
359
- batch_tool_list.append(single_tool['name'])
521
+ def _find_tool_params(self, tool_name):
522
+ """
523
+ Find raw tool parameters by name.
524
+
525
+ Eliminates duplication across multiple methods.
526
+
527
+ Returns:
528
+ List of parameter definitions from beratools.json, or empty list if not found.
529
+ """
530
+ if self.bera_tools and "toolbox" in self.bera_tools:
531
+ for toolbox in self.bera_tools["toolbox"]:
532
+ for single_tool in toolbox["tools"]:
533
+ if tool_name == single_tool["name"]:
534
+ return single_tool.get("parameters", [])
535
+ return []
536
+
537
+ def validate_tool_parameter(self, value, param_def):
538
+ """
539
+ Validate and convert parameter to correct type based on beratools.json definition.
540
+
541
+ Orchestrates validation by dispatching to type-specific validators.
542
+
543
+ Args:
544
+ value: The value from GUI input
545
+ param_def: The original parameter definition from beratools.json
546
+
547
+ Returns:
548
+ Converted and validated value
549
+
550
+ Raises:
551
+ ValueError: If validation fails
552
+ """
553
+ # GUARD CLAUSE 1: Handle None/empty for optional parameters
554
+ if value is None or value == "":
555
+ if param_def.get("optional", False):
556
+ return param_def.get("default", None)
557
+
558
+ # GUARD CLAUSE 2: Skip validation for output parameters (they don't exist yet)
559
+ if param_def.get("output", False):
560
+ return value
561
+
562
+ # DISPATCH to type-specific validator
563
+ param_type = param_def.get("type")
564
+ if param_type == "number":
565
+ return self._validate_number(value, param_def)
566
+ elif param_type == "file":
567
+ return self._validate_file(value, param_def)
568
+ elif param_type == "directory":
569
+ return self._validate_directory(value, param_def)
570
+ elif param_type == "text":
571
+ return self._validate_text(value, param_def)
572
+ elif param_type == "list":
573
+ return self._validate_list(value, param_def)
574
+
575
+ return value
576
+
577
+ def _validate_number(self, value, param_def):
578
+ """Validate and convert numeric types (int, float)."""
579
+ subtype = param_def.get("subtype", "")
580
+ variable = param_def.get("variable", "unknown")
581
+
582
+ if subtype == "int":
583
+ if not isinstance(value, int) or isinstance(value, bool):
584
+ try:
585
+ value = int(value)
586
+ except (ValueError, TypeError):
587
+ raise ValueError(f"Parameter '{variable}' must be an integer, got {type(value).__name__}")
588
+
589
+ elif subtype == "float":
590
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
591
+ try:
592
+ value = float(value)
593
+ except (ValueError, TypeError):
594
+ raise ValueError(f"Parameter '{variable}' must be a number, got {type(value).__name__}")
595
+
596
+ return value
597
+
598
+ def _validate_file(self, value, param_def):
599
+ """Validate file exists and is accessible."""
600
+ variable = param_def.get("variable", "unknown")
601
+
602
+ if not isinstance(value, str):
603
+ raise ValueError(f"Parameter '{variable}' must be a file path string")
604
+
605
+ # Extract file path and layer name (supports both | and :: separators)
606
+ actual_file_path, layer_name = decode_file_layer(value)
607
+ file_path = Path(actual_file_path)
608
+
609
+ if not file_path.exists():
610
+ raise ValueError(f"File not found: {actual_file_path}")
611
+ if not file_path.is_file():
612
+ raise ValueError(f"Path is not a file: {actual_file_path}")
613
+
614
+ return value
615
+
616
+ def _validate_directory(self, value, param_def):
617
+ """Validate directory exists and is accessible."""
618
+ variable = param_def.get("variable", "unknown")
619
+
620
+ if not isinstance(value, str):
621
+ raise ValueError(f"Parameter '{variable}' must be a directory path string")
622
+
623
+ dir_path = Path(value)
624
+
625
+ if not dir_path.exists():
626
+ raise ValueError(f"Directory not found: {value}")
627
+ if not dir_path.is_dir():
628
+ raise ValueError(f"Path is not a directory: {value}")
629
+
630
+ return value
631
+
632
+ def _validate_text(self, value, param_def):
633
+ """Validate and convert to text/string type."""
634
+ variable = param_def.get("variable", "unknown")
635
+
636
+ if not isinstance(value, str):
637
+ try:
638
+ value = str(value)
639
+ except Exception:
640
+ raise ValueError(f"Parameter '{variable}' must be text/string")
641
+
642
+ return value
643
+
644
+ def _validate_list(self, value, param_def):
645
+ """Validate list parameter with allowed values and type conversion."""
646
+ subtype = param_def.get("subtype", "")
647
+ variable = param_def.get("variable", "unknown")
648
+ allowed_values = param_def.get("data", [])
649
+
650
+ # Convert to correct type based on subtype
651
+ if subtype == "bool":
652
+ value = self._convert_bool(value, variable)
653
+ elif subtype == "int":
654
+ value = self._convert_int(value, variable)
655
+ elif subtype == "float":
656
+ value = self._convert_float(value, variable)
657
+
658
+ # Validate against allowed values
659
+ if value not in allowed_values:
660
+ raise ValueError(
661
+ f"Parameter '{variable}' value '{value}' not in allowed options: {allowed_values}"
662
+ )
663
+
664
+ return value
665
+
666
+ def _convert_bool(self, value, variable):
667
+ """Convert value to boolean."""
668
+ if isinstance(value, str):
669
+ if value.lower() in ["true", "1", "yes"]:
670
+ return True
671
+ elif value.lower() in ["false", "0", "no"]:
672
+ return False
673
+ else:
674
+ raise ValueError(f"Parameter '{variable}' must be boolean, got '{value}'")
675
+ elif not isinstance(value, bool):
676
+ try:
677
+ return bool(value)
678
+ except Exception:
679
+ raise ValueError(f"Parameter '{variable}' must be boolean")
680
+ return value
681
+
682
+ def _convert_int(self, value, variable):
683
+ """Convert value to integer."""
684
+ if not isinstance(value, int) or isinstance(value, bool):
685
+ try:
686
+ return int(value)
687
+ except (ValueError, TypeError):
688
+ raise ValueError(f"Parameter '{variable}' must be integer")
689
+ return value
690
+
691
+ def _convert_float(self, value, variable):
692
+ """Convert value to float."""
693
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
694
+ try:
695
+ return float(value)
696
+ except (ValueError, TypeError):
697
+ raise ValueError(f"Parameter '{variable}' must be float")
698
+ return value
699
+
700
+ def validate_tool_params(self, tool_name, kwargs):
701
+ """
702
+ Validate and convert all tool parameters against beratools.json definitions.
703
+
704
+ Args:
705
+ tool_name: Tool name to lookup in beratools.json
706
+ kwargs: Dict of parameter values from GUI input
707
+
708
+ Returns:
709
+ Dict of validated/converted values
710
+
711
+ Raises:
712
+ ValueError: If any parameter validation fails
713
+ """
714
+ raw_params = self._find_tool_params(tool_name)
715
+
716
+ # Build a mapping of variable -> beratools.json param definition
717
+ params_by_variable = {}
718
+ for param in raw_params:
719
+ if "variable" in param:
720
+ params_by_variable[param["variable"]] = param
721
+
722
+ validated_kwargs = {}
723
+ for key, value in kwargs.items():
724
+ # Skip framework parameters (handled separately)
725
+ if key in ["processes", "call_mode", "log_level"]:
726
+ validated_kwargs[key] = value
727
+ continue
728
+
729
+ # Validate tool parameters against beratools.json definitions
730
+ if key in params_by_variable:
731
+ param_def = params_by_variable[key]
732
+ try:
733
+ validated_value = self.validate_tool_parameter(value, param_def)
734
+ validated_kwargs[key] = validated_value
735
+ except ValueError as e:
736
+ raise ValueError(f"Invalid argument '{key}': {str(e)}")
737
+ else:
738
+ # Unknown parameter - pass through (may be framework param or typo)
739
+ validated_kwargs[key] = value
360
740
 
361
- if tool_name == single_tool['name']:
362
- tool = single_tool
741
+ return validated_kwargs
363
742
 
743
+ def get_bera_tool_params(self, tool_name):
744
+ new_param_whole = {"parameters": []}
745
+ tool = {}
746
+
747
+ # Find tool in beratools.json
748
+ if self.bera_tools and "toolbox" in self.bera_tools:
749
+ for toolbox in self.bera_tools["toolbox"]:
750
+ for single_tool in toolbox["tools"]:
751
+ if tool_name == single_tool["name"]:
752
+ tool = single_tool
753
+
754
+ # Copy non-parameter fields to result
364
755
  for key, value in tool.items():
365
- if key != 'parameters':
756
+ if key != "parameters":
366
757
  new_param_whole[key] = value
367
758
 
368
759
  # convert json format for parameters
369
- if 'parameters' not in tool.keys():
370
- print('issue')
371
-
372
- for param in tool['parameters']:
373
- single_param = {'name': param['parameter']}
374
- if 'variable' in param.keys():
375
- single_param['flag'] = param['variable']
376
- # restore saved parameters
377
- saved_value = self.get_saved_tool_params(tool['tool_api'], param['variable'])
378
- if saved_value is not None:
379
- single_param['saved_value'] = saved_value
380
- else:
381
- single_param['flag'] = 'FIXME'
382
-
383
- single_param['output'] = param['output']
384
- if not param['output']:
385
- if param['type'] == 'list':
386
- if tool_name == 'Batch Processing':
387
- single_param['parameter_type'] = {'OptionList': batch_tool_list}
388
- single_param['data_type'] = 'String'
389
- else:
390
- single_param['parameter_type'] = {'OptionList': param['data']}
391
- single_param['data_type'] = 'String'
392
- if param['typelab'] == 'text':
393
- single_param['data_type'] = 'String'
394
- elif param['typelab'] == 'int':
395
- single_param['data_type'] = 'Integer'
396
- elif param['typelab'] == 'float':
397
- single_param['data_type'] = 'Float'
398
- elif param['typelab'] == 'bool':
399
- single_param['data_type'] = 'Boolean'
400
- elif param['type'] == 'text':
401
- single_param['parameter_type'] = 'String'
402
- elif param['type'] == 'number':
403
- if param['typelab'] == 'int':
404
- single_param['parameter_type'] = 'Integer'
405
- else:
406
- single_param['parameter_type'] = 'Float'
407
- elif param['type'] == 'file':
408
- single_param['parameter_type'] = {'ExistingFile': [param['typelab']]}
760
+ if "parameters" not in tool.keys():
761
+ print("issue")
762
+
763
+ parameters = self._find_tool_params(tool_name)
764
+ if parameters:
765
+ for param in parameters:
766
+ # Parse subtype string to list once, here
767
+ if isinstance(param.get("subtype", ""), str):
768
+ param["subtype"] = [s.strip() for s in param["subtype"].split("|")]
769
+ single_param = {"name": param["label"]}
770
+ self._set_param_flag_and_saved_value(single_param, param, tool)
771
+ single_param["output"] = param["output"]
772
+ if not param["output"]:
773
+ self._set_param_type_for_input(single_param, param)
409
774
  else:
410
- single_param['parameter_type'] = {'ExistingFile': ''}
411
- else:
412
- single_param["parameter_type"] = {'NewFile': [param['typelab']]}
413
-
414
- single_param['description'] = param['description']
415
-
416
- if param['type'] == 'raster':
417
- for i in single_param["parameter_type"].keys():
418
- single_param['parameter_type'][i] = 'Raster'
419
- elif param['type'] == 'lidar':
420
- for i in single_param["parameter_type"].keys():
421
- single_param['parameter_type'][i] = 'Lidar'
422
- elif param['type'] == 'vector':
423
- for i in single_param["parameter_type"].keys():
424
- single_param['parameter_type'][i] = 'Vector'
425
- if 'layer' in param.keys():
426
- layer_value = self.get_saved_tool_params(tool['tool_api'], param['layer'])
427
- single_param['layer'] = {'layer_name': param['layer'], 'layer_value': layer_value}
428
-
429
- elif param['type'] == 'Directory':
430
- single_param['parameter_type'] = {'Directory': [param['typelab']]}
431
-
432
- single_param['default_value'] = param['default']
433
- if "optional" in param.keys():
434
- single_param['optional'] = param['optional']
435
- else:
436
- single_param['optional'] = False
775
+ self._set_param_type_for_output(single_param, param)
776
+
777
+ single_param["description"] = param["description"]
778
+
779
+ single_param["default_value"] = param["default"]
780
+ single_param["optional"] = param.get("optional", False)
437
781
 
438
- new_param_whole['parameters'].append(single_param)
782
+ new_param_whole["parameters"].append(single_param)
439
783
 
440
784
  return new_param_whole
441
785
 
442
786
  def get_bera_tool_parameters_list(self, tool_name):
443
787
  params = self.get_bera_tool_params(tool_name)
444
788
  param_list = {}
445
- for item in params['parameters']:
446
- param_list[item['flag']] = item['default_value']
789
+ parameters = params.get("parameters")
790
+ if parameters is None:
791
+ return param_list
792
+ for item in parameters:
793
+ if item is not None and "variable" in item and "default_value" in item:
794
+ param_list[item["variable"]] = item["default_value"]
447
795
 
448
796
  return param_list
449
797
 
450
798
  def get_bera_tool_args(self, tool_name):
451
799
  params = self.get_bera_tool_params(tool_name)
452
- tool_args = params['parameters']
800
+ tool_args = params["parameters"]
453
801
 
454
802
  return tool_args
455
803
 
456
804
  def get_bera_tool_name(self, tool_api):
457
805
  tool_name = None
458
- for toolbox in self.bera_tools['toolbox']:
459
- for tool in toolbox['tools']:
460
- if tool_api == tool['tool_api']:
461
- tool_name = tool['name']
806
+ if self.bera_tools and "toolbox" in self.bera_tools:
807
+ for toolbox in self.bera_tools["toolbox"]:
808
+ for tool in toolbox["tools"]:
809
+ if tool_api == tool["tool_api"]:
810
+ tool_name = tool["name"]
462
811
 
463
812
  return tool_name
464
813
 
465
814
  def get_bera_tool_api(self, tool_name):
466
815
  tool_api = None
467
- for toolbox in self.bera_tools['toolbox']:
468
- for tool in toolbox['tools']:
469
- if tool_name == tool['name']:
470
- tool_api = tool['tool_api']
816
+ if self.bera_tools and "toolbox" in self.bera_tools:
817
+ for toolbox in self.bera_tools["toolbox"]:
818
+ for tool in toolbox["tools"]:
819
+ if tool_name == tool["name"]:
820
+ tool_api = tool["tool_api"]
471
821
 
472
822
  return tool_api
473
823
 
474
824
  def get_bera_tool_type(self, tool_name):
475
825
  tool_type = None
476
- for toolbox in self.bera_tools['toolbox']:
477
- for tool in toolbox['tools']:
478
- if tool_name == tool['name']:
479
- tool_type = tool['tool_type']
826
+ if self.bera_tools and "toolbox" in self.bera_tools:
827
+ for toolbox in self.bera_tools["toolbox"]:
828
+ for tool in toolbox["tools"]:
829
+ if tool_name == tool["name"]:
830
+ tool_type = tool["tool_type"]
480
831
 
481
832
  return tool_type