BERATools 0.2.3__py3-none-any.whl → 0.2.5__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.5.dist-info/METADATA +134 -0
  38. beratools-0.2.5.dist-info/RECORD +50 -0
  39. {beratools-0.2.3.dist-info → beratools-0.2.5.dist-info}/WHEEL +1 -1
  40. beratools-0.2.5.dist-info/entry_points.txt +3 -0
  41. beratools-0.2.5.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.3.dist-info/METADATA +0 -108
  76. beratools-0.2.3.dist-info/RECORD +0 -74
  77. beratools-0.2.3.dist-info/entry_points.txt +0 -2
  78. beratools-0.2.3.dist-info/licenses/LICENSE +0 -22
@@ -12,18 +12,19 @@ Description:
12
12
 
13
13
  The purpose of this script is to provide all kinds of widgets for tool parameters.
14
14
  """
15
+
15
16
  import json
16
- import os
17
17
  import sys
18
18
  from collections import OrderedDict
19
19
  from pathlib import Path
20
20
 
21
- import numpy as np
22
21
  import pyogrio
22
+ from numpy import ndarray
23
23
  from PyQt5 import QtCore, QtWidgets
24
24
 
25
25
  BT_LABEL_MIN_WIDTH = 130
26
26
 
27
+
27
28
  class ToolWidgets(QtWidgets.QWidget):
28
29
  """ToolWidgets class for creating widgets for tool parameters."""
29
30
 
@@ -34,7 +35,7 @@ class ToolWidgets(QtWidgets.QWidget):
34
35
 
35
36
  self.tool_name = tool_name
36
37
  self.show_advanced = show_advanced
37
- self.current_tool_api = ''
38
+ self.current_tool_api = ""
38
39
  self.widget_list = []
39
40
  self.setWindowTitle("Tool widgets")
40
41
 
@@ -44,7 +45,7 @@ class ToolWidgets(QtWidgets.QWidget):
44
45
  for item in self.widget_list:
45
46
  layout.addWidget(item)
46
47
 
47
- self.save_button = QtWidgets.QPushButton('Save Parameters')
48
+ self.save_button = QtWidgets.QPushButton("Save Parameters")
48
49
  self.save_button.clicked.connect(self.save_tool_parameters)
49
50
  self.save_button.setFixedSize(200, 50)
50
51
  layout.addSpacing(20)
@@ -58,7 +59,7 @@ class ToolWidgets(QtWidgets.QWidget):
58
59
  if v:
59
60
  args.update(v)
60
61
  else:
61
- print(f'[Missing argument]: {widget.name} not specified.', 'missing')
62
+ print(f"[Missing argument]: {widget.name} not specified.", "missing")
62
63
  param_missing = True
63
64
 
64
65
  if param_missing:
@@ -68,27 +69,43 @@ class ToolWidgets(QtWidgets.QWidget):
68
69
 
69
70
  def create_widgets(self, tool_args):
70
71
  param_num = 0
72
+ valid_types = {"number", "file", "list", "text", "directory"}
71
73
  for p in tool_args:
72
- json_str = json.dumps(p, sort_keys=True, indent=2, separators=(',', ': '))
73
- pt = p['parameter_type']
74
+ # Validate parameter type is not a subtype value
75
+ sub_type = p.get("type", None)
76
+ if sub_type and sub_type not in valid_types:
77
+ print(f"Invalid sub type '{sub_type}' for '{p.get('label', '')}'. ")
78
+ continue
79
+
80
+ json_str = json.dumps(p, sort_keys=True, indent=2, separators=(",", ": "))
81
+ pt = p["parameter_type"]
74
82
  widget = None
75
83
 
76
- if 'ExistingFile' in pt or 'NewFile' in pt or 'Directory' in pt:
84
+ if "ExistingFile" in pt or "NewFile" in pt or "Directory" in pt:
77
85
  widget = FileSelector(json_str, None)
78
86
  param_num = param_num + 1
79
- elif 'FileList' in pt:
87
+ elif "FileList" in pt:
80
88
  widget = MultiFileSelector(json_str, None)
81
89
  param_num = param_num + 1
82
- elif 'Boolean' in pt:
90
+ elif "Boolean" in pt:
83
91
  widget = BooleanInput(json_str)
84
92
  param_num = param_num + 1
85
- elif 'OptionList' in pt:
86
- widget = OptionsInput(json_str)
93
+ elif "OptionList" in pt:
94
+ option_list = p["parameter_type"].get("OptionList", [])
95
+ # Check if OptionList contains only boolean values (handle both string and bool types)
96
+ is_bool_list = (
97
+ option_list == ["True", "False"] or
98
+ option_list == ["true", "false"] or
99
+ option_list == [True, False] or
100
+ option_list == [False, True]
101
+ )
102
+ if is_bool_list:
103
+ widget = BooleanInput(json_str)
104
+ else:
105
+ widget = OptionsInput(json_str)
87
106
  param_num = param_num + 1
88
- elif ('Float' in pt or 'Integer' in pt or
89
- 'Text' in pt or 'String' in pt or 'StringOrNumber' in pt or
90
- 'StringList' in pt or 'VectorAttributeField' in pt):
91
- widget = DataInput(json_str)
107
+ elif "float" in pt or "int" in pt:
108
+ widget = NumericInput(json_str)
92
109
  param_num = param_num + 1
93
110
  else:
94
111
  msg_box = QtWidgets.QMessageBox()
@@ -97,17 +114,19 @@ class ToolWidgets(QtWidgets.QWidget):
97
114
  msg_box.exec()
98
115
 
99
116
  param_value = None
100
- if 'saved_value' in p.keys():
101
- param_value = p['saved_value']
117
+ if "saved_value" in p.keys():
118
+ param_value = p["saved_value"]
102
119
  if param_value is None:
103
- param_value = p['default_value']
120
+ param_value = p["default_value"]
121
+ if param_value is '':
122
+ param_value = p["default_value"]
104
123
  if param_value is not None:
105
124
  if type(widget) is OptionsInput:
106
- widget.value = param_value
125
+ widget.set_value(param_value)
107
126
  elif widget:
108
- widget.value = param_value
127
+ widget.set_value(param_value)
109
128
  else:
110
- print('No default value found: {}'.format(p['name']))
129
+ print("No default value found: {}".format(p["name"]))
111
130
 
112
131
  # hide optional widgets
113
132
  if widget:
@@ -124,14 +143,14 @@ class ToolWidgets(QtWidgets.QWidget):
124
143
  def update_widgets(self, values_dict):
125
144
  for key, value in values_dict.items():
126
145
  for item in self.widget_list:
127
- if key == item.flag:
146
+ if key == item.variable:
128
147
  item.set_value(value)
129
148
 
130
149
  def save_tool_parameters(self):
131
150
  params = {}
132
151
  for item in self.widget_list:
133
- if item.flag:
134
- params[item.flag] = item.get_value()
152
+ if item.variable:
153
+ params[item.variable] = item.get_value()
135
154
 
136
155
  self.signal_save_tool_params.emit(params)
137
156
 
@@ -146,18 +165,16 @@ def get_layers(gpkg_file):
146
165
  layers_info = pyogrio.list_layers(gpkg_file)
147
166
 
148
167
  # Check if layers_info is in the expected format
149
- if isinstance(layers_info, np.ndarray) and all(
150
- isinstance(layer, np.ndarray) and len(layer) >= 2 for layer in layers_info
168
+ if isinstance(layers_info, ndarray) and all(
169
+ isinstance(layer, ndarray) and len(layer) >= 2 for layer in layers_info
151
170
  ):
152
- # Create a dictionary where the key is the layer name
171
+ # Create a dictionary where the key is the layer name
153
172
  # and the value is the geometry type
154
173
  layers_dict = OrderedDict((layer[0], layer[1]) for layer in layers_info)
155
174
  return layers_dict
156
175
  else:
157
176
  # If the format is not correct, raise an exception with a detailed message
158
- raise ValueError(
159
- "Expected a list of lists or tuples with layer name and geometry type."
160
- )
177
+ raise ValueError("Expected a list of lists or tuples with layer name and geometry type.")
161
178
 
162
179
  except Exception as e:
163
180
  print(f"Error retrieving layers from GeoPackage '{gpkg_file}': {e}")
@@ -167,282 +184,360 @@ def get_layers(gpkg_file):
167
184
  class FileSelector(QtWidgets.QWidget):
168
185
  """FileSelector class for creating file selection widgets."""
169
186
 
187
+ VECTOR_FORMATS = {"gpkg": ["vector"], "shp": ["vector"]}
188
+
189
+ @staticmethod
190
+ def has_vector_subtype(param_type):
191
+ if isinstance(param_type, dict):
192
+ for v in param_type.values():
193
+ if isinstance(v, list) and "vector" in v:
194
+ return True
195
+ return False
196
+
170
197
  def __init__(self, json_str, parent=None):
171
198
  super(FileSelector, self).__init__(parent)
199
+ self.selected_layer = ""
200
+ self.parse_params(json_str)
201
+ self.initialize_values()
202
+ self.handle_vector_io()
203
+ self.setup_ui()
172
204
 
173
- # Parsing the JSON data
205
+ def parse_params(self, json_str):
174
206
  params = json.loads(json_str)
175
- self.name = params['name']
176
- self.description = params['description']
177
- self.flag = params['flag']
178
- self.layer_flag = None
179
- self.saved_layer = ''
180
- if 'layer' in params.keys():
181
- self.layer_flag = params['layer']['layer_name']
182
- self.saved_layer = params['layer']['layer_value']
183
-
207
+ self.name = params["name"]
208
+ self.description = params["description"]
209
+ self.variable = params["variable"]
184
210
  self.gpkg_layers = None
185
- self.output = params['output'] # Ensure output flag is read
186
- self.parameter_type = params['parameter_type']
211
+ self.output = params["output"]
212
+ self.parameter_type = params["parameter_type"]
213
+
187
214
  self.file_type = ""
188
215
  if "ExistingFile" in self.parameter_type:
189
- self.file_type = params['parameter_type']['ExistingFile']
216
+ self.file_type = params["parameter_type"]["ExistingFile"]
190
217
  elif "NewFile" in self.parameter_type:
191
- self.file_type = params['parameter_type']['NewFile']
192
- self.optional = params['optional']
193
-
194
- self.default_value = params['default_value']
195
- self.value = self.default_value
196
- self.selected_layer = None # Add attribute for selected layer
197
- if 'saved_value' in params.keys():
198
- self.value = params['saved_value']
199
-
218
+ self.file_type = params["parameter_type"]["NewFile"]
219
+
220
+ if isinstance(self.file_type, str):
221
+ self.file_type = [self.file_type]
222
+ elif not isinstance(self.file_type, list):
223
+ self.file_type = list(self.file_type)
224
+
225
+ self.optional = params["optional"]
226
+ self.default_value = params["default_value"]
227
+ self.saved_value = params.get("saved_value", None)
228
+ self.is_vector = self.has_vector_subtype(self.parameter_type)
229
+
230
+ def initialize_values(self):
231
+ # Use dict for vector values
232
+ if self.is_vector:
233
+ val = self.saved_value if self.saved_value is not None else self.default_value
234
+ # Handle case where saved_value is already a dict (from previous saves)
235
+ if isinstance(val, dict):
236
+ self.value = val
237
+ elif val and isinstance(val, str) and "|" in val:
238
+ path, layer = val.rsplit("|", 1)
239
+ self.value = {"path": path, "layer": layer}
240
+ else:
241
+ self.value = {"path": val if val else "", "layer": ""}
242
+ else:
243
+ self.value = self.saved_value if self.saved_value is not None else self.default_value
244
+
245
+ def handle_vector_io(self):
246
+ if not self.is_vector:
247
+ return
248
+ path = self.value["path"]
249
+ layer = self.value["layer"]
250
+ ext = Path(path).suffix.lower().replace(".", "")
251
+ if ext not in self.VECTOR_FORMATS:
252
+ return
253
+ gpkg_path = Path(path)
254
+ dir_exists = gpkg_path.parent.exists()
255
+ file_exists = gpkg_path.exists()
256
+ if self.output:
257
+ # Output: preserve path/layer even if file doesn't exist
258
+ pass
259
+ else:
260
+ # Input: validate file/directory existence
261
+ if not file_exists:
262
+ self.value = {"path": "", "layer": ""}
263
+ print(f"[Error] Invalid Input: File does not exist: {path}")
264
+ return
265
+ elif not dir_exists:
266
+ self.value = {"path": "", "layer": ""}
267
+ print(f"[Error] Invalid Input: Directory does not exist for file: {path}")
268
+ return
269
+ else:
270
+ try:
271
+ layers_dict = get_layers(path)
272
+ if layer:
273
+ valid_layers = [str(k) for k in layers_dict.keys()]
274
+ if layer not in valid_layers:
275
+ self.value = {"path": "", "layer": ""}
276
+ print(f"[Error] Invalid Layer: Layer '{layer}' not found in file: {path}")
277
+ except Exception:
278
+ self.value = {"path": "", "layer": ""}
279
+ print(f"[Error] Layer Error: Could not load layers from file: {path}")
280
+
281
+ def setup_ui(self):
200
282
  self.layout = QtWidgets.QHBoxLayout()
201
283
  self.label = QtWidgets.QLabel(self.name)
202
284
  self.label.setMinimumWidth(200)
203
- self.in_file = QtWidgets.QLineEdit(self.value)
285
+ if self.is_vector:
286
+ self.in_file = QtWidgets.QLineEdit(self.value["path"])
287
+ else:
288
+ self.in_file = QtWidgets.QLineEdit(self.value)
204
289
  self.btn_select = QtWidgets.QPushButton("...")
205
290
  self.btn_select.clicked.connect(self.select_file)
206
-
207
- # ComboBox for displaying GeoPackage layers
208
291
  self.layer_combo = QtWidgets.QComboBox()
209
- self.layer_combo.setVisible(False) # Initially hidden
210
- # Connect layer change event
211
- self.layer_combo.currentTextChanged.connect(self.set_layer)
292
+ self.layer_combo.setVisible(False)
293
+ self.layer_combo.currentTextChanged.connect(self.set_layer)
212
294
  self.layout.addWidget(self.label)
213
295
  self.layout.addWidget(self.in_file)
214
296
  self.layout.addWidget(self.layer_combo)
215
297
  self.layout.addWidget(self.btn_select)
216
-
217
298
  self.setLayout(self.layout)
218
-
219
- # text changed
299
+ # Populate combo box if vector and file exists
300
+ if self.is_vector and self.value["path"] and Path(self.value["path"]).exists():
301
+ try:
302
+ layers_dict = get_layers(self.value["path"])
303
+ self.layer_combo.clear()
304
+ for layer_name, geometry_type in layers_dict.items():
305
+ self.layer_combo.addItem(f"{layer_name} ({geometry_type})")
306
+ self.layer_combo.setVisible(True)
307
+ if self.value["layer"]:
308
+ index = self.layer_combo.findText(self.value["layer"])
309
+ if index >= 0:
310
+ self.layer_combo.setCurrentIndex(index)
311
+ except Exception:
312
+ self.layer_combo.clear()
313
+ self.layer_combo.setVisible(False)
220
314
  self.in_file.textChanged.connect(self.file_name_edited)
315
+ self.update_combo_visibility()
221
316
 
222
- # Handle showing the layer combo and making it editable when needed
223
- if self.value.lower().endswith('.gpkg'):
224
- self.layer_combo.setVisible(True) # Show the combo box if it's a .gpkg
225
- if self.output:
226
- self.layer_combo.setEditable(True) # Ensure editable if output is True
227
- self.layer_combo.addItem("") # Add an empty item to the combo box
228
- # If the .gpkg file doesn't exist, show empty layer
229
- if not os.path.exists(self.value):
230
- self.layer_combo.clear() # Clear the combo box
231
- self.layer_combo.addItem("layer_name") # Show "layer_name"
317
+ def update_gpkg_combo(self, path, is_output, selected_layer):
318
+ """Handle combo population, and layer selection for .gpkg files."""
319
+ self.layer_combo.setVisible(True)
320
+ if Path(path).exists():
321
+ if is_output:
322
+ self.layer_combo.setEditable(True)
323
+ if self.layer_combo.count() == 0:
324
+ self.layer_combo.addItem("Result_layer")
325
+ self.load_gpkg_layers(path)
326
+ elif self.layer_combo.itemText(0) != "Result_layer":
327
+ self.layer_combo.insertItem(0, "Result_layer")
328
+ self.load_gpkg_layers(path)
232
329
  else:
233
- self.layer_combo.setEditable(False) # Non-editable if output is False
234
- self.load_gpkg_layers(self.value) # Load layers if output is False
235
-
236
- # check saved layer existence, if True then set it to selected
237
- index = self.search_saved_combo_items()
238
- if index != -1:
239
- self.layer_combo.setCurrentIndex(index)
240
-
241
- # If the file is not a .gpkg, don't show the combo box at all
242
- elif self.layer_combo.isVisible():
243
- self.layer_combo.setVisible(False)
244
-
245
- self.update_combo_visibility() # Update combo visibility after init
330
+ self.layer_combo.setEditable(False)
331
+ if self.layer_combo.count() == 0 or self.layer_combo.itemText(0) == "Result_layer":
332
+ self.layer_combo.clear()
333
+ self.load_gpkg_layers(path)
334
+ else:
335
+ self.layer_combo.clear()
336
+ if is_output:
337
+ self.layer_combo.setEditable(True)
338
+ self.layer_combo.addItem("Result_layer")
339
+ else:
340
+ self.layer_combo.addItem("Result_layer")
341
+ # Set selected layer
342
+ if selected_layer:
343
+ if is_output and self.layer_combo.isEditable():
344
+ self.layer_combo.setCurrentText(selected_layer)
345
+ else:
346
+ index = self.layer_combo.findText(selected_layer)
347
+ if index >= 0:
348
+ self.layer_combo.setCurrentIndex(index)
349
+ self.layer_combo.adjustSize()
246
350
 
247
351
  def update_combo_visibility(self):
248
- if self.value.lower().endswith('.gpkg'):
249
- self.layer_combo.setVisible(True)
250
- if os.path.exists(self.value):
251
- if self.output:
252
- self.layer_combo.setEditable(True)
253
- if self.layer_combo.count() == 0:
254
- self.layer_combo.addItem("layer_name")
255
- self.load_gpkg_layers(self.value)
256
- elif self.layer_combo.itemText(0) != "layer_name":
257
- self.layer_combo.insertItem(0, "layer_name")
258
- self.load_gpkg_layers(self.value)
259
- else: # output is False
260
- self.layer_combo.setEditable(False)
261
- if (
262
- self.layer_combo.count() == 0
263
- or self.layer_combo.itemText(0) == "layer_name"
264
- ):
265
- self.layer_combo.clear()
266
- self.load_gpkg_layers(self.value)
267
- else: # gpkg does not exist
268
- self.layer_combo.clear()
269
- if self.output:
270
- self.layer_combo.setEditable(True)
271
- self.layer_combo.addItem("layer_name")
272
- else:
273
- self.layer_combo.addItem("layer_name")
274
-
275
- self.layer_combo.adjustSize()
352
+ # Support both string and dict for self.value
353
+ if self.is_vector:
354
+ path = self.value.get("path", "")
355
+ layer = self.value.get("layer", "")
356
+ is_gpkg = path.lower().endswith(".gpkg")
357
+ if is_gpkg:
358
+ self.update_gpkg_combo(path, self.output, layer)
359
+ else:
360
+ self.layer_combo.setVisible(False)
276
361
  else:
277
- self.layer_combo.setVisible(False)
278
-
362
+ if isinstance(self.value, str) and self.value.lower().endswith(".gpkg"):
363
+ selected_layer = getattr(self, "selected_layer", "")
364
+ self.update_gpkg_combo(self.value, self.output, selected_layer)
365
+ else:
366
+ self.layer_combo.setVisible(False)
279
367
  self.adjustSize()
280
368
  if self.parentWidget():
281
369
  self.parentWidget().layout().invalidate()
282
370
  self.parentWidget().adjustSize()
283
371
  self.parentWidget().update()
284
372
 
285
- def select_file(self):
286
- try:
287
- dialog = QtWidgets.QFileDialog(self)
288
- dialog.setViewMode(QtWidgets.QFileDialog.Detail)
289
- dialog.setDirectory(str(Path(self.value).parent))
290
- dialog.selectFile(Path(self.value).name)
291
- file_names = None
373
+ def get_file_filters(self):
374
+ """Return file type filter string based on self.file_type and current value."""
292
375
 
293
- file_types = "All files (*.*)"
376
+ def get_first_type(type_val):
377
+ if isinstance(type_val, list):
378
+ return type_val[0]
379
+ return type_val
294
380
 
295
- if 'RasterAndVector' in self.file_type:
296
- file_types = """Shapefiles (*.shp);;
297
- Raster files (*.dep *.tif *.tiff *.bil *.flt *.sdat *.asc *grd)"""
298
- elif 'Raster' in self.file_type:
299
- file_types = """Tiff raster files (*.tif *.tiff);;
381
+ file_types = "All files (*.*)"
382
+ ft = get_first_type(self.file_type)
383
+ if ft == "raster":
384
+ file_types = """Tiff raster files (*.tif *.tiff);;
300
385
  Other raster files (*.dep *.bil *.flt *.sdat *.asc *grd)"""
