BERATools 0.3.8__tar.gz → 0.3.9__tar.gz
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-0.3.8 → beratools-0.3.9}/PKG-INFO +1 -1
- {beratools-0.3.8 → beratools-0.3.9}/beratools/__init__.py +1 -1
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_seed_line_correction.py +1 -3
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/bt_data.py +23 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/bt_gui_main.py +44 -12
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/tool_widgets.py +107 -33
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/check_seed_line.py +50 -25
- {beratools-0.3.8 → beratools-0.3.9}/.gitignore +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/LICENSE +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/README.md +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/cli/__init__.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/cli/entry.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/__init__.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_canopy_footprint_absolute.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_centerline.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_check_seed_line.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_common.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_cost.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_dijkstra.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_line_grouping.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_merge_lines.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_split_with_lines.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_vertex_optimization.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/algo_vertex_preclean.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/canopy_threshold_relative.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/constants.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/line_footprint_functions.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/logger.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/core/tool_base.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/__init__.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/polygon_centerline/LICENSE.md +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/polygon_centerline/UPSTREAM.md +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/polygon_centerline/__init__.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/polygon_centerline/_src.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/external/polygon_centerline/exceptions.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/BERALogo.ico +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/BERALogo.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/BERA_GUI_Main.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/BERA_Splash.gif +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/BERA_WizardImage.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/beratools.json +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/closed.gif +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/closed.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/gui.json +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/open.gif +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/open.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/tool.gif +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/assets/tool.png +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/geometry_types.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/gui/main.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/__init__.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/canopy_footprint_absolute.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/centerline.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/common.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/ground_footprint.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/line_footprint_relative.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/tool_template.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/tools/vertex_optimization.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/utility/env_checks.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/utility/spatial_common.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/beratools/utility/tool_args.py +0 -0
- {beratools-0.3.8 → beratools-0.3.9}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BERATools
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: An advanced forest line feature analysis platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/appliedgrg/beratools
|
|
6
6
|
Author-email: AppliedGRG <appliedgrg@gmail.com>, Richard Zeng <richardqzeng@gmail.com>
|
|
@@ -593,9 +593,7 @@ class SeedLineCorrection:
|
|
|
593
593
|
return self.get_optimized_lines()
|
|
594
594
|
|
|
595
595
|
def compute(self):
|
|
596
|
-
compute_processes = self.processes
|
|
597
|
-
if self.optimize_internal_vertices and (compute_processes is None or int(compute_processes) <= 0):
|
|
598
|
-
compute_processes = 1
|
|
596
|
+
compute_processes = self.processes if self.processes is not None else 0
|
|
599
597
|
|
|
600
598
|
vertex_grp = bt_base.execute_multiprocessing(
|
|
601
599
|
algo_common.process_single_item,
|
|
@@ -426,6 +426,7 @@ class BTData(object):
|
|
|
426
426
|
self.show_advanced = BT_SHOW_ADVANCED_OPTIONS
|
|
427
427
|
self.selected_cpu_cores = -1
|
|
428
428
|
self.recent_tool = None
|
|
429
|
+
self.last_browse_dir = None
|
|
429
430
|
self.ascii_art = ""
|
|
430
431
|
self.get_working_dir()
|
|
431
432
|
self.get_user_folder()
|
|
@@ -457,6 +458,10 @@ class BTData(object):
|
|
|
457
458
|
self.settings = self.settings_manager.settings
|
|
458
459
|
gui_settings = self.settings.get("gui_parameters", {})
|
|
459
460
|
self.recent_tool = gui_settings.get("recent_tool", None)
|
|
461
|
+
if "last_browse_dir" in gui_settings and gui_settings["last_browse_dir"] is not None:
|
|
462
|
+
saved_dir = Path(gui_settings["last_browse_dir"]).expanduser()
|
|
463
|
+
if saved_dir.exists() and saved_dir.is_dir():
|
|
464
|
+
self.last_browse_dir = saved_dir.as_posix()
|
|
460
465
|
self.load_gui_data()
|
|
461
466
|
self.get_tool_history()
|
|
462
467
|
|
|
@@ -655,6 +660,24 @@ class BTData(object):
|
|
|
655
660
|
if not api_result:
|
|
656
661
|
self.recent_tool = None
|
|
657
662
|
|
|
663
|
+
if "last_browse_dir" in gui_settings and gui_settings["last_browse_dir"] is not None:
|
|
664
|
+
saved_dir = Path(gui_settings["last_browse_dir"]).expanduser()
|
|
665
|
+
if saved_dir.exists() and saved_dir.is_dir():
|
|
666
|
+
self.last_browse_dir = saved_dir.as_posix()
|
|
667
|
+
|
|
668
|
+
def set_last_browse_dir(self, path_str):
|
|
669
|
+
if not path_str:
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
browse_dir = Path(path_str).expanduser()
|
|
673
|
+
if browse_dir.exists() and browse_dir.is_dir():
|
|
674
|
+
new_browse_dir = browse_dir.as_posix()
|
|
675
|
+
self.save_setting("last_browse_dir", new_browse_dir)
|
|
676
|
+
self.last_browse_dir = new_browse_dir
|
|
677
|
+
|
|
678
|
+
def get_last_browse_dir(self):
|
|
679
|
+
return self.last_browse_dir
|
|
680
|
+
|
|
658
681
|
def load_gui_data(self):
|
|
659
682
|
gui_settings = {}
|
|
660
683
|
if not self.gui_setting_file.exists():
|
|
@@ -17,6 +17,7 @@ import json
|
|
|
17
17
|
import os
|
|
18
18
|
import platform
|
|
19
19
|
import shlex
|
|
20
|
+
import signal
|
|
20
21
|
import sys
|
|
21
22
|
import webbrowser
|
|
22
23
|
from pathlib import Path
|
|
@@ -211,7 +212,6 @@ class ClickSlider(QtWidgets.QSlider):
|
|
|
211
212
|
if event.button() == QtCore.Qt.LeftButton:
|
|
212
213
|
val = self.pixel_pos_to_range_value(event.pos())
|
|
213
214
|
self.setValue(val)
|
|
214
|
-
self.sliderMoved.emit(val)
|
|
215
215
|
|
|
216
216
|
def pixel_pos_to_range_value(self, pos):
|
|
217
217
|
opt = QtWidgets.QStyleOptionSlider()
|
|
@@ -262,12 +262,13 @@ class BTSlider(QtWidgets.QWidget):
|
|
|
262
262
|
layout.addWidget(self.slider)
|
|
263
263
|
self.setLayout(layout)
|
|
264
264
|
|
|
265
|
-
self.slider.
|
|
265
|
+
self.slider.valueChanged.connect(self.slider_moved)
|
|
266
266
|
|
|
267
267
|
def slider_moved(self, value):
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
actual_value = int(self.slider.value())
|
|
269
|
+
bt.set_selected_cpu_cores(actual_value)
|
|
270
|
+
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), f"{actual_value}")
|
|
271
|
+
self.label.setText(self.generate_label_text(actual_value))
|
|
271
272
|
|
|
272
273
|
def generate_label_text(self, value=None):
|
|
273
274
|
if not value:
|
|
@@ -362,7 +363,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
362
363
|
self.action_bera_tools_guide = None
|
|
363
364
|
self.action_about_bera_tools = None
|
|
364
365
|
self._create_help_menu()
|
|
365
|
-
self.working_dir = bt.work_dir
|
|
366
366
|
self.tool_api = None
|
|
367
367
|
self.tool_name = "Centerline"
|
|
368
368
|
self.recent_tool = bt.recent_tool
|
|
@@ -435,7 +435,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
435
435
|
|
|
436
436
|
# ToolWidgets
|
|
437
437
|
tool_args = bt.get_bera_tool_args(self.tool_name)
|
|
438
|
-
self.tool_widget = ToolWidgets(self.recent_tool, tool_args, bt.show_advanced)
|
|
438
|
+
self.tool_widget = ToolWidgets(self.recent_tool, tool_args, bt.show_advanced, bt_data_obj=bt)
|
|
439
|
+
self.tool_scroll_area = QtWidgets.QScrollArea()
|
|
440
|
+
self.tool_scroll_area.setWidgetResizable(True)
|
|
441
|
+
self.tool_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
|
442
|
+
self.tool_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
|
443
|
+
self.tool_scroll_area.setFrameShape(QtWidgets.QFrame.NoFrame)
|
|
444
|
+
self.tool_scroll_area.setWidget(self.tool_widget)
|
|
439
445
|
|
|
440
446
|
# bottom buttons
|
|
441
447
|
slider = BTSlider(bt.selected_cpu_cores, bt.max_cpu_cores)
|
|
@@ -460,7 +466,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
460
466
|
|
|
461
467
|
self.top_right_layout = QtWidgets.QVBoxLayout()
|
|
462
468
|
self.top_right_layout.addLayout(self.btn_layout_top)
|
|
463
|
-
self.top_right_layout.addWidget(self.
|
|
469
|
+
self.top_right_layout.addWidget(self.tool_scroll_area)
|
|
464
470
|
self.top_right_layout.addLayout(btn_layout_bottom)
|
|
465
471
|
tool_widget_grp = QtWidgets.QGroupBox("Tool")
|
|
466
472
|
tool_widget_grp.setLayout(self.top_right_layout)
|
|
@@ -469,6 +475,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
469
475
|
self.text_edit = QtWidgets.QPlainTextEdit()
|
|
470
476
|
self.text_edit.setFont(QtGui.QFont("Consolas", 9))
|
|
471
477
|
self.text_edit.setReadOnly(True)
|
|
478
|
+
self.text_edit.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
479
|
+
self.text_edit.customContextMenuRequested.connect(self._show_log_context_menu)
|
|
472
480
|
self.print_about()
|
|
473
481
|
|
|
474
482
|
# progress bar
|
|
@@ -549,10 +557,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
549
557
|
self.btn_layout_top.itemAt(0).widget().setText(self.tool_name)
|
|
550
558
|
|
|
551
559
|
# update tool widget
|
|
552
|
-
self.tool_widget = ToolWidgets(self.tool_name, tool_args, bt.show_advanced)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
560
|
+
self.tool_widget = ToolWidgets(self.tool_name, tool_args, bt.show_advanced, bt_data_obj=bt)
|
|
561
|
+
old_widget = self.tool_scroll_area.takeWidget()
|
|
562
|
+
if old_widget is not None:
|
|
563
|
+
old_widget.deleteLater()
|
|
564
|
+
self.tool_scroll_area.setWidget(self.tool_widget)
|
|
556
565
|
self.top_right_layout.update()
|
|
557
566
|
|
|
558
567
|
def save_tool_parameter(self):
|
|
@@ -629,6 +638,17 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
629
638
|
self.text_edit.insertPlainText(text)
|
|
630
639
|
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
631
640
|
|
|
641
|
+
def clear_log_messages(self):
|
|
642
|
+
self.text_edit.clear()
|
|
643
|
+
|
|
644
|
+
def _show_log_context_menu(self, pos):
|
|
645
|
+
menu = self.text_edit.createStandardContextMenu()
|
|
646
|
+
menu.addSeparator()
|
|
647
|
+
clear_action = menu.addAction("Clear Messages")
|
|
648
|
+
action = menu.exec_(self.text_edit.mapToGlobal(pos))
|
|
649
|
+
if action == clear_action:
|
|
650
|
+
self.clear_log_messages()
|
|
651
|
+
|
|
632
652
|
def print_line_to_output(self, text, tag=None):
|
|
633
653
|
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
634
654
|
self.text_edit.insertPlainText(text + "\n")
|
|
@@ -652,6 +672,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
|
652
672
|
|
|
653
673
|
self.tool_widget.adjustSize()
|
|
654
674
|
self.tool_widget.updateGeometry()
|
|
675
|
+
self.tool_scroll_area.widget().adjustSize()
|
|
676
|
+
self.tool_scroll_area.viewport().update()
|
|
655
677
|
self.top_right_layout.activate()
|
|
656
678
|
self.top_right_layout.update()
|
|
657
679
|
|
|
@@ -807,4 +829,14 @@ def runner():
|
|
|
807
829
|
pass
|
|
808
830
|
|
|
809
831
|
QtCore.QTimer.singleShot(100, signal_ready)
|
|
832
|
+
|
|
833
|
+
# Allow Ctrl+C in the terminal to quit the application.
|
|
834
|
+
# Qt overrides Python's SIGINT handler; use a custom handler that
|
|
835
|
+
# calls app.quit() and a timer to let Python process signals.
|
|
836
|
+
signal.signal(signal.SIGINT, lambda *_: app.quit())
|
|
837
|
+
timer = QtCore.QTimer(app)
|
|
838
|
+
timer.start(500)
|
|
839
|
+
timer.timeout.connect(lambda: None)
|
|
840
|
+
app.aboutToQuit.connect(timer.stop)
|
|
841
|
+
|
|
810
842
|
app.exec_()
|
|
@@ -30,6 +30,8 @@ from beratools.gui.geometry_types import (
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
BT_LABEL_MIN_WIDTH = 130
|
|
33
|
+
BT_WIDGET_MIN_HEIGHT = 36
|
|
34
|
+
BT_NUMERIC_BOX_WIDTH = 160
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
class ToolWidgets(QtWidgets.QWidget):
|
|
@@ -37,12 +39,13 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
37
39
|
|
|
38
40
|
signal_save_tool_params = QtCore.pyqtSignal(object)
|
|
39
41
|
|
|
40
|
-
def __init__(self, tool_name, tool_args, show_advanced, parent=None):
|
|
42
|
+
def __init__(self, tool_name, tool_args, show_advanced, bt_data_obj=None, parent=None):
|
|
41
43
|
super(ToolWidgets, self).__init__(parent)
|
|
42
44
|
|
|
43
45
|
self.tool_name = tool_name
|
|
44
46
|
self.show_advanced = show_advanced
|
|
45
47
|
self.current_tool_api = ""
|
|
48
|
+
self.bt_data = bt_data_obj
|
|
46
49
|
self.widget_list = []
|
|
47
50
|
self.ui_widget_list = []
|
|
48
51
|
self.widgets_by_variable = {}
|
|
@@ -52,8 +55,14 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
52
55
|
|
|
53
56
|
self.create_widgets(tool_args)
|
|
54
57
|
layout = QtWidgets.QVBoxLayout()
|
|
58
|
+
layout.setAlignment(QtCore.Qt.AlignTop)
|
|
59
|
+
layout.setSpacing(8)
|
|
55
60
|
|
|
56
61
|
for item in self.ui_widget_list:
|
|
62
|
+
item_layout = self._get_widget_layout(item)
|
|
63
|
+
if item_layout is not None:
|
|
64
|
+
m = item_layout.contentsMargins()
|
|
65
|
+
item_layout.setContentsMargins(m.left(), 0, m.right(), 0)
|
|
57
66
|
layout.addWidget(item)
|
|
58
67
|
|
|
59
68
|
self.save_button = QtWidgets.QPushButton("Save Parameters")
|
|
@@ -92,7 +101,7 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
92
101
|
widget = None
|
|
93
102
|
|
|
94
103
|
if "ExistingFile" in pt or "NewFile" in pt or "Directory" in pt:
|
|
95
|
-
widget = FileSelector(json_str, None)
|
|
104
|
+
widget = FileSelector(json_str, None, bt_data_obj=self.bt_data)
|
|
96
105
|
elif "FileList" in pt:
|
|
97
106
|
widget = MultiFileSelector(json_str, None)
|
|
98
107
|
elif "Boolean" in pt:
|
|
@@ -135,6 +144,7 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
135
144
|
|
|
136
145
|
# hide optional widgets
|
|
137
146
|
if widget:
|
|
147
|
+
widget.setMinimumHeight(BT_WIDGET_MIN_HEIGHT)
|
|
138
148
|
self._set_widget_tooltip(widget)
|
|
139
149
|
widget.depends_on = p.get("depends_on")
|
|
140
150
|
widget.unit = p.get("unit", "")
|
|
@@ -214,7 +224,6 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
214
224
|
self.ui_widget_list = [widget for widget in self.widget_list if widget]
|
|
215
225
|
self._depends_on_specs = {}
|
|
216
226
|
self._inline_controllers = {}
|
|
217
|
-
inline_controller_widgets = []
|
|
218
227
|
|
|
219
228
|
for widget in self.widget_list:
|
|
220
229
|
if not widget:
|
|
@@ -266,43 +275,61 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
266
275
|
)
|
|
267
276
|
elif self._attach_inline_widget(controller_widgets[0], widget):
|
|
268
277
|
self._inline_controllers[controller_vars[0]] = widget.variable
|
|
269
|
-
inline_controller_widgets.append(controller_widgets[0])
|
|
270
278
|
if widget in self.ui_widget_list:
|
|
271
279
|
self.ui_widget_list.remove(widget)
|
|
272
|
-
|
|
273
280
|
self._depends_on_specs[widget.variable] = depends_on
|
|
274
281
|
for controller_widget in controller_widgets:
|
|
275
282
|
self._connect_widget_change_signal(controller_widget, self._update_dependency_states)
|
|
276
283
|
|
|
277
|
-
self.
|
|
284
|
+
self._align_all_labels()
|
|
278
285
|
self._update_dependency_states()
|
|
279
286
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
seen_ids.add(widget_id)
|
|
289
|
-
unique_widgets.append(widget)
|
|
290
|
-
|
|
291
|
-
if not unique_widgets:
|
|
292
|
-
return
|
|
287
|
+
def _align_all_labels(self):
|
|
288
|
+
"""Set every widget's label (QLabel or QCheckBox) to the same fixed width
|
|
289
|
+
so that all input controls start at the same horizontal position.
|
|
290
|
+
Inline widgets are skipped — their label is hidden and alignment is
|
|
291
|
+
governed by the controller checkbox they are embedded in."""
|
|
292
|
+
inlined_vars = set(self._inline_controllers.values())
|
|
293
|
+
anchor_width = BT_LABEL_MIN_WIDTH
|
|
294
|
+
checkbox_indicator_overhead = 0
|
|
293
295
|
|
|
294
|
-
|
|
295
|
-
for
|
|
296
|
-
if
|
|
297
|
-
|
|
296
|
+
# First pass: measure natural widths of all labels (skip inlined widgets)
|
|
297
|
+
for widget in self.widget_list:
|
|
298
|
+
if not widget or widget.variable in inlined_vars:
|
|
299
|
+
continue
|
|
300
|
+
label = getattr(widget, "label", None)
|
|
301
|
+
if label is None:
|
|
302
|
+
continue
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
if isinstance(widget, BooleanInput):
|
|
305
|
+
text_width = widget.checkbox.fontMetrics().horizontalAdvance(
|
|
306
|
+
widget.checkbox.text()
|
|
307
|
+
)
|
|
308
|
+
if not checkbox_indicator_overhead:
|
|
309
|
+
style = widget.checkbox.style()
|
|
310
|
+
opt = QtWidgets.QStyleOptionButton()
|
|
311
|
+
opt.initFrom(widget.checkbox)
|
|
312
|
+
checkbox_indicator_overhead = (
|
|
313
|
+
style.pixelMetric(
|
|
314
|
+
QtWidgets.QStyle.PM_IndicatorWidth, opt, widget.checkbox
|
|
315
|
+
)
|
|
316
|
+
+ style.pixelMetric(
|
|
317
|
+
QtWidgets.QStyle.PM_CheckBoxLabelSpacing, opt, widget.checkbox
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
anchor_width = max(anchor_width, text_width + checkbox_indicator_overhead + 8)
|
|
321
|
+
elif isinstance(label, QtWidgets.QLabel):
|
|
322
|
+
anchor_width = max(anchor_width, label.sizeHint().width())
|
|
301
323
|
|
|
302
|
-
|
|
303
|
-
for
|
|
304
|
-
if
|
|
305
|
-
|
|
324
|
+
# Second pass: apply the uniform width (skip inlined widgets)
|
|
325
|
+
for widget in self.widget_list:
|
|
326
|
+
if not widget or widget.variable in inlined_vars:
|
|
327
|
+
continue
|
|
328
|
+
label = getattr(widget, "label", None)
|
|
329
|
+
if label is None:
|
|
330
|
+
continue
|
|
331
|
+
label.setMinimumWidth(anchor_width)
|
|
332
|
+
label.setMaximumWidth(anchor_width)
|
|
306
333
|
|
|
307
334
|
def _attach_inline_widget(self, controller_widget, dependent_widget):
|
|
308
335
|
controller_layout = self._get_widget_layout(controller_widget)
|
|
@@ -310,12 +337,15 @@ class ToolWidgets(QtWidgets.QWidget):
|
|
|
310
337
|
return False
|
|
311
338
|
|
|
312
339
|
dependent_widget.set_inline_mode(getattr(dependent_widget, "unit", ""))
|
|
340
|
+
controller_layout.setAlignment(QtCore.Qt.AlignVCenter)
|
|
341
|
+
# Remove trailing stretch so the inline widget can expand to fill available space
|
|
313
342
|
insert_index = controller_layout.count()
|
|
314
343
|
if insert_index > 0:
|
|
315
344
|
last_item = controller_layout.itemAt(insert_index - 1)
|
|
316
345
|
if last_item and last_item.spacerItem() is not None:
|
|
346
|
+
controller_layout.removeItem(last_item)
|
|
317
347
|
insert_index -= 1
|
|
318
|
-
controller_layout.insertWidget(insert_index, dependent_widget)
|
|
348
|
+
controller_layout.insertWidget(insert_index, dependent_widget, 1)
|
|
319
349
|
return True
|
|
320
350
|
|
|
321
351
|
@staticmethod
|
|
@@ -452,8 +482,9 @@ class FileSelector(QtWidgets.QWidget):
|
|
|
452
482
|
return True
|
|
453
483
|
return False
|
|
454
484
|
|
|
455
|
-
def __init__(self, json_str, parent=None):
|
|
485
|
+
def __init__(self, json_str, parent=None, bt_data_obj=None):
|
|
456
486
|
super(FileSelector, self).__init__(parent)
|
|
487
|
+
self.bt_data = bt_data_obj
|
|
457
488
|
self.selected_layer = ""
|
|
458
489
|
self.parse_params(json_str)
|
|
459
490
|
self.initialize_values()
|
|
@@ -774,8 +805,10 @@ class FileSelector(QtWidgets.QWidget):
|
|
|
774
805
|
dialog.setViewMode(QtWidgets.QFileDialog.Detail)
|
|
775
806
|
# Handle both string and dict values (for vector files)
|
|
776
807
|
path_value = self.value.get("path", "") if isinstance(self.value, dict) else self.value
|
|
777
|
-
|
|
778
|
-
|
|
808
|
+
start_dir, start_dir_source = self._resolve_dialog_start_directory(path_value)
|
|
809
|
+
if start_dir:
|
|
810
|
+
dialog.setDirectory(str(start_dir))
|
|
811
|
+
if path_value and start_dir_source == "path_value":
|
|
779
812
|
dialog.selectFile(Path(path_value).name)
|
|
780
813
|
dialog.setNameFilter(file_types)
|
|
781
814
|
if "ExistingFile" in self.parameter_type:
|
|
@@ -784,6 +817,36 @@ class FileSelector(QtWidgets.QWidget):
|
|
|
784
817
|
dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
|
|
785
818
|
return dialog
|
|
786
819
|
|
|
820
|
+
def _resolve_dialog_start_directory(self, path_value):
|
|
821
|
+
if self.bt_data:
|
|
822
|
+
last_browse_dir = self.bt_data.get_last_browse_dir()
|
|
823
|
+
if last_browse_dir:
|
|
824
|
+
browse_dir = Path(last_browse_dir).expanduser()
|
|
825
|
+
if browse_dir.exists() and browse_dir.is_dir():
|
|
826
|
+
return browse_dir, "last_browse"
|
|
827
|
+
|
|
828
|
+
if path_value:
|
|
829
|
+
current_path = Path(path_value).expanduser()
|
|
830
|
+
if current_path.exists():
|
|
831
|
+
if current_path.is_dir():
|
|
832
|
+
return current_path, "path_value"
|
|
833
|
+
return current_path.parent, "path_value"
|
|
834
|
+
|
|
835
|
+
parent_dir = current_path.parent
|
|
836
|
+
if parent_dir.exists() and parent_dir.is_dir():
|
|
837
|
+
return parent_dir, "path_value"
|
|
838
|
+
|
|
839
|
+
return Path.home(), "home"
|
|
840
|
+
|
|
841
|
+
def _save_last_browse_dir(self, selected_path):
|
|
842
|
+
if not self.bt_data:
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
selected = Path(selected_path).expanduser()
|
|
846
|
+
browse_dir = selected if selected.is_dir() else selected.parent
|
|
847
|
+
if browse_dir.exists() and browse_dir.is_dir():
|
|
848
|
+
self.bt_data.set_last_browse_dir(str(browse_dir))
|
|
849
|
+
|
|
787
850
|
def process_selected_file(self, result, dialog):
|
|
788
851
|
"""Handle file name modification, extension adding, and GeoPackage logic."""
|
|
789
852
|
base_name = str(Path(result).with_suffix(""))
|
|
@@ -829,6 +892,7 @@ class FileSelector(QtWidgets.QWidget):
|
|
|
829
892
|
return
|
|
830
893
|
result = file_names[0]
|
|
831
894
|
result = self.process_selected_file(result, dialog)
|
|
895
|
+
self._save_last_browse_dir(result)
|
|
832
896
|
self.handle_gpkg_selection(result)
|
|
833
897
|
except Exception as e:
|
|
834
898
|
print(e)
|
|
@@ -1066,13 +1130,17 @@ class OptionsInput(QtWidgets.QWidget):
|
|
|
1066
1130
|
self.combobox.addItems(self.option_list)
|
|
1067
1131
|
self.combobox.setCurrentIndex(default_index)
|
|
1068
1132
|
|
|
1133
|
+
self.combobox.setFixedWidth(BT_NUMERIC_BOX_WIDTH)
|
|
1069
1134
|
self.layout = QtWidgets.QHBoxLayout()
|
|
1135
|
+
self.layout.setAlignment(QtCore.Qt.AlignLeft)
|
|
1070
1136
|
self.layout.addWidget(self.label)
|
|
1071
1137
|
self.layout.addWidget(self.combobox)
|
|
1072
1138
|
self.setLayout(self.layout)
|
|
1073
1139
|
|
|
1074
1140
|
def set_inline_mode(self, unit=""):
|
|
1075
1141
|
self.label.setVisible(False)
|
|
1142
|
+
self.label.setMinimumWidth(0)
|
|
1143
|
+
self.label.setMaximumWidth(0)
|
|
1076
1144
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
1077
1145
|
|
|
1078
1146
|
def selection_change(self, i):
|
|
@@ -1146,7 +1214,10 @@ class NumericInput(QtWidgets.QWidget):
|
|
|
1146
1214
|
except (ValueError, TypeError):
|
|
1147
1215
|
self.data_input.setValue(0)
|
|
1148
1216
|
self.data_input.valueChanged.connect(self.update_value)
|
|
1217
|
+
self.data_input.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
|
1218
|
+
self.data_input.setFixedWidth(BT_NUMERIC_BOX_WIDTH)
|
|
1149
1219
|
self.layout = QtWidgets.QHBoxLayout()
|
|
1220
|
+
self.layout.setAlignment(QtCore.Qt.AlignLeft)
|
|
1150
1221
|
self.layout.addWidget(self.label)
|
|
1151
1222
|
self.layout.addWidget(self.data_input)
|
|
1152
1223
|
self.layout.addWidget(self.unit_label)
|
|
@@ -1157,7 +1228,10 @@ class NumericInput(QtWidgets.QWidget):
|
|
|
1157
1228
|
|
|
1158
1229
|
def set_inline_mode(self, unit=""):
|
|
1159
1230
|
self.label.setVisible(False)
|
|
1231
|
+
self.label.setMinimumWidth(0)
|
|
1232
|
+
self.label.setMaximumWidth(0)
|
|
1160
1233
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
1234
|
+
self.layout.setAlignment(QtCore.Qt.AlignLeft)
|
|
1161
1235
|
if unit:
|
|
1162
1236
|
self.unit = unit
|
|
1163
1237
|
self.unit_label.setText(unit)
|
|
@@ -521,12 +521,14 @@ def _choose_anchor(endpoint_a, endpoint_b):
|
|
|
521
521
|
line_id_b = _parse_line_id(endpoint_b.get("line_id"))
|
|
522
522
|
|
|
523
523
|
score_a = (
|
|
524
|
+
-int(endpoint_a.get("degree", 0)),
|
|
524
525
|
-endpoint_a["line_length"],
|
|
525
526
|
0 if line_id_a is not None else 1,
|
|
526
527
|
line_id_a if line_id_a is not None else float("inf"),
|
|
527
528
|
endpoint_a["discovery_order"],
|
|
528
529
|
)
|
|
529
530
|
score_b = (
|
|
531
|
+
-int(endpoint_b.get("degree", 0)),
|
|
530
532
|
-endpoint_b["line_length"],
|
|
531
533
|
0 if line_id_b is not None else 1,
|
|
532
534
|
line_id_b if line_id_b is not None else float("inf"),
|
|
@@ -584,7 +586,7 @@ def _snap_close_endpoints(gdf, tolerance):
|
|
|
584
586
|
if len(endpoints) < 2:
|
|
585
587
|
return gdf
|
|
586
588
|
|
|
587
|
-
|
|
589
|
+
neighbor_graph = {idx: set() for idx in range(len(endpoints))}
|
|
588
590
|
for i in range(len(endpoints)):
|
|
589
591
|
for j in range(i + 1, len(endpoints)):
|
|
590
592
|
ep_i = endpoints[i]
|
|
@@ -605,34 +607,55 @@ def _snap_close_endpoints(gdf, tolerance):
|
|
|
605
607
|
if dist <= close_threshold:
|
|
606
608
|
continue
|
|
607
609
|
if dist <= snap_threshold:
|
|
608
|
-
|
|
610
|
+
neighbor_graph[i].add(j)
|
|
611
|
+
neighbor_graph[j].add(i)
|
|
609
612
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
x[0],
|
|
613
|
-
min(x[1]["discovery_order"], x[2]["discovery_order"]),
|
|
614
|
-
max(x[1]["discovery_order"], x[2]["discovery_order"]),
|
|
615
|
-
)
|
|
616
|
-
)
|
|
613
|
+
if not any(neighbor_graph.values()):
|
|
614
|
+
return gdf
|
|
617
615
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if mover_key in moved_keys:
|
|
627
|
-
continue
|
|
628
|
-
if mover_key in anchor_locked_keys:
|
|
629
|
-
continue
|
|
630
|
-
if anchor_key in moved_keys:
|
|
616
|
+
for idx, endpoint in enumerate(endpoints):
|
|
617
|
+
endpoint["degree"] = len(neighbor_graph[idx])
|
|
618
|
+
|
|
619
|
+
visited = set()
|
|
620
|
+
components = []
|
|
621
|
+
active_nodes = [idx for idx, neighbors in neighbor_graph.items() if neighbors]
|
|
622
|
+
for node in active_nodes:
|
|
623
|
+
if node in visited:
|
|
631
624
|
continue
|
|
625
|
+
stack = [node]
|
|
626
|
+
component = []
|
|
627
|
+
while stack:
|
|
628
|
+
curr = stack.pop()
|
|
629
|
+
if curr in visited:
|
|
630
|
+
continue
|
|
631
|
+
visited.add(curr)
|
|
632
|
+
component.append(curr)
|
|
633
|
+
for neighbor in neighbor_graph[curr]:
|
|
634
|
+
if neighbor not in visited:
|
|
635
|
+
stack.append(neighbor)
|
|
636
|
+
|
|
637
|
+
if len(component) > 1:
|
|
638
|
+
components.append(component)
|
|
639
|
+
|
|
640
|
+
if not components:
|
|
641
|
+
return gdf
|
|
632
642
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
643
|
+
updates = {}
|
|
644
|
+
for component in components:
|
|
645
|
+
anchor_endpoint = endpoints[component[0]]
|
|
646
|
+
for idx in component[1:]:
|
|
647
|
+
candidate = endpoints[idx]
|
|
648
|
+
anchor_endpoint, _ = _choose_anchor(anchor_endpoint, candidate)
|
|
649
|
+
|
|
650
|
+
anchor_coord = tuple(anchor_endpoint["point"].coords[0])
|
|
651
|
+
anchor_key = (anchor_endpoint["gdf_index"], anchor_endpoint["endpoint_idx"])
|
|
652
|
+
|
|
653
|
+
for idx in component:
|
|
654
|
+
endpoint = endpoints[idx]
|
|
655
|
+
endpoint_key = (endpoint["gdf_index"], endpoint["endpoint_idx"])
|
|
656
|
+
if endpoint_key == anchor_key:
|
|
657
|
+
continue
|
|
658
|
+
updates[endpoint_key] = anchor_coord
|
|
636
659
|
|
|
637
660
|
if not updates:
|
|
638
661
|
return gdf
|
|
@@ -1038,6 +1061,8 @@ def check_seed_line(
|
|
|
1038
1061
|
raise ValueError("Input line and raster CRS do not match.")
|
|
1039
1062
|
|
|
1040
1063
|
gdf = gpd.read_file(in_file, layer=in_layer)
|
|
1064
|
+
if config.clip_to_chm_footprint and in_raster and not _seedlines_within_chm_footprint(gdf, in_raster):
|
|
1065
|
+
raise ValueError("Input line(s) do not overlap input raster.")
|
|
1041
1066
|
if "fid" in gdf.columns:
|
|
1042
1067
|
gdf = gdf.rename(columns={"fid": "orig_fid"})
|
|
1043
1068
|
input_count = len(gdf)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|