BERATools 0.1.0__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 (44) hide show
  1. beratools/__init__.py +3 -0
  2. beratools/core/__init__.py +0 -0
  3. beratools/core/algo_centerline.py +476 -0
  4. beratools/core/algo_common.py +489 -0
  5. beratools/core/algo_cost.py +185 -0
  6. beratools/core/algo_dijkstra.py +492 -0
  7. beratools/core/algo_footprint_rel.py +693 -0
  8. beratools/core/algo_line_grouping.py +941 -0
  9. beratools/core/algo_merge_lines.py +255 -0
  10. beratools/core/algo_split_with_lines.py +296 -0
  11. beratools/core/algo_vertex_optimization.py +451 -0
  12. beratools/core/constants.py +56 -0
  13. beratools/core/logger.py +92 -0
  14. beratools/core/tool_base.py +126 -0
  15. beratools/gui/__init__.py +11 -0
  16. beratools/gui/assets/BERALogo.png +0 -0
  17. beratools/gui/assets/beratools.json +471 -0
  18. beratools/gui/assets/closed.gif +0 -0
  19. beratools/gui/assets/closed.png +0 -0
  20. beratools/gui/assets/gui.json +8 -0
  21. beratools/gui/assets/open.gif +0 -0
  22. beratools/gui/assets/open.png +0 -0
  23. beratools/gui/assets/tool.gif +0 -0
  24. beratools/gui/assets/tool.png +0 -0
  25. beratools/gui/bt_data.py +485 -0
  26. beratools/gui/bt_gui_main.py +700 -0
  27. beratools/gui/main.py +27 -0
  28. beratools/gui/tool_widgets.py +730 -0
  29. beratools/tools/__init__.py +7 -0
  30. beratools/tools/canopy_threshold_relative.py +769 -0
  31. beratools/tools/centerline.py +127 -0
  32. beratools/tools/check_seed_line.py +48 -0
  33. beratools/tools/common.py +622 -0
  34. beratools/tools/line_footprint_absolute.py +203 -0
  35. beratools/tools/line_footprint_fixed.py +480 -0
  36. beratools/tools/line_footprint_functions.py +884 -0
  37. beratools/tools/line_footprint_relative.py +75 -0
  38. beratools/tools/tool_template.py +72 -0
  39. beratools/tools/vertex_optimization.py +57 -0
  40. beratools-0.1.0.dist-info/METADATA +134 -0
  41. beratools-0.1.0.dist-info/RECORD +44 -0
  42. beratools-0.1.0.dist-info/WHEEL +4 -0
  43. beratools-0.1.0.dist-info/entry_points.txt +2 -0
  44. beratools-0.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,730 @@
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to provide all kinds of widgets for tool parameters.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import sys
19
+ from collections import OrderedDict
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+ import pyogrio
24
+ from PyQt5 import QtCore, QtWidgets
25
+
26
+ BT_LABEL_MIN_WIDTH = 130
27
+
28
+
29
+ class ToolWidgets(QtWidgets.QWidget):
30
+ """ToolWidgets class for creating widgets for tool parameters."""
31
+
32
+ signal_save_tool_params = QtCore.pyqtSignal(object)
33
+
34
+ def __init__(self, tool_name, tool_args, show_advanced, parent=None):
35
+ super(ToolWidgets, self).__init__(parent)
36
+
37
+ self.tool_name = tool_name
38
+ self.show_advanced = show_advanced
39
+ self.current_tool_api = ""
40
+ self.widget_list = []
41
+ self.setWindowTitle("Tool widgets")
42
+
43
+ self.create_widgets(tool_args)
44
+ layout = QtWidgets.QVBoxLayout()
45
+
46
+ for item in self.widget_list:
47
+ layout.addWidget(item)
48
+
49
+ self.save_button = QtWidgets.QPushButton("Save Parameters")
50
+ self.save_button.clicked.connect(self.save_tool_parameters)
51
+ self.save_button.setFixedSize(200, 50)
52
+ layout.addSpacing(20)
53
+ self.setLayout(layout)
54
+
55
+ def get_widgets_arguments(self):
56
+ args = {}
57
+ param_missing = False
58
+ for widget in self.widget_list:
59
+ v = widget.get_value()
60
+ if v:
61
+ args.update(v)
62
+ else:
63
+ print(f"[Missing argument]: {widget.name} not specified.", "missing")
64
+ param_missing = True
65
+
66
+ if param_missing:
67
+ args = None
68
+
69
+ return args
70
+
71
+ def create_widgets(self, tool_args):
72
+ param_num = 0
73
+ for p in tool_args:
74
+ json_str = json.dumps(p, sort_keys=True, indent=2, separators=(",", ": "))
75
+ pt = p["parameter_type"]
76
+ widget = None
77
+
78
+ if "ExistingFile" in pt or "NewFile" in pt or "directory" in pt:
79
+ widget = FileSelector(json_str, None)
80
+ param_num = param_num + 1
81
+ elif "FileList" in pt:
82
+ widget = MultiFileSelector(json_str, None)
83
+ param_num = param_num + 1
84
+ elif "Boolean" in pt:
85
+ widget = BooleanInput(json_str)
86
+ param_num = param_num + 1
87
+ elif "OptionList" in pt:
88
+ widget = OptionsInput(json_str)
89
+ param_num = param_num + 1
90
+ elif (
91
+ "Float" in pt
92
+ or "Integer" in pt
93
+ or "Text" in pt
94
+ or "String" in pt
95
+ or "StringOrNumber" in pt
96
+ or "StringList" in pt
97
+ or "VectorAttributeField" in pt
98
+ ):
99
+ widget = DataInput(json_str)
100
+ param_num = param_num + 1
101
+ else:
102
+ msg_box = QtWidgets.QMessageBox()
103
+ msg_box.setIcon(QtWidgets.QMessageBox.Warning)
104
+ msg_box.setText("Unsupported parameter type: {}.".format(pt))
105
+ msg_box.exec()
106
+
107
+ param_value = None
108
+ if "saved_value" in p.keys():
109
+ param_value = p["saved_value"]
110
+ if param_value is None:
111
+ param_value = p["default_value"]
112
+ if param_value is not None:
113
+ if type(widget) is OptionsInput:
114
+ widget.value = param_value
115
+ elif widget:
116
+ widget.value = param_value
117
+ else:
118
+ print("No default value found: {}".format(p["name"]))
119
+
120
+ # hide optional widgets
121
+ if widget:
122
+ if widget.optional and widget.label:
123
+ widget.label.setStyleSheet(
124
+ "QtWidgets.QLabel { background-color : transparent; color : blue; }"
125
+ )
126
+
127
+ if not self.show_advanced and widget.optional:
128
+ widget.hide()
129
+
130
+ self.widget_list.append(widget)
131
+
132
+ def update_widgets(self, values_dict):
133
+ for key, value in values_dict.items():
134
+ for item in self.widget_list:
135
+ if key == item.flag:
136
+ item.set_value(value)
137
+
138
+ def save_tool_parameters(self):
139
+ params = {}
140
+ for item in self.widget_list:
141
+ if item.flag:
142
+ params[item.flag] = item.get_value()
143
+
144
+ self.signal_save_tool_params.emit(params)
145
+
146
+ def load_default_args(self):
147
+ for item in self.widget_list:
148
+ item.set_default_value()
149
+
150
+
151
+ def get_layers(gpkg_file):
152
+ try:
153
+ # Get the list of layers and their geometry types from the GeoPackage file
154
+ layers_info = pyogrio.list_layers(gpkg_file)
155
+
156
+ # Check if layers_info is in the expected format
157
+ if isinstance(layers_info, np.ndarray) and all(
158
+ isinstance(layer, np.ndarray) and len(layer) >= 2 for layer in layers_info
159
+ ):
160
+ # Create a dictionary where the key is the layer name
161
+ # and the value is the geometry type
162
+ layers_dict = OrderedDict((layer[0], layer[1]) for layer in layers_info)
163
+ return layers_dict
164
+ else:
165
+ # If the format is not correct, raise an exception with a detailed message
166
+ raise ValueError("Expected a list of lists or tuples with layer name and geometry type.")
167
+
168
+ except Exception as e:
169
+ print(f"Error retrieving layers from GeoPackage '{gpkg_file}': {e}")
170
+ raise
171
+
172
+
173
+ class FileSelector(QtWidgets.QWidget):
174
+ """FileSelector class for creating file selection widgets."""
175
+
176
+ def __init__(self, json_str, parent=None):
177
+ super(FileSelector, self).__init__(parent)
178
+
179
+ # Parsing the JSON data
180
+ params = json.loads(json_str)
181
+ self.name = params["name"]
182
+ self.description = params["description"]
183
+ self.flag = params["flag"]
184
+ self.layer_flag = None
185
+ self.saved_layer = ""
186
+ if "layer" in params.keys():
187
+ self.layer_flag = params["layer"]["layer_name"]
188
+ self.saved_layer = params["layer"]["layer_value"]
189
+
190
+ self.gpkg_layers = None
191
+ self.output = params["output"] # Ensure output flag is read
192
+ self.parameter_type = params["parameter_type"]
193
+ self.file_type = ""
194
+ if "ExistingFile" in self.parameter_type:
195
+ self.file_type = params["parameter_type"]["ExistingFile"]
196
+ elif "NewFile" in self.parameter_type:
197
+ self.file_type = params["parameter_type"]["NewFile"]
198
+ self.optional = params["optional"]
199
+
200
+ self.default_value = params["default_value"]
201
+ self.value = self.default_value
202
+ self.selected_layer = None # Add attribute for selected layer
203
+ if "saved_value" in params.keys():
204
+ self.value = params["saved_value"]
205
+
206
+ self.layout = QtWidgets.QHBoxLayout()
207
+ self.label = QtWidgets.QLabel(self.name)
208
+ self.label.setMinimumWidth(200)
209
+ self.in_file = QtWidgets.QLineEdit(self.value)
210
+ self.btn_select = QtWidgets.QPushButton("...")
211
+ self.btn_select.clicked.connect(self.select_file)
212
+
213
+ # ComboBox for displaying GeoPackage layers
214
+ self.layer_combo = QtWidgets.QComboBox()
215
+ self.layer_combo.setVisible(False) # Initially hidden
216
+ # Connect layer change event
217
+ self.layer_combo.currentTextChanged.connect(self.set_layer)
218
+ self.layout.addWidget(self.label)
219
+ self.layout.addWidget(self.in_file)
220
+ self.layout.addWidget(self.layer_combo)
221
+ self.layout.addWidget(self.btn_select)
222
+
223
+ self.setLayout(self.layout)
224
+
225
+ # text changed
226
+ self.in_file.textChanged.connect(self.file_name_edited)
227
+
228
+ # Handle showing the layer combo and making it editable when needed
229
+ if self.value.lower().endswith(".gpkg"):
230
+ self.layer_combo.setVisible(True) # Show the combo box if it's a .gpkg
231
+ if self.output:
232
+ self.layer_combo.setEditable(True) # Ensure editable if output is True
233
+ self.layer_combo.addItem("") # Add an empty item to the combo box
234
+ # If the .gpkg file doesn't exist, show empty layer
235
+ if not os.path.exists(self.value):
236
+ self.layer_combo.clear() # Clear the combo box
237
+ self.layer_combo.addItem("layer_name") # Show "layer_name"
238
+ else:
239
+ self.layer_combo.setEditable(False) # Non-editable if output is False
240
+ self.load_gpkg_layers(self.value) # Load layers if output is False
241
+
242
+ # check saved layer existence, if True then set it to selected
243
+ index = self.search_saved_combo_items()
244
+ if index != -1:
245
+ self.layer_combo.setCurrentIndex(index)
246
+
247
+ # If the file is not a .gpkg, don't show the combo box at all
248
+ elif self.layer_combo.isVisible():
249
+ self.layer_combo.setVisible(False)
250
+
251
+ self.update_combo_visibility() # Update combo visibility after init
252
+
253
+ def update_combo_visibility(self):
254
+ if self.value.lower().endswith(".gpkg"):
255
+ self.layer_combo.setVisible(True)
256
+ if os.path.exists(self.value):
257
+ if self.output:
258
+ self.layer_combo.setEditable(True)
259
+ if self.layer_combo.count() == 0:
260
+ self.layer_combo.addItem("layer_name")
261
+ self.load_gpkg_layers(self.value)
262
+ elif self.layer_combo.itemText(0) != "layer_name":
263
+ self.layer_combo.insertItem(0, "layer_name")
264
+ self.load_gpkg_layers(self.value)
265
+ else: # output is False
266
+ self.layer_combo.setEditable(False)
267
+ if self.layer_combo.count() == 0 or self.layer_combo.itemText(0) == "layer_name":
268
+ self.layer_combo.clear()
269
+ self.load_gpkg_layers(self.value)
270
+ else: # gpkg does not exist
271
+ self.layer_combo.clear()
272
+ if self.output:
273
+ self.layer_combo.setEditable(True)
274
+ self.layer_combo.addItem("layer_name")
275
+ else:
276
+ self.layer_combo.addItem("layer_name")
277
+
278
+ self.layer_combo.adjustSize()
279
+ else:
280
+ self.layer_combo.setVisible(False)
281
+
282
+ self.adjustSize()
283
+ if self.parentWidget():
284
+ self.parentWidget().layout().invalidate()
285
+ self.parentWidget().adjustSize()
286
+ self.parentWidget().update()
287
+
288
+ def select_file(self):
289
+ try:
290
+ dialog = QtWidgets.QFileDialog(self)
291
+ dialog.setViewMode(QtWidgets.QFileDialog.Detail)
292
+ dialog.setDirectory(str(Path(self.value).parent))
293
+ dialog.selectFile(Path(self.value).name)
294
+ file_names = None
295
+
296
+ file_types = "All files (*.*)"
297
+
298
+ if "RasterAndVector" in self.file_type:
299
+ file_types = """Shapefiles (*.shp);;
300
+ Raster files (*.dep *.tif *.tiff *.bil *.flt *.sdat *.asc *grd)"""
301
+ elif "Raster" in self.file_type:
302
+ file_types = """Tiff raster files (*.tif *.tiff);;
303
+ Other raster files (*.dep *.bil *.flt *.sdat *.asc *grd)"""
304
+ elif "Lidar" in self.file_type:
305
+ file_types = "LiDAR files (*.las *.zlidar *.laz *.zip)"
306
+ elif "Vector" in self.file_type:
307
+ file_types = """GeoPackage (*.gpkg);;
308
+ Shapefiles (*.shp)"""
309
+ elif "Text" in self.file_type:
310
+ file_types = "Text files (*.txt);; all files (*.*)"
311
+ elif "Csv" in self.file_type:
312
+ file_types = "CSV files (*.csv);; all files (*.*)"
313
+ elif "Dat" in self.file_type:
314
+ file_types = "Binary data files (*.dat);; all files (*.*)"
315
+ elif "Html" in self.file_type:
316
+ file_types = "HTML files (*.html)"
317
+ elif "json" in self.file_type or "JSON" in self.file_type:
318
+ file_types = "JSON files (*.json)"
319
+
320
+ # Check for GeoPackage/Shapefile first in filter order by current value
321
+ if self.value.lower().endswith(".gpkg"):
322
+ file_types = """GeoPackage (*.gpkg);;
323
+ Shapefiles (*.shp);;
324
+ All files (*.*)"""
325
+ elif self.value.lower().endswith(".shp"):
326
+ file_types = """Shapefiles (*.shp);;
327
+ GeoPackage (*.gpkg);;
328
+ All files (*.*)"""
329
+
330
+ dialog.setNameFilter(file_types)
331
+
332
+ if "ExistingFile" in self.parameter_type:
333
+ dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
334
+ else:
335
+ dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
336
+
337
+ if dialog.exec_():
338
+ file_names = dialog.selectedFiles()
339
+
340
+ if not file_names:
341
+ return
342
+
343
+ result = file_names[0]
344
+ base_name, selected_ext = os.path.splitext(result)
345
+ selected_filter = dialog.selectedNameFilter()
346
+
347
+ if selected_filter:
348
+ filter_parts = selected_filter.split("(*")
349
+ if len(filter_parts) > 1:
350
+ extensions_str = filter_parts[1].replace(")", "")
351
+ extensions = extensions_str.split(" ")
352
+
353
+ if extensions:
354
+ preferred_ext = extensions[0].strip()
355
+ if not preferred_ext.startswith("."):
356
+ preferred_ext = "." + preferred_ext
357
+ if not selected_ext:
358
+ result = f"{base_name}{preferred_ext}"
359
+ elif not selected_ext: # No filter and no extension
360
+ result = f"{base_name}.txt"
361
+
362
+ self.set_value(result)
363
+
364
+ if result.lower().endswith(".gpkg"):
365
+ if not os.path.exists(result):
366
+ self.layer_combo.clear()
367
+ self.layer_combo.addItem("layer_name")
368
+ else:
369
+ self.load_gpkg_layers(result)
370
+ if self.output:
371
+ self.layer_combo.setEditable(True)
372
+ else:
373
+ self.layer_combo.setVisible(False)
374
+
375
+ # Update combo visibility after file selection
376
+ self.update_combo_visibility()
377
+ except Exception as e:
378
+ print(e)
379
+ msg_box = QtWidgets.QMessageBox()
380
+ msg_box.setIcon(QtWidgets.QMessageBox.Warning)
381
+ msg_box.setText("Could not find the selected file.")
382
+ msg_box.exec()
383
+
384
+ def load_gpkg_layers(self, gpkg_file):
385
+ """Load layers from a GeoPackage and populate the combo box using get_layers."""
386
+ try:
387
+ # Print the file path to verify it's correct
388
+ # print(f"Attempting to load layers from: {gpkg_file}")
389
+
390
+ # Use get_layers to load layers from the GeoPackage
391
+ self.gpkg_layers = get_layers(gpkg_file)
392
+
393
+ # Check if layers is empty
394
+ if not self.gpkg_layers:
395
+ raise ValueError("No layers found in the GeoPackage.")
396
+
397
+ # Clear any existing layers in the combo box
398
+ self.layer_combo.clear()
399
+
400
+ # Iterate over the layers dictionary
401
+ # and add each layer name with geometry type to the combo box
402
+ for layer_name, geometry_type in self.gpkg_layers.items():
403
+ self.layer_combo.addItem(f"{layer_name} ({geometry_type})")
404
+
405
+ # Set the tooltip for the layer list widget
406
+ self.layer_combo.setToolTip("Select layer")
407
+
408
+ # Make the combo box visible
409
+ self.layer_combo.setVisible(True)
410
+
411
+ except Exception as e:
412
+ # Print the full error message for debugging purposes
413
+ print(f"Error loading GeoPackage layers: {e}")
414
+
415
+ # Show a message box with the error
416
+ msg_box = QtWidgets.QMessageBox()
417
+ msg_box.setIcon(QtWidgets.QMessageBox.Warning)
418
+ msg_box.setText(f"Could not load layers from GeoPackage: {gpkg_file}")
419
+ msg_box.setDetailedText(str(e)) # Show detailed error message
420
+ msg_box.exec()
421
+
422
+ def file_name_edited(self):
423
+ # Step 1: Get the current value in the file input field
424
+ new_value = self.in_file.text()
425
+ self.value = new_value # update file name
426
+
427
+ # Step 2: Check if the new value ends with a .gpkg extension
428
+ if new_value.lower().endswith(".gpkg"):
429
+ # If it's a GeoPackage, check if the file exists
430
+ if os.path.exists(new_value):
431
+ # File exists, load layers from the GeoPackage
432
+ self.load_gpkg_layers(new_value)
433
+ self.layer_combo.setVisible(True) # Show the layer combo box
434
+ self.update_combo_visibility() # Ensure layers are updated properly
435
+ else:
436
+ # File doesn't exist, clear the layer combo box and show message
437
+ self.layer_combo.clear()
438
+ self.layer_combo.addItem("layer_name")
439
+ if self.output:
440
+ self.layer_combo.setEditable(True)
441
+
442
+ # Show the layer combo box but indicate no layers
443
+ self.layer_combo.setVisible(True)
444
+ else:
445
+ # If it's not a GeoPackage, hide the layer combo box
446
+ self.layer_combo.setVisible(False)
447
+
448
+ # Optional: Adjust the combo box visibility and layout
449
+ self.adjustSize()
450
+ if self.parentWidget():
451
+ self.parentWidget().layout().invalidate()
452
+ self.parentWidget().adjustSize()
453
+ self.parentWidget().update()
454
+
455
+ def set_value(self, value):
456
+ # Check if the value has an extension
457
+ base_name, ext = os.path.splitext(value)
458
+
459
+ # Only append an extension if none exists AND the value doesn't end with a dot
460
+ if not ext: # If there's no extension
461
+ if not value.endswith("."): # If the user hasn't typed a dot at the end
462
+ # Don't force the .txt extension unless the filename doesn't have one
463
+ # Add default extension for other cases
464
+ if not value.endswith(".gpkg") and not value.endswith(".shp"):
465
+ value = f"{base_name}.txt"
466
+ # If the value ends with a dot (like `file.`), don't append anything yet
467
+
468
+ # If the value ends with a dot, don't append an extension.
469
+ elif value.endswith("."):
470
+ value = base_name # Strip the dot
471
+
472
+ self.value = value
473
+ self.in_file.setText(self.value)
474
+ self.in_file.setToolTip(self.value)
475
+ self.update_combo_visibility()
476
+
477
+ def set_layer(self, layer):
478
+ # Store only the selected layer's name (key) from the combo box display
479
+ # The layer is in the format: "layer_name (geometry_type)"
480
+ # Get only the layer name (before the space)
481
+ self.selected_layer = layer.split(" ")[0]
482
+ # print(f"Selected Layer: {self.selected_layer}")
483
+
484
+ def get_value(self):
485
+ # Return both the file path and the selected layer
486
+ value = {self.flag: self.value}
487
+ if self.layer_flag and self.selected_layer:
488
+ # Store the layer name (key)
489
+ value.update({self.layer_flag: self.selected_layer})
490
+
491
+ return value
492
+
493
+ def search_saved_combo_items(self):
494
+ """
495
+ Search saved layer in combo box items.
496
+
497
+ Returns:
498
+ If found, then return the index, or return -1
499
+
500
+ """
501
+ if not self.gpkg_layers:
502
+ return -1
503
+
504
+ for idx, key in enumerate(self.gpkg_layers.keys()):
505
+ if key == self.saved_layer:
506
+ return idx
507
+
508
+ return -1
509
+
510
+
511
+ # TODO: check if this class is needed
512
+ class MultiFileSelector(QtWidgets.QWidget):
513
+ """MultiFileSelector class for creating multiple file selection widgets."""
514
+
515
+ def __init__(self, json_str, parent=None):
516
+ super(MultiFileSelector, self).__init__(parent)
517
+ pass
518
+
519
+
520
+ # TODO: check if this class is needed
521
+ class BooleanInput(QtWidgets.QWidget):
522
+ """BooleanInput class for creating boolean input widgets."""
523
+
524
+ def __init__(self, json_str, parent=None):
525
+ super(BooleanInput, self).__init__(parent)
526
+ pass
527
+
528
+
529
+ class OptionsInput(QtWidgets.QWidget):
530
+ """OptionsInput class for creating option selection widgets."""
531
+
532
+ def __init__(self, json_str, parent=None):
533
+ super(OptionsInput, self).__init__(parent)
534
+
535
+ # first make sure that the json data has the correct fields
536
+ params = json.loads(json_str)
537
+ self.name = params["name"]
538
+ self.description = params["description"]
539
+ self.flag = params["flag"]
540
+ self.parameter_type = params["parameter_type"]
541
+ self.optional = params["optional"]
542
+ self.data_type = params["data_type"]
543
+
544
+ self.default_value = str(params["default_value"])
545
+ self.value = self.default_value
546
+ if "saved_value" in params.keys():
547
+ self.value = params["saved_value"]
548
+
549
+ self.label = QtWidgets.QLabel(self.name)
550
+ self.label.setMinimumWidth(BT_LABEL_MIN_WIDTH)
551
+ self.combobox = QtWidgets.QComboBox()
552
+ self.combobox.currentIndexChanged.connect(self.selection_change)
553
+
554
+ i = 1
555
+ default_index = -1
556
+ self.option_list = params["parameter_type"]["OptionList"]
557
+ if self.option_list:
558
+ # convert to strings
559
+ self.option_list = [str(item) for item in self.option_list]
560
+ values = ()
561
+ for v in self.option_list:
562
+ values += (v,)
563
+ if v == str(self.value):
564
+ default_index = i - 1
565
+ i = i + 1
566
+
567
+ self.combobox.addItems(self.option_list)
568
+ self.combobox.setCurrentIndex(default_index)
569
+
570
+ self.layout = QtWidgets.QHBoxLayout()
571
+ self.layout.addWidget(self.label)
572
+ self.layout.addWidget(self.combobox)
573
+ self.setLayout(self.layout)
574
+
575
+ def selection_change(self, i):
576
+ self.value = self.option_list[i]
577
+
578
+ def set_value(self, value):
579
+ self.value = value
580
+ for v in self.option_list:
581
+ if value == v:
582
+ self.combobox.setCurrentIndex(self.option_list.index(v))
583
+
584
+ def set_default_value(self):
585
+ self.value = self.default_value
586
+ for v in self.option_list:
587
+ if self.value == v:
588
+ self.combobox.setCurrentIndex(self.option_list.index(v))
589
+
590
+ def get_value(self):
591
+ return {self.flag: self.value}
592
+
593
+
594
+ class DataInput(QtWidgets.QWidget):
595
+ """DataInput class for creating data input widgets."""
596
+
597
+ def __init__(self, json_str, parent=None):
598
+ super(DataInput, self).__init__(parent)
599
+
600
+ # first make sure that the json data has the correct fields
601
+ params = json.loads(json_str)
602
+ self.name = params["name"]
603
+ self.description = params["description"]
604
+ self.flag = params["flag"]
605
+ self.parameter_type = params["parameter_type"]
606
+ self.optional = params["optional"]
607
+
608
+ self.default_value = params["default_value"]
609
+ self.value = self.default_value
610
+ if "saved_value" in params.keys():
611
+ self.value = params["saved_value"]
612
+
613
+ self.label = QtWidgets.QLabel(self.name)
614
+ self.label.setMinimumWidth(BT_LABEL_MIN_WIDTH)
615
+ self.data_input = None
616
+
617
+ if "Integer" in self.parameter_type:
618
+ self.data_input = QtWidgets.QSpinBox()
619
+ elif "Float" in self.parameter_type or "Double" in self.parameter_type:
620
+ self.data_input = QtWidgets.QDoubleSpinBox()
621
+
622
+ if self.data_input:
623
+ self.data_input.setValue(self.value)
624
+
625
+ self.data_input.valueChanged.connect(self.update_value)
626
+
627
+ self.layout = QtWidgets.QHBoxLayout()
628
+ self.layout.addWidget(self.label)
629
+ self.layout.addWidget(self.data_input)
630
+ self.setLayout(self.layout)
631
+
632
+ def update_value(self):
633
+ self.value = self.data_input.value()
634
+
635
+ def get_value(self):
636
+ v = self.value
637
+ if v is not None:
638
+ if "Integer" in self.parameter_type:
639
+ value = int(self.value)
640
+ elif "Float" in self.parameter_type:
641
+ value = float(self.value)
642
+ elif "Double" in self.parameter_type:
643
+ value = float(self.value)
644
+ else: # String or StringOrNumber types
645
+ value = self.value
646
+
647
+ return {self.flag: value}
648
+ else:
649
+ if not self.optional:
650
+ msg_box = QtWidgets.QMessageBox()
651
+ msg_box.setIcon(QtWidgets.QMessageBox.Warning)
652
+ msg_box.setText("Unknown non-optional parameter {}.".format(self.flag))
653
+ msg_box.exec()
654
+
655
+ return None
656
+
657
+ def set_value(self, value):
658
+ if self.data_input:
659
+ self.data_input.setValue(value)
660
+ self.update_value()
661
+
662
+ def set_default_value(self):
663
+ if self.data_input:
664
+ self.data_input.setValue(self.default_value)
665
+ self.update_value()
666
+
667
+
668
+ class DoubleSlider(QtWidgets.QSlider):
669
+ """DoubleSlider class for creating double slider widgets."""
670
+
671
+ # create our signal that we can connect to if necessary
672
+ doubleValueChanged = QtCore.pyqtSignal(float)
673
+
674
+ def __init__(self, decimals=3, *args, **kargs):
675
+ super(DoubleSlider, self).__init__(QtCore.Qt.Horizontal)
676
+ self._multi = 10**decimals
677
+
678
+ self.opt = QtWidgets.QStyleOptionSlider()
679
+ self.initStyleOption(self.opt)
680
+
681
+ self.valueChanged.connect(self.emit_double_value_changed)
682
+
683
+ def emit_double_value_changed(self):
684
+ value = float(super(DoubleSlider, self).value()) / self._multi
685
+ self.doubleValueChanged.emit(value)
686
+
687
+ def value(self):
688
+ return float(super(DoubleSlider, self).value()) / self._multi
689
+
690
+ def setMinimum(self, value):
691
+ return super(DoubleSlider, self).setMinimum(value * self._multi)
692
+
693
+ def setMaximum(self, value):
694
+ return super(DoubleSlider, self).setMaximum(value * self._multi)
695
+
696
+ def setSingleStep(self, value):
697
+ return super(DoubleSlider, self).setSingleStep(value * self._multi)
698
+
699
+ def singleStep(self):
700
+ return float(super(DoubleSlider, self).singleStep()) / self._multi
701
+
702
+ def setValue(self, value):
703
+ super(DoubleSlider, self).setValue(int(value * self._multi))
704
+
705
+ def sliderChange(self, change):
706
+ if change == QtWidgets.QAbstractSlider.SliderValueChange:
707
+ sr = self.style().subControlRect(
708
+ QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle
709
+ )
710
+ bottom_right_corner = sr.bottomLeft()
711
+ QtWidgets.QToolTip.showText(
712
+ self.mapToGlobal(QtCore.QPoint(bottom_right_corner.x(), bottom_right_corner.y())),
713
+ str(self.value()),
714
+ self,
715
+ )
716
+
717
+
718
+ if __name__ == "__main__":
719
+ from bt_data import BTData
720
+
721
+ bt = BTData()
722
+
723
+ app = QtWidgets.QApplication(sys.argv)
724
+ dlg = ToolWidgets(
725
+ "Raster Line Attributes",
726
+ bt.get_bera_tool_args("Raster Line Attributes"),
727
+ bt.show_advanced,
728
+ )
729
+ dlg.show()
730
+ sys.exit(app.exec_())