301
- elif 'Lidar' in self.file_type:
302
- file_types = "LiDAR files (*.las *.zlidar *.laz *.zip)"
303
- elif 'Vector' in self.file_type:
304
- file_types = """GeoPackage (*.gpkg);;
305
- Shapefiles (*.shp)"""
306
- elif 'Text' in self.file_type:
307
- file_types = "Text files (*.txt);; all files (*.*)"
308
- elif 'Csv' in self.file_type:
309
- file_types = "CSV files (*.csv);; all files (*.*)"
310
- elif 'Dat' in self.file_type:
311
- file_types = "Binary data files (*.dat);; all files (*.*)"
312
- elif 'Html' in self.file_type:
313
- file_types = "HTML files (*.html)"
314
- elif 'json' in self.file_type or 'JSON' in self.file_type:
315
- file_types = "JSON files (*.json)"
316
-
317
- # Check for GeoPackage/Shapefile first in filter order by current value
318
- if self.value.lower().endswith('.gpkg'):
319
- file_types = """GeoPackage (*.gpkg);;
386
+ elif ft == "lidar":
387
+ file_types = "LiDAR files (*.las *.zlidar *.laz *.zip)"
388
+ elif ft == "vector":
389
+ file_types = """GeoPackage (*.gpkg);;
390
+ Shapefiles (*.shp)"""
391
+ elif ft == "text":
392
+ file_types = "Text files (*.txt);; all files (*.*)"
393
+ elif ft == "csv":
394
+ file_types = "CSV files (*.csv);; all files (*.*)"
395
+ elif ft == "dat":
396
+ file_types = "Binary data files (*.dat);; all files (*.*)"
397
+ elif ft == "html":
398
+ file_types = "HTML files (*.html)"
399
+ elif ft == "json":
400
+ file_types = "JSON files (*.json)"
401
+
402
+ # Check for GeoPackage/Shapefile first in filter order by current value
403
+ if isinstance(self.value, str) and self.value.lower().endswith(".gpkg"):
404
+ file_types = """GeoPackage (*.gpkg);;
320
405
  Shapefiles (*.shp);;
