• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nens / ThreeDiToolbox / #2589

19 Sep 2025 08:50AM UTC coverage: 35.01% (-0.1%) from 35.146%
#2589

push

coveralls-python

web-flow
Merge 38792c162 into f6f4be1e7

62 of 260 new or added lines in 40 files covered. (23.85%)

6 existing lines in 5 files now uncovered.

4859 of 13879 relevant lines covered (35.01%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.18
/tool_graph/graph_view.py
1
from qgis.core import Qgis
1✔
2
from qgis.core import QgsFeature
1✔
3
from qgis.core import QgsFeatureRequest
1✔
4
from qgis.core import QgsProject
1✔
5
from qgis.core import QgsValueMapFieldFormatter
1✔
6
from qgis.core import QgsVectorLayer
1✔
7
from qgis.core import QgsWkbTypes
1✔
8
from qgis.gui import QgsMapToolIdentify
1✔
9
from qgis.gui import QgsRubberBand
1✔
10
from qgis.PyQt.QtCore import pyqtSignal
1✔
11
from qgis.PyQt.QtCore import pyqtSlot
1✔
12
from qgis.PyQt.QtCore import QEvent
1✔
13
from qgis.PyQt.QtCore import QMetaObject
1✔
14
from qgis.PyQt.QtCore import QSize
1✔
15
from qgis.PyQt.QtCore import Qt
1✔
16
from qgis.PyQt.QtGui import QColor
1✔
17
from qgis.PyQt.QtWidgets import QAbstractItemView
1✔
18
from qgis.PyQt.QtWidgets import QAction
1✔
19
from qgis.PyQt.QtWidgets import QActionGroup
1✔
20
from qgis.PyQt.QtWidgets import QCheckBox
1✔
21
from qgis.PyQt.QtWidgets import QColorDialog
1✔
22
from qgis.PyQt.QtWidgets import QComboBox
1✔
23
from qgis.PyQt.QtWidgets import QDockWidget
1✔
24
from qgis.PyQt.QtWidgets import QHBoxLayout
1✔
25
from qgis.PyQt.QtWidgets import QMenu
1✔
26
from qgis.PyQt.QtWidgets import QMessageBox
1✔
27
from qgis.PyQt.QtWidgets import QSizePolicy
1✔
28
from qgis.PyQt.QtWidgets import QSpacerItem
1✔
29
from qgis.PyQt.QtWidgets import QSplitter
1✔
30
from qgis.PyQt.QtWidgets import QTableView
1✔
31
from qgis.PyQt.QtWidgets import QTabWidget
1✔
32
from qgis.PyQt.QtWidgets import QToolButton
1✔
33
from qgis.PyQt.QtWidgets import QVBoxLayout
1✔
34
from qgis.PyQt.QtWidgets import QWidget
1✔
35
from threedi_results_analysis.datasource.threedi_results import normalized_object_type
1✔
36
from threedi_results_analysis.threedi_plugin_model import ThreeDiGridItem
1✔
37
from threedi_results_analysis.threedi_plugin_model import ThreeDiPluginModel
1✔
38
from threedi_results_analysis.threedi_plugin_model import ThreeDiResultItem
1✔
39
from threedi_results_analysis.tool_graph.graph_model import LocationTimeseriesModel
1✔
40
from threedi_results_analysis.utils.constants import TOOLBOX_MESSAGE_TITLE
1✔
41
from threedi_results_analysis.utils.user_messages import messagebar_message
1✔
42
from threedi_results_analysis.utils.user_messages import statusbar_message
1✔
43
from threedi_results_analysis.utils.utils import generate_parameter_config
1✔
44
from threedi_results_analysis.utils.widgets import PenStyleWidget
1✔
45
from typing import List
1✔
46

47
import logging
1✔
48
import pyqtgraph as pg
1✔
49

50

51
logger = logging.getLogger(__name__)
1✔
52

53
pg.setConfigOption("background", "w")
1✔
54
pg.setConfigOption("foreground", "k")
1✔
55

56
# Layer providers that we can use for the graph
57
VALID_PROVIDERS = ["spatialite", "memory", "ogr"]
1✔
58
# providers which don't have a primary key
59
PROVIDERS_WITHOUT_PRIMARY_KEY = ["memory", "ogr"]
1✔
60

61
FLOWLINE_OR_PUMP = 'flowline_or_pump'
1✔
62
NODE_OR_CELL = 'node_or_cell'
1✔
63

64

65
def is_threedi_layer(vector_layer: QgsVectorLayer) -> bool:
1✔
66
    """
67
    Checks whether a layer has been generated by the 3Di toolbox.
68

69
    It is an extensive check, trying to be backwards compatible with older tools.
70
    """
71
    if not vector_layer:
×
72
        return False
×
73

74
    provider = vector_layer.dataProvider()
×
75
    valid_object_type = normalized_object_type(vector_layer.name())
×
76

77
    if provider.name() in ["spatialite", "memory", "ogr"] and valid_object_type:
×
78
        return True
×
79
    elif vector_layer.objectName() in ("flowline", "node", "pump_linestring", "cell", "pump"):
×
80
        return True
×
81

82
    return False
×
83

84

85
class GraphPlot(pg.PlotWidget):
1✔
86
    """Graph element"""
87

88
    def __init__(self, parent=None):
1✔
89
        super().__init__(parent)
×
90
        self.showGrid(True, True, 0.5)
×
91
        self.current_parameter = None
×
92
        self.location_model = None
×
93
        self.result_model = None
×
94
        self.parent = parent
×
95
        self.absolute = False
×
96
        self.current_time_units = "hrs"
×
97
        self.setLabel("bottom", "Time", self.current_time_units)
×
98
        # Auto SI prefix scaling doesn't work properly with m3, m2 etc.
99
        self.getAxis("left").enableAutoSIPrefix(False)
×
100

101
    def on_close(self):
1✔
102
        """
103
        unloading widget and remove all required stuff
104
        :return:
105
        """
106
        if self.location_model:
×
107
            self.location_model.dataChanged.disconnect(self.location_data_changed)
×
108
            self.location_model.rowsInserted.disconnect(self.on_insert_locations)
×
109
            self.location_model.rowsAboutToBeRemoved.disconnect(
×
110
                self.on_remove_locations
111
            )
112
            self.location_model = None
×
113

114
    def closeEvent(self, event):
1✔
115
        """
116
        overwrite of QDockWidget class to emit signal
117
        :param event: QEvent
118
        """
119
        self.on_close()
×
120
        event.accept()
×
121

122
    def set_location_model(self, model):
1✔
123
        self.location_model = model
×
124
        self.location_model.dataChanged.connect(self.location_data_changed)
×
125
        self.location_model.rowsInserted.connect(self.on_insert_locations)
×
126
        self.location_model.rowsAboutToBeRemoved.connect(self.on_remove_locations)
×
127

128
    def set_result_model(self, model: ThreeDiPluginModel):
1✔
129
        self.result_model = model
×
130

131
    def set_absolute(self, absolute: bool):
1✔
132
        # Remove and re-add to set correct absoluteness
133
        for item in self.location_model.rows:
×
134
            self.removeItem(
×
135
                item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
136
            )
137

138
            if item.active.value:
×
139
                self.addItem(
×
140
                    item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=absolute)
141
                )
