def toSearchRect(self, point): size = 20 rect = QRectF() rect.setLeft(point.x() - size) rect.setRight(point.x() + size) rect.setTop(point.y() - size) rect.setBottom(point.y() + size) transform = self.canvas.getCoordinateTransform() ll = transform.toMapCoordinates(rect.left(), rect.bottom()) ur = transform.toMapCoordinates(rect.right(), rect.top()) rect = QgsRectangle(ur, ll) return rect
class PlotItem(LogItem): # emitted when the style is updated style_updated = pyqtSignal() def __init__(self, size=QSizeF(400, 200), render_type=POINT_RENDERER, x_orientation=ORIENTATION_LEFT_TO_RIGHT, y_orientation=ORIENTATION_UPWARD, allow_mouse_translation=False, allow_wheel_zoom=False, symbology=None, parent=None): """ Parameters ---------- size: QSize Size of the item render_type: Literal[POINT_RENDERER, LINE_RENDERER, POLYGON_RENDERER] Type of renderer x_orientation: Literal[ORIENTATION_LEFT_TO_RIGHT, ORIENTATION_RIGHT_TO_LEFT] y_orientation: Literal[ORIENTATION_UPWARD, ORIENTATION_DOWNWARD] allow_mouse_translation: bool Allow the user to translate the item with the mouse (??) allow_wheel_zoom: bool Allow the user to zoom with the mouse wheel symbology: QDomDocument QGIS symbology to use for the renderer parent: QObject """ LogItem.__init__(self, parent) self.__item_size = size self.__data_rect = None self.__data = None self.__delta = None self.__x_orientation = x_orientation self.__y_orientation = y_orientation # origin point of the graph translation, if any self.__translation_orig = None self.__render_type = render_type # type: Literal[POINT_RENDERER, LINE_RENDERER, POLYGON_RENDERER] self.__allow_mouse_translation = allow_mouse_translation self.__allow_wheel_zoom = allow_wheel_zoom self.__layer = None self.__default_renderers = [QgsFeatureRenderer.defaultRenderer(POINT_RENDERER), QgsFeatureRenderer.defaultRenderer(LINE_RENDERER), QgsFeatureRenderer.defaultRenderer(POLYGON_RENDERER)] symbol = self.__default_renderers[1].symbol() symbol.setWidth(1.0) symbol = self.__default_renderers[0].symbol() symbol.setSize(5.0) symbol = self.__default_renderers[2].symbol() symbol.symbolLayers()[0].setStrokeWidth(1.0) if not symbology: self.__renderer = self.__default_renderers[self.__render_type] else: self.__renderer = QgsFeatureRenderer.load(symbology.documentElement(), QgsReadWriteContext()) # index of the current point to label self.__old_point_to_label = None self.__point_to_label = None def boundingRect(self): return QRectF(0, 0, self.__item_size.width(), self.__item_size.height()) def height(self): return self.__item_size.height() def set_height(self, height): self.__item_size.setHeight(height) def width(self): return self.__item_size.width() def set_width(self, width): self.__item_size.setWidth(width) def set_data_window(self, window): """window: QRectF""" self.__data_rect = window def min_depth(self): if self.__data_rect is None: return None return self.__data_rect.x() def max_depth(self): if self.__data_rect is None: return None return (self.__data_rect.x() + self.__data_rect.width()) def set_min_depth(self, min_depth): if self.__data_rect is not None: self.__data_rect.setX(min_depth) def set_max_depth(self, max_depth): if self.__data_rect is not None: w = max_depth - self.__data_rect.x() self.__data_rect.setWidth(w) def layer(self): return self.__layer def set_layer(self, layer): self.__layer = layer def data_window(self): return self.__data_rect def set_data(self, x_values, y_values): self.__x_values = x_values self.__y_values = y_values if len(self.__x_values) != len(self.__y_values): raise ValueError("X and Y array has different length : " "{} != {}".format(len(self.__x_values), len(self.__y_values))) # Remove None values for i in reversed(range(len(self.__y_values))): if (self.__y_values[i] is None or self.__x_values[i] is None or math.isnan(self.__y_values[i]) or math.isnan(self.__x_values[i])): self.__y_values.pop(i) self.__x_values.pop(i) if not self.__x_values: return # Initialize data rect to display all data # with a 20% buffer around Y values min_x = min(self.__x_values) max_x = max(self.__x_values) min_y = min(self.__y_values) max_y = max(self.__y_values) if max_y == 0.0: max_y = 1.0 h = max_y - min_y min_y -= h * 0.1 max_y += h * 0.1 self.__data_rect = QRectF( min_x, min_y, max_x-min_x, max_y-min_y) def renderer(self): return self.__renderer def paint(self, painter, option, widget): self.draw_background(painter) if self.__data_rect is None: return # Look for the first and last X values that fit our rendering rect imin_x = bisect.bisect_left(self.__x_values, self.__data_rect.x()) imax_x = bisect.bisect_right(self.__x_values, self.__data_rect.right()) # For lines and polygons, retain also one value before the min and one after the max # so that lines do not appear truncated # Do this only if we have at least one point to render within out rect if imin_x > 0 and imin_x < len(self.__x_values) and self.__x_values[imin_x] >= self.__data_rect.x(): # FIXME add a test to avoid adding a point too "far away" ? imin_x -= 1 if imax_x < len(self.__x_values) - 1 and self.__x_values[imax_x] <= self.__data_rect.right(): # FIXME add a test to avoid adding a point too "far away" ? imax_x += 1 x_values_slice = np.array(self.__x_values[imin_x:imax_x]) y_values_slice = np.array(self.__y_values[imin_x:imax_x]) if len(x_values_slice) == 0: return # filter points that are not None (nan in numpy arrays) n_points = len(x_values_slice) if self.__x_orientation == ORIENTATION_LEFT_TO_RIGHT and self.__y_orientation == ORIENTATION_UPWARD: if self.__data_rect.width() > 0: rw = float(self.__item_size.width()) / self.__data_rect.width() else: rw = float(self.__item_size.width()) if self.__data_rect.height() > 0: rh = float(self.__item_size.height()) / self.__data_rect.height() else: rh = float(self.__item_size.height()) xx = (x_values_slice - self.__data_rect.x()) * rw yy = (y_values_slice - self.__data_rect.y()) * rh elif self.__x_orientation == ORIENTATION_DOWNWARD and self.__y_orientation == ORIENTATION_LEFT_TO_RIGHT: if self.__data_rect.width() > 0: rw = float(self.__item_size.height()) / self.__data_rect.width() else: rw = float(self.__item_size.height()) if self.__data_rect.height() > 0: rh = float(self.__item_size.width()) / self.__data_rect.height() else: rh = float(self.__item_size.width()) xx = (y_values_slice - self.__data_rect.y()) * rh yy = self.__item_size.height() - (x_values_slice - self.__data_rect.x()) * rw if self.__render_type == LINE_RENDERER: # WKB structure of a linestring # # 01 : endianness # 02 00 00 00 : WKB type (linestring) # nn nn nn nn : number of points (int32) # Then, for each point: # xx xx xx xx xx xx xx xx : X coordinate (float64) # yy yy yy yy yy yy yy yy : Y coordinate (float64) wkb = np.zeros(8*2*n_points+9, dtype='uint8') wkb[0] = 1 # wkb endianness wkb[1] = 2 # linestring size_view = np.ndarray(buffer=wkb, dtype='int32', offset=5, shape=(1,)) size_view[0] = n_points coords_view = np.ndarray(buffer=wkb, dtype='float64', offset=9, shape=(n_points,2)) coords_view[:,0] = xx[:] coords_view[:,1] = yy[:] elif self.__render_type == POINT_RENDERER: # WKB structure of a multipoint # # 01 : endianness # 04 00 00 00 : WKB type (multipoint) # nn nn nn nn : number of points (int32) # Then, for each point: # 01 : endianness # 01 00 00 00 : WKB type (point) # xx xx xx xx xx xx xx xx : X coordinate (float64) # yy yy yy yy yy yy yy yy : Y coordinate (float64) wkb = np.zeros((8*2+5)*n_points+9, dtype='uint8') wkb[0] = 1 # wkb endianness wkb[1] = 4 # multipoint size_view = np.ndarray(buffer=wkb, dtype='int32', offset=5, shape=(1,)) size_view[0] = n_points coords_view = np.ndarray(buffer=wkb, dtype='float64', offset=9+5, shape=(n_points,2), strides=(16+5,8)) coords_view[:,0] = xx[:] coords_view[:,1] = yy[:] # header of each point h_view = np.ndarray(buffer=wkb, dtype='uint8', offset=9, shape=(n_points,2), strides=(16+5,1)) h_view[:,0] = 1 # endianness h_view[:,1] = 1 # point elif self.__render_type == POLYGON_RENDERER: # WKB structure of a polygon # # 01 : endianness # 03 00 00 00 : WKB type (polygon) # 01 00 00 00 : Number of rings (always 1 here) # nn nn nn nn : number of points (int32) # Then, for each point: # xx xx xx xx xx xx xx xx : X coordinate (float64) # yy yy yy yy yy yy yy yy : Y coordinate (float64) # # We add two additional points to close the polygon wkb = np.zeros(8*2*(n_points+2)+9+4, dtype='uint8') wkb[0] = 1 # wkb endianness wkb[1] = 3 # polygon wkb[5] = 1 # number of rings size_view = np.ndarray(buffer=wkb, dtype='int32', offset=9, shape=(1,)) size_view[0] = n_points+2 coords_view = np.ndarray(buffer=wkb, dtype='float64', offset=9+4, shape=(n_points,2)) coords_view[:,0] = xx[:] coords_view[:,1] = yy[:] # two extra points extra_coords = np.ndarray(buffer=wkb, dtype='float64', offset=8*2*n_points+9+4, shape=(2,2)) if self.__x_orientation == ORIENTATION_LEFT_TO_RIGHT and self.__y_orientation == ORIENTATION_UPWARD: extra_coords[0,0] = coords_view[-1,0] extra_coords[0,1] = 0.0 extra_coords[1,0] = coords_view[0,0] extra_coords[1,1] = 0.0 elif self.__x_orientation == ORIENTATION_DOWNWARD and self.__y_orientation == ORIENTATION_LEFT_TO_RIGHT: extra_coords[0,0] = 0.0 extra_coords[0,1] = coords_view[-1,1] extra_coords[1,0] = 0.0 extra_coords[1,1] = coords_view[0,1] # build a geometry from the WKB # since numpy arrays have buffer protocol, sip is able to read it geom = QgsGeometry() geom.fromWkb(wkb.tobytes()) painter.setClipRect(0, 0, self.__item_size.width(), self.__item_size.height()) fields = QgsFields() #fields.append(QgsField("", QVariant.String)) feature = QgsFeature(fields, 1) feature.setGeometry(geom) context = qgis_render_context(painter, self.__item_size.width(), self.__item_size.height()) context.setExtent(QgsRectangle(0, 1, self.__item_size.width(), self.__item_size.height())) self.__renderer.startRender(context, fields) self.__renderer.renderFeature(feature, context) self.__renderer.stopRender(context) if self.__point_to_label is not None: i = self.__point_to_label x, y = self.__x_values[i], self.__y_values[i] if self.__x_orientation == ORIENTATION_LEFT_TO_RIGHT and self.__y_orientation == ORIENTATION_UPWARD: px = (x - self.__data_rect.x()) * rw py = self.__item_size.height() - (y - self.__data_rect.y()) * rh elif self.__x_orientation == ORIENTATION_DOWNWARD and self.__y_orientation == ORIENTATION_LEFT_TO_RIGHT: px = (y - self.__data_rect.y()) * rh py = (x - self.__data_rect.x()) * rw painter.drawLine(px-5, py, px+5, py) painter.drawLine(px, py-5, px, py+5) def mouseMoveEvent(self, event): if not self.__x_values: return if self.__x_orientation == ORIENTATION_LEFT_TO_RIGHT and self.__y_orientation == ORIENTATION_UPWARD: xx = (event.scenePos().x() - self.pos().x()) / self.width() * self.__data_rect.width() + self.__data_rect.x() elif self.__x_orientation == ORIENTATION_DOWNWARD and self.__y_orientation == ORIENTATION_LEFT_TO_RIGHT: xx = (event.scenePos().y() - self.pos().y()) / self.height() * self.__data_rect.width() + self.__data_rect.x() i = bisect.bisect_left(self.__x_values, xx) if i >= 0 and i < len(self.__x_values): # switch the attached point when we are between two points if i > 0 and (xx - self.__x_values[i-1]) < (self.__x_values[i] - xx): i -= 1 self.__point_to_label = i else: self.__point_to_label = None if self.__point_to_label != self.__old_point_to_label: self.update() if self.__point_to_label is not None: x, y = self.__x_values[i], self.__y_values[i] if self.__x_orientation == ORIENTATION_LEFT_TO_RIGHT and self.__y_orientation == ORIENTATION_UPWARD: dt = datetime.fromtimestamp(x, UTC()) txt = "Time: {} Value: {}".format(dt.strftime("%x %X"), y) elif self.__x_orientation == ORIENTATION_DOWNWARD and self.__y_orientation == ORIENTATION_LEFT_TO_RIGHT: txt = "Depth: {} Value: {}".format(x, y) self.tooltipRequested.emit(txt) self.__old_point_to_label = self.__point_to_label def edit_style(self): from qgis.gui import QgsSingleSymbolRendererWidget from qgis.core import QgsStyle style = QgsStyle() sw = QStackedWidget() sw.addWidget for i in range(3): if self.__renderer and i == self.__render_type: w = QgsSingleSymbolRendererWidget(self.__layer, style, self.__renderer) else: w = QgsSingleSymbolRendererWidget(self.__layer, style, self.__default_renderers[i]) sw.addWidget(w) combo = QComboBox() combo.addItem("Points") combo.addItem("Line") combo.addItem("Polygon") combo.currentIndexChanged[int].connect(sw.setCurrentIndex) combo.setCurrentIndex(self.__render_type) dlg = QDialog() vbox = QVBoxLayout() btn = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) btn.accepted.connect(dlg.accept) btn.rejected.connect(dlg.reject) vbox.addWidget(combo) vbox.addWidget(sw) vbox.addWidget(btn) dlg.setLayout(vbox) dlg.resize(800, 600) r = dlg.exec_() if r == QDialog.Accepted: self.__render_type = combo.currentIndex() self.__renderer = sw.currentWidget().renderer().clone() self.update() self.style_updated.emit() def qgis_style(self): """Returns the current style, as a QDomDocument""" from PyQt5.QtXml import QDomDocument from qgis.core import QgsReadWriteContext doc = QDomDocument() elt = self.__renderer.save(doc, QgsReadWriteContext()) doc.appendChild(elt) return (doc, self.__render_type)
def paint_MODEL_SPACE_2D(self): rect = self.rect() painter = QPainter(self) painter.fillRect(rect, self.plugIn.canvas.canvasColor()) painter.setRenderHint(QPainter.Antialiasing) # PICKBOX x1 = rect.width() / 3 y1 = rect.height() - rect.height() / 3 color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "PICKBOXCOLOR"))) pickSize = 5 painter.setPen(QPen(color, 1, Qt.SolidLine)) painter.drawRect(x1 - pickSize, y1 - pickSize, 2 * pickSize, 2 * pickSize) # CROSSHAIRS color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "CURSORCOLOR"))) cursorSize = 20 painter.setPen(QPen(color, 1, Qt.SolidLine)) painter.drawLine(x1 - pickSize, y1, x1 - pickSize - cursorSize, y1) painter.drawLine(x1 + pickSize, y1, x1 + pickSize + cursorSize, y1) painter.drawLine(x1, y1 - pickSize, x1, y1 - pickSize - cursorSize) painter.drawLine(x1, y1 + pickSize, x1, y1 + pickSize + cursorSize) # AUTOTRECK_VECTOR x1 = rect.width() / 3 color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "AUTOTRECKINGVECTORCOLOR"))) painter.setPen(QPen(color, 1, Qt.DashLine)) painter.drawLine(x1, 0, x1, rect.height()) painter.drawLine(x1 + rect.height() * 2 / 3, 0, x1 - rect.height() / 3, rect.height()) # AUTOSNAP x1 = rect.width() / 3 y1 = rect.height() / 3 color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "AUTOSNAPCOLOR"))) pickSize = 5 painter.setPen(QPen(color, 2, Qt.SolidLine)) painter.drawRect(x1 - pickSize, y1 - pickSize, 2 * pickSize, 2 * pickSize) # DYNAMIC INPUT x1 = rect.width() / 3 y1 = rect.height() - rect.height() / 3 cursorSize = 20 fMetrics = painter.fontMetrics() msg1 = "12.3456" sz1 = fMetrics.size(Qt.TextSingleLine, msg1 + "__") dynInputRect1 = QRectF(x1 + cursorSize, y1 + cursorSize, sz1.width(), sz1.height() + 2) msg2 = "78.9012" sz2 = fMetrics.size(Qt.TextSingleLine, msg2 + "__") dynInputRect2 = QRectF(dynInputRect1.right() + sz1.height() / 3, dynInputRect1.top(), sz2.width(), sz2.height() + 2) # DYNAMIC INPUT COMMAND DESCR BACKGROUND color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "DYNEDITBACKCOLOR"))) painter.fillRect(dynInputRect1, color) painter.fillRect(dynInputRect2, color) # DYNAMIC INPUT COMMAND DESCR BORDER color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "DYNEDITBORDERCOLOR"))) painter.setPen(QPen(color, 1, Qt.SolidLine)) painter.drawRect(dynInputRect1) painter.drawRect(dynInputRect2) # DYNAMIC INPUT COMMAND DESCR FOREGROUND color = QColor( self.tempQadVariables.get( QadMsg.translate("Environment variables", "DYNEDITFORECOLOR"))) painter.setPen(QPen(color, 1, Qt.SolidLine)) painter.drawText(dynInputRect1, msg1) painter.drawText(dynInputRect2, msg2)