class GraphWidget(QWidget): def __init__( self, parent=None, ts_datasources=None, parameter_config=[], name="", geometry_type=QgsWkbTypes.Point, ): super().__init__(parent) self.name = name self.ts_datasources = ts_datasources self.parent = parent self.geometry_type = geometry_type self.setup_ui() self.model = LocationTimeseriesModel( ts_datasources=self.ts_datasources) self.graph_plot.set_location_model(self.model) self.graph_plot.set_ds_model(self.ts_datasources) self.location_timeseries_table.setModel(self.model) # set listeners self.parameter_combo_box.currentIndexChanged.connect( self.parameter_change) self.remove_timeseries_button.clicked.connect( self.remove_objects_table) # init parameter selection self.set_parameter_list(parameter_config) if self.geometry_type == QgsWkbTypes.Point: self.marker = QgsVertexMarker(self.parent.iface.mapCanvas()) else: self.marker = QgsRubberBand(self.parent.iface.mapCanvas()) self.marker.setColor(Qt.red) self.marker.setWidth(2) def set_parameter_list(self, parameter_config): # reset nr_old_parameters = self.parameter_combo_box.count() self.parameters = dict([(p["name"], p) for p in parameter_config]) self.parameter_combo_box.insertItems( 0, [p["name"] for p in parameter_config]) # todo: find best matching parameter based on previous selection if nr_old_parameters > 0: self.parameter_combo_box.setCurrentIndex(0) nr_parameters_tot = self.parameter_combo_box.count() for i in reversed( list( range(nr_parameters_tot - nr_old_parameters, nr_parameters_tot))): self.parameter_combo_box.removeItem(i) # self.graph_plot.set_parameter(self.current_parameter) def on_close(self): """ unloading widget and remove all required stuff :return: """ self.parameter_combo_box.currentIndexChanged.disconnect( self.parameter_change) self.remove_timeseries_button.clicked.disconnect( self.remove_objects_table) def closeEvent(self, event): """ overwrite of QDockWidget class to emit signal :param event: QEvent """ self.on_close() event.accept() def highlight_feature(self, obj_id, obj_type): pass # todo: selection generated errors and crash of Qgis. Implement method # with QgsRubberband and/ or QgsVertexMarker transform = QgsCoordinateTransform( QgsCoordinateReferenceSystem(4326), QgsProject.instance().crs(), QgsProject.instance(), ) layers = self.parent.iface.mapCanvas().layers() for lyr in layers: # Clear other layers # lyr.removeSelection() if lyr.name() == obj_type: # query layer for object filt = u'"id" = {0}'.format(obj_id) request = QgsFeatureRequest().setFilterExpression(filt) features = lyr.getFeatures(request) for feature in features: if self.geometry_type == QgsWkbTypes.Point: geom = feature.geometry() geom.transform(transform) self.marker.setCenter(geom.asPoint()) self.marker.setVisible(True) else: self.marker.setToGeometry(feature.geometry(), lyr) def unhighlight_all_features(self): """Remove the highlights from all layers""" if self.geometry_type == QgsWkbTypes.Point: self.marker.setVisible(False) else: self.marker.reset() pass # todo: selection generated errors and crash of Qgis. Implement method # with QgsRubberband and/ or QgsVertexMarker def setup_ui(self): """ Create Qt widgets and elements """ self.setObjectName(self.name) self.hLayout = QHBoxLayout(self) self.hLayout.setObjectName("hLayout") # add graphplot self.graph_plot = GraphPlot(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth( self.graph_plot.sizePolicy().hasHeightForWidth()) self.graph_plot.setSizePolicy(sizePolicy) self.graph_plot.setMinimumSize(QSize(250, 250)) self.hLayout.addWidget(self.graph_plot) # add layout for timeseries table and other controls self.vLayoutTable = QVBoxLayout(self) self.hLayout.addLayout(self.vLayoutTable) # add combobox for parameter selection self.parameter_combo_box = QComboBox(self) self.vLayoutTable.addWidget(self.parameter_combo_box) # add timeseries table self.location_timeseries_table = LocationTimeseriesTable(self) self.location_timeseries_table.hoverEnterRow.connect( self.highlight_feature) self.location_timeseries_table.hoverExitAllRows.connect( self.unhighlight_all_features) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.location_timeseries_table.sizePolicy().hasHeightForWidth()) self.location_timeseries_table.setSizePolicy(sizePolicy) self.location_timeseries_table.setMinimumSize(QSize(250, 0)) self.vLayoutTable.addWidget(self.location_timeseries_table) # add buttons below table self.hLayoutButtons = QHBoxLayout(self) self.vLayoutTable.addLayout(self.hLayoutButtons) self.remove_timeseries_button = QPushButton(self) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.remove_timeseries_button.sizePolicy().hasHeightForWidth()) self.remove_timeseries_button.setSizePolicy(sizePolicy) self.remove_timeseries_button.setObjectName("remove_timeseries_button") self.hLayoutButtons.addWidget(self.remove_timeseries_button) self.hLayoutButtons.addItem( QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) self.retranslateUi() def retranslateUi(self): """ set translated widget text """ self.remove_timeseries_button.setText("Delete") def parameter_change(self, nr): """ set current selected parameter and trigger refresh of graphs :param nr: nr of selected option of combobox :return: """ self.current_parameter = self.parameters[ self.parameter_combo_box.currentText()] self.graph_plot.set_parameter(self.current_parameter) def get_feature_index(self, layer, feature): """ get the id of the selected id feature :param layer: selected Qgis layer to be added :param feature: selected Qgis feature to be added :return: idx (integer) We can't do ``feature.id()``, so we have to pick something that we have agreed on. For now we have hardcoded the 'id' field as the default, but that doesn't mean it's always the case in the future when more layers are added! """ idx = feature.id() if layer.dataProvider().name() in PROVIDERS_WITHOUT_PRIMARY_KEY: idx = feature["id"] return idx def get_object_name(self, layer, feature): """ get the object_name (display_name / type) of the selected id feature :param layer: selected Qgis layer to be added :param feature: selected Qgis feature to be added :return: object_name (string) To get a object_name we use the following logic: - get the '*display_name*' column if available; - if not: get the 'type' column if available; - if not: object_name = 'N/A' """ object_name = None for column_nr, field in enumerate(layer.fields()): if "display_name" in field.name(): object_name = feature[column_nr] if object_name is None: for column_nr, field in enumerate(layer.fields()): if field.name() == "type": object_name = feature[column_nr] break else: object_name = "N/A" logger.warning( "Layer has no 'display_name', it's probably a result " "layer, but putting a placeholder object name just " "for safety.") return object_name def get_new_items(self, layer, features, filename, existing_items): """ get a list of new items (that have been selected by user) to be added to graph (if they do not already exist in the graph items :param layer: selected Qgis layer to be added :param features: selected Qgis features to be added :param filename: selected Qgis features to be added :param existing_items: selected Qgis features to be added :return: new_items (list) """ new_items = [] for feature in features: new_idx = self.get_feature_index(layer, feature) new_object_name = self.get_object_name(layer, feature) # check if object not already exist if (layer.name() + "_" + str(new_idx)) not in existing_items: item = { "object_type": layer.name(), "object_id": new_idx, "object_name": new_object_name, "file_path": filename, } new_items.append(item) return new_items def add_objects(self, layer, features): """ :param layer: layer of features :param features: Qgis layer features to be added :return: boolean: new objects are added """ # Get the active database as URI, conn_info is something like: # u"dbname='/home/jackieleng/git/threedi-turtle/var/models/ # DS_152_1D_totaal_bergingsbak/results/ # DS_152_1D_totaal_bergingsbak_result.sqlite'" if layer.name() not in ("flowlines", "nodes", "pumplines"): msg = """Please select results from either the 'flowlines', 'nodes' or 'pumplines' layer.""" messagebar_message("Info", msg, level=0, duration=5) return conn_info = QgsDataSourceUri( layer.dataProvider().dataSourceUri()).connectionInfo() try: filename = conn_info.split("'")[1] except IndexError: raise RuntimeError( "Active database (%s) doesn't look like an sqlite filename" % conn_info) # get attribute information from selected layers existing_items = [ "%s_%s" % (item.object_type.value, str(item.object_id.value)) for item in self.model.rows ] items = self.get_new_items(layer, features, filename, existing_items) if len(items) > 20: msg = ("%i new objects selected. Adding those to the plot can " "take a while. Do you want to continue?" % len(items)) reply = QMessageBox.question(self, "Add objects", msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.No: return False self.model.insertRows(items) msg = "%i new objects added to plot " % len(items) skipped_items = len(features) - len(items) if skipped_items > 0: msg += "(skipped %s already present objects)" % skipped_items statusbar_message(msg) return True def remove_objects_table(self): """ removes selected objects from table :return: """ selection_model = self.location_timeseries_table.selectionModel() # get unique rows in selected fields rows = set( [index.row() for index in selection_model.selectedIndexes()]) for row in reversed(sorted(rows)): self.model.removeRows(row, 1)