142

143
        self.absolute = absolute
×
144
        self.plotItem.vb.menu.viewAll.triggered.emit()
×
145

146
    def on_insert_locations(self, parent, start, end):
1✔
147
        """
148
        add list of items to graph. based on Qt addRows model trigger
149
        :param parent: parent of event (Qt parameter)
150
        :param start: first row nr
151
        :param end: last row nr
152
        """
153
        for i in range(start, end + 1):
×
154
            item = self.location_model.rows[i]
×
155
            self.addItem(
×
156
                item.plots(
157
                    self.current_parameter["parameters"],
158
                    absolute=self.absolute,
159
                    time_units=self.current_time_units,
160
                )
161
            )
162

163
    def on_remove_locations(self, index, start, end):
1✔
164
        """
165
        remove items from graph. based on Qt model removeRows
166
        trigger
167
        :param index: Qt Index (not used)
168
        :param start: first row nr
169
        :param end: last row nr
170
        """
171
        for i in range(start, end + 1):
×
172
            item = self.location_model.rows[i]
×
173
            self.removeItem(
×
174
                        item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
175
                    )
176

177
        self.plotItem.vb.menu.viewAll.triggered.emit()
×
178

179
    def location_data_changed(self, index):
1✔
180
        """
181
        change graphs based on changes in locations
182
        :param index: index of changed field
183
        """
184
        item = self.location_model.rows[index.row()]
×
185

186
        if self.location_model.columns[index.column()].name == "active":
×
187
            if item.active.value:
×
188
                self.show_timeseries(index.row())
×
189
            else:
190
                self.hide_timeseries(index.row())
×
191

192
        elif self.location_model.columns[index.column()].name == "hover":
×
193
            width = 2
×
194
            if item.hover.value:
×
195
                width = 5
×
196
            item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute).setPen(
×
197
                color=item.color.qvalue, width=width, style=item.result.value._pattern)
198

199
        elif self.location_model.columns[index.column()].name == "color":
×
200
            item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute).setPen(
×
201
                color=item.color.qvalue, width=2, style=item.result.value._pattern)
202

203
    def hide_timeseries(self, location_nr):
1✔
204
        """
205
        hide timeseries of location in graph
206
        :param row_nr: integer, row number of location
207
        """
208

209
        plot = self.location_model.rows[location_nr].plots(
×
210
            self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute
211
        )
212
        self.removeItem(plot)
×
213

214
    def show_timeseries(self, location_nr):
1✔
215
        """
216
        show timeseries of location in graph
217
        :param row_nr: integer, row number of location
218
        """
219

220
        plot = self.location_model.rows[location_nr].plots(
×
221
            self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute
222
        )
223
        self.addItem(plot)
×
224

225
    def set_parameter(self, parameter, time_units):
1✔
226
        """
227
        on selection of parameter (in combobox), change timeseries in graphs
228
        :param parameter: parameter identification string
229
        :param time_units: current time units string
230
        """
231

232
        if self.current_parameter == parameter and self.current_time_units == time_units:
×
233
            return
×
234

235
        old_parameter = self.current_parameter
×
236
        old_time_units = self.current_time_units
×
237
        self.current_parameter = parameter
×
238
        self.current_time_units = time_units
×
239

240
        for item in self.location_model.rows:
×
241
            if not item.active.value:
×
242
                continue
×
243

244
            self.removeItem(
×
245
                item.plots(old_parameter["parameters"], time_units=old_time_units, absolute=self.absolute)
246
            )
247
            self.addItem(
×
248
                item.plots(self.current_parameter["parameters"], time_units=self.current_time_units, absolute=self.absolute)
249
            )
250

251
        self.setLabel(
×
252
            "left", self.current_parameter["name"], self.current_parameter["unit"]
253
        )
254

255

256
class LocationTimeseriesTable(QTableView):
1✔
257

258
    hoverExitRow = pyqtSignal(int)
1✔
259
    hoverExitAllRows = pyqtSignal()  # exit the whole widget
1✔
260
    hoverEnterRow = pyqtSignal(int, str, ThreeDiResultItem)
1✔
261
    deleteRequested = pyqtSignal(list)
1✔
262

263
    def __init__(self, parent=None):
1✔
264
        super().__init__(parent)
×
265
        self.setStyleSheet("QTreeView::item:hover{background-color:#FFFF00;}")
×
266
        self.setMouseTracking(True)
×
267
        self.verticalHeader().hide()
×
NEW
268
        self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
×
269
        self.model = None
×
270

271
        self._last_hovered_row = None
×
NEW
272
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
×
273
        self.customContextMenuRequested.connect(self.customMenuRequested)
×
274
        self.viewport().installEventFilter(self)
×
275

276
    def customMenuRequested(self, pos):
1✔
277
        selectionModel = self.selectionModel()
×
278
        menu = QMenu(self)
×
279
        action_delete = QAction("Delete", self)
×
280
        action_delete.triggered.connect(lambda _, sel_inx=selectionModel.selectedRows(): self.deleteRequested.emit(sel_inx))
×
281
        menu.addAction(action_delete)
×
282
        menu.popup(self.viewport().mapToGlobal(pos))
×
283

284
    def on_close(self):
1✔
285
        """
286
        unloading widget and remove all required stuff
287
        :return:
288
        """
289
        self.setMouseTracking(False)
