• 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

12.6
/tool_sideview/sideview_view.py
1
from functools import reduce
1✔
2
from qgis.core import QgsPointXY
1✔
3
from qgis.core import QgsProject
1✔
4
from qgis.PyQt.QtCore import pyqtSignal, pyqtSlot
1✔
5
from qgis.PyQt.QtCore import Qt
1✔
6
from qgis.PyQt.QtGui import QColor
1✔
7
from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem
1✔
8
from qgis.PyQt.QtWidgets import QAbstractItemView
1✔
9
from qgis.PyQt.QtWidgets import QDockWidget, QSplitter
1✔
10
from qgis.PyQt.QtWidgets import QHBoxLayout
1✔
11
from qgis.PyQt.QtWidgets import QPushButton
1✔
12
from qgis.PyQt.QtWidgets import QCheckBox
1✔
13
from qgis.PyQt.QtWidgets import QLabel
1✔
14
from qgis.PyQt.QtWidgets import QSizePolicy
1✔
15
from qgis.PyQt.QtWidgets import QSpacerItem
1✔
16
from qgis.PyQt.QtWidgets import QComboBox
1✔
17
from qgis.PyQt.QtWidgets import QApplication
1✔
18
from qgis.PyQt.QtWidgets import QVBoxLayout
1✔
19
from qgis.PyQt.QtWidgets import QTableView
1✔
20
from qgis.PyQt.QtWidgets import QWidget
1✔
21
from threedigrid.admin.constants import NO_DATA_VALUE
1✔
22
from shapely.geometry import LineString, Point
1✔
23
from threedi_results_analysis.tool_sideview.route import Route, RouteMapTool
1✔
24
from threedi_results_analysis.tool_sideview.sideview_visualisation import SideViewMapVisualisation
1✔
25
from threedi_results_analysis.tool_sideview.utils import LineType
1✔
26
from threedi_results_analysis.utils.user_messages import messagebar_message, messagebar_pop_message
1✔
27
from threedi_results_analysis.utils.widgets import PenStyleWidget
1✔
28
from threedi_results_analysis.tool_sideview.sideview_graph_generator import SideViewGraphGenerator
1✔
29
from threedi_results_analysis.threedi_plugin_model import ThreeDiGridItem, ThreeDiResultItem
1✔
30

31
from threedigrid.admin.gridresultadmin import GridH5ResultAdmin
1✔
32
from bisect import bisect_left
1✔
33
import logging
1✔
34
import numpy as np
1✔
35
import os
1✔
36
import pyqtgraph as pg
1✔
37

38
logger = logging.getLogger(__name__)
1✔
39

40
UPPER_LIMIT = 10000
1✔
41
LOWER_LIMIT = -10000
1✔
42

43
COLOR_LIST = [
1✔
44
    (28, 180, 234),
45
    (234, 28, 178),
46
    (178, 234, 28),
47
    (86, 28, 234),
48
    (234, 86, 28),
49
    (28, 233, 86),
50
]
51

52

53
class SideViewPlotWidget(pg.PlotWidget):
1✔
54
    """Side view plot element"""
55

56
    profile_route_updated = pyqtSignal()
1✔
57
    profile_hovered = pyqtSignal(float)
1✔
58

59
    def __init__(
1✔
60
        self,
61
        parent,
62
        model,
63
        sideview_result_model,
64
    ):
65
        """
66

67
        :param parent: Qt parent widget
68
        """
69
        super().__init__(parent)
×
70

71
        self.model = model  # global model from result manager
×
72
        self.sideview_result_model = sideview_result_model  # Sideview model containing patterns and selections
×
73
        self.sideview_nodes = []
×
74
        self.waterlevel_plots = {}  # map from result id to (plot, fill)
×
75
        self.current_grid_id = None
×
76

77
        self.show_dots = True
×
78

79
        self.showGrid(True, True, 0.5)
×
80
        self.setLabel("bottom", "Distance", "m")
×
81
        self.setLabel("left", "Elevation", "m MSL")
×
82

83
        pen = pg.mkPen(color=QColor(150, 150, 150), width=1)
×
84
        self.bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
85

86
        # Used for intersection dots with horizontal lines
87
        self.node_indicator_intersection_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), symbolBrush=pg.mkBrush(150, 150, 150), symbolSize=7)
×
88

89
        pen = pg.mkPen(color=QColor(150, 150, 150), width=2)
×
90
        self.sewer_bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
91
        self.sewer_upper_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
92

93
        # Required for top fill of sewers
94
        self.sewer_top_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
95
        self.sewer_exchange_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
96

97
        self.channel_bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
98

99
        pen = pg.mkPen(color=QColor(100, 100, 100), width=4)
×
100
        self.culvert_lowest_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
101
        self.culvert_bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
102
        self.culvert_upper_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
103
        self.culvert_top_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
104

105
        pen = pg.mkPen(color=QColor(255, 0, 0), width=1)
×
106
        self.weir_bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
107
        pen = pg.mkPen(color=QColor(250, 217, 213), width=1)
×
108
        self.weir_middle_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
109
        self.weir_upper_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
110
        self.weir_top_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
111

112
        pen = pg.mkPen(color=QColor(0, 255, 0), width=1)
×
113
        self.orifice_bottom_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
114
        pen = pg.mkPen(color=QColor(208, 240, 192), width=1)
×
115
        self.orifice_middle_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
116
        self.orifice_upper_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
117
        self.orifice_top_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
118

119
        pen = pg.mkPen(color=QColor(150, 150, 150), width=2)
×
120
        self.exchange_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
121

122
        # Required for fill in bottom of graph
123
        pen = pg.mkPen(color=QColor(190, 190, 190), width=1)
×
124
        self.absolute_bottom = pg.PlotDataItem(np.array([(0.0, LOWER_LIMIT), (10000, LOWER_LIMIT)]), pen=pen)
×
125
        self.bottom_fill = pg.FillBetweenItem(
×
126
            self.bottom_plot, self.absolute_bottom, pg.mkBrush(200, 200, 200)
127
        )
128

129
        # Add some structure specific fills
130

131
        self.culvert_lower_fill = pg.FillBetweenItem(
×
132
            self.culvert_bottom_plot, self.culvert_lowest_plot, pg.mkBrush(100, 100, 100)
133
        )
134
        self.culvert_middle_fill = pg.FillBetweenItem(
×
135
            self.culvert_upper_plot, self.culvert_bottom_plot, pg.mkBrush(230, 230, 230)
136
        )
