class ShapeTool(QgsMapTool): #signal emitted when the mouse is clicked. This indicates that the tool finished its job toolFinished = pyqtSignal() def __init__(self, canvas, geometryType, param, type, color = QColor( 254, 178, 76, 63 )): """ Constructor """ QgsMapTool.__init__(self, canvas) self.canvas = canvas self.active = False self.geometryType = self.tr(geometryType) self.param=param self.type=type self.cursor=None self.rubberBand = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry) self.setColor(color) self.reset() self.rotAngle = 0 self.currentCentroid = None self.rotate = False def setColor(self, mFillColor): """ Adjusting the color to create the rubber band """ self.rubberBand.setColor(mFillColor) self.rubberBand.setWidth(1) def reset(self): """ Resetting the rubber band """ self.startPoint = self.endPoint = None self.isEmittingPoint = False try: self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) except: pass def rotateRect(self, centroid, e): """ Calculates the angle for the rotation. """ item_position = self.canvas.mapToGlobal(e.pos()) c = self.toCanvasCoordinates(centroid) c = self.canvas.mapToGlobal(c) rotAngle = pi - atan2( item_position.y() - c.y(), item_position.x() - c.x()) return rotAngle def canvasPressEvent(self, e): """ When the canvas is pressed the tool finishes its job """ # enforce mouse restoring if clicked right after rotation QApplication.restoreOverrideCursor() self.canvas.unsetMapTool(self) self.toolFinished.emit() def _baseDistanceInMeters(self): """ Calculates the distance in meters of 2 points 1 unit map away on current canvas CRS. :return: (float) distance in meters between two points 1 map unit apart from each other. """ source_crs = self.canvas.mapSettings().destinationCrs() dest_crs = QgsCoordinateReferenceSystem(3857) tr = QgsCoordinateTransform( source_crs, dest_crs, QgsCoordinateTransformContext()) p1t = QgsGeometry().fromPointXY(QgsPointXY(1, 0)) p1t.transform(tr) p2t = QgsGeometry().fromPointXY(QgsPointXY(0, 0)) p2t.transform(tr) return QgsDistanceArea().measureLine(p1t.asPoint(), p2t.asPoint()) def getAdjustedSize(self, size): """ If map unit is not metric, the figure to be drawn needs to have its size adjusted. This is necessary because input parameters are designed to be meters on tool's GUI. :param size: (float) tool's radius/length reference size in meters. :return: (float) """ source_crs = self.canvas.mapSettings().destinationCrs() if source_crs.mapUnits() != QgsUnitTypes.DistanceMeters: return size / self._baseDistanceInMeters() return size def canvasMoveEvent(self, e): """ Deals with mouse move event to update the rubber band position in the canvas """ ctrlIsHeld = QApplication.keyboardModifiers() == Qt2.ControlModifier if e.button() != None and not ctrlIsHeld: if self.rotate: # change rotate status self.rotate = False QApplication.restoreOverrideCursor() self.endPoint = self.toMapCoordinates( e.pos() ) elif e.button() != None and ctrlIsHeld \ and self.geometryType == self.tr(u"Square"): # calculate angle between mouse and last rubberband centroid before holding control self.rotAngle = self.rotateRect(self.currentCentroid, e) if not self.rotate: # only override mouse if it is not overriden already QApplication.setOverrideCursor(QCursor(Qt2.BlankCursor)) self.rotate = True if self.geometryType == self.tr(u"Circle"): self.showCircle(self.endPoint) elif self.geometryType == self.tr(u"Square"): self.showRect(self.endPoint, sqrt(self.param)/2, self.rotAngle) def showCircle(self, startPoint): """ Draws a circle in the canvas """ nPoints = 50 x = startPoint.x() y = startPoint.y() if self.type == self.tr('distance'): r = self.getAdjustedSize(self.param) self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) for itheta in range(nPoints+1): theta = itheta*(2.0*pi/nPoints) self.rubberBand.addPoint(QgsPointXY(x+r*cos(theta), y+r*sin(theta))) self.rubberBand.show() else: r = self.getAdjustedSize(sqrt(self.param/pi)) self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) for itheta in range(nPoints+1): theta = itheta*(2.0*pi/nPoints) self.rubberBand.addPoint(QgsPointXY(x+r*cos(theta), y+r*sin(theta))) self.rubberBand.show() def showRect(self, startPoint, param, rotAngle=0): """ Draws a rectangle in the canvas """ self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) x = startPoint.x() # center point x y = startPoint.y() # center point y # rotation angle is always applied in reference to center point # to avoid unnecessary calculations c = cos(rotAngle) s = sin(rotAngle) # translating coordinate system to rubberband centroid param = self.getAdjustedSize(param) for posx, posy in ((-1, -1), (-1, 1), (1, 1), (1, -1)): px = posx * param py = posy * param pnt = QgsPointXY(px * c - py * s + x, py * c + px * s + y) self.rubberBand.addPoint(pnt, False) self.rubberBand.setVisible(True) self.rubberBand.updateRect() self.rubberBand.update() self.rubberBand.show() self.currentCentroid = startPoint def deactivate(self): """ Deactivates the tool and hides the rubber band """ self.rubberBand.hide() QgsMapTool.deactivate(self) # restore mouse in case tool is disabled right after rotation QApplication.restoreOverrideCursor() def activate(self): """ Activates the tool """ QgsMapTool.activate(self)
class DiscoveryPlugin: def __init__(self, _iface): # Save reference to the QGIS interface self.iface = _iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # Variables to facilitate delayed queries and database connection management self.db_timer = QTimer() self.line_edit_timer = QTimer() self.line_edit_timer.setSingleShot(True) self.line_edit_timer.timeout.connect(self.reset_line_edit_after_move) self.next_query_time = None self.last_query_time = time.time() self.db_conn = None self.search_delay = 0.5 # s self.query_sql = '' self.query_text = '' self.query_dict = {} self.db_idle_time = 60.0 # s self.display_time = 5000 # ms self.bar_info_time = 30 # s self.search_results = [] self.tool_bar = None self.search_line_edit = None self.completer = None self.conn_info = {} self.marker = QgsVertexMarker(iface.mapCanvas()) self.marker.setIconSize(15) self.marker.setPenWidth(2) self.marker.setColor(QColor(226, 27, 28)) #51,160,44)) self.marker.setZValue(11) self.marker.setVisible(False) self.marker2 = QgsVertexMarker(iface.mapCanvas()) self.marker2.setIconSize(16) self.marker2.setPenWidth(4) self.marker2.setColor(QColor(255, 255, 255, 200)) self.marker2.setZValue(10) self.marker2.setVisible(False) self.is_displayed = False self.rubber_band = QgsRubberBand(iface.mapCanvas(), False) self.rubber_band.setVisible(False) self.rubber_band.setWidth(3) self.rubber_band.setStrokeColor(QColor(226, 27, 28)) self.rubber_band.setFillColor(QColor(226, 27, 28, 63)) def initGui(self): # Create a new toolbar self.tool_bar = self.iface.addToolBar('Discovery') self.tool_bar.setObjectName('Discovery_Plugin') # Create action that will start plugin configuration self.action_config = QAction( QIcon(os.path.join(self.plugin_dir, "discovery_logo.png")), u"Configure Discovery", self.tool_bar) self.action_config.triggered.connect(self.show_config_dialog) self.tool_bar.addAction(self.action_config) # Add combobox for configs self.config_combo = QComboBox() settings = QgsSettings() settings.beginGroup("/Discovery") config_list = settings.value("config_list") if config_list: for conf in config_list: self.config_combo.addItem(conf) elif settings.childGroups(): # support for prev version key = "Config1" config_list = [] config_list.append(key) settings.setValue("config_list", config_list) self.config_combo.addItem(key) settings.setValue(key + "data_type", settings.value("data_type")) settings.setValue(key + "file", settings.value("file")) settings.setValue(key + "connection", settings.value("connection")) settings.setValue(key + "schema", settings.value("schema")) settings.setValue(key + "table", settings.value("table")) settings.setValue(key + "search_column", settings.value("search_column")) settings.setValue(key + "echo_search_column", settings.value("echo_search_column")) settings.setValue(key + "display_columns", settings.value("display_columns")) settings.setValue(key + "geom_column", settings.value("geom_column")) settings.setValue(key + "scale_expr", settings.value("scale_expr")) settings.setValue(key + "bbox_expr", settings.value("bbox_expr")) delete_config_from_settings("", settings) self.tool_bar.addWidget(self.config_combo) # Add search edit box self.search_line_edit = QgsFilterLineEdit() self.search_line_edit.setPlaceholderText('Search for...') self.search_line_edit.setMaximumWidth(768) self.tool_bar.addWidget(self.search_line_edit) self.config_combo.currentIndexChanged.connect( self.change_configuration) # Set up the completer self.completer = QCompleter([]) # Initialise with en empty list self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setMaxVisibleItems(1000) self.completer.setModelSorting( QCompleter.UnsortedModel) # Sorting done in PostGIS self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion ) # Show all fetched possibilities self.completer.activated[QModelIndex].connect(self.on_result_selected) self.completer.highlighted[QModelIndex].connect( self.on_result_highlighted) self.search_line_edit.setCompleter(self.completer) # Connect any signals self.search_line_edit.textEdited.connect(self.on_search_text_changed) # Search results self.search_results = [] # Set up a timer to periodically perform db queries as required self.db_timer.timeout.connect(self.do_db_operations) self.db_timer.start(100) # Read config self.read_config(config_list[0] if config_list else "") self.locator_filter = locator_filter.DiscoveryLocatorFilter(self) self.iface.registerLocatorFilter(self.locator_filter) # Debug # import pydevd; pydevd.settrace('localhost', port=5678) def unload(self): # Stop timer self.db_timer.stop() # Disconnect any signals self.db_timer.timeout.disconnect(self.do_db_operations) self.completer.highlighted[QModelIndex].disconnect( self.on_result_highlighted) self.completer.activated[QModelIndex].disconnect( self.on_result_selected) self.search_line_edit.textEdited.disconnect( self.on_search_text_changed) # Remove the new toolbar self.tool_bar.clear() # Clear all actions self.iface.mainWindow().removeToolBar(self.tool_bar) self.iface.deregisterLocatorFilter(self.locator_filter) self.locator_filter = None def clear_suggestions(self): model = self.completer.model() model.setStringList([]) def on_search_text_changed(self, new_search_text): """ This function is called whenever the user modified the search text 1. Open a database connection 2. Make the query 3. Update the QStringListModel with these results 4. Store the other details in self.search_results """ self.query_text = new_search_text if len(new_search_text) < 3: # Clear any previous suggestions in case the user is 'backspacing' self.clear_suggestions() return if self.data_type == "postgres": query_text, query_dict = dbutils.get_search_sql( new_search_text, self.postgisgeomcolumn, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn, self.extra_expr_columns, self.postgisschema, self.postgistable) self.schedule_search(query_text, query_dict) elif self.data_type == "gpkg": query_text = (new_search_text, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn.split(","), self.extra_expr_columns, self.layer) self.schedule_search(query_text, None) elif self.data_type == "mssql": query_text = mssql_utils.get_search_sql( new_search_text, self.postgisgeomcolumn, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn, self.extra_expr_columns, self.postgisschema, self.postgistable) self.schedule_search(query_text, None) def do_db_operations(self): if self.next_query_time is not None and self.next_query_time < time.time( ): # It's time to run a query self.next_query_time = None # Prevent this query from being repeated self.last_query_time = time.time() self.perform_search() else: # We're not performing a query, close the db connection if it's been open for > 60s if time.time() > self.last_query_time + self.db_idle_time: self.db_conn = None def perform_search(self): db = self.get_db() self.search_results = [] suggestions = [] if self.data_type == "postgres": cur = db.cursor() try: cur.execute(self.query_sql, self.query_dict) except psycopg2.Error as e: err_info = "Failed to execute the search query. Please, check your settings. Error message:\n\n" err_info += f"{e.pgerror}" QMessageBox.critical(None, "Discovery", err_info) return result_set = cur.fetchall() elif self.data_type == "mssql": result_set = mssql_utils.execute(db, self.query_sql) elif self.data_type == "gpkg": result_set = gpkg_utils.search_gpkg(*self.query_sql) for row in result_set: geom, epsg, suggestion_text = row[0], row[1], row[2] extra_data = {} for idx, extra_col in enumerate(self.extra_expr_columns): extra_data[extra_col] = row[3 + idx] self.search_results.append( (geom, epsg, suggestion_text, extra_data)) suggestions.append(suggestion_text) model = self.completer.model() model.setStringList(suggestions) self.completer.complete() def schedule_search(self, query_text, query_dict): # Update the search text and the time after which the query should be executed self.query_sql = query_text self.query_dict = query_dict self.next_query_time = time.time() + self.search_delay def show_bar_info(self, info_text): """Optional show info bar message with selected result information""" self.iface.messageBar().clearWidgets() if self.bar_info_time: self.iface.messageBar().pushMessage("Discovery", info_text, level=Qgis.Info, duration=self.bar_info_time) def on_result_selected(self, result_index): # What to do when the user makes a selection self.select_result(self.search_results[result_index.row()]) def select_result(self, result_data): geometry_text, src_epsg, suggestion_text, extra_data = result_data location_geom = QgsGeometry.fromWkt(geometry_text) canvas = self.iface.mapCanvas() dst_srid = canvas.mapSettings().destinationCrs().authid() transform = QgsCoordinateTransform( QgsCoordinateReferenceSystem(src_epsg), QgsCoordinateReferenceSystem(dst_srid), canvas.mapSettings().transformContext()) # Ensure the geometry from the DB is reprojected to the same SRID as the map canvas location_geom.transform(transform) location_centroid = location_geom.centroid().asPoint() # show temporary marker if location_geom.type() == QgsWkbTypes.PointGeometry: self.show_marker(location_centroid) elif location_geom.type() == QgsWkbTypes.LineGeometry or \ location_geom.type() == QgsWkbTypes.PolygonGeometry: self.show_line_rubber_band(location_geom) else: #unsupported geometry type pass # Adjust map canvas extent zoom_method = 'Move and Zoom' if zoom_method == 'Move and Zoom': # with higher priority try to use exact bounding box to zoom to features (if provided) bbox_str = eval_expression(self.bbox_expr, extra_data) rect = bbox_str_to_rectangle(bbox_str) if rect is not None: # transform the rectangle in case of OTF projection rect = transform.transformBoundingBox(rect) else: # bbox is not available - so let's just use defined scale # compute target scale. If the result is 2000 this means the target scale is 1:2000 rect = location_geom.boundingBox() if rect.isEmpty(): scale_denom = eval_expression(self.scale_expr, extra_data, default=2000.) rect = canvas.mapSettings().extent() rect.scale(scale_denom / canvas.scale(), location_centroid) else: # enlarge geom bbox to have some margin rect.scale(1.2) canvas.setExtent(rect) elif zoom_method == 'Move': current_extent = QgsGeometry.fromRect( self.iface.mapCanvas().extent()) dx = location_centroid.x() - location_centroid.x() dy = location_centroid.y() - location_centroid.y() current_extent.translate(dx, dy) canvas.setExtent(current_extent.boundingBox()) canvas.refresh() self.line_edit_timer.start(0) if self.info_to_clipboard: QApplication.clipboard().setText(suggestion_text) suggestion_text += ' (copied to clipboard)' self.show_bar_info(suggestion_text) def on_result_highlighted(self, result_idx): self.line_edit_timer.start(0) def reset_line_edit_after_move(self): self.search_line_edit.setText(self.query_text) def get_db(self): # Create a new new connection if required if self.db_conn is None: if self.data_type == "postgres": self.db_conn = dbutils.get_connection(self.conn_info) elif self.data_type == "mssql": self.db_conn = mssql_utils.get_mssql_conn(self.conn_info) return self.db_conn def change_configuration(self): self.search_line_edit.setText("") self.line_edit_timer.start(0) self.read_config(self.config_combo.currentText()) def read_config(self, key=""): # the following code reads the configuration file which setups the plugin to search in the correct database, # table and method settings = QgsSettings() settings.beginGroup("/Discovery") connection = settings.value(key + "connection", "", type=str) self.data_type = settings.value(key + "data_type", "", type=str) self.file = settings.value(key + "file", "", type=str) self.postgisschema = settings.value(key + "schema", "", type=str) self.postgistable = settings.value(key + "table", "", type=str) self.postgissearchcolumn = settings.value(key + "search_column", "", type=str) self.echosearchcolumn = settings.value(key + "echo_search_column", True, type=bool) self.postgisdisplaycolumn = settings.value(key + "display_columns", "", type=str) self.postgisgeomcolumn = settings.value(key + "geom_column", "", type=str) if settings.value("marker_time_enabled", True, type=bool): self.display_time = settings.value("marker_time", 5000, type=int) else: self.display_time = -1 if settings.value("bar_info_time_enabled", True, type=bool): self.bar_info_time = settings.value("bar_info_time", 30, type=int) else: self.bar_info_time = 0 self.info_to_clipboard = settings.value("info_to_clipboard", True, type=bool) scale_expr = settings.value(key + "scale_expr", "", type=str) bbox_expr = settings.value(key + "bbox_expr", "", type=str) if self.is_displayed: self.hide_marker() self.hide_rubber_band() self.is_displayed = False self.make_enabled(False) # assume the config is invalid first self.db_conn = None if self.data_type == "postgres": self.conn_info = dbutils.get_postgres_conn_info(connection) self.layer = None if len(connection) == 0 or len(self.postgisschema) == 0 or len(self.postgistable) == 0 or \ len(self.postgissearchcolumn) == 0 or len(self.postgisgeomcolumn) == 0: return if len(self.conn_info) == 0: iface.messageBar().pushMessage( "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical) return if self.data_type == "mssql": self.conn_info = mssql_utils.get_mssql_conn_info(connection) self.layer = None if len(connection) == 0 or len(self.postgisschema) == 0 or len(self.postgistable) == 0 or \ len(self.postgissearchcolumn) == 0 or len(self.postgisgeomcolumn) == 0: return if len(self.conn_info) == 0: iface.messageBar().pushMessage( "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical) return elif self.data_type == "gpkg": self.layer = QgsVectorLayer( self.file + '|layername=' + self.postgistable, self.postgistable, 'ogr') self.conn_info = None self.extra_expr_columns = [] self.scale_expr = None self.bbox_expr = None self.make_enabled(True) # optional scale expression when zooming in to results if len(scale_expr) != 0: expr = QgsExpression(scale_expr) if expr.hasParserError(): iface.messageBar().pushMessage("Discovery", "Invalid scale expression: " + expr.parserErrorString(), level=Qgis.Warning) else: self.scale_expr = scale_expr self.extra_expr_columns += expr.referencedColumns() # optional bbox expression when zooming in to results if len(bbox_expr) != 0: expr = QgsExpression(bbox_expr) if expr.hasParserError(): iface.messageBar().pushMessage("Discovery", "Invalid bbox expression: " + expr.parserErrorString(), level=Qgis.Warning) else: self.bbox_expr = bbox_expr self.extra_expr_columns += expr.referencedColumns() def show_config_dialog(self): dlg = config_dialog.ConfigDialog() if (self.config_combo.currentIndex() >= 0): dlg.configOptions.setCurrentIndex(self.config_combo.currentIndex()) if dlg.exec_(): dlg.write_config() self.config_combo.clear() for key in [ dlg.configOptions.itemText(i) for i in range(dlg.configOptions.count()) ]: self.config_combo.addItem(key) self.config_combo.setCurrentIndex(dlg.configOptions.currentIndex()) self.change_configuration() def make_enabled(self, enabled): self.search_line_edit.setEnabled(enabled) self.search_line_edit.setPlaceholderText( "Search for..." if enabled else "Search disabled: check configuration") def show_marker(self, point): for m in [self.marker, self.marker2]: m.setCenter(point) m.setOpacity(1.0) m.setVisible(True) if self.display_time == -1: self.is_displayed = True else: QTimer.singleShot(self.display_time, self.hide_marker) def hide_marker(self): opacity = self.marker.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.marker.setOpacity(opacity) self.marker2.setOpacity(opacity) QTimer.singleShot(100, self.hide_marker) else: self.marker.setVisible(False) self.marker2.setVisible(False) def show_line_rubber_band(self, geom): self.rubber_band.reset(geom.type()) self.rubber_band.setToGeometry(geom, None) self.rubber_band.setVisible(True) self.rubber_band.setOpacity(1.0) self.rubber_band.show() if self.display_time == -1: self.is_displayed = True else: QTimer.singleShot(self.display_time, self.hide_rubber_band) pass def hide_rubber_band(self): opacity = self.rubber_band.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.rubber_band.setOpacity(opacity) QTimer.singleShot(100, self.hide_rubber_band) else: self.rubber_band.setVisible(False) self.rubber_band.hide()
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)