×
290
        self.viewport().removeEventFilter(self)
×
291

292
    def closeEvent(self, event):
1✔
293
        """
294
        overwrite of QDockWidget class to emit signal
295
        :param event: QEvent
296
        """
297
        self.on_close()
×
298
        event.accept()
×
299

300
    def eventFilter(self, widget, event):
1✔
301
        if widget is self.viewport():
×
NEW
302
            if event.type() == QEvent.Type.MouseButtonDblClick:
×
303

NEW
304
                if event.button() == Qt.MouseButton.RightButton:
×
305
                    return True
×
306
                # map mouse position to index
307
                column = self.indexAt(event.pos()).column()
×
308
                if self.model.columns[column].name == "color":
×
309
                    row = self.indexAt(event.pos()).row()
×
310
                    item = self.model.rows[row]
×
311
                    selected_color = QColorDialog.getColor()
×
312
                    if not selected_color.isValid():  # User pressed cancel
×
313
                        return True
×
314

315
                    item.color.value = (selected_color.red(), selected_color.green(), selected_color.blue())
×
316

317
                return QTableView.eventFilter(self, widget, event)
×
318

NEW
319
            elif event.type() == QEvent.Type.MouseMove:
×
320
                row = self.indexAt(event.pos()).row()
×
321
                if row == 0 and self.model and row > self.model.rowCount():
×
322
                    row = None
×
323

NEW
324
            elif event.type() == QEvent.Type.Leave:
×
325
                row = None
×
326
                self.hoverExitAllRows.emit()
×
327
            else:
328
                row = self._last_hovered_row
×
329

330
            if row != self._last_hovered_row:
×
331
                if self._last_hovered_row is not None:
×
332
                    try:
×
333
                        self.hover_exit(self._last_hovered_row)
×
334
                    except IndexError:
×
335
                        logger.warning(
×
336
                            "Hover row index %s out of range" % self._last_hovered_row
337
                        )
338
                    # self.hoverExitRow.emit(self._last_hovered_row)
339
                # self.hoverEnterRow.emit(row)
340
                if row is not None:
×
341
                    try:
×
342
                        self.hover_enter(row)
×
343
                    except IndexError:
×
344
                        logger.warning("Hover row index %s out of range" % row)
×
345
                self._last_hovered_row = row
×
346
                pass
×
347
        return QTableView.eventFilter(self, widget, event)
×
348

349
    def hover_exit(self, row_nr):
1✔
350
        if row_nr >= 0:
×
351
            item = self.model.rows[row_nr]
×
352
            item.hover.value = False
×
353

354
    def hover_enter(self, row_nr):
1✔
355
        if row_nr >= 0:
×
356
            item = self.model.rows[row_nr]
×
357
            self.hoverEnterRow.emit(item.object_id.value, item.object_type.value, item.result.value)
×
358
            item.hover.value = True
×
359

360
    def setModel(self, model):
1✔
361
        super().setModel(model)
×
362
        self.model = model
×
363
        self.model.dataChanged.connect(self._update_table_widgets)
×
364
        self.model.rowsInserted.connect(self._update_table_widgets)
×
365
        self.model.rowsAboutToBeRemoved.connect(self._update_table_widgets)
×
366
        self.setVisible(False)
×
367
        self.resizeColumnsToContents()
×
368
        self.horizontalHeader().setStretchLastSection(True)
×
369
        self.setVisible(True)
×
370
        self.model.set_column_sizes_on_view(self)
×
371
        # Columns checkbox can be set small always
372
        self.setColumnWidth(0, 20)  # checkbox
×
373

374
    def _update_table_widgets(self):
1✔
375
        """The PenStyle widget is not part of the model, but explicitely added/overlayed to the table"""
376
        for i in range(self.model.rowCount()):
×
377
            item = self.model.rows[i]
×
378
            index = self.model.index(i, 1)
×
379
            pen_color = QColor(item.color.value[0], item.color.value[1], item.color.value[2])
×
380
            # If index widget A is replaced with index widget B, index widget A will be deleted.
381
            patternWidget = PenStyleWidget(item.result.value._pattern, pen_color, self)
×
382
            # patternWidget.setAutoFillBackground(True)
383
            patternWidget.setPalette(self.palette())
×
384
            self.setIndexWidget(index, patternWidget)
×
385

386

387
class GraphWidget(QWidget):
1✔
388
    def __init__(
1✔
389
        self,
390
        parent=None,
391
        model: ThreeDiPluginModel = None,
392
        parameter_config=[],
393
        name="",
394
        geometry_type=QgsWkbTypes.Type.Point,
395
    ):
396
        super().__init__(parent)
×
397

398
        self.name = name
×
399
        self.model = model
×
400
        self.parent = parent
×
401
        self.geometry_type = geometry_type
×
402

403
        self.setup_ui()
×
404

405
        self.location_model = LocationTimeseriesModel(self.model)
×
406
        self.graph_plot.set_location_model(self.location_model)
×
407
        self.graph_plot.set_result_model(self.model)
×
408
        self.location_timeseries_table.setModel(self.location_model)
×
409
        self._updateHiddenColumns(self.showFullLegendCheckbox.checkState())
×
410

411
        # set listeners
412
        self.parameter_combo_box.currentIndexChanged.connect(self.parameter_change)
×
413
        self.ts_units_combo_box.currentIndexChanged.connect(self.time_units_change)
×
414
        self.showFullLegendCheckbox.stateChanged.connect(self._updateHiddenColumns)
×
415
        self.location_timeseries_table.deleteRequested.connect(self._removeRows)
×
416

417
        # init parameter selection
418
        self.set_parameter_list(parameter_config)
×
419

420
        self.marker = QgsRubberBand(self.parent.iface.mapCanvas())
×
NEW
421
        self.marker.setColor(Qt.GlobalColor.red)
×
422
        self.marker.setWidth(2)
×
423

424
    def _removeRows(self, index_list):
1✔
425
        # Also here, remove in decreasing order to keep table idx valid
426
        row_list = [index.row() for index in index_list]
×
427
        row_list.sort(reverse=True)
×
428
        for row in row_list:
×
429
            self.location_model.removeRows(row, 1)
×
430

431
    def _updateHiddenColumns(self, state):
1✔
NEW
432
        if state == Qt.CheckState.Unchecked:
×
433
            for i in range(3, 7):
×
434
                self.location_timeseries_table.setColumnHidden(i, True)
×
435
        else:
436
            for i in range(7):
×
437
                self.location_timeseries_table.setColumnHidden(i, False)
×
438

439
    def refresh_table(self):
1✔
440
        # trigger all listeners by emiting dataChanged signal
441
        logger.info("Refreshing table")
×
442
        self.location_model.beginResetModel()
×
443
        self.location_model.endResetModel()
×
444
        self.location_timeseries_table._update_table_widgets()
×
445

446
    @pyqtSlot(ThreeDiResultItem)
1✔
447
    def result_removed(self, result_item: ThreeDiResultItem):
1✔
448
        # Remove corresponding plots that refer to this item
449
        item_idx_to_remove = []
×
450
        for count, item in enumerate(self.location_model.rows):
×
451
            if item.result.value is result_item:
×
452
                item_idx_to_remove.append(count)
×
453

454
        # We delete them descending to keep the row idx consistent
455
        for item_idx in reversed(item_idx_to_remove):
×
456
            self.location_model.removeRows(item_idx, 1)
×
457

458
        # In case there are no more other results in results model, we clean up the parameter combobox
459
        if len(self.model.get_results(checked_only=False)) == 1:
×
460
            self.parameter_combo_box.clear()
×
461

462
    def set_parameter_list(self, parameter_config):
1✔
463
        self.parameter_combo_box.clear()
×
464

465
        if not parameter_config:
×
466
            return
×
467

468
        self.parameters = dict([(p["name"], p) for p in parameter_config])
×
469

470
        params = sorted([p["name"] for p in parameter_config])
×
471

472
        Q_CUM = 'Net cumulative discharge'
×
473
        active = {'Waterlevel', Q_CUM}
×
474
        if Q_CUM not in params:
×
475
            active.add('Discharge')
×
476
        active_idx = None
×
477

478
        for idx, param in enumerate(params):
×
479
            self.parameter_combo_box.addItem(param)
×
480
            if param in active:
×
481
                active_idx = idx
×
482

483
        self.parameter_combo_box.setCurrentIndex(active_idx)
×
484

485
    def on_close(self):
1✔
486
        """
487
        unloading widget and remove all required stuff
488
        :return:
489
        """
490
        self.parameter_combo_box.currentIndexChanged.disconnect(self.parameter_change)
×
491

492
    def closeEvent(self, event):
1✔
493
        """
494
        overwrite of QDockWidget class to emit signal
495
        :param event: QEvent
496
        """
497
        self.on_close()
×
498
        event.accept()
×
499

500
    def highlight_feature(self, obj_id, obj_type, result_item: ThreeDiResultItem):
1✔
501

502
        for table_name, layer_id in result_item.parent().layer_ids.items():
×
503

504
            if obj_type == table_name:
×
505
                # query layer for object
506
                filt = u'"id" = {0}'.format(obj_id)
×
507
                request = QgsFeatureRequest().setFilterExpression(filt)
×
508
                lyr = QgsProject.instance().mapLayer(layer_id)
×
509
                features = lyr.getFeatures(request)
×
510
                for feature in features:
×
511
                    self.marker.setToGeometry(feature.geometry(), lyr)
×
512

513
    def unhighlight_all_features(self):
1✔
514
        """Remove the highlights from all layers"""
515
        self.marker.reset()
×
516

517
    def setup_ui(self):
1✔
518

519
        mainLayout = QHBoxLayout(self)
×
520
        self.setLayout(mainLayout)
×
521

522
        splitterWidget = QSplitter(self)
×
523

524
        # add plot
525
        self.graph_plot = GraphPlot(self)
×
NEW
526
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
×
527
        sizePolicy.setHorizontalStretch(1)
×
528
        sizePolicy.setVerticalStretch(1)
×
529
        sizePolicy.setHeightForWidth(self.graph_plot.sizePolicy().hasHeightForWidth())
×
530
        self.graph_plot.setSizePolicy(sizePolicy)
×
531
        self.graph_plot.setMinimumSize(QSize(250, 250))
×
532
        splitterWidget.addWidget(self.graph_plot)
×
533

534
        # add widget for timeseries table and other controls
535
        legendWidget = QWidget(self)
×
536
        vLayoutTable = QVBoxLayout(self)
×
537
        legendWidget.setLayout(vLayoutTable)
×
538

539
        # add comboboxes
540
        self.ts_units_combo_box = QComboBox(self)
×
541
        self.ts_units_combo_box.insertItems(0, ["hrs", "mins", "s"])
×
542
        self.parameter_combo_box = QComboBox(self)
×
543
        vLayoutTable.addWidget(self.parameter_combo_box)
×
544
        vLayoutTable.addWidget(self.ts_units_combo_box)
×
545

546
        # add timeseries table
547
        self.location_timeseries_table = LocationTimeseriesTable(self)
×
548
        self.location_timeseries_table.hoverEnterRow.connect(self.highlight_feature)
×
549
        self.location_timeseries_table.hoverExitAllRows.connect(self.unhighlight_all_features)
×
NEW
550
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
×
551
        sizePolicy.setHorizontalStretch(0)
×
552
        sizePolicy.setVerticalStretch(0)
×
553
        sizePolicy.setHeightForWidth(self.location_timeseries_table.sizePolicy().hasHeightForWidth())
×
554
        self.location_timeseries_table.setSizePolicy(sizePolicy)
×
555
        self.location_timeseries_table.setMinimumSize(QSize(250, 0))
×
556
        vLayoutTable.addWidget(self.location_timeseries_table)
×
557

558
        # add button below table
559
        hLayoutButtons = QHBoxLayout(self)
×
NEW
560
        hLayoutButtons.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
×
561

562
        self.showFullLegendCheckbox = QCheckBox("Show full legend", self)
×
NEW
563
        self.showFullLegendCheckbox.setCheckState(Qt.CheckState.Unchecked)
×
564
        hLayoutButtons.addWidget(self.showFullLegendCheckbox)
×
565

566
        vLayoutTable.addLayout(hLayoutButtons)
×
567

568
        splitterWidget.addWidget(legendWidget)
×
569

570
        mainLayout.addWidget(splitterWidget)
×
571

572
    def parameter_change(self, nr):
