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