137
        self.culvert_upper_fill = pg.FillBetweenItem(
×
138
            self.culvert_top_plot, self.culvert_upper_plot, pg.mkBrush(100, 100, 100)
139
        )
140

141
        self.orifice_lower_fill = pg.FillBetweenItem(
×
142
            self.orifice_middle_plot, self.orifice_bottom_plot, pg.mkBrush(51, 160, 44)
143
        )
144
        self.orifice_middle_fill = pg.FillBetweenItem(
×
145
            self.orifice_upper_plot, self.orifice_middle_plot, pg.mkBrush(165, 230, 161)
146
        )
147
        self.orifice_upper_fill = pg.FillBetweenItem(
×
148
            self.orifice_top_plot, self.orifice_upper_plot, pg.mkBrush(51, 160, 44)
149
        )
150

151
        self.weir_lower_fill = pg.FillBetweenItem(
×
152
            self.weir_middle_plot, self.weir_bottom_plot, pg.mkBrush(227, 26, 28)
153
        )
154
        self.weir_middle_fill = pg.FillBetweenItem(
×
155
            self.weir_upper_plot, self.weir_middle_plot, pg.mkBrush(255, 153, 155)
156
        )
157
        self.weir_upper_fill = pg.FillBetweenItem(
×
158
            self.weir_top_plot, self.weir_upper_plot, pg.mkBrush(227, 26, 28)
159
        )
160

161
        self.sewer_top_fill = pg.FillBetweenItem(
×
162
            self.sewer_exchange_plot, self.sewer_top_plot, pg.mkBrush(200, 200, 200)
163
        )
164

165
        self.addItem(self.bottom_fill)
×
166
        self.addItem(self.sewer_top_fill)
×
167
        self.addItem(self.bottom_plot)
×
168
        self.addItem(self.sewer_bottom_plot)
×
169
        self.addItem(self.sewer_upper_plot)
×
170
        self.addItem(self.sewer_top_plot)
×
171
        self.addItem(self.sewer_exchange_plot)
×
172
        self.addItem(self.channel_bottom_plot)
×
173
        self.addItem(self.culvert_lowest_plot)
×
174
        self.addItem(self.culvert_bottom_plot)
×
175
        self.addItem(self.culvert_upper_plot)
×
176
        self.addItem(self.culvert_top_plot)
×
177
        self.addItem(self.weir_bottom_plot)
×
178
        self.addItem(self.weir_middle_plot)
×
179
        self.addItem(self.weir_upper_plot)
×
180
        self.addItem(self.weir_top_plot)
×
181
        self.addItem(self.orifice_bottom_plot)
×
182
        self.addItem(self.orifice_upper_plot)
×
183
        self.addItem(self.orifice_middle_plot)
×
184
        self.addItem(self.orifice_top_plot)
×
185
        self.addItem(self.node_indicator_intersection_plot)
×
186
        self.addItem(self.orifice_upper_fill)
×
187
        self.addItem(self.orifice_middle_fill)
×
188
        self.addItem(self.orifice_lower_fill)
×
189
        self.addItem(self.weir_upper_fill)
×
190
        self.addItem(self.weir_middle_fill)
×
191
        self.addItem(self.weir_lower_fill)
×
192
        self.addItem(self.culvert_upper_fill)
×
193
        self.addItem(self.culvert_middle_fill)
×
194
        self.addItem(self.culvert_lower_fill)
×
195
        self.addItem(self.exchange_plot)
×
196

197
        # Set the z-order of the curves (note that fill take minimum of its two defining curve as z-value)
198
        self.bottom_plot.setZValue(10)
×
199
        self.sewer_bottom_plot.setZValue(10)
×
200
        self.sewer_upper_plot.setZValue(10)
×
201
        self.sewer_top_plot.setZValue(10)
×
202
        self.sewer_exchange_plot.setZValue(10)
×
203
        self.channel_bottom_plot.setZValue(10)
×
204
        self.culvert_lowest_plot.setZValue(10)
×
205
        self.culvert_bottom_plot.setZValue(10)
×
206
        self.culvert_upper_plot.setZValue(10)
×
207
        self.culvert_top_plot.setZValue(10)
×
208
        self.weir_bottom_plot.setZValue(10)
×
209
        self.weir_middle_plot.setZValue(10)
×
210
        self.weir_upper_plot.setZValue(10)
×
211
        self.weir_top_plot.setZValue(10)
×
212
        self.orifice_bottom_plot.setZValue(10)
×
213
        self.orifice_upper_plot.setZValue(10)
×
214
        self.orifice_middle_plot.setZValue(10)
×
215
        self.orifice_top_plot.setZValue(10)
×
216

217
        self.exchange_plot.setZValue(100)
×
218
        self.node_indicator_intersection_plot.setZValue(55)
×
219
        self.orifice_upper_fill.setZValue(20)
×
220
        self.orifice_middle_fill.setZValue(3)
×
221
        self.orifice_lower_fill.setZValue(20)
×
222
        self.weir_upper_fill.setZValue(20)
×
223
        self.weir_middle_fill.setZValue(3)
×
224
        self.weir_lower_fill.setZValue(20)
×
225
        self.culvert_upper_fill.setZValue(20)
×
226
        self.culvert_middle_fill.setZValue(3)
×
227
        self.culvert_lower_fill.setZValue(20)
×
228
        self.bottom_fill.setZValue(7)
×
229
        self.sewer_top_fill.setZValue(7)
×
230

231
        # set listeners to signals
232
        self.profile_route_updated.connect(self.update_water_level_cache)
×
233

234
        # set code for hovering
235
        self.vb = self.plotItem.vb
×
236
        self.proxy = pg.SignalProxy(
×
237
            self.scene().sigMouseMoved, rateLimit=10, slot=self.mouse_hover
238
        )
239

240
        # Hijack the "A" button (it always autoscales to all plots, causing the interesting part to be flattened)
241
        self.getPlotItem().autoBtn.mode = ""
×
242
        self.getPlotItem().autoBtn.clicked.connect(self.auto_scale)
×
243

244
    def auto_scale(self, include_waterlevels: bool = True) -> None:
1✔
245
        range_plots = [self.bottom_plot, self.exchange_plot, self.culvert_upper_plot]
×
246
        if include_waterlevels:
×
247
            for waterlevel_plot, _, _ in self.waterlevel_plots.values():
×
248
                range_plots.append(waterlevel_plot)