1✔
573
        """
574
        set current selected parameter and trigger refresh of graphs
575
        :param nr: nr of selected option of combobox
576
        :return:
577
        """
578
        if nr == -1:
×
579
            return  # Combobox cleared
×
580

581
        self.current_parameter = self.parameters[self.parameter_combo_box.currentText()]
×
582
        time_units = self.ts_units_combo_box.currentText()
×
583
        self.graph_plot.setLabel("bottom", "Time", time_units)
×
584
        self.graph_plot.set_parameter(self.current_parameter, time_units)
×
585
        self.graph_plot.plotItem.vb.menu.viewAll.triggered.emit()
×
586

587
    def time_units_change(self):
1✔
588
        parameter_idx = self.parameter_combo_box.currentIndex()
×
589
        self.parameter_change(parameter_idx)
×
590

591
    def get_feature_index(self, layer, feature):
1✔
592
        """
593
        get the id of the selected id feature
594
        :param layer: selected Qgis layer to be added
595
        :param feature: selected Qgis feature to be added
596
        :return: idx (integer)
597
        We can't do ``feature.id()``, so we have to pick something that we
598
        have agreed on. For now we have hardcoded the 'id' field as the
599
        default, but that doesn't mean it's always the case in the future
600
        when more layers are added!
601
        """
602
        idx = feature.id()
×
603
        if layer.dataProvider().name() in PROVIDERS_WITHOUT_PRIMARY_KEY:
×
604
            idx = feature["id"]
×
605
        return idx
×
606

607
    def get_object_name(self, layer, feature):
1✔
608
        """
609
        get the object_name (display_name / type)  of the selected id feature
610
        :param layer: selected Qgis layer to be added
611
        :param feature: selected Qgis feature to be added
612
        :return: object_name (string)
613
        To get a object_name we use the following logic:
614
        - get the 'display_name' column if available;
615
        - if not: get the 'type' column if available;
616
        - if not: get the 'line_type' column if available;
617
        - if not: get the 'node_type' column if available;
618
        - if not: object_name = 'N/A'
619
        """
620
        for column_nr, field in enumerate(layer.fields()):
×
621
            if "display_name" in field.name():
×
622
                return feature[column_nr]
×
623
        for column_nr, field in enumerate(layer.fields()):
×
624
            if field.name() == "type":
×
625
                return feature[column_nr]
×
626

627
        # Apply ValueMap field formatter
628
        for column_nr, field in enumerate(layer.fields()):
×
629
            if field.name() == "line_type":
×
630
                config = layer.editorWidgetSetup(column_nr).config()
×
631
                return QgsValueMapFieldFormatter().representValue(layer, column_nr, config, None, feature[column_nr])
×
632
        for column_nr, field in enumerate(layer.fields()):
×
633
            if field.name() == "node_type":
×
634
                config = layer.editorWidgetSetup(column_nr).config()
×
635
                return QgsValueMapFieldFormatter().representValue(layer, column_nr, config, None, feature[column_nr])
×
636

637
        logger.warning("Layer has no 'display_name', it's probably a result "
×
638
                       "layer, but putting a placeholder object name just "
639
                       "for safety."
640
                       )
641

642
        return "N/A"
×
643

644
    def add_objects(self, layer: QgsVectorLayer, features: List[QgsFeature]) -> bool:
1✔
645
        """
646
        :param layer: layer of features
647
        :param features: Qgis layer features to be added
648
        :return: boolean: new objects are added
649
        """
650

651
        if not is_threedi_layer(layer):
×
652
            msg = """Please select results from either the 'flowlines', 'nodes', 'cells' or
×
653
            'pumplines' layer."""
NEW
654
            messagebar_message(TOOLBOX_MESSAGE_TITLE, msg, Qgis.MessageLevel.Warning, 5.0)
×
655
            return False
×
656

657
        if len(self.model.get_results(checked_only=False)) == 0:
×
658
            logger.warning("No results loaded")
×
659
            return False
×
660

661
        # Retrieve summary of existing items in model (!= graph plots)
662
        existing_items = [
×
663
            f"{item.object_type.value}_{str(item.object_id.value)}_{item.result.value.id}" for item in self.location_model.rows
664
        ]
665

666
        # Determine new items
667
        new_items = []
×
668
        for feature in features:
×
669
            new_idx = self.get_feature_index(layer, feature)
×
670
            new_object_name = self.get_object_name(layer, feature)
×
671

672
            result_items = self.model.get_results(checked_only=False)
×
673
            for result_item in result_items:
×
674
                # Check whether this result belongs to the selected grid
675
                if layer.id() not in result_item.parent().layer_ids.values():
×
676
                    continue
×
677

678
                # Check whether a pump isn't already plotted as pump_linestring or vice versa (QGIS doesn't know they are the same thing)
679
                if layer.objectName() == "pump_linestring":
×
680
                    if ("pump_" + str(new_idx) + "_" + result_item.id) in existing_items:
×
681
                        logger.error("Pump already plotted as node item")
×
682
                        continue
×
683
                elif layer.objectName() == "pump":
×
684
                    if ("pump_linestring_" + str(new_idx) + "_" + result_item.id) in existing_items:
×
685
                        logger.error("Pump already plotted as line item")
×
686
                        continue
×
687

688
                # Check whether a node isn't already plotted as cell or vice versa (QGIS doesn't know they are the same thing)
689
                if layer.objectName() == "cell":
×
690
                    if ("node_" + str(new_idx) + "_" + result_item.id) in existing_items:
×
691
                        logger.error("Cell already plotted as node item")
×
692
                        continue
×
693
                elif layer.objectName() == "node":
×
694
                    if ("cell_" + str(new_idx) + "_" + result_item.id) in existing_items:
×
695
                        # Assert whether this is a 2D node
696
                        assert new_object_name.startswith("2D")
×
697
                        logger.error("Node already plotted as cell item")
×
698
                        continue
×
699

700
                if (layer.objectName() + "_" + str(new_idx) + "_" + result_item.id) not in existing_items:
×
701
                    item = {
×
702
                        "object_type": layer.objectName(),
703
                        "object_id": new_idx,
704
                        "object_name": new_object_name,
705
                        "object_label": f"{result_item.parent().text()} | {result_item.text()} | ID: {new_idx}",
706
                        "result": result_item,
707
                        "color": self.location_model.get_color(new_idx, layer.id()),
708
                    }