321
406
  All files (*.*)"""
322
- elif self.value.lower().endswith('.shp'):
323
- file_types = """Shapefiles (*.shp);;
407
+ elif isinstance(self.value, str) and self.value.lower().endswith(".shp"):
408
+ file_types = """Shapefiles (*.shp);;
324
409
  GeoPackage (*.gpkg);;
325
410
  All files (*.*)"""
326
-
327
- dialog.setNameFilter(file_types)
328
-
329
- if "ExistingFile" in self.parameter_type:
330
- dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
411
+ return file_types
412
+
413
+ def setup_file_dialog(self, file_types):
414
+ """Initialize and configure QFileDialog."""
415
+ dialog = QtWidgets.QFileDialog(self)
416
+ dialog.setViewMode(QtWidgets.QFileDialog.Detail)
417
+ # Handle both string and dict values (for vector files)
418
+ path_value = self.value.get("path", "") if isinstance(self.value, dict) else self.value
419
+ if path_value:
420
+ dialog.setDirectory(str(Path(path_value).parent))
421
+ dialog.selectFile(Path(path_value).name)
422
+ dialog.setNameFilter(file_types)
423
+ if "ExistingFile" in self.parameter_type:
424
+ dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
425
+ else:
426
+ dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
427
+ return dialog
428
+
429
+ def process_selected_file(self, result, dialog):
430
+ """Handle file name modification, extension adding, and GeoPackage logic."""
431
+ base_name = str(Path(result).with_suffix(""))
432
+ selected_ext = Path(result).suffix
433
+ selected_filter = dialog.selectedNameFilter()
434
+ if selected_filter:
435
+ filter_parts = selected_filter.split("(*")
436
+ if len(filter_parts) > 1:
437
+ extensions_str = filter_parts[1].replace(")", "")
438
+ extensions = extensions_str.split(" ")
439
+ if extensions:
440
+ preferred_ext = extensions[0].strip()
441
+ if not preferred_ext.startswith("."):
442
+ preferred_ext = "." + preferred_ext
443
+ if not selected_ext:
444
+ result = f"{base_name}{preferred_ext}"
445
+ elif not selected_ext:
446
+ result = f"{base_name}.txt"
447
+ self.set_value(result)
448
+ return result
449
+
450
+ def handle_gpkg_selection(self, result):
451
+ """GeoPackage-specific logic after file selection."""
452
+ if result.lower().endswith(".gpkg"):
453
+ if not Path(result).exists():
454
+ self.layer_combo.clear()
455
+ self.layer_combo.addItem("Result_layer")
331
456
  else:
332
- dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
457
+ self.load_gpkg_layers(result)
458
+ if self.output:
459
+ self.layer_combo.setEditable(True)
460
+ else:
461
+ self.layer_combo.setVisible(False)
462
+ self.update_combo_visibility()
333
463
 
464
+ def select_file(self):
465
+ def get_first_type(type_val):
466
+ if isinstance(type_val, list):
467
+ return type_val[0]
468
+ return type_val
469
+
470
+ try:
471
+ file_types = self.get_file_filters()
472
+ dialog = self.setup_file_dialog(file_types)
473
+ file_names = None
334
474
  if dialog.exec_():
335
475
  file_names = dialog.selectedFiles()
336
-
337
476
  if not file_names:
338
477
  return
339
-
340
478
  result = file_names[0]
341
- base_name, selected_ext = os.path.splitext(result)
342
- selected_filter = dialog.selectedNameFilter()
343
-
344
- if selected_filter:
345
- filter_parts = selected_filter.split("(*")
346
- if len(filter_parts) > 1:
347
- extensions_str = filter_parts[1].replace(")", "")
348
- extensions = extensions_str.split(" ")
349
-
350
- if extensions:
351
- preferred_ext = extensions[0].strip()
352
- if not preferred_ext.startswith("."):
353
- preferred_ext = "." + preferred_ext
354
- if not selected_ext:
355
- result = f"{base_name}{preferred_ext}"
356
- elif not selected_ext: # No filter and no extension
357
- result = f"{base_name}.txt"
358
-
359
- self.set_value(result)
360
-
361
- if result.lower().endswith('.gpkg'):
362
- if not os.path.exists(result):
363
- self.layer_combo.clear()
364
- self.layer_combo.addItem("layer_name")
365
- else:
366
- self.load_gpkg_layers(result)
367
- if self.output:
368
- self.layer_combo.setEditable(True)
369
- else:
370
- self.layer_combo.setVisible(False)
371
-
372
- # Update combo visibility after file selection
373
- self.update_combo_visibility()
479
+ result = self.process_selected_file(result, dialog)
480
+ self.handle_gpkg_selection(result)
374
481
  except Exception as e:
375
482
  print(e)
376
- msg_box = QtWidgets.QMessageBox()
377
- msg_box.setIcon(QtWidgets.QMessageBox.Warning)
378
- msg_box.setText("Could not find the selected file.")
379
- msg_box.exec()
483
+ print("[Error] Could not find the selected file.")
380
484
 
381
485
  def load_gpkg_layers(self, gpkg_file):
382
486
  """Load layers from a GeoPackage and populate the combo box using get_layers."""
383
487
  try:
384
- # Print the file path to verify it's correct
385
- # print(f"Attempting to load layers from: {gpkg_file}")
386
-
387
- # Use get_layers to load layers from the GeoPackage
388
488
  self.gpkg_layers = get_layers(gpkg_file)
389
-
390
- # Check if layers is empty
391
489
  if not self.gpkg_layers:
392
490
  raise ValueError("No layers found in the GeoPackage.")
393
491
 
394
- # Clear any existing layers in the combo box
395
492
  self.layer_combo.clear()
396
493
 
397
- # Iterate over the layers dictionary
398
- # and add each layer name with geometry type to the combo box
399
- for layer_name, geometry_type in self.gpkg_layers.items():
400
- self.layer_combo.addItem(f"{layer_name} ({geometry_type})")
494
+ # Determine selected layer name (without geometry type)
495
+ selected_layer_name = self.selected_layer.split(" ")[0] if self.selected_layer else ""
496
+ loaded_layer_names = [str(k) for k in self.gpkg_layers.keys()]
497
+
498
+ # Output logic: add provided layer if missing
499
+ if self.output and selected_layer_name and selected_layer_name not in loaded_layer_names:
500
+ self.layer_combo.addItem(selected_layer_name)
501
+ for layer_name, geometry_type in self.gpkg_layers.items():
502
+ self.layer_combo.addItem(f"{layer_name} ({geometry_type})")
503
+ self.layer_combo.setEditable(True)
504
+ self.layer_combo.setCurrentText(selected_layer_name)
505
+ else:
506
+ for layer_name, geometry_type in self.gpkg_layers.items():
507
+ self.layer_combo.addItem(f"{layer_name} ({geometry_type})")
508
+ self.layer_combo.setEditable(self.output)
509
+ if self.selected_layer:
510
+ index = self.layer_combo.findText(self.selected_layer)
511
+ if index >= 0:
512
+ self.layer_combo.setCurrentIndex(index)
401
513
 
402
- # Set the tooltip for the layer list widget
403
514
  self.layer_combo.setToolTip("Select layer")
404
-
405
- # Make the combo box visible
406
515
  self.layer_combo.setVisible(True)
407
516
 
408
517
  except Exception as e:
409
- # Print the full error message for debugging purposes
410
- print(f"Error loading GeoPackage layers: {e}")
411
-
412
- # Show a message box with the error
413
- msg_box = QtWidgets.QMessageBox()
414
- msg_box.setIcon(QtWidgets.QMessageBox.Warning)
415
- msg_box.setText(f"Could not load layers from GeoPackage: {gpkg_file}")
416
- msg_box.setDetailedText(str(e)) # Show detailed error message
417
- msg_box.exec()
518
+ print(f"[Error] Could not load layers from GeoPackage: {gpkg_file}\nDetails: {e}")
418
519
 
419
520
  def file_name_edited(self):
420
- # Step 1: Get the current value in the file input field
421
521
  new_value = self.in_file.text()
422
- self.value = new_value # update file name
423
-
424
- # Step 2: Check if the new value ends with a .gpkg extension
425
- if new_value.lower().endswith('.gpkg'):
426
- # If it's a GeoPackage, check if the file exists
427
- if os.path.exists(new_value):
428
- # File exists, load layers from the GeoPackage
522
+ if self.is_vector:
523
+ self.value["path"] = new_value
524
+ else:
525
+ self.value = new_value
526
+ # Step 2: Check if the new value ends with a supported vector extension
527
+ ext = Path(new_value).suffix.lower().replace(".", "")
528
+ if self.is_vector and ext in self.VECTOR_FORMATS:
529
+ if Path(new_value).exists():
429
530
  self.load_gpkg_layers(new_value)
430
- self.layer_combo.setVisible(True) # Show the layer combo box
431
- self.update_combo_visibility() # Ensure layers are updated properly
531
+ self.layer_combo.setVisible(True)
532
+ self.update_combo_visibility()
432
533
  else:
433
- # File doesn't exist, clear the layer combo box and show message
434
534
  self.layer_combo.clear()
435
- self.layer_combo.addItem("layer_name")
535
+ self.layer_combo.addItem("Result_layer")
436
536
  if self.output:
437
537
  self.layer_combo.setEditable(True)
438
-
439
- # Show the layer combo box but indicate no layers
440
- self.layer_combo.setVisible(True)
538
+ self.layer_combo.setVisible(True)
441
539
  else:
442
- # If it's not a GeoPackage, hide the layer combo box
443
540
  self.layer_combo.setVisible(False)
444
-
445
- # Optional: Adjust the combo box visibility and layout
446
541
  self.adjustSize()
447
542
  if self.parentWidget():
448
543
  self.parentWidget().layout().invalidate()
@@ -450,75 +545,72 @@ class FileSelector(QtWidgets.QWidget):
450
545
  self.parentWidget().update()
451
546
 
452
547
  def set_value(self, value):
453
- # Check if the value has an extension
454
- base_name, ext = os.path.splitext(value)
455
-
456
- # Only append an extension if none exists AND the value doesn't end with a dot
457
- if not ext: # If there's no extension
458
- if not value.endswith("."): # If the user hasn't typed a dot at the end
459
- # Don't force the .txt extension unless the filename doesn't have one
460
- # Add default extension for other cases
461
- if not value.endswith(".gpkg") and not value.endswith(".shp"):
462
- value = f"{base_name}.txt"
463
- # If the value ends with a dot (like `file.`), don't append anything yet
464
-
465
- # If the value ends with a dot, don't append an extension.
466
- elif value.endswith("."):
467
- value = base_name # Strip the dot
468
-
469
- self.value = value
470
- self.in_file.setText(self.value)
471
- self.in_file.setToolTip(self.value)
548
+ # Accept both string and dict for compatibility
549
+ if self.is_vector:
550
+ if isinstance(value, dict):
551
+ self.value = value
552
+ elif isinstance(value, str):
553
+ if "|" in value:
554
+ path, layer = value.rsplit("|", 1)
555
+ self.value = {"path": path, "layer": layer}
556
+ else:
557
+ self.value = {"path": value, "layer": ""}
558
+ self.in_file.setText(self.value["path"])
559
+ self.in_file.setToolTip(self.value["path"])
560
+ else:
561
+ try: # load saved or default filepath like values
562
+ base_name = str(Path(value).with_suffix(""))
563
+ ext = Path(value).suffix
564
+ if not ext:
565
+ if not value.endswith("."):
566
+ if not value.endswith(".gpkg") and not value.endswith(".shp"):
567
+ value = f"{base_name}.txt"
568
+ elif value.endswith("."):
569
+ value = base_name
570
+ self.value = value
571
+ self.in_file.setText(self.value)
572
+ self.in_file.setToolTip(self.value)
573
+ except Exception as e:
574
+ # do nothing if first time initialize the tool with no previous values
575
+ self.value = ''
576
+ self.in_file.setText(self.value)
577
+ self.in_file.setToolTip(self.value)
472
578
  self.update_combo_visibility()
473
579
 
474
580
  def set_layer(self, layer):
475
- # Store only the selected layer's name (key) from the combo box display
476
- # The layer is in the format: "layer_name (geometry_type)"
477
- # Get only the layer name (before the space)
478
- self.selected_layer = layer.split(" ")[0]
479
- # print(f"Selected Layer: {self.selected_layer}")
581
+ # For vector, update dict
582
+ if self.is_vector:
583
+ # Remove geometry type if present
584
+ if "(" in layer:
585
+ layer = layer.split(" (")[0]
586
+ self.value["layer"] = layer
587
+ else:
588
+ self.selected_layer = layer
480
589
 
481
590
  def get_value(self):
482
- # Return both the file path and the selected layer
483
- value = {self.flag: self.value}
484
- if self.layer_flag and self.selected_layer:
485
- # Store the layer name (key)
486
- value.update({self.layer_flag: self.selected_layer})
487
-
488
- return value
489
-
490
- def search_saved_combo_items(self):
491
- """
492
- Search saved layer in combo box items.
493
-
494
- Returns:
495
- If found, then return the index, or return -1
496
-
497
- """
498
- if not self.gpkg_layers:
499
- return -1
500
-
501
- for idx, key in enumerate(self.gpkg_layers.keys()):
502
- if key == self.saved_layer:
503
- return idx
504
-
505
- return -1
506
-
507
- # TODO: check if this class is needed
508
- class MultiFileSelector(QtWidgets.QWidget):
509
- """MultiFileSelector class for creating multiple file selection widgets."""
510
-
511
- def __init__(self, json_str, parent=None):
512
- super(MultiFileSelector, self).__init__(parent)
513
- pass
514
-
515
- # TODO: check if this class is needed
516
- class BooleanInput(QtWidgets.QWidget):
517
- """BooleanInput class for creating boolean input widgets."""
518
-
519
- def __init__(self, json_str, parent=None):
520
- super(BooleanInput, self).__init__(parent)
521
- pass
591
+ # For vector, encode from dict for compatibility
592
+ if self.is_vector:
593
+ path = self.value.get("path", "")
594
+ layer = self.value.get("layer", "")
595
+ # If no layer selected but combo has items, use first layer
596
+ if not layer and self.layer_combo.count() > 0:
597
+ first_layer = self.layer_combo.itemText(0)
598
+ # Remove geometry type if present
599
+ if "(" in first_layer:
600
+ first_layer = first_layer.split(" (")[0]
601
+ self.value["layer"] = first_layer
602
+ layer = first_layer
603
+ if self.output:
604
+ # Output: always encode path|layer
605
+ encoded_value = f"{path}|{layer}" if layer else path
606
+ else:
607
+ # Input: if path is empty, return empty
608
+ if not path:
609
+ return {self.variable: ""}
610
+ encoded_value = f"{path}|{layer}" if layer else path
611
+ return {self.variable: encoded_value}
612
+ else:
613
+ return {self.variable: self.value}
522
614
 
523
615
 
524
616
  class OptionsInput(QtWidgets.QWidget):
@@ -529,17 +621,17 @@ class OptionsInput(QtWidgets.QWidget):
529
621
 
530
622
  # first make sure that the json data has the correct fields
531
623
  params = json.loads(json_str)
532
- self.name = params['name']
533
- self.description = params['description']
534
- self.flag = params['flag']
535
- self.parameter_type = params['parameter_type']
536
- self.optional = params['optional']
537
- self.data_type = params['data_type']
538
-
539
- self.default_value = str(params['default_value'])
624
+ self.name = params["name"]
625
+ self.description = params["description"]
626
+ self.variable = params["variable"]
627
+ self.parameter_type = params["parameter_type"]
628
+ self.optional = params["optional"]
629
+ self.data_type = params["data_type"]
630
+
631
+ self.default_value = str(params["default_value"])
540
632
  self.value = self.default_value
541
- if 'saved_value' in params.keys():
542
- self.value = params['saved_value']
633
+ if "saved_value" in params.keys():
634
+ self.value = params["saved_value"]
543
635
 
544
636
  self.label = QtWidgets.QLabel(self.name)
545
637
  self.label.setMinimumWidth(BT_LABEL_MIN_WIDTH)
@@ -548,10 +640,10 @@ class OptionsInput(QtWidgets.QWidget):
548
640
 
549
641
  i = 1
550
642
  default_index = -1
551
- self.option_list = params['parameter_type']['OptionList']
643
+ self.option_list = params["parameter_type"]["OptionList"]
552
644
  if self.option_list:
553
645
  # convert to strings
554
- self.option_list = [str(item) for item in self.option_list]
646
+ self.option_list = [str(item) for item in self.option_list]
555
647
  values = ()
556
648
  for v in self.option_list:
557
649
  values += (v,)
@@ -583,70 +675,80 @@ class OptionsInput(QtWidgets.QWidget):
583
675
  self.combobox.setCurrentIndex(self.option_list.index(v))
584
676
 
585
677
  def get_value(self):
586
- return {self.flag: self.value}
678
+ return {self.variable: self.value}
587
679
 
588
680
 
589
- class DataInput(QtWidgets.QWidget):
590
- """DataInput class for creating data input widgets."""
681
+ class NumericInput(QtWidgets.QWidget):
682
+ """NumericInput class for creating numeric input widgets (int and float)."""
591
683
 
592
684
  def __init__(self, json_str, parent=None):
593
- super(DataInput, self).__init__(parent)
594
-
595
- # first make sure that the json data has the correct fields
685
+ super(NumericInput, self).__init__(parent)
596
686
  params = json.loads(json_str)
597
- self.name = params['name']
598
- self.description = params['description']
599
- self.flag = params['flag']
600
- self.parameter_type = params['parameter_type']
601
- self.optional = params['optional']
602
-
603
- self.default_value = params['default_value']
687
+ self.name = params["name"]
688
+ self.description = params["description"]
689
+ self.variable = params["variable"]
690
+ self.parameter_type = params["parameter_type"]
691
+ self.optional = params["optional"]
692
+ self.default_value = params["default_value"]
604
693
  self.value = self.default_value
605
- if 'saved_value' in params.keys():
606
- self.value = params['saved_value']
607
-
694
+ if "saved_value" in params.keys():
695
+ self.value = params["saved_value"]
608
696
  self.label = QtWidgets.QLabel(self.name)
609
697
  self.label.setMinimumWidth(BT_LABEL_MIN_WIDTH)
610
698
  self.data_input = None
611
-
612
- if "Integer" in self.parameter_type:
699
+ subtypes = []
700
+ if isinstance(self.parameter_type, list):
701
+ subtypes = self.parameter_type
702
+ elif isinstance(self.parameter_type, str):
703
+ subtypes = [self.parameter_type]
704
+ elif isinstance(self.parameter_type, dict):
705
+ for v in self.parameter_type.values():
706
+ if isinstance(v, list):
707
+ subtypes.extend(v)
708
+ else:
709
+ subtypes.append(v)
710
+ main_subtype = subtypes[0] if subtypes else None
711
+ if main_subtype == "int":
613
712
  self.data_input = QtWidgets.QSpinBox()
614
- elif "Float" in self.parameter_type or "Double" in self.parameter_type:
713
+ elif main_subtype == "float":
615
714
  self.data_input = QtWidgets.QDoubleSpinBox()
616
-
617
- if self.data_input:
618
- self.data_input.setValue(self.value)
619
-
620
- self.data_input.valueChanged.connect(self.update_value)
621
-
715
+ else:
716
+ if main_subtype is not None:
717
+ raise ValueError(f"Unsupported parameter type: {main_subtype}")
718
+ if self.data_input and main_subtype in ("int", "float"):
719
+ try:
720
+ if main_subtype == "int":
721
+ self.data_input.setValue(int(self.value))
722
+ elif main_subtype == "float":
723
+ self.data_input.setValue(float(self.value))
724
+ except (ValueError, TypeError):
725
+ self.data_input.setValue(0)
726
+ self.data_input.valueChanged.connect(self.update_value)
622
727
  self.layout = QtWidgets.QHBoxLayout()
623
728
  self.layout.addWidget(self.label)
624
729
  self.layout.addWidget(self.data_input)
625
730
  self.setLayout(self.layout)
626
731
 
627
732
  def update_value(self):
628
- self.value = self.data_input.value()
733
+ if self.data_input is not None:
734
+ self.value = self.data_input.value()
629
735
 
630
736
  def get_value(self):
631
737
  v = self.value
632
738
  if v is not None:
633
- if "Integer" in self.parameter_type:
739
+ if "int" in self.parameter_type:
634
740
  value = int(self.value)
635
- elif "Float" in self.parameter_type:
741
+ elif "float" in self.parameter_type:
636
742
  value = float(self.value)
637
- elif "Double" in self.parameter_type:
638
- value = float(self.value)
639
- else: # String or StringOrNumber types
743
+ else:
640
744
  value = self.value
641
-
642
- return {self.flag: value}
745
+ return {self.variable: value}
643
746
  else:
644
747
  if not self.optional:
645
748
  msg_box = QtWidgets.QMessageBox()
646
749
  msg_box.setIcon(QtWidgets.QMessageBox.Warning)
647
- msg_box.setText("Unknown non-optional parameter {}.".format(self.flag))
750
+ msg_box.setText("Unknown non-optional parameter {}.".format(self.variable))
648
751
  msg_box.exec()
649
-
650
752
  return None
651
753
 
652
754
  def set_value(self, value):
@@ -660,6 +762,80 @@ class DataInput(QtWidgets.QWidget):
660
762
  self.update_value()
661
763
 
662
764
 
765
+ class BooleanInput(QtWidgets.QWidget):
766
+ """BooleanInput class for creating boolean checkbox widgets."""
767
+
768
+ def __init__(self, json_str, parent=None):
769
+ super(BooleanInput, self).__init__(parent)
770
+ params = json.loads(json_str)
771
+ self.name = params["name"]
772
+ self.description = params["description"]
773
+ self.variable = params["variable"]
774
+ self.parameter_type = params["parameter_type"]
775
+ self.optional = params["optional"]
776
+ self.default_value = params["default_value"]
777
+ # Detect if this is pure Boolean or OptionList boolean
778
+ self._detect_boolean_source(params)
779
+ self.value = self._convert_to_bool(self.default_value)
780
+ if "saved_value" in params.keys():
781
+ self.value = self._convert_to_bool(params["saved_value"])
782
+ self.checkbox = QtWidgets.QCheckBox(f"{self.name} - {self.description}")
783
+ self.checkbox.setChecked(self.value)
784
+ self.checkbox.stateChanged.connect(self.update_value)
785
+ self.label = self.checkbox # Reference checkbox as label for styling
786
+ self.layout = QtWidgets.QHBoxLayout()
787
+ self.layout.addWidget(self.checkbox)
788
+ self.layout.addStretch()
789
+ self.setLayout(self.layout)
790
+
791
+ def _detect_boolean_source(self, params):
792
+ """Determine if this is pure Boolean or OptionList boolean."""
793
+ pt = params["parameter_type"]
794
+
795
+ if isinstance(pt, str) and pt == "Boolean":
796
+ self.is_option_list = False
797
+ elif isinstance(pt, dict) and "OptionList" in pt:
798
+ option_list = pt["OptionList"]
799
+ # Check if it's a boolean list (handle both string and bool types)
800
+ is_bool_list = (
801
+ option_list == ["True", "False"] or
802
+ option_list == ["true", "false"] or
803
+ option_list == [True, False] or
804
+ option_list == [False, True]
805
+ )
806
+ if is_bool_list:
807
+ self.is_option_list = True
808
+ else:
809
+ raise ValueError("OptionList is not boolean, use OptionsInput instead")
810
+ else:
811
+ raise ValueError(f"Unsupported parameter type for BooleanInput: {pt}")
812
+
813
+ def _convert_to_bool(self, value):
814
+ """Convert various value types to boolean."""
815
+ if isinstance(value, bool):
816
+ return value
817
+ if isinstance(value, str):
818
+ return value.lower() in ("true", "1", "yes", "on")
819
+ if isinstance(value, (int, float)):
820
+ return bool(value)
821
+ return False
822
+
823
+ def update_value(self):
824
+ self.value = self.checkbox.isChecked()
825
+
826
+ def set_value(self, value):
827
+ """Set checkbox state from various value types."""
828
+ self.value = self._convert_to_bool(value)
829
+ self.checkbox.setChecked(self.value)
830
+
831
+ def get_value(self):
832
+ return {self.variable: self.checkbox.isChecked()}
833
+
834
+ def set_default_value(self):
835
+ self.value = self._convert_to_bool(self.default_value)
836
+ self.checkbox.setChecked(self.value)
837
+
838
+
663
839
  class DoubleSlider(QtWidgets.QSlider):
664
840
  """DoubleSlider class for creating double slider widgets."""
665
841
 
@@ -668,7 +844,7 @@ class DoubleSlider(QtWidgets.QSlider):
668
844
 
669
845
  def __init__(self, decimals=3, *args, **kargs):
670
846
  super(DoubleSlider, self).__init__(QtCore.Qt.Horizontal)
671
- self._multi = 10 ** decimals
847
+ self._multi = 10**decimals
672
848
 
673
849
  self.opt = QtWidgets.QStyleOptionSlider()
674
850
  self.initStyleOption(self.opt)
@@ -704,22 +880,22 @@ class DoubleSlider(QtWidgets.QSlider):
704
880
  )
705
881
  bottom_right_corner = sr.bottomLeft()
706
882
  QtWidgets.QToolTip.showText(
707
- self.mapToGlobal(
708
- QtCore.QPoint(bottom_right_corner.x(), bottom_right_corner.y())
709
- ),
883
+ self.mapToGlobal(QtCore.QPoint(bottom_right_corner.x(), bottom_right_corner.y())),
710
884
  str(self.value()),
711
885
  self,
712
886
  )
713
887
 
714
888
 
715
- if __name__ == '__main__':
889
+ if __name__ == "__main__":
716
890
  from bt_data import BTData
717
891
 
718
892
  bt = BTData()
719
893
 
720
894
  app = QtWidgets.QApplication(sys.argv)
721
- dlg = ToolWidgets('Raster Line Attributes',
722
- bt.get_bera_tool_args('Raster Line Attributes'),
723
- bt.show_advanced)
895
+ dlg = ToolWidgets(
896
+ "Raster Line Attributes",
897
+ bt.get_bera_tool_args("Raster Line Attributes"),
898
+ bt.show_advanced,
899
+ )
724
900
  dlg.show()
725
901
  sys.exit(app.exec_())