×
249

250
        self.autoRange(items=range_plots)
×
251

252
    def mouse_hover(self, evt):
1✔
253
        mouse_point_x = self.plotItem.vb.mapSceneToView(evt[0]).x()
×
254
        self.profile_hovered.emit(mouse_point_x)
×
255

256
    def set_sideprofile(self, route_path, current_grid: ThreeDiGridItem):
1✔
257

258
        self.sideview_nodes = []  # Required to plot nodes and water level
×
259
        bottom_line = []  # Bottom of structures
×
260
        upper_line = []  # Top of structures
×
261
        middle_line = []  # Typically crest-level
×
262
        exchange_line = []  # exchange level
×
263
        upper_limit_line = []  # For top fill of weirs, orifices and culverts
×
264
        lower_limit_line = []  # For bottom of culverts
×
265

266
        self.current_grid_id = current_grid.id if current_grid else None
×
267

268
        generator = SideViewGraphGenerator(current_grid.path) if current_grid else None
×
269

270
        for route_part in route_path:
×
271
            first_node = True
×
272
            messagebar_message("Sideview", "Profile being generated, this might take a while...", 0, 0)
×
273
            QApplication.processEvents()
×
274

275
            for (begin_dist, end_dist, direction, feature) in Route.aggregate_route_parts(route_part):
×
276

277
                begin_dist = float(begin_dist)
×
278
                end_dist = float(end_dist)
×
279

280
                begin_node_id = feature["calculation_node_id_start"]
×
281
                end_node_id = feature["calculation_node_id_end"]
×
282
                if direction != 1:
×
283
                    begin_node_id, end_node_id = end_node_id, begin_node_id
×
284

285
                begin_level, end_level, begin_height, end_height, crest_level, ltype = generator.retrieve_profile_info_from_flowline(feature["id"])
×
286
                if direction != 1:
×
287
                    begin_level, end_level = end_level, begin_level
×
288
                    begin_height, end_height = end_height, begin_height
×
289

290
                if (ltype == LineType.PIPE) or (ltype == LineType.CULVERT) or (ltype == LineType.ORIFICE) or (ltype == LineType.WEIR) or (ltype == LineType.CHANNEL):
×
291

292
                    # logger.info(f"Adding line {feature['id']}, start_height: {begin_height}, end_height: {end_height}, start_level: {begin_level}, end_level: {end_level}, crest_level {crest_level}")
293

294
                    bottom_line.append((begin_dist, begin_level, ltype))
×
295
                    bottom_line.append((end_dist, end_level, ltype))
×
296

297
                    if (ltype == LineType.ORIFICE) or (ltype == LineType.WEIR):
×
298
                        # Orifices and weirs require different visualisation, and their height is relative to crest level
299
                        middle_line.append((begin_dist, crest_level, ltype))
×
300
                        middle_line.append((end_dist, crest_level, ltype))
×
301
                        upper_line.append((begin_dist, crest_level + begin_height, ltype))
×
302
                        upper_line.append((end_dist, crest_level + end_height, ltype))
×
303

304
                        if begin_height == 0.0 and end_height == 0.0:  # Open cross section
×
305
                            upper_limit_line.append((begin_dist, crest_level, ltype))
×
306
                            upper_limit_line.append((end_dist, crest_level, ltype))
×
307
                        else:
308
                            upper_limit_line.append((begin_dist, UPPER_LIMIT, ltype))
×
309
                            upper_limit_line.append((end_dist, UPPER_LIMIT, ltype))
×
310
                    elif ltype == LineType.CULVERT:
×
311
                        upper_line.append((begin_dist, begin_level + begin_height, ltype))
×
312
                        upper_line.append((end_dist, end_level + end_height, ltype))
×
313
                        upper_limit_line.append((begin_dist, UPPER_LIMIT, ltype))
×
314
                        upper_limit_line.append((end_dist, UPPER_LIMIT, ltype))
×
315
                        lower_limit_line.append((begin_dist, LOWER_LIMIT, ltype))
×
316
                        lower_limit_line.append((end_dist, LOWER_LIMIT, ltype))
×
317
                    else:
318
                        upper_line.append((begin_dist, begin_level + begin_height, ltype))
×
319
                        upper_line.append((end_dist, end_level + end_height, ltype))
×
320

321
                else:
322
                    logger.error(f"Unknown line type: {ltype}")
×
323
                    return
×
324

325
                node_level_1, node_height_1 = generator.retrieve_profile_info_from_node(begin_node_id)
×
326
                node_level_2, node_height_2 = generator.retrieve_profile_info_from_node(end_node_id)
×
327

328
                # Only draw exchange when nodes have heights
329
                if (node_height_1 > 0.0 and node_height_2 > 0.0):
×
330
                    exchange_line.append((begin_dist, node_level_1 + node_height_1))
×
331
                    exchange_line.append((end_dist, node_level_2 + node_height_2))
×
332

333
                # store node information for water level line
334
                if first_node:
×
335
                    self.sideview_nodes.append(
×
336
                        {"distance": begin_dist, "id": begin_node_id, "height": node_height_1, "level":  node_level_1}
337
                    )
338
                    first_node = False
×
339

340
                self.sideview_nodes.append(
×
341
                    {"distance": end_dist, "id": end_node_id, "height": node_height_2, "level":  node_level_2}
342
                )
343

344
        if len(route_path) > 0:
×
345
            # Draw data into graph, split lines into seperate parts for the different line types
346

347
            tables = {
×
348
                LineType.PIPE: [],
349
                LineType.CHANNEL: [],
350
                LineType.CULVERT: [],
351
                LineType.PUMP: [],
352
                LineType.WEIR: [],
353
                LineType.ORIFICE: [],
354
            }
355

356
            for point in bottom_line:
×
357
                tables[point[2]].append((point[0], point[1]))
×
358

359
            ts_table = np.array([(b[0], b[1]) for b in bottom_line], dtype=float)
×
360
            ts_exchange_table = np.array(exchange_line, dtype=float)
×
361

362
            self.exchange_plot.setData(ts_exchange_table, connect="pairs")
×
363
            self.bottom_plot.setData(ts_table, connect="pairs")
×
364
            self.absolute_bottom.setData(np.array([(b[0], LOWER_LIMIT) for b in bottom_line], dtype=float), connect="pairs")
×
365

366
            self.sewer_bottom_plot.setData(np.array(tables[LineType.PIPE], dtype=float), connect="pairs")
×
367
            self.channel_bottom_plot.setData(np.array(tables[LineType.CHANNEL], dtype=float), connect="pairs")
×
368
            self.culvert_bottom_plot.setData(np.array(tables[LineType.CULVERT], dtype=float), connect="pairs")
×
369
            self.weir_bottom_plot.setData(np.array(tables[LineType.WEIR], dtype=float), connect="pairs")
×
370
            self.orifice_bottom_plot.setData(np.array(tables[LineType.ORIFICE], dtype=float), connect="pairs")
×
371

372
            tables = {
×
373
                LineType.PIPE: [],
374
                LineType.CHANNEL: [],
375
                LineType.CULVERT: [],
376
                LineType.PUMP: [],
377
                LineType.WEIR: [],
378
                LineType.ORIFICE: [],
379
            }
380

381
            for point in upper_line:
×
382
                tables[point[2]].append((point[0], point[1]))
×
383

384
            self.sewer_upper_plot.setData(np.array(tables[LineType.PIPE], dtype=float), connect="pairs")
×
385
            self.culvert_upper_plot.setData(np.array(tables[LineType.CULVERT], dtype=float), connect="pairs")
×
386
            self.weir_upper_plot.setData(np.array(tables[LineType.WEIR], dtype=float), connect="pairs")
×
387
            self.orifice_upper_plot.setData(np.array(tables[LineType.ORIFICE], dtype=float), connect="pairs")
×
388

389
            # pyqtgraph has difficulties with filling between lines consisting of different
390
            # number of segments, therefore we need to draw a dedicated sewer-exchange line
391
            sewer_top_table = []
×
392
            sewer_exchange_table = []
×
393
            for point_index in range(0, len(tables[LineType.PIPE]), 2):
×
394
                point_1 = tables[LineType.PIPE][point_index]
×
395
                point_2 = tables[LineType.PIPE][point_index+1]
×
396
                # find the corresponding exchange height at this distance
397
                exchange_point_found = False
×
398
                for exchange_point_index in range(0, len(ts_exchange_table), 2):
×
399
                    exchange_point_1 = ts_exchange_table[exchange_point_index]
×
400
                    exchange_point_2 = ts_exchange_table[exchange_point_index+1]
×
401
                    if exchange_point_1[0] == point_1[0] and exchange_point_2[0] == point_2[0]:
×
402
                        sewer_top_table.append((point_1[0], point_1[1]))
×
403
                        sewer_top_table.append((point_2[0], point_2[1]))
×
404
                        sewer_exchange_table.append((point_1[0], exchange_point_1[1]))
×
405
                        sewer_exchange_table.append((point_2[0], exchange_point_2[1]))
×
406
                        exchange_point_found = True
×
407
                        break
×
408

409
                # In case no exchange level, fill to top
410
                if not exchange_point_found:
×
411
                    sewer_top_table.append((point_1[0], point_1[1]))
×
412
                    sewer_top_table.append((point_2[0], point_2[1]))
×
413
                    sewer_exchange_table.append((point_1[0], UPPER_LIMIT))
×
414
                    sewer_exchange_table.append((point_2[0], UPPER_LIMIT))
×
415

416
            self.sewer_top_plot.setData(np.array(sewer_top_table, dtype=float), connect="pairs")
×
417
            self.sewer_exchange_plot.setData(np.array(sewer_exchange_table, dtype=float), connect="pairs")
×
418

419
            tables = {
×
420
                LineType.PIPE: [],
421
                LineType.CHANNEL: [],
422
                LineType.CULVERT: [],
423
                LineType.PUMP: [],
424
                LineType.WEIR: [],
425
                LineType.ORIFICE: [],
426
            }
427

428
            for point in middle_line:
×
429
                tables[point[2]].append((point[0], point[1]))
×
430

431
            self.weir_middle_plot.setData(np.array(tables[LineType.WEIR], dtype=float), connect="pairs")
×
432
            self.orifice_middle_plot.setData(np.array(tables[LineType.ORIFICE], dtype=float), connect="pairs")
×
433

434
            tables = {
×
435
                LineType.PIPE: [],
436
                LineType.CHANNEL: [],
437
                LineType.CULVERT: [],
438
                LineType.PUMP: [],
439
                LineType.WEIR: [],
440
                LineType.ORIFICE: [],
441
            }
442

443
            for point in upper_limit_line:
×
444
                tables[point[2]].append((point[0], point[1]))
×
445

446
            self.culvert_top_plot.setData(np.array(tables[LineType.CULVERT], dtype=float), connect="pairs")
×
447
            self.weir_top_plot.setData(np.array(tables[LineType.WEIR], dtype=float), connect="pairs")
×
448
            self.orifice_top_plot.setData(np.array(tables[LineType.ORIFICE], dtype=float), connect="pairs")
×
449

450
            tables = {
×
451
                LineType.PIPE: [],
452
                LineType.CHANNEL: [],
453
                LineType.CULVERT: [],
454
                LineType.PUMP: [],
455
                LineType.WEIR: [],
456
                LineType.ORIFICE: [],
457
            }
458

459
            for point in lower_limit_line:
×
460
                tables[point[2]].append((point[0], point[1]))
×
461
            self.culvert_lowest_plot.setData(np.array(tables[LineType.CULVERT], dtype=float), connect="pairs")
×
462

463
            # draw nodes
464
            node_indicator_table = []
×
465
            for node in self.sideview_nodes:
×
466
                node_indicator_table.append((node["distance"], LOWER_LIMIT))
×
467
                node_indicator_table.append((node["distance"], UPPER_LIMIT))
×
468

469
            # Determine intersections between vertical node lines and horizontal lines
470
            if self.show_dots:
×
471
                horizontal_lines = [exchange_line, upper_line, ts_table]
×
472

473
                intersections = []
×
474
                for i in range(0, len(node_indicator_table), 2):
×
475
                    vert_line = LineString([(node_indicator_table[i][0], node_indicator_table[i])[1], (node_indicator_table[i+1][0], node_indicator_table[i+1][1])])
×
476

477
                    for line in horizontal_lines:
×
478
                        # Some lines may contain gaps and therefore we do not represent them as a single LineString
479
                        # but treat each segment as a LineString
480
                        for idx in range(0, len(line), 2):
×
481
                            segment = LineString([(line[idx][0], line[idx][1]), (line[idx+1][0], line[idx+1][1])])
×
482
                            intersection = vert_line.intersection(segment)
×
483
                            if isinstance(intersection, Point):
×
484
                                intersections.append(intersection.coords[0])
×
485
                                intersections.append((0.0, np.nan))  # add a line break
×
486

487
                logger.info(f"{len(intersections)} intersections")
×
488

489
                self.node_indicator_intersection_plot.setData(np.array(intersections, dtype=float), symbol='h', size=2, connect='finite')
×
490
            else:
491
                self.node_indicator_intersection_plot.setData(np.array([(0.0, np.nan)], dtype=float), symbol='h', size=2, connect='finite')
×
492

493
            # reset water level lines
494
            ts_table = np.array(np.array([(0.0, np.nan)]), dtype=float)
×
495
            for plot, fill, dots in self.waterlevel_plots.values():
×
496
                plot.setData(ts_table)
×
497
                dots.setData(ts_table)
×
498

499
            self.auto_scale(include_waterlevels=False)
×
500

501
            self.profile_route_updated.emit()
×
502
        else:
503
            # reset sideview
504
            ts_table = np.array(np.array([(0.0, np.nan)]), dtype=float)
×
505
            self.bottom_plot.setData(ts_table)
×
506
            self.sewer_top_plot.setData(ts_table)
×
507
            self.sewer_exchange_plot.setData(ts_table)
×
508
            self.sewer_bottom_plot.setData(ts_table)
×
509
            self.sewer_upper_plot.setData(ts_table)
×
510
            self.channel_bottom_plot.setData(ts_table)
×
511
            self.culvert_bottom_plot.setData(ts_table)
×
512
            self.culvert_upper_plot.setData(ts_table)
×
513
            self.culvert_lowest_plot.setData(ts_table)
×
514
            self.culvert_top_plot.setData(ts_table)
×
515
            self.weir_bottom_plot.setData(ts_table)
×
516
            self.weir_upper_plot.setData(ts_table)
×
517
            self.weir_middle_plot.setData(ts_table)
×
518
            self.orifice_bottom_plot.setData(ts_table)
×
519
            self.orifice_upper_plot.setData(ts_table)
×
520
            self.orifice_middle_plot.setData(ts_table)
×
521
            self.exchange_plot.setData(ts_table)
×
522
            self.node_indicator_intersection_plot.setData(ts_table)
×
523

524
            for plot, fill, dots in self.waterlevel_plots.values():
×
525
                self.removeItem(plot)
×
526
                self.removeItem(fill)
×
527
                self.removeItem(dots)
×
528
            self.waterlevel_plots = {}
×
529

530
            # Clear node list used to draw results
531
            self.sideview_nodes = []
×
532
            messagebar_pop_message()
×
533

534
    def update_water_level_cache(self, update_range=True):
1✔
535

536
        for plot, fill, dots in self.waterlevel_plots.values():
×
537
            self.removeItem(plot)
×
538
            self.removeItem(fill)
×
539
            self.removeItem(dots)
×
540
        self.waterlevel_plots = {}
×
541

542
        # Iterate through the selection model
543
        for row_number in range(self.sideview_result_model.rowCount()):
×
544
            # Get checkbox item (this contains result object id)
545
            check_item = self.sideview_result_model.item(row_number, 0)
×
NEW
546
            if check_item.checkState() != Qt.CheckState.Checked:
×
547
                continue
×
548

549
            result_id = check_item.data()
×
550
            pattern_item = self.sideview_result_model.item(row_number, 1)
×
551
            plot_pattern, plot_color = pattern_item.data()
×
552

553
            logger.error(f"Retrieved result: {result_id} with pattern {plot_pattern} from model")
×
554
            # Create the waterlevel plots
555
            pen = pg.mkPen(color=plot_color, width=2, style=plot_pattern)
×
556
            water_level_plot = pg.PlotDataItem(np.array([(0.0, np.nan)]), pen=pen)
×
557
            water_level_plot.setZValue(100)  # always visible
×
558
            water_fill = pg.FillBetweenItem(water_level_plot, self.absolute_bottom, pg.mkBrush(plot_color[0], plot_color[1], plot_color[2], 128))
×
559
            water_fill.setZValue(5)
×
560
            water_level_nodes = pg.PlotDataItem(np.array([(0.0, np.nan)]), symbolBrush=pg.mkBrush(plot_color[0], plot_color[1], plot_color[2]), symbolSize=7)
×
561
            water_level_nodes.setZValue(100)
×
562
            self.addItem(water_level_plot)
×
563
            self.addItem(water_fill)
×
564
            self.addItem(water_level_nodes)
×
565

566
            self.waterlevel_plots[result_id] = (water_level_plot, water_fill, water_level_nodes)
×
567

568
            result = self.model.get_result(result_id)
×
569

570
            gra = GridH5ResultAdmin(str(result.parent().path.with_suffix('.h5')), result.path)
×
571
            node_ids = [int(node["id"]) for node in self.sideview_nodes]
×
572
            data = gra.nodes.filter(id__in=node_ids).only("s1", "id").timeseries(indexes=slice(None)).data
×
573
            node_levels = data["s1"]
×
574
            node_id_table = data["id"].tolist()
×
575

576
            for node in self.sideview_nodes:
×
577
                if "timeseries" not in node:
×
578
                    node["timeseries"] = {}
×
579

580
                # find index in node table
581
                column = node_id_table.index(node["id"])
×
582
                levels = node_levels[:, column]
×
583
                levels[levels == NO_DATA_VALUE] = np.NaN
×
584

585
                node["timeseries"][result.id] = levels
×
586

587
        self.update_waterlevel(update_range)
×
588
        messagebar_pop_message()
×
589

590
    def update_waterlevel(self, update_range=False):
1✔
591

592
        if not self.waterlevel_plots:
×
593
            return
×
594

595
        for row_number in range(self.sideview_result_model.rowCount()):
×
596
            # Get checkbox item (this contains result object)
597
            check_item = self.sideview_result_model.item(row_number, 0)
×
NEW
598
            if check_item.checkState() != Qt.CheckState.Checked:
×
599
                continue
×
600

601
            result_id = check_item.data()
×
602
            result = self.model.get_result(result_id)
×
603
            current_delta = result._timedelta
×
604
            current_seconds = current_delta.total_seconds()
×
605
            parameter_timestamps = result.threedi_result.get_timestamps("s1")
×
606
            timestamp_nr = bisect_left(parameter_timestamps, current_seconds)
×
607
            timestamp_nr = min(timestamp_nr, parameter_timestamps.size - 1)
×
608

609
            logger.info(f"Drawing for result {result.id} for nr {timestamp_nr}")
×
610

611
            water_level_line = []
×
612
            water_nodes = []
×
613
            for node in self.sideview_nodes:
×
614
                water_level = node["timeseries"][result.id][timestamp_nr]
×
615
                point = (node["distance"], water_level)
×
616
                water_level_line.append(point)
×
617
                water_nodes += [(point), (0.0, np.nan)]
×
618
                # logger.error(f"Node shape {node['timeseries'].shape}, distance {node['distance']} and level {water_level}")
619

620
            self.waterlevel_plots[result.id][0].setData(np.array(water_level_line, dtype=float))
×
621

622
            # logger.error(water_level_line)
623
            # Draw dots at intersections between this water line and vertical node lines:
624
            # This is actually at the beginning of each segment of the water level line
625
            if self.show_dots:
×
626
                self.waterlevel_plots[result.id][2].setData(np.array(water_nodes, dtype=float), symbol='h', size=2, connect='finite')
×
627
            else:
628
                self.waterlevel_plots[result.id][2].setData(np.array([(0.0, np.nan)], dtype=float), symbol='h', size=2, connect='finite')
×
629

630
        if update_range:
×
631
            self.auto_scale(include_waterlevels=True)
×
632

633
    def on_close(self):
1✔
634
        self.profile_route_updated.disconnect(self.update_water_level_cache)
×
635

636
    def closeEvent(self, event):
1✔
637
        self.on_close()
×
638
        event.accept()
×
639

640

641
class SideViewDockWidget(QDockWidget):
1✔
642
    """Main Dock Widget for showing 3Di results in Graphs"""
643

644
    # todo:
645
    # detecteer dichtsbijzijnde punt in plaats van willekeurige binnen gebied
646
    # let op CRS van vreschillende lagen en CRS changes
647

648
    closingWidget = pyqtSignal(int)
1✔
649

650
    def __init__(
1✔
651
        self, iface, nr, model, parent=None
652
    ):
653
        super().__init__(parent)
×
654

655
        self.iface = iface
×
656
        self.nr = nr
×
657
        self.model = model  # Global Result manager model
×
658
        self.sideview_result_model = QStandardItemModel(self)  # Specific sideview model to store loaded results
×
659
        self.sideview_result_model.setHorizontalHeaderLabels(["active", "pattern", "result"])
×
660
        self.sideview_result_model.itemChanged.connect(self.result_item_toggled)
×
661
        # Also used to check whether we have a current grid
662
        self.current_grid_id = None
×
663

664
        # In case this dock widget becomes (in)visible, we disable the route tool
665
        self.visibilityChanged.connect(self.unset_route_tool)
×
666

667
        self.setup_ui()
×
668

669
    def update_waterlevel(self):
1✔
670
        self.side_view_plot_widget.update_waterlevel()
×
671

672
    @pyqtSlot(ThreeDiResultItem)
1✔
673
    def result_added(self, item: ThreeDiResultItem):
1✔
674
        if item.parent().id != self.current_grid_id:
×
675
            return
×
676

677
        # Update table and redraw sideview
678
        self._add_result_to_table(item)
×
679
        self.side_view_plot_widget.update_water_level_cache()
×
680

681
    @pyqtSlot(ThreeDiResultItem)
1✔
682
    def result_changed(self, item: ThreeDiResultItem):
1✔
683
        if item.parent().id != self.current_grid_id:
×
684
            return
×
685

686
        # Update table, no need to redraw anything
687
        for row_number in range(self.sideview_result_model.rowCount()):
×
688
            # Get checkbox item (this contains result object id)
689
            check_item = self.sideview_result_model.item(row_number, 0)
×
690
            result_id = check_item.data()
×
691
            if item.id == result_id:
×
692
                name_item = self.sideview_result_model.item(row_number, 2)
×
693
                name_item.setText(item.text())
×
694
                return
×
695

696
        # We should never reach this
697
        raise Exception("Result should be in sideview model!")
×
698

699
    @pyqtSlot(ThreeDiResultItem)
1✔
700
    def result_removed(self, item: ThreeDiResultItem):
1✔
701
        if item.parent().id != self.current_grid_id:
×
702
            return
×
703

704
        # Update table and redraw sideview
705
        for row_number in range(self.sideview_result_model.rowCount()):
×
706
            # Get checkbox item (this contains result object id)
707
            check_item = self.sideview_result_model.item(row_number, 0)
×
708
            result_id = check_item.data()
×
709
            if item.id == result_id:
×
710
                self.sideview_result_model.removeRow(row_number)
×
711
                self.side_view_plot_widget.update_water_level_cache()
×
712
                return
×
713

714
        # We should never reach this
715
        raise Exception("Result should be in sideview model!")
×
716

717
    @pyqtSlot(ThreeDiGridItem)
1✔
718
    def grid_changed(self, item: ThreeDiGridItem):
1✔
719
        idx = self.select_grid_combobox.findData(item.id)
×
720
        assert idx != -1
×
721
        # Change name in combobox
722
        self.select_grid_combobox.setItemText(idx, item.text())
×
723
        item_id = self.select_grid_combobox.itemData(idx)
×
724
        if self.current_grid_id == item_id:
×
725
            self.setWindowTitle(f"3Di Side View {self.nr}: {item.text()}")
×
726

727
    @pyqtSlot(ThreeDiGridItem)
1✔
728
    def grid_added(self, item: ThreeDiGridItem):
1✔
729
        assert item.id != self.current_grid_id
×
730
        currentIndex = self.select_grid_combobox.currentIndex()
×
731
        self.select_grid_combobox.addItem(item.text(), item.id)
×
732
        self.select_grid_combobox.setCurrentIndex(currentIndex)
×
733

734
    @pyqtSlot(ThreeDiGridItem)
1✔
735
    def grid_removed(self, item: ThreeDiGridItem):
1✔
736
        idx = self.select_grid_combobox.findData(item.id)
×
737
        assert idx != -1
×
738
        item_id = self.select_grid_combobox.itemData(idx)
×
739
        if self.current_grid_id == item_id:
×
740
            # Also removes all waterlevel plots
741
            self.deinitialize_route()
×
742
            # Removes all plots from table
743
            self.sideview_result_model.clear()
×
744
            self.setWindowTitle(f"3Di Side View {self.nr}:")
×
745

746
        self.select_grid_combobox.removeItem(idx)
×
747

748
    @pyqtSlot(int)
1✔
749
    def grid_selected(self, grid_index: int):
1✔
750
        # Because we connected the "activated" signal instead of the "currentIndexChanged" signal,
751
        # programmatically changing the index does not emit a signal
752
        self.select_grid_combobox.setCurrentIndex(grid_index)
×
753

754
        item_id = self.select_grid_combobox.itemData(grid_index)
×
755
        grid = self.model.get_grid(item_id)
×
756
        assert grid
×
757
        self.initialize_route(grid)
×
758
        self.setWindowTitle(f"3Di Side view {self.nr}: {grid.text()}")
×
759

760
    def result_item_toggled(self, _: QStandardItem):
1✔
761
        # For now, just rebuild and redraw the whole sideview, taking into account new checks, but no autoscaling
762
        self.side_view_plot_widget.update_water_level_cache(False)
×
763

764
    def unset_route_tool(self):
1✔
765
        if self.current_grid_id is None:
×
766
            return
×
767

768
        if self.iface.mapCanvas().mapTool() is self.route_tool:
×
769
            self.iface.mapCanvas().unsetMapTool(self.route_tool)
×
770
            self.select_sideview_button.setChecked(False)
×
771

772
    def toggle_route_tool(self):
1✔
773
        if self.current_grid_id is None:
×
774
            return
×
775

776
        if self.iface.mapCanvas().mapTool() is self.route_tool:
×
777
            self.iface.mapCanvas().unsetMapTool(self.route_tool)
×
778
            self.select_sideview_button.setChecked(False)
×
779
        else:
780
            self.iface.mapCanvas().setMapTool(self.route_tool)
×
781
            self.select_sideview_button.setChecked(True)
×
782

783
    def initialize_route(self, grid_item: ThreeDiGridItem):
1✔
784
        if self.current_grid_id is not None:
×
785
            self.deinitialize_route()
×
786

787
        self.current_grid_id = grid_item.id
×
788
        layer_id = grid_item.layer_ids["flowline"]
×
789
        # Note that we are NOT owner of this layer (that is results manager)
790
        self.graph_layer = QgsProject.instance().mapLayer(layer_id)
×
791

792
        # Init route (for shortest path)
793
        self.route = Route(self.graph_layer)
×
794

795
        # Retrieve relevant results and put in table
796
        self._populate_result_table(grid_item)
×
797

798
        # Add (internal) graph layer to canvas for testing
799
        # QgsProject.instance().addMapLayer(self.route.get_graph_layer(), True)
800

801
        # Link route map tool (allows node selection)
802
        self.route_tool = RouteMapTool(
×
803
            self.iface.mapCanvas(), self.graph_layer, self.on_route_point_select
804
        )
805
        self.route_tool.deactivated.connect(self.unset_route_tool)
×
806

807
        self.map_visualisation = SideViewMapVisualisation(self.iface, self.graph_layer.crs())
×
808

809
        # connect graph hover to point visualisation on map
810
        self.side_view_plot_widget.profile_hovered.connect(self.map_visualisation.hover_graph)
×
811

812
        # Enable buttons
813
        self.select_sideview_button.setEnabled(True)
×
814
        self.reset_sideview_button.setEnabled(True)
×
815

816
        # Add tree layer to map (service area, for fun and testing purposes)
817
        self.vl_tree_layer = self.route.get_virtual_tree_layer()
×
818
        self.vl_tree_layer.loadNamedStyle(
×
819
            os.path.join(
820
                os.path.dirname(os.path.realpath(__file__)),
821
                "layer_styles",
822
                "tree.qml",
823
            )
824
        )
825
        QgsProject.instance().addMapLayer(self.vl_tree_layer)
×
826

827
    def deinitialize_route(self):
1✔
828
        self.reset_sideview()
×
829
        self.unset_route_tool()
×
830

831
        self.current_grid_id = None
×
832
        self.select_sideview_button.setEnabled(False)
×
833
        self.reset_sideview_button.setEnabled(False)
×
834

835
        self.graph_layer = None  # We are not owner of this layer
×
836

837
        # Note that route.graph_layer is an interal layer used to build the graph,
838
        # only added to canvas for testing purposes
839
        # QgsProject.instance().removeMapLayer(self.route.get_graph_layer())
840

841
        self.route = None
×
842
        self.route_tool = None
×
843
        self.map_visualisation = None
×
844
        QgsProject.instance().removeMapLayer(self.vl_tree_layer)
×
845
        self.vl_tree_layer = None
×
846
        self.iface.mapCanvas().refreshAllLayers()
×
847

848
    def _populate_result_table(self, grid_item: ThreeDiGridItem):
1✔
849
        self.sideview_result_model.clear()
×
850
        self.sideview_result_model.setHorizontalHeaderLabels(["active", "pattern", "result"])
×
851

852
        results = []
×
853
        self.model.get_results_from_item(grid_item, False, results)
×
854

855
        for result in results:
×
856
            self._add_result_to_table(result)
×
857

858
    def _add_result_to_table(self, result_item: ThreeDiResultItem):
1✔
859
        checkbox_table_item = QStandardItem("")
×
860
        checkbox_table_item.setData(result_item.id)
×
861
        checkbox_table_item.setCheckable(True)
×
NEW
862
        checkbox_table_item.setCheckState(Qt.CheckState.Checked)
×
863
        checkbox_table_item.setEditable(False)
×
864

865
        result_table_item = QStandardItem(result_item.text())
×
866
        result_table_item.setEditable(False)
×
867

868
        # pick new pattern
NEW
869
        pattern = Qt.PenStyle.SolidLine
×
870
        color = COLOR_LIST[self.sideview_result_model.rowCount() % len(COLOR_LIST)]
×
871
        pattern_table_item = QStandardItem("")
×
872
        pattern_table_item.setEditable(False)
×
873
        pattern_table_item.setData((pattern, color))
×
874
        self.sideview_result_model.appendRow([checkbox_table_item, pattern_table_item, result_table_item])
×
875

876
        # Add a PenStyle display in the table
877
        index = self.sideview_result_model.index(self.sideview_result_model.rowCount()-1, 1)
×
878
        self.table_view.setIndexWidget(index, PenStyleWidget(pattern, QColor(*color), self.table_view))
×
879

880
    def on_route_point_select(self, selected_features, clicked_coordinate):
1✔
881
        """Select and add the closest point from the list of selected features.
882

883
        Args:
884
            selected_features: list of features selected by click
885
            clicked_coordinate: (transformed) of the click
886
        """
887
        assert not self.graph_layer.crs().isGeographic()
×
888

889
        def squared_distance_clicked(coordinate):
×
890
            """Calculate the squared distance w.r.t. the clicked location."""
891
            x1, y1 = clicked_coordinate
×
892
            x2, y2 = coordinate.x(), coordinate.y()
×
893
            return ((x1-x2)**2 + (y1-y2)**2)
×
894

895
        # Only look at first and last vertex
896
        selected_coordinates = reduce(
×
897
            lambda accum, f: accum
898
            + [f.geometry().vertexAt(0), f.geometry().vertexAt(len(f.geometry().asPolyline())-1)],
899
            selected_features,
900
            [],
901
        )
902

903
        if len(selected_coordinates) == 0:
×
904
            return
×
905

906
        closest_point = min(selected_coordinates, key=squared_distance_clicked)
×
907
        next_point = QgsPointXY(closest_point)
×
908

909
        success, msg = self.route.add_point(next_point)
×
910

911
        self.select_sideview_button.setText("Continue side view trajectory")
×
912
        if not success:
×
913
            messagebar_message("Sideview", msg, 0, 3)
×
914
            return
×
915

916
        self.side_view_plot_widget.set_sideprofile(self.route.path, self.model.get_grid(self.current_grid_id))
×
917
        self.map_visualisation.set_sideview_route(self.route)
×
918

919
    def reset_sideview(self):
1✔
920
        self.route.reset()
×
921
        self.map_visualisation.reset()
×
922
        # Also removes all waterlevel plots
923
        self.side_view_plot_widget.set_sideprofile([], None)
×
924
        self.select_sideview_button.setText("Choose side view trajectory")
×
925

926
    def update_dots(self, state):
1✔
927
        # Just redraw the whole thing for now
NEW
928
        self.side_view_plot_widget.show_dots = (state == Qt.CheckState.Checked)
×
929
        self.side_view_plot_widget.set_sideprofile(self.route.path, self.model.get_grid(self.current_grid_id))
×
930

931
    def on_close(self):
1✔
932
        """
933
        unloading widget
934
        """
935
        if self.current_grid_id is not None:
×
936
            self.route_tool.deactivated.disconnect(self.unset_route_tool)
×
937
            self.unset_route_tool()
×
938
            self.map_visualisation.close()
×
939
            self.side_view_plot_widget.profile_hovered.disconnect(self.map_visualisation.hover_graph)
×
940
            QgsProject.instance().removeMapLayer(self.vl_tree_layer.id())
×
941

942
        self.select_sideview_button.clicked.disconnect(self.toggle_route_tool)
×
943
        self.reset_sideview_button.clicked.disconnect(self.reset_sideview)
×
944
        self.side_view_plot_widget.on_close()
×
945

946
    def closeEvent(self, event):
1✔
947
        self.on_close()
×
948
        self.closingWidget.emit(self.nr)
×
949
        event.accept()
×
950

951
    def setup_ui(self):
1✔
952

NEW
953
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
×
954
        self.setWindowTitle(f"3Di Side view {self.nr}: ")
×
955

956
        self.dock_widget_content = QWidget(self)
×
957

958
        self.main_vlayout = QVBoxLayout(self)
×
959
        self.dock_widget_content.setLayout(self.main_vlayout)
×
960

961
        self.button_bar_hlayout = QHBoxLayout(self)
×
962
        self.button_bar_hlayout.setSpacing(10)
×
963

964
        self.select_sideview_button = QPushButton("Choose side view trajectory", self.dock_widget_content)
×
965
        self.button_bar_hlayout.addWidget(self.select_sideview_button)
×
966
        self.reset_sideview_button = QPushButton("Reset side view trajectory", self.dock_widget_content)
×
967
        self.select_sideview_button.setEnabled(False)
×
968
        self.reset_sideview_button.setEnabled(False)
×
969
        self.button_bar_hlayout.addWidget(self.reset_sideview_button)
×
970
        self.show_nodes_checkbox = QCheckBox("Show nodes", self.dock_widget_content)
×
971
        self.show_nodes_checkbox.setChecked(True)
×
972
        self.show_nodes_checkbox.stateChanged.connect(self.update_dots)
×
973
        self.button_bar_hlayout.addWidget(self.show_nodes_checkbox)
×
NEW
974
        spacer_item = QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
×
975
        self.button_bar_hlayout.addItem(spacer_item)
×
976
        self.button_bar_hlayout.addWidget(QLabel("Computational grid: ", self.dock_widget_content))
×
977
        self.select_grid_combobox = QComboBox(self.dock_widget_content)
×
978
        self.button_bar_hlayout.addWidget(self.select_grid_combobox)
×
979
        self.main_vlayout.addItem(self.button_bar_hlayout)
×
980

981
        # populate the combobox, but select none
982
        for grid in self.model.get_grids():
×
983
            self.select_grid_combobox.addItem(grid.text(), grid.id)
×
984
        self.select_grid_combobox.setCurrentIndex(-1)
×
985
        self.select_grid_combobox.activated.connect(self.grid_selected)
×
986

987
        plotContainerWidget = QSplitter(self)
×
988
        self.side_view_plot_widget = SideViewPlotWidget(plotContainerWidget, self.model, self.sideview_result_model)
×
989
        plotContainerWidget.addWidget(self.side_view_plot_widget)
×
990
        self.table_view = QTableView(self)
×
991
        self.table_view.setModel(self.sideview_result_model)
×
992
        self.table_view.horizontalHeader().setStretchLastSection(True)
×
993
        self.table_view.verticalHeader().hide()
×
NEW
994
        self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
×
995
        self.table_view.resizeColumnsToContents()
×
NEW
996
        self.table_view.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
×
997
        plotContainerWidget.addWidget(self.table_view)
×
998
        plotContainerWidget.setStretchFactor(0, 8)
×
999
        plotContainerWidget.setStretchFactor(1, 1)
×
1000
        self.main_vlayout.addWidget(plotContainerWidget)
×
1001

1002
        self.setWidget(self.dock_widget_content)
×
1003

1004
        self.select_sideview_button.setCheckable(True)
×
1005
        self.select_sideview_button.clicked.connect(self.toggle_route_tool)
×
1006
        self.reset_sideview_button.clicked.connect(self.reset_sideview)
×
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