709
                    new_items.append(item)
×
710

711
        # Small usability tweak, if we are adding a pump flowline, set a specific parameter
712
        if not existing_items and "pump" in layer.objectName() and new_items:
×
713
            # Find the corresponding name for q_cum, set parameter (and set combobox)
714
            pump_params = [p for p in self.parameters.values() if p["parameters"] == "q_pump"]
×
715
            if pump_params:
×
716
                self.graph_plot.set_parameter(pump_params[0], self.ts_units_combo_box.currentText())
×
717
                combo_idx = self.parameter_combo_box.findText(pump_params[0]["name"])
×
718
                assert combo_idx != -1
×
719
                # Prevent the combobox to trigger other signals (and set the parameter again)
720
                self.parameter_combo_box.blockSignals(True)
×
721
                self.parameter_combo_box.setCurrentIndex(combo_idx)
×
722
                self.parameter_combo_box.blockSignals(False)
×
723

724
        if len(new_items) > 20:
×
725
            msg = (
×
726
                "%i new objects selected. Adding those to the plot can "
727
                "take a while. Do you want to continue?" % len(new_items)
728
            )
729
            reply = QMessageBox.question(
×
730
                self, "Add objects", msg, QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No
731
            )
732

NEW
733
            if reply == QMessageBox.StandardButton.No:
×
734
                return False
×
735

736
        self.location_model.insertRows(new_items)
×
737
        msg = "%i new objects added to plot " % len(new_items)
×
738
        skipped_items = len(features) - len(new_items)
×
739
        if skipped_items > 0:
×
740
            msg += "(skipped %s already present objects)" % skipped_items
×
741

742
        statusbar_message(msg)
×
743
        return True
×
744

745
    def remove_objects_table(self):
1✔
746
        """
747
        removes selected objects from table
748
        :return:
749
        """
750
        selection_model = self.location_timeseries_table.selectionModel()
×
751
        # get unique rows in selected fields
752
        rows = set([index.row() for index in selection_model.selectedIndexes()])
×
753
        for row in reversed(sorted(rows)):
×
754
            self.location_model.removeRows(row, 1)
×
755

756

757
class GraphDockWidget(QDockWidget):
1✔
758
    """Main Dock Widget for showing 3Di results in Graphs"""
759

760
    closingWidget = pyqtSignal(int)
1✔
761

762
    def __init__(self, iface, nr, model: ThreeDiPluginModel):
1✔
763
        super().__init__()
×
764

765
        self.iface = iface
×
766
        self.nr = nr
×
767
        self.model = model
×
768

769
        self.setup_ui()
×
770

771
        parameter_config = self._get_active_parameter_config()
×
772

773
        # add graph widgets
774
        self.q_graph_widget = GraphWidget(
×
775
            self,
776
            self.model,
777
            parameter_config["q"],
778
            "Flowlines && pumps",
779
            QgsWkbTypes.Type.LineString,
780
        )
781
        self.h_graph_widget = GraphWidget(
×
782
            self,
783
            self.model,
784
            parameter_config["h"],
785
            "Nodes && cells",
786
            QgsWkbTypes.Type.Point,
787
        )
788
        self.graphTabWidget.addTab(self.q_graph_widget, self.q_graph_widget.name)
×
789
        self.graphTabWidget.addTab(self.h_graph_widget, self.h_graph_widget.name)
×
790

791
        # add listeners
792
        self.addFlowlinePumpButton.clicked.connect(self.add_flowline_pump_button_clicked)
×
793
        self.addNodeCellButton.clicked.connect(self.add_node_cell_button_clicked)
×
794

795
        # add map tools
796
        self.map_tool_add_flowline_pump = AddFlowlinePumpMapTool(
×
797
            widget=self, canvas=self.iface.mapCanvas(),
798
        )
799
        self.map_tool_add_flowline_pump.setButton(self.addFlowlinePumpButton)
×
NEW
800
        self.map_tool_add_flowline_pump.setCursor(Qt.CursorShape.CrossCursor)
×
801
        self.map_tool_add_node_cell = AddNodeCellMapTool(
×
802
            widget=self, canvas=self.iface.mapCanvas(),
803
        )
804
        self.map_tool_add_node_cell.setButton(self.addNodeCellButton)
×
NEW
805
        self.map_tool_add_node_cell.setCursor(Qt.CursorShape.CrossCursor)
×
806

807
        # In case this dock widget becomes (in)visible, we disable the route tools
808
        self.visibilityChanged.connect(self.unset_map_tools)
×
809

810
    def on_close(self):
1✔
811
        """
812
        unloading widget and remove all required stuff
813
        :return:
814
        """
815
        self.addFlowlinePumpButton.clicked.disconnect(self.add_flowline_pump_button_clicked)
×
816
        self.addNodeCellButton.clicked.disconnect(self.add_node_cell_button_clicked)
×
817

818
        self.map_tool_add_flowline_pump = None
×
819
        self.map_tool_add_node_cell = None
×
820

821
        # self.q_graph_widget.close()
822
        # self.h_graph_widget.close()
823

824
    def closeEvent(self, event):
1✔
825
        """
826
        overwrite of QDockWidget class to emit signal
827
        :param event: QEvent
828
        """
829
        self.on_close()
×
830
        self.closingWidget.emit(self.nr)
×
831
        event.accept()
×
832

833
    def _get_active_parameter_config(self, result_item_ignored: ThreeDiResultItem = None):
1✔
834
        """
835
        Generates a parameter dict based on results, takes union of parameters from results.
836
        """
837
        q_vars = []
×
838
        h_vars = []
×
839

840
        for result in self.model.get_results(checked_only=False):
×
841
            if result is result_item_ignored:  # about to be deleted
×
842
                continue
×
843

844
            threedi_result = result.threedi_result
×
845
            available_subgrid_vars = threedi_result.available_subgrid_map_vars
×
846
            available_agg_vars = threedi_result.available_aggregation_vars[:]  # a copy
×
847
            available_wq_vars = threedi_result.available_water_quality_vars[:]  # a copy
×
848
            available_sca_vars = threedi_result.available_structure_control_actions_vars[:]  # a copy
×
849
            if not available_agg_vars:
×
850
                messagebar_message("Warning", "No aggregation netCDF was found.", level=1, duration=5)
