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.
- beratools/__init__.py +8 -3
- beratools/core/{algo_footprint_rel.py → algo_canopy_footprint_exp.py} +176 -139
- beratools/core/algo_centerline.py +61 -77
- beratools/core/algo_common.py +48 -57
- beratools/core/algo_cost.py +18 -25
- beratools/core/algo_dijkstra.py +37 -45
- beratools/core/algo_line_grouping.py +100 -100
- beratools/core/algo_merge_lines.py +40 -8
- beratools/core/algo_split_with_lines.py +289 -304
- beratools/core/algo_vertex_optimization.py +25 -46
- beratools/core/canopy_threshold_relative.py +755 -0
- beratools/core/constants.py +8 -9
- beratools/{tools → core}/line_footprint_functions.py +411 -258
- beratools/core/logger.py +18 -2
- beratools/core/tool_base.py +17 -75
- beratools/gui/assets/BERALogo.ico +0 -0
- beratools/gui/assets/BERA_Splash.gif +0 -0
- beratools/gui/assets/BERA_WizardImage.png +0 -0
- beratools/gui/assets/beratools.json +475 -2171
- beratools/gui/bt_data.py +585 -234
- beratools/gui/bt_gui_main.py +129 -91
- beratools/gui/main.py +4 -7
- beratools/gui/tool_widgets.py +530 -354
- beratools/tools/__init__.py +0 -7
- beratools/tools/{line_footprint_absolute.py → canopy_footprint_absolute.py} +81 -56
- beratools/tools/canopy_footprint_exp.py +113 -0
- beratools/tools/centerline.py +30 -37
- beratools/tools/check_seed_line.py +127 -0
- beratools/tools/common.py +65 -586
- beratools/tools/{line_footprint_fixed.py → ground_footprint.py} +140 -117
- beratools/tools/line_footprint_relative.py +64 -35
- beratools/tools/tool_template.py +48 -40
- beratools/tools/vertex_optimization.py +20 -34
- beratools/utility/env_checks.py +53 -0
- beratools/utility/spatial_common.py +210 -0
- beratools/utility/tool_args.py +138 -0
- beratools-0.2.4.dist-info/METADATA +134 -0
- beratools-0.2.4.dist-info/RECORD +50 -0
- {beratools-0.2.2.dist-info → beratools-0.2.4.dist-info}/WHEEL +1 -1
- beratools-0.2.4.dist-info/entry_points.txt +3 -0
- beratools-0.2.4.dist-info/licenses/LICENSE +674 -0
- beratools/core/algo_tiler.py +0 -428
- beratools/gui/__init__.py +0 -11
- beratools/gui/batch_processing_dlg.py +0 -513
- beratools/gui/map_window.py +0 -162
- beratools/tools/Beratools_r_script.r +0 -1120
- beratools/tools/Ht_metrics.py +0 -116
- beratools/tools/batch_processing.py +0 -136
- beratools/tools/canopy_threshold_relative.py +0 -672
- beratools/tools/canopycostraster.py +0 -222
- beratools/tools/fl_regen_csf.py +0 -428
- beratools/tools/forest_line_attributes.py +0 -408
- beratools/tools/line_grouping.py +0 -45
- beratools/tools/ln_relative_metrics.py +0 -615
- beratools/tools/r_cal_lpi_elai.r +0 -25
- beratools/tools/r_generate_pd_focalraster.r +0 -101
- beratools/tools/r_interface.py +0 -80
- beratools/tools/r_point_density.r +0 -9
- beratools/tools/rpy_chm2trees.py +0 -86
- beratools/tools/rpy_dsm_chm_by.py +0 -81
- beratools/tools/rpy_dtm_by.py +0 -63
- beratools/tools/rpy_find_cellsize.py +0 -43
- beratools/tools/rpy_gnd_csf.py +0 -74
- beratools/tools/rpy_hummock_hollow.py +0 -85
- beratools/tools/rpy_hummock_hollow_raster.py +0 -71
- beratools/tools/rpy_las_info.py +0 -51
- beratools/tools/rpy_laz2las.py +0 -40
- beratools/tools/rpy_lpi_elai_lascat.py +0 -466
- beratools/tools/rpy_normalized_lidar_by.py +0 -56
- beratools/tools/rpy_percent_above_dbh.py +0 -80
- beratools/tools/rpy_points2trees.py +0 -88
- beratools/tools/rpy_vegcoverage.py +0 -94
- beratools/tools/tiler.py +0 -48
- beratools/tools/zonal_threshold.py +0 -144
- beratools-0.2.2.dist-info/METADATA +0 -108
- beratools-0.2.2.dist-info/RECORD +0 -74
- beratools-0.2.2.dist-info/entry_points.txt +0 -2
- beratools-0.2.2.dist-info/licenses/LICENSE +0 -22
beratools/gui/tool_widgets.py
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
87
|
+
elif "FileList" in pt:
|
|
80
88
|
widget = MultiFileSelector(json_str, None)
|
|
81
89
|
param_num = param_num + 1
|
|
82
|
-
elif
|
|
90
|
+
elif "Boolean" in pt:
|
|
83
91
|
widget = BooleanInput(json_str)
|
|
84
92
|
param_num = param_num + 1
|
|
85
|
-
elif
|
|
86
|
-
|
|
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
|
|
89
|
-
|
|
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
|
|
101
|
-
param_value = p[
|
|
117
|
+
if "saved_value" in p.keys():
|
|
118
|
+
param_value = p["saved_value"]
|
|
102
119
|
if param_value is None:
|
|
103
|
-
param_value = p[
|
|
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.
|
|
125
|
+
widget.set_value(param_value)
|
|
107
126
|
elif widget:
|
|
108
|
-
widget.
|
|
127
|
+
widget.set_value(param_value)
|
|
109
128
|
else:
|
|
110
|
-
print(
|
|
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.
|
|
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.
|
|
134
|
-
params[item.
|
|
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,
|
|
150
|
-
isinstance(layer,
|
|
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
|
-
|
|
205
|
+
def parse_params(self, json_str):
|
|
174
206
|
params = json.loads(json_str)
|
|
175
|
-
self.name = params[
|
|
176
|
-
self.description = params[
|
|
177
|
-
self.
|
|
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[
|
|
186
|
-
self.parameter_type = params[
|
|
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[
|
|
216
|
+
self.file_type = params["parameter_type"]["ExistingFile"]
|
|
190
217
|
elif "NewFile" in self.parameter_type:
|
|
191
|
-
self.file_type = params[
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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)
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
self.layer_combo.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
self.
|
|
231
|
-
|
|
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)
|
|
234
|
-
self.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
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
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
398
|
-
|
|
399
|
-
for
|
|
400
|
-
|
|
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
|
-
|
|
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.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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)
|
|
431
|
-
self.update_combo_visibility()
|
|
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("
|
|
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
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
value =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
#
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
#
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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[
|
|
533
|
-
self.description = params[
|
|
534
|
-
self.
|
|
535
|
-
self.parameter_type = params[
|
|
536
|
-
self.optional = params[
|
|
537
|
-
self.data_type = params[
|
|
538
|
-
|
|
539
|
-
self.default_value = str(params[
|
|
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
|
|
542
|
-
self.value = params[
|
|
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[
|
|
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.
|
|
678
|
+
return {self.variable: self.value}
|
|
587
679
|
|
|
588
680
|
|
|
589
|
-
class
|
|
590
|
-
"""
|
|
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(
|
|
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[
|
|
598
|
-
self.description = params[
|
|
599
|
-
self.
|
|
600
|
-
self.parameter_type = params[
|
|
601
|
-
self.optional = params[
|
|
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
|
|
606
|
-
self.value = params[
|
|
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
|
|
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
|
|
713
|
+
elif main_subtype == "float":
|
|
615
714
|
self.data_input = QtWidgets.QDoubleSpinBox()
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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.
|
|
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 "
|
|
739
|
+
if "int" in self.parameter_type:
|
|
634
740
|
value = int(self.value)
|
|
635
|
-
elif "
|
|
741
|
+
elif "float" in self.parameter_type:
|
|
636
742
|
value = float(self.value)
|
|
637
|
-
|
|
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.
|
|
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
|
|
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__ ==
|
|
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(
|
|
722
|
-
|
|
723
|
-
|
|
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_())
|