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.
- beratools/__init__.py +3 -0
- beratools/core/__init__.py +0 -0
- beratools/core/algo_centerline.py +476 -0
- beratools/core/algo_common.py +489 -0
- beratools/core/algo_cost.py +185 -0
- beratools/core/algo_dijkstra.py +492 -0
- beratools/core/algo_footprint_rel.py +693 -0
- beratools/core/algo_line_grouping.py +941 -0
- beratools/core/algo_merge_lines.py +255 -0
- beratools/core/algo_split_with_lines.py +296 -0
- beratools/core/algo_vertex_optimization.py +451 -0
- beratools/core/constants.py +56 -0
- beratools/core/logger.py +92 -0
- beratools/core/tool_base.py +126 -0
- beratools/gui/__init__.py +11 -0
- beratools/gui/assets/BERALogo.png +0 -0
- beratools/gui/assets/beratools.json +471 -0
- beratools/gui/assets/closed.gif +0 -0
- beratools/gui/assets/closed.png +0 -0
- beratools/gui/assets/gui.json +8 -0
- beratools/gui/assets/open.gif +0 -0
- beratools/gui/assets/open.png +0 -0
- beratools/gui/assets/tool.gif +0 -0
- beratools/gui/assets/tool.png +0 -0
- beratools/gui/bt_data.py +485 -0
- beratools/gui/bt_gui_main.py +700 -0
- beratools/gui/main.py +27 -0
- beratools/gui/tool_widgets.py +730 -0
- beratools/tools/__init__.py +7 -0
- beratools/tools/canopy_threshold_relative.py +769 -0
- beratools/tools/centerline.py +127 -0
- beratools/tools/check_seed_line.py +48 -0
- beratools/tools/common.py +622 -0
- beratools/tools/line_footprint_absolute.py +203 -0
- beratools/tools/line_footprint_fixed.py +480 -0
- beratools/tools/line_footprint_functions.py +884 -0
- beratools/tools/line_footprint_relative.py +75 -0
- beratools/tools/tool_template.py +72 -0
- beratools/tools/vertex_optimization.py +57 -0
- beratools-0.1.0.dist-info/METADATA +134 -0
- beratools-0.1.0.dist-info/RECORD +44 -0
- beratools-0.1.0.dist-info/WHEEL +4 -0
- beratools-0.1.0.dist-info/entry_points.txt +2 -0
- beratools-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,700 @@
|
|
|
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 main GUI functions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import webbrowser
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from re import compile
|
|
22
|
+
|
|
23
|
+
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
24
|
+
|
|
25
|
+
import beratools.core.constants as bt_const
|
|
26
|
+
import beratools.tools.common as bt_common
|
|
27
|
+
from beratools.gui import bt_data
|
|
28
|
+
from beratools.gui.tool_widgets import ToolWidgets
|
|
29
|
+
|
|
30
|
+
# A regular expression, to extract the % complete.
|
|
31
|
+
progress_re = compile("Total complete: (\d+)%")
|
|
32
|
+
bt = bt_data.BTData()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def simple_percent_parser(output):
|
|
36
|
+
"""
|
|
37
|
+
Match lines using the progress_re regex.
|
|
38
|
+
|
|
39
|
+
Return a single integer for the % progress.
|
|
40
|
+
"""
|
|
41
|
+
m = progress_re.search(output)
|
|
42
|
+
if m:
|
|
43
|
+
pc_complete = m.group(1)
|
|
44
|
+
return int(pc_complete)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _SearchProxyModel(QtCore.QSortFilterProxyModel):
|
|
48
|
+
def setFilterRegExp(self, pattern):
|
|
49
|
+
if isinstance(pattern, str):
|
|
50
|
+
pattern = QtCore.QRegExp(pattern, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString)
|
|
51
|
+
super(_SearchProxyModel, self).setFilterRegExp(pattern)
|
|
52
|
+
|
|
53
|
+
def _accept_index(self, idx):
|
|
54
|
+
if idx.isValid():
|
|
55
|
+
text = idx.data(QtCore.Qt.DisplayRole)
|
|
56
|
+
if self.filterRegExp().indexIn(text) >= 0:
|
|
57
|
+
return True
|
|
58
|
+
for row in range(idx.model().rowCount(idx)):
|
|
59
|
+
if self._accept_index(idx.model().index(row, 0, idx)):
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def filterAcceptsRow(self, sourceRow, sourceParent):
|
|
64
|
+
idx = self.sourceModel().index(sourceRow, 0, sourceParent)
|
|
65
|
+
return self._accept_index(idx)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BTTreeView(QtWidgets.QWidget):
|
|
69
|
+
"""Tree view for BERA Tools GUI."""
|
|
70
|
+
|
|
71
|
+
tool_changed = QtCore.pyqtSignal(str) # tool selection changed
|
|
72
|
+
|
|
73
|
+
def __init__(self, parent=None):
|
|
74
|
+
super(BTTreeView, self).__init__(parent)
|
|
75
|
+
|
|
76
|
+
# controls
|
|
77
|
+
self.tool_search = QtWidgets.QLineEdit()
|
|
78
|
+
self.tool_search.setPlaceholderText("Search...")
|
|
79
|
+
|
|
80
|
+
self.tags_model = _SearchProxyModel()
|
|
81
|
+
self.tree_model = QtGui.QStandardItemModel()
|
|
82
|
+
self.tags_model.setSourceModel(self.tree_model)
|
|
83
|
+
# self.tags_model.setDynamicSortFilter(True)
|
|
84
|
+
self.tags_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
85
|
+
|
|
86
|
+
self.tree_view = QtWidgets.QTreeView()
|
|
87
|
+
self.tree_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
88
|
+
self.tree_view.setHeaderHidden(True)
|
|
89
|
+
self.tree_view.setRootIsDecorated(True)
|
|
90
|
+
self.tree_view.setUniformRowHeights(True)
|
|
91
|
+
self.tree_view.setModel(self.tags_model)
|
|
92
|
+
|
|
93
|
+
# layout
|
|
94
|
+
main_layout = QtWidgets.QVBoxLayout()
|
|
95
|
+
main_layout.addWidget(self.tool_search)
|
|
96
|
+
main_layout.addWidget(self.tree_view)
|
|
97
|
+
self.setLayout(main_layout)
|
|
98
|
+
|
|
99
|
+
# signals
|
|
100
|
+
self.tool_search.textChanged.connect(self.search_text_changed)
|
|
101
|
+
|
|
102
|
+
# init
|
|
103
|
+
first_child = self.create_model()
|
|
104
|
+
|
|
105
|
+
self.tree_view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
|
106
|
+
self.tree_view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
|
107
|
+
self.tree_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
108
|
+
self.tree_view.setFirstColumnSpanned(0, self.tree_view.rootIndex(), True)
|
|
109
|
+
self.tree_view.setUniformRowHeights(True)
|
|
110
|
+
|
|
111
|
+
self.tree_model.setHorizontalHeaderLabels(["Tools"])
|
|
112
|
+
self.tree_sel_model = self.tree_view.selectionModel()
|
|
113
|
+
self.tree_sel_model.selectionChanged.connect(self.tree_view_selection_changed)
|
|
114
|
+
|
|
115
|
+
index = None
|
|
116
|
+
# select recent tool
|
|
117
|
+
if bt.recent_tool:
|
|
118
|
+
index = self.get_tool_index(bt.recent_tool)
|
|
119
|
+
else:
|
|
120
|
+
# index_set = self.tree_model.index(0, 0)
|
|
121
|
+
index = self.tree_model.indexFromItem(first_child)
|
|
122
|
+
|
|
123
|
+
self.select_tool_by_index(index)
|
|
124
|
+
self.tree_view.collapsed.connect(self.tree_item_collapsed)
|
|
125
|
+
self.tree_view.expanded.connect(self.tree_item_expanded)
|
|
126
|
+
|
|
127
|
+
def create_model(self):
|
|
128
|
+
first_child = self.add_tool_list_to_tree(bt.toolbox_list, bt.sorted_tools)
|
|
129
|
+
|
|
130
|
+
return first_child
|
|
131
|
+
|
|
132
|
+
def search_text_changed(self, text=None):
|
|
133
|
+
self.tags_model.setFilterRegExp(self.tool_search.text())
|
|
134
|
+
|
|
135
|
+
if len(self.tool_search.text()) >= 1 and self.tags_model.rowCount() > 0:
|
|
136
|
+
self.tree_view.expandAll()
|
|
137
|
+
else:
|
|
138
|
+
self.tree_view.collapseAll()
|
|
139
|
+
|
|
140
|
+
def add_tool_list_to_tree(self, toolbox_list, sorted_tools):
|
|
141
|
+
first_child = None
|
|
142
|
+
for i, toolbox in enumerate(toolbox_list):
|
|
143
|
+
parent = QtGui.QStandardItem(
|
|
144
|
+
QtGui.QIcon(os.path.join(bt_const.ASSETS_PATH, "close.gif")), toolbox
|
|
145
|
+
)
|
|
146
|
+
for j, tool in enumerate(sorted_tools[i]):
|
|
147
|
+
child = QtGui.QStandardItem(QtGui.QIcon(os.path.join(bt_const.ASSETS_PATH, "tool.gif")), tool)
|
|
148
|
+
if i == 0 and j == 0:
|
|
149
|
+
first_child = child
|
|
150
|
+
|
|
151
|
+
parent.appendRow([child])
|
|
152
|
+
self.tree_model.appendRow(parent)
|
|
153
|
+
|
|
154
|
+
return first_child
|
|
155
|
+
|
|
156
|
+
def tree_view_selection_changed(self, new, old):
|
|
157
|
+
if len(new.indexes()) == 0:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
selected = new.indexes()[0]
|
|
161
|
+
source_index = self.tags_model.mapToSource(selected)
|
|
162
|
+
item = self.tree_model.itemFromIndex(source_index)
|
|
163
|
+
parent = item.parent()
|
|
164
|
+
if not parent:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
tool = item.text()
|
|
168
|
+
self.tool_changed.emit(tool)
|
|
169
|
+
|
|
170
|
+
def tree_item_expanded(self, index):
|
|
171
|
+
source_index = self.tags_model.mapToSource(index)
|
|
172
|
+
item = self.tree_model.itemFromIndex(source_index)
|
|
173
|
+
if not item:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if item.hasChildren():
|
|
177
|
+
item.setIcon(QtGui.QIcon(os.path.join(bt_const.ASSETS_PATH, "open.gif")))
|
|
178
|
+
|
|
179
|
+
def tree_item_collapsed(self, index):
|
|
180
|
+
source_index = self.tags_model.mapToSource(index)
|
|
181
|
+
item = self.tree_model.itemFromIndex(source_index)
|
|
182
|
+
if not item:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if item.hasChildren():
|
|
186
|
+
item.setIcon(QtGui.QIcon(os.path.join(bt_const.ASSETS_PATH, "close.gif")))
|
|
187
|
+
|
|
188
|
+
def get_tool_index(self, tool_name):
|
|
189
|
+
item = self.tree_model.findItems(tool_name, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive)
|
|
190
|
+
if len(item) > 0:
|
|
191
|
+
item = item[0]
|
|
192
|
+
|
|
193
|
+
index = self.tree_model.indexFromItem(item)
|
|
194
|
+
return index
|
|
195
|
+
|
|
196
|
+
def select_tool_by_index(self, index):
|
|
197
|
+
proxy_index = self.tags_model.mapFromSource(index)
|
|
198
|
+
self.tree_sel_model.select(proxy_index, QtCore.QItemSelectionModel.ClearAndSelect)
|
|
199
|
+
self.tree_view.expand(proxy_index.parent())
|
|
200
|
+
self.tree_sel_model.setCurrentIndex(proxy_index, QtCore.QItemSelectionModel.Current)
|
|
201
|
+
|
|
202
|
+
def select_tool_by_name(self, name):
|
|
203
|
+
index = self.get_tool_index(name)
|
|
204
|
+
self.select_tool_by_index(index)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ClickSlider(QtWidgets.QSlider):
|
|
208
|
+
"""Custom slider for BERA Tools GUI."""
|
|
209
|
+
|
|
210
|
+
def mousePressEvent(self, event):
|
|
211
|
+
super(ClickSlider, self).mousePressEvent(event)
|
|
212
|
+
if event.button() == QtCore.Qt.LeftButton:
|
|
213
|
+
val = self.pixel_pos_to_range_value(event.pos())
|
|
214
|
+
self.setValue(val)
|
|
215
|
+
self.sliderMoved.emit(val)
|
|
216
|
+
|
|
217
|
+
def pixel_pos_to_range_value(self, pos):
|
|
218
|
+
opt = QtWidgets.QStyleOptionSlider()
|
|
219
|
+
self.initStyleOption(opt)
|
|
220
|
+
gr = self.style().subControlRect(
|
|
221
|
+
QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self
|
|
222
|
+
)
|
|
223
|
+
sr = self.style().subControlRect(
|
|
224
|
+
QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if self.orientation() == QtCore.Qt.Horizontal:
|
|
228
|
+
slider_length = sr.width()
|
|
229
|
+
slider_min = gr.x()
|
|
230
|
+
slider_max = gr.right() - slider_length + 1
|
|
231
|
+
else:
|
|
232
|
+
slider_length = sr.height()
|
|
233
|
+
slider_min = gr.y()
|
|
234
|
+
slider_max = gr.bottom() - slider_length + 1
|
|
235
|
+
pr = pos - sr.center() + sr.topLeft()
|
|
236
|
+
p = pr.x() if self.orientation() == QtCore.Qt.Horizontal else pr.y()
|
|
237
|
+
return QtWidgets.QStyle.sliderValueFromPosition(
|
|
238
|
+
self.minimum(),
|
|
239
|
+
self.maximum(),
|
|
240
|
+
p - slider_min,
|
|
241
|
+
slider_max - slider_min,
|
|
242
|
+
opt.upsideDown,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class BTSlider(QtWidgets.QWidget):
|
|
247
|
+
"""Slider for BERA Tools GUI."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, current, maximum, parent=None):
|
|
250
|
+
super(BTSlider, self).__init__(parent)
|
|
251
|
+
|
|
252
|
+
self.value = current
|
|
253
|
+
self.slider = ClickSlider(QtCore.Qt.Horizontal)
|
|
254
|
+
self.slider.setFixedWidth(120)
|
|
255
|
+
self.slider.setTickInterval(2)
|
|
256
|
+
self.slider.setTickPosition(QtWidgets.QSlider.TicksAbove)
|
|
257
|
+
self.slider.setRange(1, maximum)
|
|
258
|
+
self.slider.setValue(current)
|
|
259
|
+
self.label = QtWidgets.QLabel(self.generate_label_text(current))
|
|
260
|
+
|
|
261
|
+
layout = QtWidgets.QHBoxLayout()
|
|
262
|
+
layout.addWidget(self.label)
|
|
263
|
+
layout.addWidget(self.slider)
|
|
264
|
+
self.setLayout(layout)
|
|
265
|
+
|
|
266
|
+
self.slider.sliderMoved.connect(self.slider_moved)
|
|
267
|
+
|
|
268
|
+
def slider_moved(self, value):
|
|
269
|
+
bt.set_max_procs(value)
|
|
270
|
+
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), f"{value}")
|
|
271
|
+
self.label.setText(self.generate_label_text())
|
|
272
|
+
|
|
273
|
+
def generate_label_text(self, value=None):
|
|
274
|
+
if not value:
|
|
275
|
+
value = self.slider.value()
|
|
276
|
+
|
|
277
|
+
return f"Use CPU Cores: {value:3d}"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class BTListView(QtWidgets.QWidget):
|
|
281
|
+
"""List view for BERA Tools GUI."""
|
|
282
|
+
|
|
283
|
+
tool_changed = QtCore.pyqtSignal(str)
|
|
284
|
+
|
|
285
|
+
def __init__(self, data_list=None, parent=None):
|
|
286
|
+
super(BTListView, self).__init__(parent)
|
|
287
|
+
|
|
288
|
+
delete_icon = QtWidgets.QStyle.SP_DialogCloseButton
|
|
289
|
+
delete_icon = self.style().standardIcon(delete_icon)
|
|
290
|
+
clear_icon = QtWidgets.QStyle.SP_DialogResetButton
|
|
291
|
+
clear_icon = self.style().standardIcon(clear_icon)
|
|
292
|
+
btn_delete = QtWidgets.QPushButton()
|
|
293
|
+
btn_clear = QtWidgets.QPushButton()
|
|
294
|
+
btn_delete.setIcon(delete_icon)
|
|
295
|
+
btn_clear.setIcon(clear_icon)
|
|
296
|
+
btn_delete.setToolTip("Delete selected tool history")
|
|
297
|
+
btn_clear.setToolTip("clear all tool history")
|
|
298
|
+
btn_delete.setFixedWidth(40)
|
|
299
|
+
btn_clear.setFixedWidth(40)
|
|
300
|
+
|
|
301
|
+
layout_h = QtWidgets.QHBoxLayout()
|
|
302
|
+
layout_h.addWidget(btn_delete)
|
|
303
|
+
layout_h.addWidget(btn_clear)
|
|
304
|
+
layout_h.addStretch(1)
|
|
305
|
+
|
|
306
|
+
self.list_view = QtWidgets.QListView()
|
|
307
|
+
self.list_view.setFlow(QtWidgets.QListView.TopToBottom)
|
|
308
|
+
self.list_view.setBatchSize(5)
|
|
309
|
+
|
|
310
|
+
self.list_model = QtCore.QStringListModel() # model
|
|
311
|
+
if data_list:
|
|
312
|
+
self.list_model.setStringList(data_list)
|
|
313
|
+
|
|
314
|
+
self.list_view.setModel(self.list_model) # set model
|
|
315
|
+
self.sel_model = self.list_view.selectionModel()
|
|
316
|
+
|
|
317
|
+
self.list_view.setLayoutMode(QtWidgets.QListView.SinglePass)
|
|
318
|
+
btn_delete.clicked.connect(self.delete_selected_item)
|
|
319
|
+
btn_clear.clicked.connect(self.clear_all_items)
|
|
320
|
+
self.sel_model.selectionChanged.connect(self.selection_changed)
|
|
321
|
+
|
|
322
|
+
layout = QtWidgets.QVBoxLayout()
|
|
323
|
+
layout.addLayout(layout_h)
|
|
324
|
+
layout.addWidget(self.list_view)
|
|
325
|
+
self.setLayout(layout)
|
|
326
|
+
|
|
327
|
+
def selection_changed(self, new, old):
|
|
328
|
+
indexes = new.indexes()
|
|
329
|
+
if len(indexes) == 0:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
selection = new.indexes()[0]
|
|
333
|
+
tool = self.list_model.itemData(selection)[0]
|
|
334
|
+
self.tool_changed.emit(tool)
|
|
335
|
+
|
|
336
|
+
def set_data_list(self, data_list):
|
|
337
|
+
self.list_model.setStringList(data_list)
|
|
338
|
+
|
|
339
|
+
def delete_selected_item(self):
|
|
340
|
+
selection = self.sel_model.currentIndex()
|
|
341
|
+
self.list_model.removeRow(selection.row())
|
|
342
|
+
bt.remove_tool_history_item(selection.row())
|
|
343
|
+
|
|
344
|
+
def clear_all_items(self):
|
|
345
|
+
self.list_model.setStringList([])
|
|
346
|
+
bt.remove_tool_history_all()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class MainWindow(QtWidgets.QMainWindow):
|
|
350
|
+
"""Main window for BERA Tools GUI."""
|
|
351
|
+
|
|
352
|
+
def __init__(self):
|
|
353
|
+
super().__init__()
|
|
354
|
+
|
|
355
|
+
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
356
|
+
self.title = "BERA Tools"
|
|
357
|
+
self.setWindowTitle(self.title)
|
|
358
|
+
self.working_dir = bt.work_dir
|
|
359
|
+
self.tool_api = None
|
|
360
|
+
self.tool_name = "Centerline"
|
|
361
|
+
self.recent_tool = bt.recent_tool
|
|
362
|
+
if self.recent_tool:
|
|
363
|
+
self.tool_name = self.recent_tool
|
|
364
|
+
self.tool_api = bt.get_bera_tool_api(self.tool_name)
|
|
365
|
+
|
|
366
|
+
self.update_procs(bt.get_max_cpu_cores())
|
|
367
|
+
|
|
368
|
+
# QProcess run tools
|
|
369
|
+
self.process = None
|
|
370
|
+
self.cancel_op = False
|
|
371
|
+
|
|
372
|
+
# BERA tool list
|
|
373
|
+
self.bera_tools = bt.bera_tools
|
|
374
|
+
self.tools_list = bt.tools_list
|
|
375
|
+
self.sorted_tools = bt.sorted_tools
|
|
376
|
+
self.toolbox_list = bt.toolbox_list
|
|
377
|
+
self.upper_toolboxes = bt.upper_toolboxes
|
|
378
|
+
self.lower_toolboxes = bt.lower_toolboxes
|
|
379
|
+
|
|
380
|
+
self.current_file_path = Path(__file__).resolve().parent
|
|
381
|
+
bt.set_bera_dir(self.current_file_path)
|
|
382
|
+
|
|
383
|
+
# Tree view
|
|
384
|
+
self.tree_view = BTTreeView()
|
|
385
|
+
self.tree_view.tool_changed.connect(self.set_tool)
|
|
386
|
+
|
|
387
|
+
# group box for tree view
|
|
388
|
+
tree_box = QtWidgets.QGroupBox()
|
|
389
|
+
tree_box.setTitle("Tools available")
|
|
390
|
+
tree_layout = QtWidgets.QVBoxLayout()
|
|
391
|
+
tree_layout.addWidget(self.tree_view)
|
|
392
|
+
tree_box.setLayout(tree_layout)
|
|
393
|
+
|
|
394
|
+
# QListWidget
|
|
395
|
+
self.tool_history = BTListView()
|
|
396
|
+
self.tool_history.set_data_list(bt.tool_history)
|
|
397
|
+
self.tool_history.tool_changed.connect(self.set_tool)
|
|
398
|
+
|
|
399
|
+
# group box
|
|
400
|
+
tool_history_box = QtWidgets.QGroupBox()
|
|
401
|
+
tool_history_layout = QtWidgets.QVBoxLayout()
|
|
402
|
+
tool_history_layout.addWidget(self.tool_history)
|
|
403
|
+
tool_history_box.setTitle("Tool history")
|
|
404
|
+
tool_history_box.setLayout(tool_history_layout)
|
|
405
|
+
|
|
406
|
+
# left layout
|
|
407
|
+
page_layout = QtWidgets.QHBoxLayout()
|
|
408
|
+
self.left_layout = QtWidgets.QVBoxLayout()
|
|
409
|
+
self.right_layout = QtWidgets.QVBoxLayout()
|
|
410
|
+
|
|
411
|
+
self.left_layout.addWidget(tree_box)
|
|
412
|
+
self.left_layout.addWidget(tool_history_box)
|
|
413
|
+
|
|
414
|
+
# top buttons
|
|
415
|
+
label = QtWidgets.QLabel(f"{self.tool_name}")
|
|
416
|
+
label.setFont(QtGui.QFont("Consolas", 14))
|
|
417
|
+
self.btn_advanced = QtWidgets.QPushButton("Show Advanced Options")
|
|
418
|
+
self.btn_advanced.setFixedWidth(180)
|
|
419
|
+
btn_help = QtWidgets.QPushButton("help")
|
|
420
|
+
btn_code = QtWidgets.QPushButton("Code")
|
|
421
|
+
btn_help.setFixedWidth(250)
|
|
422
|
+
btn_code.setFixedWidth(100)
|
|
423
|
+
|
|
424
|
+
self.btn_layout_top = QtWidgets.QHBoxLayout()
|
|
425
|
+
self.btn_layout_top.setAlignment(QtCore.Qt.AlignRight)
|
|
426
|
+
self.btn_layout_top.addWidget(label)
|
|
427
|
+
self.btn_layout_top.addStretch(1)
|
|
428
|
+
self.btn_layout_top.addWidget(self.btn_advanced)
|
|
429
|
+
self.btn_layout_top.addWidget(btn_code)
|
|
430
|
+
|
|
431
|
+
# ToolWidgets
|
|
432
|
+
tool_args = bt.get_bera_tool_args(self.tool_name)
|
|
433
|
+
self.tool_widget = ToolWidgets(self.recent_tool, tool_args, bt.show_advanced)
|
|
434
|
+
|
|
435
|
+
# bottom buttons
|
|
436
|
+
slider = BTSlider(bt.max_procs, bt.max_cpu_cores)
|
|
437
|
+
btn_default_args = QtWidgets.QPushButton("Load Default Arguments")
|
|
438
|
+
self.btn_run = QtWidgets.QPushButton("Run")
|
|
439
|
+
btn_cancel = QtWidgets.QPushButton("Cancel")
|
|
440
|
+
btn_default_args.setFixedWidth(150)
|
|
441
|
+
slider.setFixedWidth(250)
|
|
442
|
+
self.btn_run.setFixedWidth(120)
|
|
443
|
+
btn_cancel.setFixedWidth(120)
|
|
444
|
+
|
|
445
|
+
btn_layout_bottom = QtWidgets.QHBoxLayout()
|
|
446
|
+
btn_layout_bottom.setAlignment(QtCore.Qt.AlignRight)
|
|
447
|
+
btn_layout_bottom.addStretch(1)
|
|
448
|
+
btn_layout_bottom.addWidget(btn_default_args)
|
|
449
|
+
btn_layout_bottom.addWidget(slider)
|
|
450
|
+
btn_layout_bottom.addWidget(self.btn_run)
|
|
451
|
+
btn_layout_bottom.addWidget(btn_cancel)
|
|
452
|
+
|
|
453
|
+
self.top_right_layout = QtWidgets.QVBoxLayout()
|
|
454
|
+
self.top_right_layout.addLayout(self.btn_layout_top)
|
|
455
|
+
self.top_right_layout.addWidget(self.tool_widget)
|
|
456
|
+
self.top_right_layout.addLayout(btn_layout_bottom)
|
|
457
|
+
tool_widget_grp = QtWidgets.QGroupBox("Tool")
|
|
458
|
+
tool_widget_grp.setLayout(self.top_right_layout)
|
|
459
|
+
|
|
460
|
+
# Text widget
|
|
461
|
+
self.text_edit = QtWidgets.QPlainTextEdit()
|
|
462
|
+
self.text_edit.setFont(QtGui.QFont("Consolas", 9))
|
|
463
|
+
self.text_edit.setReadOnly(True)
|
|
464
|
+
self.print_about()
|
|
465
|
+
|
|
466
|
+
# progress bar
|
|
467
|
+
self.progress_label = QtWidgets.QLabel()
|
|
468
|
+
self.progress_bar = QtWidgets.QProgressBar(self)
|
|
469
|
+
self.progress_var = 0
|
|
470
|
+
|
|
471
|
+
# progress layout
|
|
472
|
+
progress_layout = QtWidgets.QHBoxLayout()
|
|
473
|
+
progress_layout.addWidget(self.progress_label)
|
|
474
|
+
progress_layout.addWidget(self.progress_bar)
|
|
475
|
+
|
|
476
|
+
self.right_layout.addWidget(tool_widget_grp)
|
|
477
|
+
self.right_layout.addWidget(self.text_edit)
|
|
478
|
+
self.right_layout.addLayout(progress_layout)
|
|
479
|
+
|
|
480
|
+
# main layouts
|
|
481
|
+
page_layout.addLayout(self.left_layout, 3)
|
|
482
|
+
page_layout.addLayout(self.right_layout, 7)
|
|
483
|
+
|
|
484
|
+
# signals and slots
|
|
485
|
+
self.btn_advanced.clicked.connect(self.show_advanced)
|
|
486
|
+
btn_help.clicked.connect(self.show_help)
|
|
487
|
+
btn_code.clicked.connect(self.view_code)
|
|
488
|
+
btn_default_args.clicked.connect(self.load_default_args)
|
|
489
|
+
self.btn_run.clicked.connect(self.start_process)
|
|
490
|
+
btn_cancel.clicked.connect(self.stop_process)
|
|
491
|
+
|
|
492
|
+
widget = QtWidgets.QWidget(self)
|
|
493
|
+
widget.setLayout(page_layout)
|
|
494
|
+
self.setCentralWidget(widget)
|
|
495
|
+
|
|
496
|
+
def set_tool(self, tool=None):
|
|
497
|
+
if tool:
|
|
498
|
+
self.tool_name = tool
|
|
499
|
+
|
|
500
|
+
# let tree view select the tool
|
|
501
|
+
self.tree_view.select_tool_by_name(self.tool_name)
|
|
502
|
+
self.tool_api = bt.get_bera_tool_api(self.tool_name)
|
|
503
|
+
tool_args = bt.get_bera_tool_args(self.tool_name)
|
|
504
|
+
|
|
505
|
+
# update tool label
|
|
506
|
+
self.btn_layout_top.itemAt(0).widget().setText(self.tool_name)
|
|
507
|
+
|
|
508
|
+
# update tool widget
|
|
509
|
+
self.tool_widget = ToolWidgets(self.tool_name, tool_args, bt.show_advanced)
|
|
510
|
+
widget = self.top_right_layout.itemAt(1).widget()
|
|
511
|
+
self.top_right_layout.removeWidget(widget)
|
|
512
|
+
self.top_right_layout.insertWidget(1, self.tool_widget)
|
|
513
|
+
self.top_right_layout.update()
|
|
514
|
+
|
|
515
|
+
def save_tool_parameter(self):
|
|
516
|
+
# Retrieve tool parameters from GUI
|
|
517
|
+
args = self.tool_widget.get_widgets_arguments()
|
|
518
|
+
# bt.load_saved_tool_info()
|
|
519
|
+
bt.add_tool_history(self.tool_api, args)
|
|
520
|
+
bt.save_tool_info()
|
|
521
|
+
|
|
522
|
+
# update tool history list
|
|
523
|
+
bt.get_tool_history()
|
|
524
|
+
self.tool_history.set_data_list(bt.tool_history)
|
|
525
|
+
|
|
526
|
+
def get_current_tool_parameters(self):
|
|
527
|
+
self.tool_api = bt.get_bera_tool_api(self.tool_name)
|
|
528
|
+
return bt.get_bera_tool_params(self.tool_name)
|
|
529
|
+
|
|
530
|
+
def show_help(self):
|
|
531
|
+
# open the user manual section for the current tool
|
|
532
|
+
webbrowser.open_new_tab(self.get_current_tool_parameters()["tech_link"])
|
|
533
|
+
|
|
534
|
+
def print_about(self):
|
|
535
|
+
self.text_edit.clear()
|
|
536
|
+
self.print_to_output(bt.about())
|
|
537
|
+
|
|
538
|
+
def print_license(self):
|
|
539
|
+
self.text_edit.clear()
|
|
540
|
+
self.print_to_output(bt.license())
|
|
541
|
+
|
|
542
|
+
def update_procs(self, value):
|
|
543
|
+
max_procs = int(value)
|
|
544
|
+
bt.set_max_procs(max_procs)
|
|
545
|
+
|
|
546
|
+
def print_to_output(self, text):
|
|
547
|
+
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
548
|
+
self.text_edit.insertPlainText(text)
|
|
549
|
+
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
550
|
+
|
|
551
|
+
def print_line_to_output(self, text, tag=None):
|
|
552
|
+
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
553
|
+
self.text_edit.insertPlainText(text + "\n")
|
|
554
|
+
self.text_edit.moveCursor(QtGui.QTextCursor.End)
|
|
555
|
+
|
|
556
|
+
def show_advanced(self):
|
|
557
|
+
if bt.show_advanced:
|
|
558
|
+
bt.show_advanced = False
|
|
559
|
+
self.btn_advanced.setText("Show Advanced Options")
|
|
560
|
+
else:
|
|
561
|
+
bt.show_advanced = True
|
|
562
|
+
self.btn_advanced.setText("Hide Advanced Options")
|
|
563
|
+
|
|
564
|
+
self.set_tool()
|
|
565
|
+
|
|
566
|
+
def view_code(self):
|
|
567
|
+
webbrowser.open_new_tab(self.get_current_tool_parameters()["tech_link"])
|
|
568
|
+
|
|
569
|
+
def custom_callback(self, value):
|
|
570
|
+
"""Define custom callback that deals with tool output."""
|
|
571
|
+
value = str(value)
|
|
572
|
+
value.strip()
|
|
573
|
+
if value != "":
|
|
574
|
+
# remove esc string which origin is unknown
|
|
575
|
+
rm_str = "\x1b[0m"
|
|
576
|
+
if rm_str in value:
|
|
577
|
+
value = value.replace(rm_str, "")
|
|
578
|
+
|
|
579
|
+
if "%" in value:
|
|
580
|
+
try:
|
|
581
|
+
str_progress = bt_common.extract_string_from_printout(value, "%")
|
|
582
|
+
|
|
583
|
+
# remove progress string
|
|
584
|
+
value = value.replace(str_progress, "").strip()
|
|
585
|
+
progress = float(str_progress.replace("%", "").strip())
|
|
586
|
+
self.progress_bar.setValue(int(progress))
|
|
587
|
+
except ValueError as e:
|
|
588
|
+
print("custom_callback: Problem parsing data into number: ", e)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
print(e)
|
|
591
|
+
elif "PROGRESS_LABEL" in value:
|
|
592
|
+
str_label = bt_common.extract_string_from_printout(value, "PROGRESS_LABEL")
|
|
593
|
+
value = value.replace(str_label, "").strip() # remove progress string
|
|
594
|
+
value = value.replace('"', "")
|
|
595
|
+
str_label = str_label.replace("PROGRESS_LABEL", "").strip()
|
|
596
|
+
self.progress_label.setText(str_label)
|
|
597
|
+
|
|
598
|
+
if value:
|
|
599
|
+
self.print_line_to_output(value)
|
|
600
|
+
|
|
601
|
+
def message(self, s):
|
|
602
|
+
self.text_edit.appendPlainText(s)
|
|
603
|
+
|
|
604
|
+
def load_default_args(self):
|
|
605
|
+
self.tool_widget.load_default_args()
|
|
606
|
+
|
|
607
|
+
def start_process(self):
|
|
608
|
+
args = self.tool_widget.get_widgets_arguments()
|
|
609
|
+
if not args:
|
|
610
|
+
print("Please check the parameters.")
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
self.print_line_to_output("")
|
|
614
|
+
self.print_line_to_output(f"Starting tool {self.tool_name} ... \n")
|
|
615
|
+
self.print_line_to_output("-----------------------------")
|
|
616
|
+
self.print_line_to_output("Tool arguments:")
|
|
617
|
+
self.print_line_to_output(json.dumps(args, indent=4))
|
|
618
|
+
self.print_line_to_output("")
|
|
619
|
+
|
|
620
|
+
bt.recent_tool = self.tool_name
|
|
621
|
+
self.save_tool_parameter()
|
|
622
|
+
|
|
623
|
+
# Run the tool and check the return value for an error
|
|
624
|
+
for key in args.keys():
|
|
625
|
+
if type(args[key]) is not str:
|
|
626
|
+
args[key] = str(args[key])
|
|
627
|
+
|
|
628
|
+
tool_type, tool_args = bt.run_tool(self.tool_api, args, self.custom_callback)
|
|
629
|
+
|
|
630
|
+
if self.process is None: # No process running.
|
|
631
|
+
self.print_line_to_output(f"Tool {self.tool_name} started")
|
|
632
|
+
self.print_line_to_output("-----------------------")
|
|
633
|
+
self.process = QtCore.QProcess() # Keep a reference to the QProcess
|
|
634
|
+
self.process.readyReadStandardOutput.connect(self.handle_stdout)
|
|
635
|
+
self.process.readyReadStandardError.connect(self.handle_stderr)
|
|
636
|
+
self.process.stateChanged.connect(self.handle_state)
|
|
637
|
+
|
|
638
|
+
# Clean up once complete.
|
|
639
|
+
self.process.finished.connect(self.process_finished)
|
|
640
|
+
self.process.start(tool_type, tool_args)
|
|
641
|
+
|
|
642
|
+
while self.process is not None:
|
|
643
|
+
sys.stdout.flush()
|
|
644
|
+
if self.cancel_op:
|
|
645
|
+
self.cancel_op = False
|
|
646
|
+
self.process.terminate()
|
|
647
|
+
else:
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
def stop_process(self):
|
|
651
|
+
self.cancel_op = True
|
|
652
|
+
if self.process:
|
|
653
|
+
self.print_line_to_output(f"Tool {self.tool_name} terminating ...")
|
|
654
|
+
self.process.kill()
|
|
655
|
+
|
|
656
|
+
def handle_stderr(self):
|
|
657
|
+
data = self.process.readAllStandardError()
|
|
658
|
+
stderr = bytes(data).decode("utf8")
|
|
659
|
+
|
|
660
|
+
# Extract progress if it is in the data.
|
|
661
|
+
progress = simple_percent_parser(stderr)
|
|
662
|
+
if progress:
|
|
663
|
+
self.progress_bar.setValue(progress)
|
|
664
|
+
self.message(stderr)
|
|
665
|
+
|
|
666
|
+
def handle_stdout(self):
|
|
667
|
+
line = self.process.readLine()
|
|
668
|
+
line = bytes(line).decode("utf8")
|
|
669
|
+
|
|
670
|
+
# process line output
|
|
671
|
+
self.custom_callback(line)
|
|
672
|
+
sys.stdout.flush()
|
|
673
|
+
|
|
674
|
+
def handle_state(self, state):
|
|
675
|
+
states = {
|
|
676
|
+
QtCore.QProcess.NotRunning: "Not running",
|
|
677
|
+
QtCore.QProcess.Starting: "Starting",
|
|
678
|
+
QtCore.QProcess.Running: "Running",
|
|
679
|
+
}
|
|
680
|
+
state_name = states[state]
|
|
681
|
+
if state_name == "Not running":
|
|
682
|
+
self.btn_run.setEnabled(True)
|
|
683
|
+
if self.cancel_op:
|
|
684
|
+
self.message("Tool operation canceled")
|
|
685
|
+
elif state_name == "Starting":
|
|
686
|
+
self.btn_run.setEnabled(False)
|
|
687
|
+
|
|
688
|
+
def process_finished(self):
|
|
689
|
+
self.message("Process finished.")
|
|
690
|
+
self.process = None
|
|
691
|
+
self.progress_bar.setValue(0)
|
|
692
|
+
self.progress_label.setText("")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def runner():
|
|
696
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
697
|
+
window = MainWindow()
|
|
698
|
+
window.setMinimumSize(1024, 768)
|
|
699
|
+
window.show()
|
|
700
|
+
app.exec()
|