×
851

852
            parameter_config = generate_parameter_config(
×
853
                available_subgrid_vars, agg_vars=available_agg_vars, wq_vars=available_wq_vars, sca_vars=available_sca_vars
854
            )
855

856
            def _union(a: List, b: List):
×
857
                if not a:
×
858
                    return b
×
859

860
                for param in b:
×
861
                    # Check whether a contains param with same name, if so, don't add
862
                    if not [x for x in a if x["name"] == param["name"]]:
×
863
                        a.append(param)
×
864

865
                return a
×
866

867
            q_vars = _union(q_vars, parameter_config["q"])
×
868
            h_vars = _union(h_vars, parameter_config["h"])
×
869

870
        return {"q": q_vars, "h": h_vars}
×
871

872
    def result_added(self, _: ThreeDiResultItem):
1✔
873
        parameter_config = self._get_active_parameter_config()
×
874
        self.q_graph_widget.set_parameter_list(parameter_config["q"])
×
875
        self.h_graph_widget.set_parameter_list(parameter_config["h"])
×
876

877
    def result_removed(self, result_item: ThreeDiResultItem):
1✔
878
        parameter_config = self._get_active_parameter_config(result_item)
×
879
        self.q_graph_widget.result_removed(result_item)
×
880
        self.h_graph_widget.result_removed(result_item)
×
881
        self.q_graph_widget.set_parameter_list(parameter_config["q"])
×
882
        self.h_graph_widget.set_parameter_list(parameter_config["h"])
×
883

884
    def result_changed(self, _: ThreeDiResultItem):
1✔
885
        self.q_graph_widget.refresh_table()
×
886
        self.h_graph_widget.refresh_table()
×
887

888
    def grid_changed(self, result_item: ThreeDiGridItem):
1✔
889
        self.q_graph_widget.refresh_table()
×
890
        self.h_graph_widget.refresh_table()
×
891

892
    def on_btnAbsoluteState(self, state):
1✔
893
        """Toggle ``absolute`` state of the GraphPlots."""
NEW
894
        checked = (state == Qt.CheckState.Checked)
×
895
        self.q_graph_widget.graph_plot.set_absolute(checked)
×
896
        self.h_graph_widget.graph_plot.set_absolute(checked)
×
897

898
    def setup_ui(self):
1✔
899

900
        self.setObjectName("dock_widget")
×
NEW
901
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
×
902

903
        self.dockWidgetContent = QWidget(self)
×
904
        self.dockWidgetContent.setObjectName("dockWidgetContent")
×
905

906
        self.mainVLayout = QVBoxLayout(self.dockWidgetContent)
×
907
        self.dockWidgetContent.setLayout(self.mainVLayout)
×
908

909
        self.buttonBarHLayout = QHBoxLayout(self)
×
910

911
        self.buttonBarHLayout.setSpacing(10)
×
912

913
        selection_flowline_pump_menu = QMenu(self)
×
914
        action_group = QActionGroup(self)
×
915
        action_group.setExclusive(True)
×
916
        self.flowline_single_pick = selection_flowline_pump_menu.addAction("Pick single flowline/pump")
×
917
        self.flowline_single_pick.setCheckable(True)
×
918
        self.flowline_single_pick.setChecked(True)
×
919
        self.flowline_single_pick.toggled.connect(self._changeFlowlineSelectionMode)
×
920
        action_group.addAction(self.flowline_single_pick)
×
921
        selected_pick = selection_flowline_pump_menu.addAction("Add all selected flowlines/pump")
×
922
        selected_pick.setCheckable(True)
×
923
        action_group.addAction(selected_pick)
×
924

925
        selection_node_cell_menu = QMenu(self)
×
926
        action_group = QActionGroup(self)
×
927
        action_group.setExclusive(True)
×
928
        self.node_single_pick = selection_node_cell_menu.addAction("Pick single node/cell")
×
929
        self.node_single_pick.setCheckable(True)
×
930
        self.node_single_pick.setChecked(True)
×
931
        self.node_single_pick.toggled.connect(self._changeNodeCellSelectionMode)
×
932
        action_group.addAction(self.node_single_pick)
×
933
        selected_pick = selection_node_cell_menu.addAction("Add all selected nodes/cells")
×
934
        selected_pick.setCheckable(True)
×
935
        action_group.addAction(selected_pick)
×
936

937
        self.addFlowlinePumpButton = QToolButton(parent=self.dockWidgetContent)
×
938
        self.addFlowlinePumpButton.setCheckable(True)
×
939
        self.addFlowlinePumpButton.setText("Pick flowlines/pumps")
×
NEW
940
        self.addFlowlinePumpButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
×
941
        self.addFlowlinePumpButton.setMenu(selection_flowline_pump_menu)
×
942
        self.buttonBarHLayout.addWidget(self.addFlowlinePumpButton)
×
943

944
        self.addNodeCellButton = QToolButton(parent=self.dockWidgetContent)
×
945
        self.addNodeCellButton.setText("Pick nodes/cells")
×
946
        self.addNodeCellButton.setCheckable(True)
×
NEW
947
        self.addNodeCellButton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
×
948
        self.addNodeCellButton.setMenu(selection_node_cell_menu)
×
949
        self.buttonBarHLayout.addWidget(self.addNodeCellButton)
×
950

951
        self.absoluteCheckbox = QCheckBox("Absolute", parent=self.dockWidgetContent)
×
952
        self.absoluteCheckbox.setChecked(False)
×
953
        self.absoluteCheckbox.stateChanged.connect(self.on_btnAbsoluteState)
×
954
        self.buttonBarHLayout.addWidget(self.absoluteCheckbox)
×
955

NEW
956
        spacerItem = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
×
957
        self.buttonBarHLayout.addItem(spacerItem)
×
958

959
        self.mainVLayout.addItem(self.buttonBarHLayout)
×
960

961
        # add tabWidget for graphWidgets
962
        self.graphTabWidget = QTabWidget(self.dockWidgetContent)
×
NEW
963
        sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
×
964
        sizePolicy.setHorizontalStretch(6)
×
965
        sizePolicy.setVerticalStretch(0)
×
966
        sizePolicy.setHeightForWidth(
×
967
            self.graphTabWidget.sizePolicy().hasHeightForWidth()
968
        )
969
        self.graphTabWidget.setSizePolicy(sizePolicy)
×
970
        self.graphTabWidget.setObjectName("graphTabWidget")
×
971
        self.mainVLayout.addWidget(self.graphTabWidget)
×
972

973
        # add dockwidget
974
        self.setWidget(self.dockWidgetContent)
×
975
        self.setWindowTitle("3Di Time series plot %i" % self.nr)
×
976
        QMetaObject.connectSlotsByName(self)
×
977

978
    def _changeFlowlineSelectionMode(self, single_pick_selected: bool) -> None:
1✔
979
        if not single_pick_selected:
×
980
            if self.iface.mapCanvas().mapTool() is self.map_tool_add_flowline_pump:
×
981
                self.iface.mapCanvas().unsetMapTool(self.map_tool_add_flowline_pump)
×
982
            self.addFlowlinePumpButton.setCheckable(False)
×
983
            self.addFlowlinePumpButton.setText("Add flowlines/pumps")
×
984
        else:
985
            self.addFlowlinePumpButton.setCheckable(True)
×
986
            self.addFlowlinePumpButton.setText("Pick flowlines/pumps")
×
987

988
    def _changeNodeCellSelectionMode(self, single_pick_selected: bool) -> None:
1✔
989
        if not single_pick_selected:
×
990
            if self.iface.mapCanvas().mapTool() is self.map_tool_add_node_cell:
×
991
                self.iface.mapCanvas().unsetMapTool(self.map_tool_add_node_cell)
×
992
            self.addNodeCellButton.setCheckable(False)
×
993
            self.addNodeCellButton.setText("Add nodes/cells")
×
994
        else:
995
            self.addNodeCellButton.setCheckable(True)
×
996
            self.addNodeCellButton.setText("Pick nodes/cells")
×
997

998
    def add_flowline_pump_button_clicked(self):
1✔
999
        if self.flowline_single_pick.isChecked():
×
1000
            self.iface.mapCanvas().setMapTool(self.map_tool_add_flowline_pump)
×
1001
        else:
1002
            current_layer = self.iface.mapCanvas().currentLayer()
×
1003
            if not current_layer or current_layer.objectName() not in ['flowline', 'pump_linestring', 'pump']:
×
1004
                logger.error("Select features from flowline or pump layer first.")
×
1005
                return
×
1006

1007
            self.add_results([QgsMapToolIdentify.IdentifyResult(current_layer, f, dict()) for f in current_layer.selectedFeatures()], feature_type=FLOWLINE_OR_PUMP)
×
1008

1009
    def add_node_cell_button_clicked(self):
1✔
1010
        if self.node_single_pick.isChecked():
×
1011
            self.iface.mapCanvas().setMapTool(self.map_tool_add_node_cell)
×
1012
        else:
1013
            current_layer = self.iface.mapCanvas().currentLayer()
×
1014

1015
            if not current_layer or current_layer.objectName() not in ['node', 'cell']:
×
1016
                logger.error("Select features from node or cell layer first.")
×
1017
                return
×
1018

1019
            self.add_results([QgsMapToolIdentify.IdentifyResult(current_layer, f, dict()) for f in current_layer.selectedFeatures()], feature_type=NODE_OR_CELL)
×
1020

1021
    def unset_map_tools(self):
1✔
1022
        if self.iface.mapCanvas().mapTool() is self.map_tool_add_node_cell:
×
1023
            self.iface.mapCanvas().unsetMapTool(self.map_tool_add_node_cell)
×
1024
        elif self.iface.mapCanvas().mapTool() is self.map_tool_add_flowline_pump:
×
1025
            self.iface.mapCanvas().unsetMapTool(self.map_tool_add_flowline_pump)
×
1026

1027
    def add_results(self, results, feature_type, single_feature_per_layer=False):
1✔
1028
        """
1029
        Add results for features of specific types.
1030
        """
1031
        if feature_type == FLOWLINE_OR_PUMP:
×
1032
            layer_keys = ['flowline', 'pump_linestring', 'pump']
×
1033
            graph_widget = self.q_graph_widget
×
1034
        elif feature_type == NODE_OR_CELL:
×
1035
            layer_keys = ['node', 'cell']
×
1036
            graph_widget = self.h_graph_widget
×
1037
        item = self.model.invisibleRootItem()
×
1038

1039
        relevant_grid_layer_ids = []
×
1040
        for layer_key in layer_keys:
×
1041
            for i in range(item.rowCount()):
×
1042
                if layer_key in item.child(i).layer_ids:
×
1043
                    relevant_grid_layer_ids.append(item.child(i).layer_ids[layer_key])
×
1044

1045
        layers_added = set()
×
1046
        for result in results:
×
1047
            layer_id = result.mLayer.id()
×
1048
            if layer_id not in relevant_grid_layer_ids:
×
1049
                continue
×
1050
            if single_feature_per_layer and layer_id in layers_added:
×
1051
                continue
×
1052
            graph_widget.add_objects(result.mLayer, [result.mFeature])
×
1053
            layers_added.add(layer_id)
×
1054

1055
        if layers_added:
×
1056
            tab_index = self.graphTabWidget.indexOf(graph_widget)
×
1057
            self.graphTabWidget.setCurrentIndex(tab_index)
×
1058
            graph_widget.graph_plot.plotItem.vb.menu.viewAll.triggered.emit()
×
1059

1060

1061
class BaseAddMapTool(QgsMapToolIdentify):
1✔
1062
    def __init__(self, widget, *args, **kwargs):
1✔
1063
        super().__init__(*args, **kwargs)
×
1064
        self.widget = widget
×
1065

1066
    def canvasReleaseEvent(self, event):
1✔
1067
        results = self.identify(
×
1068
            x=int(event.pos().x()),
1069
            y=int(event.pos().y()),
1070
            layerList=self.parent().layers(),
1071
        )
1072
        self.widget.add_results(
×
1073
            results=results, feature_type=self.feature_type, single_feature_per_layer=True
1074
        )
1075

1076

1077
class AddFlowlinePumpMapTool(BaseAddMapTool):
1✔
1078
    feature_type = FLOWLINE_OR_PUMP
1✔
1079

1080

1081
class AddNodeCellMapTool(BaseAddMapTool):
1✔
1082
    feature_type = NODE_OR_CELL
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc