def __iniGraphicsSystem(self): ##初始化 graphics View系统 rect = QRectF(-200, -100, 400, 200) self.scene = QGraphicsScene(rect) #scene逻辑坐标系定义 self.view.setScene(self.scene) ## 画一个矩形框,大小等于scene item = QGraphicsRectItem(rect) #矩形框正好等于scene的大小 item.setFlag(QGraphicsItem.ItemIsSelectable) #可选, item.setFlag(QGraphicsItem.ItemIsFocusable) #可以有焦点 pen = QPen() pen.setWidth(2) item.setPen(pen) self.scene.addItem(item) ##一个位于scene中心的椭圆,测试局部坐标 #矩形框内创建椭圆,绘图项的局部坐标,左上角(-100,-50),宽200,高100 item2 = QGraphicsEllipseItem(-100, -50, 200, 100) item2.setPos(0, 0) item2.setBrush(QBrush(Qt.blue)) item2.setFlag(QGraphicsItem.ItemIsSelectable) #可选, item2.setFlag(QGraphicsItem.ItemIsFocusable) #可以有焦点 item2.setFlag(QGraphicsItem.ItemIsMovable) #可移动 self.scene.addItem(item2) ##一个圆,中心位于scene的边缘 item3 = QGraphicsEllipseItem(-50, -50, 100, 100) #矩形框内创建椭圆,绘图项的局部坐标 item3.setPos(rect.right(), rect.bottom()) item3.setBrush(QBrush(Qt.red)) item3.setFlag(QGraphicsItem.ItemIsSelectable) #可选, item3.setFlag(QGraphicsItem.ItemIsFocusable) #可以有焦点 item3.setFlag(QGraphicsItem.ItemIsMovable) #可移动 self.scene.addItem(item3) self.scene.clearSelection()
def __init__(self, part_instance: ObjectInstance, viewroot: GridRootItemT): """Summary Args: part_instance: ``ObjectInstance`` of the ``Part`` viewroot: ``GridRootItem`` parent: Default is ``None`` """ super(GridNucleicAcidPartItem, self).__init__(part_instance, viewroot) self._getActiveTool = viewroot.manager.activeToolGetter m_p = self._model_part self._controller = NucleicAcidPartItemController(self, m_p) self.scale_factor: float = self._RADIUS / m_p.radius() self.active_virtual_helix_item: GridVirtualHelixItem = None self.prexover_manager = PreXoverManager(self) self.hide() # hide while until after attemptResize() to avoid flicker # set this to a token value self._rect: QRectF = QRectF(0., 0., 1000., 1000.) self.boundRectToModel() self.setPen(getNoPen()) self.setRect(self._rect) self.setAcceptHoverEvents(True) # Cache of VHs that were active as of last call to activeGridChanged # If None, all grids will be redrawn and the cache will be filled. # Connect destructor. This is for removing a part from scenes. # initialize the NucleicAcidPartItem with an empty set of old coords self.setZValue(styles.ZPARTITEM) outline = QGraphicsRectItem(self) self.outline: QGraphicsRectItem = outline o_rect = self._configureOutline(outline) outline.setFlag(QGraphicsItem.ItemStacksBehindParent) outline.setZValue(styles.ZDESELECTOR) model_color = m_p.getColor() outline.setPen(getPenObj(model_color, _DEFAULT_WIDTH)) GC_SIZE = 10 self.grab_cornerTL: GrabCornerItem = GrabCornerItem( GC_SIZE, model_color, True, self) self.grab_cornerTL.setTopLeft(o_rect.topLeft()) self.grab_cornerBR: GrabCornerItem = GrabCornerItem( GC_SIZE, model_color, True, self) self.grab_cornerBR.setBottomRight(o_rect.bottomRight()) self.griditem: GridItem = GridItem(self, self._model_props['grid_type']) self.griditem.setZValue(1) self.grab_cornerTL.setZValue(2) self.grab_cornerBR.setZValue(2) # select upon creation for part in m_p.document().children(): if part is m_p: part.setSelected(True) else: part.setSelected(False) self.show()
def __init__(self, *args): super(Demo, self).__init__(*args) loadUi('MainWindow.ui', self) scene = QGraphicsScene() rectItem = QGraphicsRectItem(QRectF(0, 0, 320, 240)) rectItem.setBrush(Qt.red) #rectItem.setPen(Qt.NoPen) rectItem.setFlag(QGraphicsItem.ItemIsMovable) scene.addItem(rectItem) ellipseItem = QGraphicsEllipseItem(QRectF(0, 0, 200, 200)) ellipseItem.setBrush(Qt.blue) #ellipseItem.setPen(Qt.NoPen) ellipseItem.setFlag(QGraphicsItem.ItemIsMovable) scene.addItem(ellipseItem) rectSizeGripItem = SizeGripItem(SimpleResizer(rectItem), rectItem) ellipseSizeGripItem = SizeGripItem(SimpleResizer(ellipseItem), ellipseItem) graphicsView = QGraphicsView(self) graphicsView.setScene(scene) self.setCentralWidget(graphicsView)
def drawRect(self): rect = QRectF(self.originPos, self.currentPos) rect_item = QGraphicsRectItem(rect) rect_item.setPen(self.pen) rect_item.setFlag(QGraphicsItem.ItemIsMovable, True) if len(self.items()) > 0: self.clearLastItem() self.addItem(rect_item)
def display_markers(self): """Add markers on top of first plot.""" for item in self.idx_markers: self.scene.removeItem(item) self.idx_markers = [] window_start = self.parent.value('window_start') window_length = self.parent.value('window_length') window_end = window_start + window_length y_distance = self.parent.value('y_distance') markers = [] if self.parent.info.markers is not None: if self.parent.value('marker_show'): markers = self.parent.info.markers for mrk in markers: if window_start <= mrk['end'] and window_end >= mrk['start']: mrk_start = max((mrk['start'], window_start)) mrk_end = min((mrk['end'], window_end)) color = QColor(self.parent.value('marker_color')) item = QGraphicsRectItem(mrk_start, 0, mrk_end - mrk_start, len(self.idx_label) * y_distance) item.setPen(color) item.setBrush(color) item.setZValue(-9) self.scene.addItem(item) item = TextItem_with_BG(color.darker(200)) item.setText(mrk['name']) item.setPos(mrk['start'], len(self.idx_label) * self.parent.value('y_distance')) item.setFlag(QGraphicsItem.ItemIgnoresTransformations) item.setRotation(-90) self.scene.addItem(item) self.idx_markers.append(item)
class Tile(QGraphicsRectItem): def __init__(self, letter, points, coords, scale, on_position_change=None, move_to_rack=None, parent=None): QGraphicsRectItem.__init__(self, MARGIN, MARGIN, SQUARE_SIZE - 2 * MARGIN, SQUARE_SIZE - 2 * MARGIN, parent) if on_position_change: self.on_position_change = on_position_change if move_to_rack: self.move_to_rack = move_to_rack self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.points = points self.letter = letter self.scale = scale self.setScale(self.scale) self.setZValue(3) self.setPen(QPen(YELLOW2, 0)) self.setBrush(QBrush(YELLOW)) tile_letter = letter.upper() self.letter_item = QGraphicsSimpleTextItem(tile_letter, self) self.font = QFont("Verdana", 20) if not points: self.font.setBold(True) font_metrics = QFontMetrics(self.font) height = font_metrics.height() width = font_metrics.width(tile_letter) self.letter_item.setX((SQUARE_SIZE - width) / 2 - MARGIN) self.letter_item.setY((SQUARE_SIZE - height) / 2 - MARGIN) self.letter_item.setFont(self.font) self.letter_item.setBrush(QBrush(SEA_GREEN)) self.shadow = QGraphicsRectItem(MARGIN * 2, MARGIN * 2, SQUARE_SIZE, SQUARE_SIZE, self) self.shadow.setFlag(QGraphicsItem.ItemStacksBehindParent) self.shadow.setBrush(QBrush(TRANSPARENT_BLACK)) self.shadow.setPen(QPen(TRANSPARENT, 0)) self.shadow.hide() self.setPos(coords.x * SQUARE_SIZE * scale, coords.y * SQUARE_SIZE * scale) self.coords = None self.update_coords() self.old_position = None self.old_coords = None self.is_placed = False if points: self.add_points() def __str__(self): return self.letter def add_points(self): points = QGraphicsSimpleTextItem(str(self.points), self) font = QFont("Verdana", 10) font_metrics = QFontMetrics(font) height = font_metrics.height() width = font_metrics.width(str(self.points)) points.setFont(font) points.setBrush(QBrush(SEA_GREEN)) points.setX(SQUARE_SIZE - MARGIN - width) points.setY(SQUARE_SIZE - MARGIN - height) def resize(self, scale): self.scale = scale self.setScale(scale) self.setPos(self.coords.x * SQUARE_SIZE * scale, self.coords.y * SQUARE_SIZE * scale) def change_to_blank(self, new_letter): if self.letter == BLANK: self.letter = new_letter self.letter_item.setText(new_letter.upper()) self.font.setBold(True) font_metrics = QFontMetrics(self.font) height = font_metrics.height() width = font_metrics.width(self.letter) self.letter_item.setFont(self.font) self.letter_item.setX((SQUARE_SIZE - width) / 2 - MARGIN) self.letter_item.setY((SQUARE_SIZE - height) / 2 - MARGIN) def change_back(self): self.letter = BLANK self.letter_item.setText(BLANK) def get_letter_and_points(self): return self.letter, self.points def mousePressEvent(self, event): if self.is_placed: return if event.button() == Qt.RightButton: return self.setScale(self.scale * 1.1) self.setZValue(10) self.old_position = self.pos() self.old_coords = self.coords self.setPos(self.x() - 2 * MARGIN, self.y() - 2 * MARGIN) self.shadow.show() QGraphicsRectItem.mousePressEvent(self, event) def mouseReleaseEvent(self, event): if self.is_placed: return if event.button() == Qt.RightButton: self.move_to_rack(self) return self.setScale(self.scale) current_position = self.pos() self.setX( round((self.x() + MARGIN * 2) / (SQUARE_SIZE * self.scale)) * SQUARE_SIZE * self.scale) self.setY( round((self.y() + MARGIN * 2) / (SQUARE_SIZE * self.scale)) * SQUARE_SIZE * self.scale) if current_position != self.pos(): self.update_coords() self.on_position_change(self) self.setZValue(3) self.shadow.hide() QGraphicsRectItem.mouseReleaseEvent(self, event) def update_coords(self): x = round(self.x() / SQUARE_SIZE / self.scale) y = round(self.y() / SQUARE_SIZE / self.scale) self.coords = Coords(x, y) def move(self, position): self.setPos(position) self.update_coords() def move_to_coords(self, coords): position = QPoint(coords.x * SQUARE_SIZE * self.scale, coords.y * SQUARE_SIZE * self.scale) self.move(position) def undo_move(self): self.setPos(self.old_position) self.update_coords() def swap_with_other(self, other): other.move(self.old_position) def remove_highlight(self): self.letter_item.setBrush(QBrush(SEA_GREEN)) def place(self): self.letter_item.setBrush(QBrush(LIGHT_SEA_GREEN)) self.setBrush(QBrush(YELLOW2)) self.setPen(QPen(YELLOW2, 0)) self.setFlag(QGraphicsItem.ItemIsMovable, False) self.is_placed = True
class NodeTemplateItem(): ''' This represents one node template on the diagram. A node template can be on many diagrams This class creates the rectangle graphics item and the text graphics item and adds them to the scene. ''' def __init__(self, scene, x, y, nodeTemplateDict=None, NZID=None): self.scene = scene self.logMsg = None self.x = x self.y = y self.nodeTemplateDict = nodeTemplateDict # self.name = self.nodeTemplateDict.get("name", "") THIS HAS BEEN REPLACED BY THE name FUNCTION - SEE BELOW self.diagramType = "Node Template" self.displayText = None self.model = self.scene.parent.model self.gap = 100 self.relList = [] # assign a unique key if it doesn't already have one if NZID == None: self.NZID = str(uuid.uuid4()) else: self.NZID = NZID # init graphics objects to none self.TNode = None self.TNtext = None # draw the node template on the diagram self.drawIt() def name(self, ): return self.nodeTemplateDict.get("name", "") def getX(self, ): return self.TNode.boundingRect().x() def getY(self, ): return self.TNode.boundingRect().y() def getHeight(self, ): return self.TNode.boundingRect().height() def getWidth(self, ): return self.TNode.boundingRect().width() def getRelList(self, ): '''return a list of all relationitems that are inbound or outbound from this node template. do not include self referencing relationships ''' return [ diagramItem for key, diagramItem in self.scene.parent.itemDict.items() if diagramItem.diagramType == "Relationship Template" and ( diagramItem.startNZID == self.NZID or diagramItem.endNZID == self.NZID) ] def getPoint(self, offset=None): ''' This function is used by the template diagram to calculate the location to drop a node template on the diagram ''' if offset is None: return QPointF(self.x, self.y) else: return QPointF(self.x + offset, self.y + offset) def getFormat(self, ): ''' determine if the Node Template has a template format or should use the project default format ''' # get the node Template custom format customFormat = self.nodeTemplateDict.get("TNformat", None) if not customFormat is None: # get the template custom format self.nodeFormat = TNodeFormat(formatDict=customFormat) else: # get the project default format self.nodeFormat = TNodeFormat( formatDict=self.model.modelData["TNformat"]) def clearItem(self, ): if (not self.TNode is None and not self.TNode.scene() is None): self.TNode.scene().removeItem(self.TNode) if (not self.TNtext is None and not self.TNtext.scene() is None): self.TNtext.scene().removeItem(self.TNtext) def drawIt(self, ): # get current format as it may have changed self.getFormat() # create the qgraphicsItems if they don't exist if self.TNode is None: # create the rectangle self.TNode = QGraphicsRectItem(QRectF( self.x, self.y, self.nodeFormat.formatDict["nodeWidth"], self.nodeFormat.formatDict["nodeHeight"]), parent=None) self.TNode.setZValue(NODELAYER) self.TNode.setFlag(QGraphicsItem.ItemIsMovable, True) self.TNode.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.TNode.setFlag(QGraphicsItem.ItemIsSelectable, True) self.TNode.setSelected(True) self.TNode.setData(1, self.NZID) # get with self.INode.data(1) self.TNode.setData(ITEMTYPE, NODETEMPLATE) # create the text box self.TNtext = QGraphicsTextItem("", parent=None) self.TNtext.setPos(self.x, self.y) self.TNtext.setFlag(QGraphicsItem.ItemIsMovable, True) self.TNtext.setFlag(QGraphicsItem.ItemIsSelectable, False) self.TNtext.setData(NODEID, self.NZID) self.TNtext.setData(ITEMTYPE, NODETEMPLATETEXT) self.TNtext.setZValue(NODELAYER) # save the location self.x = self.TNode.sceneBoundingRect().x() self.y = self.TNode.sceneBoundingRect().y() # generate the html and resize the rectangle self.formatItem() # add the graphics items to the scene self.scene.addItem(self.TNode) self.scene.addItem(self.TNtext) else: # generate the html and resize the rectangle self.formatItem() def formatItem(self, ): # configure the formatting aspects of the qgraphics item pen = self.nodeFormat.pen() brush = self.nodeFormat.brush() self.TNode.setBrush(brush) self.TNode.setPen(pen) # generate the HTML genHTML = self.generateHTML() self.TNtext.prepareGeometryChange() # print("before html bounding rectangle width:{}".format(self.TNtext.boundingRect().width())) # print("before html text width:{}".format(self.TNtext.textWidth())) self.TNtext.setTextWidth( -1 ) # reset the width to unkonwn so it will calculate a new width based on the new html self.TNtext.setHtml(genHTML) # print("after html bounding rectangle width:{}".format(self.TNtext.boundingRect().width())) # print("after html text width:{}".format(self.TNtext.textWidth())) # make sure minimum width of 120 if self.TNtext.boundingRect().width() < 120: self.TNtext.setTextWidth(120) else: self.TNtext.setTextWidth( self.TNtext.boundingRect().width() ) # you have to do a setTextWidth to get the html to render correctly. # set the rectangle item to the same size as the formatted html self.TNode.prepareGeometryChange() currentRect = self.TNode.rect() # insure minimum height of 120 if self.TNtext.boundingRect().height() < 120: currentRect.setHeight(120) else: currentRect.setHeight(self.TNtext.boundingRect().height()) currentRect.setWidth(self.TNtext.boundingRect().width()) self.TNode.setRect(currentRect) def generateHTML(self, ): ''' Generate the HTML that formats the node template data inside the rectangle ''' # generate the html prefix = "<!DOCTYPE html><html><body>" # head = "<head><style>table, th, td {border: 1px solid black; border-collapse: collapse;}</style></head>" suffix = "</body></html>" # blankRow = "<tr><td><left>{}</left></td><td><left>{}</left></td><td><left>{}</left></td><td><left>{}</left></td></tr>".format("", "", "", "") name = "<center><b>{}</b></center>".format( self.nodeTemplateDict.get("name", "")) lbls = self.genLblHTML() props = self.genPropHTML() genHTML = "{}{}<hr>{}<br><hr>{}{}".format(prefix, name, lbls, props, suffix) # print("{} html: {}".format(self.name(), genHTML)) return genHTML def genLblHTML(self): # html = '<table width="90%">' html = '<table style="width:90%;border:1px solid black;">' if len(self.nodeTemplateDict.get("labels", [])) > 0: for lbl in self.nodeTemplateDict.get("labels", []): if lbl[NODEKEY] == Qt.Checked: nk = "NK" else: nk = " " if lbl[REQUIRED] == Qt.Checked: rq = "R" else: rq = "" html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( nk, lbl[LABEL], "", rq) html = html + "</table>" else: html = '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( " ", "NO{}LABELS".format(" "), "", "") html = html + "</table>" return html def genPropHTML(self): # PROPERTY, DATATYPE, PROPREQ, DEFAULT, EXISTS, UNIQUE, PROPNODEKEY html = '<table style="width:90%;border:1px solid black;">' if len(self.nodeTemplateDict.get("properties", [])) > 0: for prop in self.nodeTemplateDict.get("properties", []): if prop[PROPNODEKEY] == Qt.Checked: nk = "NK" else: nk = " " if prop[PROPREQ] == Qt.Checked: rq = "R" else: rq = "" if prop[EXISTS] == Qt.Checked: ex = "E" else: ex = "" if prop[UNIQUE] == Qt.Checked: uq = "U" else: uq = "" html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( nk, prop[PROPERTY], rq, ex, uq) html = html + "</table>" else: html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( " ", "NO{}PROPERTIES".format(" "), "", "", "") html = html + "</table>" return html def moveIt(self, dx, dy): ''' Move the node rectangle and the node textbox to the delta x,y coordinate. ''' # print("before moveIt: sceneboundingrect {} ".format( self.INode.sceneBoundingRect())) self.TNode.moveBy(dx, dy) self.x = self.TNode.sceneBoundingRect().x() self.y = self.TNode.sceneBoundingRect().y() self.TNtext.moveBy(dx, dy) # print("after moveIt: sceneboundingrect {} ".format( self.INode.sceneBoundingRect())) # now redraw all the relationships self.drawRels() def drawRels(self, ): '''Redraw all the relationship lines connected to the Node Template Rectangle''' # get a list of the relationship items connected to this node template self.relList = self.getRelList() # assign the correct inbound/outbound side for the rel for rel in self.relList: if rel.endNodeItem.NZID != rel.startNodeItem.NZID: # ignore bunny ears rel.assignSide() # get a set of all the nodes and sides involved nodeSet = set() for rel in self.relList: if rel.endNodeItem.NZID != rel.startNodeItem.NZID: # ignore bunny ears nodeSet.add((rel.endNodeItem, rel.inboundSide)) nodeSet.add((rel.startNodeItem, rel.outboundSide)) # tell each node side to assign rel locations for nodeSide in nodeSet: nodeSide[0].assignPoint(nodeSide[1]) ############################################ # now tell them all to redraw for rel in self.relList: rel.drawIt2() def calcOffset(self, index, totRels): offset = [-60, -40, -20, 0, 20, 40, 60] offsetStart = [3, 2, 2, 1, 1, 0, 0] if totRels > 7: totRels = 7 return offset[offsetStart[totRels - 1] + index] def assignPoint(self, side): # go through all the rels on a side and assign their x,y coord for that side self.relList = self.getRelList() sideList = [ rel for rel in self.relList if ((rel.startNZID == self.NZID and rel.outboundSide == side) or ( rel.endNZID == self.NZID and rel.inboundSide == side)) ] totRels = len(sideList) if totRels > 0: if side == R: # calc center of the side x = self.x + self.getWidth() y = self.y + self.getHeight() / 2 # sort the rels connected to this side by the y value sideList.sort(key=self.getSortY) # assign each of them a position on the side starting in the center and working out in both directions for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) elif side == L: x = self.x y = self.y + self.getHeight() / 2 sideList.sort(key=self.getSortY) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) elif side == TOP: x = self.x + self.getWidth() / 2 y = self.y sideList.sort(key=self.getSortX) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) elif side == BOTTOM: x = self.x + self.getWidth() / 2 y = self.y + self.getHeight() sideList.sort(key=self.getSortX) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) else: print("error, no side") def getSortY(self, rel): # if this node is the start node then return the end node's Y if rel.startNZID == self.NZID: return rel.endNodeItem.TNode.sceneBoundingRect().center().y() # if this node is the end node then return the start node's Y if rel.endNZID == self.NZID: return rel.startNodeItem.TNode.sceneBoundingRect().center().y() # this should never happen return 0 def getSortX(self, rel): # if this node is the start node then return the end node's X if rel.startNZID == self.NZID: return rel.endNodeItem.TNode.sceneBoundingRect().center().x() # if this node is the end node then return the start node's X if rel.endNZID == self.NZID: return rel.startNodeItem.TNode.sceneBoundingRect().center().x() # this should never happen return 0 def getObjectDict(self, ): ''' This function returns a dictionary with all the data that represents this node template item. The dictionary is added to the Instance Diagram dictionary.''' objectDict = {} objectDict["NZID"] = self.NZID objectDict["name"] = self.nodeTemplateDict.get("name", "") objectDict["displayText"] = self.displayText objectDict["x"] = self.TNode.sceneBoundingRect().x() objectDict["y"] = self.TNode.sceneBoundingRect().y() objectDict["diagramType"] = self.diagramType objectDict["labels"] = self.nodeTemplateDict.get("labels", []) objectDict["properties"] = self.nodeTemplateDict.get("properties", []) return objectDict def setLogMethod(self, logMethod=None): if logMethod is None: if self.logMsg is None: self.logMsg = self.noLog else: self.logMsg = logMethod def noLog(self, msg): return
class DrawItem(): ''' Piirtää kuviot ''' def __init__(self, piirtoalusta): self.scene = piirtoalusta.scene self.undoStack = piirtoalusta.undoStack self.reset() self.start = QPoint() self.end = QPoint() self.pos = QPoint() self.color = Qt.black self.font = QFont() def setRect(self): ''' Määrittää nelikulmion paikkatietojen perusteella ''' rect = QRectF() rect.setTopLeft(self.start) rect.setBottomRight(self.end) return rect def drawRect(self): ''' Piirtää esineen ja lisää sen sceneen. Palauttaa esineen Load ja Undo luokkia varten. ''' path = QPainterPath() rect = self.setRect() path.addRect(rect) path.simplified() #self.rectitem = QGraphicsPathItem(path) self.rectitem = PathItem(path, self.undoStack) self.rectitem.setPen(QPen(self.color, 2)) self.rectitem.setFlag(QGraphicsItem.ItemIsSelectable) self.rectitem.setFlag(QGraphicsItem.ItemIsMovable) self.scene.addItem(self.rectitem) return self.rectitem ''' rect = self.setRect() self.rectitem = QGraphicsRectItem(rect) self.rectitem.setPen(QPen(self.color, 2)) self.scene.addItem(self.rectitem) ''' def drawEllipse(self): rect = self.setRect() #self.ellipseitem = QGraphicsEllipseItem(rect) self.ellipseitem = EllipseItem(rect, self.undoStack) self.ellipseitem.setPen(QPen(self.color, 2)) self.ellipseitem.setFlag(QGraphicsItem.ItemIsSelectable) self.ellipseitem.setFlag(QGraphicsItem.ItemIsMovable) self.scene.addItem(self.ellipseitem) return self.ellipseitem def drawCircle(self): erotus = abs(self.end.x() - self.start.x()) if self.start.y() > self.end.y(): erotus = -erotus self.end.setY(self.start.y() + erotus) self.drawEllipse() def drawLine(self): path = QPainterPath() shape = QRectF(0, 0, 1, 1) shape.moveCenter(self.pos) path.addEllipse(shape) self.paths.connectPath(path) self.paths.simplified() #self.lineitem = QGraphicsPathItem(self.paths) self.lineitem = PathItem(self.paths, self.undoStack) self.lineitem.setPen(QPen(self.color, 2)) self.lineitem.setFlag(QGraphicsItem.ItemIsSelectable) self.lineitem.setFlag(QGraphicsItem.ItemIsMovable) self.scene.addItem(self.lineitem) return self.lineitem def drawText(self): #textitem = QGraphicsTextItem('Text') textitem = TextItem('Text', self.undoStack) textitem.setPos(self.end) textitem.setAcceptHoverEvents(False) textitem.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextEditable) textitem.setFont(self.font) textitem.setDefaultTextColor(self.color) textitem.setFlag(QGraphicsItem.ItemIsMovable) textitem.setFlag(QGraphicsItem.ItemIsSelectable) self.scene.addItem(textitem) return textitem def reset(self): self.ellipseitem = QGraphicsEllipseItem() self.rectitem = QGraphicsRectItem() self.lineitem = QGraphicsPathItem() self.paths = QPainterPath() def change_selected_color(self, items): ''' Muttaa listan 'items' esineiden värin arvoon 'self.color'. ''' for item in items: if item.type() == 10: # group self.change_selected_color( item.childItems()) # kutsuu itseään joukon jäsenille elif item.type() == 8: # tekstiä item.setDefaultTextColor(self.color) else: item.setPen(QPen(self.color, 2))
class ImageScene2D(QGraphicsScene): """ The 2D scene description of a tiled image generated by evaluating an overlay stack, together with a 2D cursor. """ axesChanged = pyqtSignal(int, bool) dirtyChanged = pyqtSignal() @property def is_swapped(self): """ Indicates whether the dimensions are swapped swapping the axis will swap the dimensions and rotating the roi will swap the dimensions :return: bool """ return bool(self._swapped) != bool(self._rotation % 2) # xor @property def stackedImageSources(self): return self._stackedImageSources @stackedImageSources.setter def stackedImageSources(self, s): self._stackedImageSources = s @property def showTileOutlines(self): return self._showTileOutlines @showTileOutlines.setter def showTileOutlines(self, show): self._showTileOutlines = show self.invalidate() @property def showTileProgress(self): return self._showTileProgress @showTileProgress.setter def showTileProgress(self, show): self._showTileProgress = show self._dirtyIndicator.setVisible(show) def resetAxes(self, finish=True): # rotation is in range(4) and indicates in which corner of the # view the origin lies. 0 = top left, 1 = top right, etc. self._rotation = 0 self._swapped = self._swappedDefault # whether axes are swapped self._newAxes() self._setSceneRect() self.scene2data, isInvertible = self.data2scene.inverted() assert isInvertible if finish: self._finishViewMatrixChange() def _newAxes(self): """Given self._rotation and self._swapped, calculates and sets the appropriate data2scene transformation. """ # TODO: this function works, but it is not elegant. There must # be a simpler way to calculate the appropriate transformation. w, h = self.dataShape assert self._rotation in range(0, 4) # unlike self._rotation, the local variable 'rotation' # indicates how many times to rotate clockwise after swapping # axes. # t1 : do axis swap t1 = QTransform() if self._swapped: t1 = QTransform(0, 1, 0, 1, 0, 0, 0, 0, 1) h, w = w, h # t2 : do rotation t2 = QTransform() t2.rotate(self._rotation * 90) # t3: shift to re-center rot2trans = {0: (0, 0), 1: (h, 0), 2: (w, h), 3: (0, w)} trans = rot2trans[self._rotation] t3 = QTransform.fromTranslate(*trans) self.data2scene = t1 * t2 * t3 if self._tileProvider: self._tileProvider.axesSwapped = self._swapped self.axesChanged.emit(self._rotation, self._swapped) def rot90(self, direction): """ direction: left ==> -1, right ==> +1""" assert direction in [-1, 1] self._rotation = (self._rotation + direction) % 4 self._newAxes() def swapAxes(self, transform): self._swapped = not self._swapped self._newAxes() def _onRotateLeft(self): self.rot90(-1) self._finishViewMatrixChange() def _onRotateRight(self): self.rot90(1) self._finishViewMatrixChange() def _onSwapAxes(self): self.swapAxes(self.data2scene) self._finishViewMatrixChange() def _finishViewMatrixChange(self): self.scene2data, isInvertible = self.data2scene.inverted() self._setSceneRect() self._tiling.data2scene = self.data2scene self._tileProvider._onSizeChanged() QGraphicsScene.invalidate(self, self.sceneRect()) @property def sceneShape(self): return (self.sceneRect().width(), self.sceneRect().height()) def _setSceneRect(self): w, h = self.dataShape rect = self.data2scene.mapRect(QRect(0, 0, w, h)) sw, sh = rect.width(), rect.height() self.setSceneRect(0, 0, sw, sh) if self._dataRectItem is not None: self.removeItem(self._dataRectItem) # this property represent a parent to QGraphicsItems which should # be clipped to the data, such as temporary capped lines for brushing. # This works around ilastik issue #516. self._dataRectItem = QGraphicsRectItem(0, 0, sw, sh) self._dataRectItem.setPen(QPen(QColor(0, 0, 0, 0))) self._dataRectItem.setFlag(QGraphicsItem.ItemClipsChildrenToShape) self.addItem(self._dataRectItem) @property def dataRectItem(self): return self._dataRectItem @property def dataShape(self): """ The shape of the scene in QGraphicsView's coordinate system. """ return self._dataShape @dataShape.setter def dataShape(self, value): """ Set the size of the scene in QGraphicsView's coordinate system. dataShape -- (widthX, widthY), where the origin of the coordinate system is in the upper left corner of the screen and 'x' points right and 'y' points down """ assert len(value) == 2 self._dataShape = value self.reset() self._finishViewMatrixChange() def setCacheSize(self, cache_size): self._tileProvider.set_cache_size(cache_size) def cacheSize(self): return self._tileProvider.cache_size def setTileWidth(self, tileWidth): self._tileWidth = tileWidth PreferencesManager().set("ImageScene2D", "tileWidth", tileWidth) def tileWidth(self): return self._tileWidth def setPrefetchingEnabled(self, enable): self._prefetching_enabled = enable def setPreemptiveFetchNumber(self, n): if n > self.cacheSize() - 1: self._n_preemptive = self.cacheSize() - 1 else: self._n_preemptive = n def preemptiveFetchNumber(self): return self._n_preemptive def invalidateViewports(self, sceneRectF): """Call invalidate on the intersection of all observing viewport-rects and rectF.""" sceneRectF = sceneRectF if sceneRectF.isValid() else self.sceneRect() for view in self.views(): QGraphicsScene.invalidate(self, sceneRectF.intersected(view.viewportRect())) def reset(self): """Reset rotations, tiling, etc. Called when first initialized and when the underlying data changes. """ self.resetAxes(finish=False) self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name, blockSize=self.tileWidth()) self._tileProvider = TileProvider(self._tiling, self._stackedImageSources) self._tileProvider.sceneRectChanged.connect(self.invalidateViewports) if self._dirtyIndicator: self.removeItem(self._dirtyIndicator) del self._dirtyIndicator self._dirtyIndicator = DirtyIndicator(self._tiling) self.addItem(self._dirtyIndicator) self._dirtyIndicator.setVisible(False) def mouseMoveEvent(self, event): """ Normally our base class (QGraphicsScene) distributes mouse events to the various QGraphicsItems in the scene. But when the mouse is being dragged, it only sends events to the one object that was under the mouse when the button was first pressed. Here, we forward all events to QGraphicsItems on the drag path, even if they're just brushed by the mouse incidentally. """ super(ImageScene2D, self).mouseMoveEvent(event) if not event.isAccepted() and event.buttons() != Qt.NoButton: if self.last_drag_pos is None: self.last_drag_pos = event.scenePos() # As a special feature, find the item and send it this event. path = QPainterPath(self.last_drag_pos) path.lineTo(event.scenePos()) items = self.items(path) for item in items: item.mouseMoveEvent(event) self.last_drag_pos = event.scenePos() else: self.last_drag_pos = None def mousePressEvent(self, event): """ By default, our base class (QGraphicsScene) only sends mouse press events to the top-most item under the mouse. When labeling edges, we want the edge label layer to accept mouse events, even if it isn't on top. Therefore, we send events to all items under the mouse, until the event is accepted. """ super(ImageScene2D, self).mousePressEvent(event) if not event.isAccepted(): items = self.items(event.scenePos()) for item in items: item.mousePressEvent(event) if event.isAccepted(): break def __init__( self, posModel, along, preemptive_fetch_number=5, parent=None, name="Unnamed Scene", swapped_default=False ): """ * preemptive_fetch_number -- number of prefetched slices; 0 turns the feature off * swapped_default -- whether axes should be swapped by default. """ QGraphicsScene.__init__(self, parent=parent) self._along = along self._posModel = posModel # QGraphicsItems can change this if they are in a state that should temporarily forbid brushing # (For example, when the slice intersection marker is in 'draggable' state.) self.allow_brushing = True self._dataShape = (0, 0) self._dataRectItem = None # A QGraphicsRectItem (or None) self._offsetX = 0 self._offsetY = 0 self.name = name self._tileWidth = PreferencesManager().get("ImageScene2D", "tileWidth", default=512) self._stackedImageSources = StackedImageSources(LayerStackModel()) self._showTileOutlines = False # FIXME: We don't show the red 'progress pies' because they look terrible. # If we could fix their timing, maybe it would be worth it. self._showTileProgress = False self._tileProvider = None self._dirtyIndicator = None self._prefetching_enabled = False self._swappedDefault = swapped_default self.reset() # BowWave preemptive caching self.setPreemptiveFetchNumber(preemptive_fetch_number) self._course = (1, 1) # (along, pos or neg direction) self._time = self._posModel.time self._channel = self._posModel.channel self._posModel.timeChanged.connect(self._onTimeChanged) self._posModel.channelChanged.connect(self._onChannelChanged) self._posModel.slicingPositionChanged.connect(self._onSlicingPositionChanged) self._allTilesCompleteEvent = threading.Event() self.dirty = False # We manually keep track of the tile-wise QGraphicsItems that # we've added to the scene in this dict, otherwise we would need # to use O(N) lookups for every tile by calling QGraphicsScene.items() self.tile_graphicsitems = defaultdict(set) # [Tile.id] -> set(QGraphicsItems) self.last_drag_pos = None # See mouseMoveEvent() def drawForeground(self, painter, rect): if self._tiling is None: return if self._showTileOutlines: tile_nos = self._tiling.intersected(rect) for tileId in tile_nos: ## draw tile outlines # Dashed black line pen = QPen() pen.setWidth(0) pen.setDashPattern([5, 5]) painter.setPen(pen) painter.drawRect(self._tiling.imageRects[tileId]) # Dashed white line # (offset to occupy the spaces in the dashed black line) pen = QPen() pen.setWidth(0) pen.setDashPattern([5, 5]) pen.setDashOffset(5) pen.setColor(QColor(Qt.white)) painter.setPen(pen) painter.drawRect(self._tiling.imageRects[tileId]) def indicateSlicingPositionSettled(self, settled): if self._showTileProgress: self._dirtyIndicator.setVisible(settled) def drawBackground(self, painter, sceneRectF): if self._tileProvider is None: return # FIXME: For some strange reason, drawBackground is called with # a much larger sceneRectF than necessasry sometimes. # This can happen after panSlicingViews(), for instance. # Somehow, the QGraphicsScene gets confused about how much area # it needs to draw immediately after the ImageView's scrollbar is panned. # As a workaround, we manually check the amount of the scene that needs to be drawn, # instead of relying on the above sceneRectF parameter to be correct. if self.views(): sceneRectF = self.views()[0].viewportRect().intersected(sceneRectF) if not sceneRectF.isValid(): return tiles = self._tileProvider.getTiles(sceneRectF) allComplete = True for tile in tiles: # We always draw the tile, even though it might not be up-to-date # In ilastik's live mode, the user sees the old result while adding # new brush strokes on top # See also ilastik issue #132 and tests/lazy_test.py if tile.qimg is not None: painter.drawImage(tile.rectF, tile.qimg) # The tile also contains a list of any QGraphicsItems that were produced by the layers. # If there are any new ones, add them to the scene. new_items = set(tile.qgraphicsitems) - self.tile_graphicsitems[tile.id] obsolete_items = self.tile_graphicsitems[tile.id] - set(tile.qgraphicsitems) for g_item in obsolete_items: self.tile_graphicsitems[tile.id].remove(g_item) self.removeItem(g_item) for g_item in new_items: self.tile_graphicsitems[tile.id].add(g_item) self.addItem(g_item) if tile.progress < 1.0: allComplete = False if self._showTileProgress: self._dirtyIndicator.setTileProgress(tile.id, tile.progress) if allComplete: if self.dirty: self.dirty = False self.dirtyChanged.emit() self._allTilesCompleteEvent.set() else: if not self.dirty: self.dirty = True self.dirtyChanged.emit() self._allTilesCompleteEvent.clear() # preemptive fetching if self._prefetching_enabled: upcoming_through_slices = self._bowWave(self._n_preemptive) for through in upcoming_through_slices: self._tileProvider.prefetch(sceneRectF, through, layer_indexes=None) def triggerPrefetch(self, layer_indexes, time_range="current", spatial_axis_range="current", sceneRectF=None): """ Trigger a one-time prefetch for the given set of layers. TODO: I'm not 100% sure what happens here for layers with multiple channels. layer_indexes: list-of-ints, or None, which means 'all visible'. time_range: (start_time, stop_time) spatial_axis_range: (start_slice, stop_slice), meaning Z/Y/X depending on our projection (self.along) sceneRectF: Used to determine which tiles to request. An invalid QRectF results in all tiles getting refreshed (visible or not). """ # Process parameters sceneRectF = sceneRectF or QRectF() if time_range == "current": time_range = (self._posModel.slicingPos5D[0], self._posModel.slicingPos5D[0] + 1) elif time_range == "all": time_range = (0, self._posModel.shape5D[0]) else: assert len(time_range) == 2 assert time_range[0] >= 0 and time_range[1] < self._posModel.shape5D[0] spatial_axis = self._along[1] if spatial_axis_range == "current": spatial_axis_range = ( self._posModel.slicingPos5D[spatial_axis], self._posModel.slicingPos5D[spatial_axis] + 1, ) elif spatial_axis_range == "all": spatial_axis_range = (0, self._posModel.shape5D[spatial_axis]) else: assert len(spatial_axis_range) == 2 assert 0 <= spatial_axis_range[0] < self._posModel.shape5D[spatial_axis] assert 0 < spatial_axis_range[1] <= self._posModel.shape5D[spatial_axis] # Construct list of 'through' coordinates through_list = [] for t in range(*time_range): for s in range(*spatial_axis_range): through_list.append((t, s)) # Make sure the tile cache is big enough to hold the prefetched data. if self._tileProvider.cache_size < len(through_list): self._tileProvider.set_cache_size(len(through_list)) # Trigger prefetches for through in through_list: self._tileProvider.prefetch(sceneRectF, through, layer_indexes) def joinRenderingAllTiles(self, viewport_only=True, rect=None): """ Wait until all tiles in the scene have been 100% rendered. If sceneRectF is None, use the viewport rect. If sceneRectF is an invalid QRectF(), then wait for all tiles. Note: If called from the GUI thread, the GUI thread will block until all tiles are rendered! """ # If this is the main thread, keep repainting (otherwise we'll deadlock). if threading.current_thread().name == "MainThread": if viewport_only: sceneRectF = self.views()[0].viewportRect() else: if rect is None or not isinstance(rect, QRectF): sceneRectF = QRectF() # invalid QRectF means 'get all tiles' else: sceneRectF = rect self._tileProvider.waitForTiles(sceneRectF) else: self._allTilesCompleteEvent.wait() def _bowWave(self, n): through = [self._posModel.slicingPos5D[axis] for axis in self._along[:-1]] t_max = [self._posModel.shape5D[axis] for axis in self._along[:-1]] BowWave = [] a = self._course[0] for d in range(1, n + 1): m = through[a] + d * self._course[1] if m < t_max[a] and m >= 0: t = list(through) t[a] = m BowWave.append(tuple(t)) return BowWave def _onSlicingPositionChanged(self, new, old): if (new[self._along[1] - 1] - old[self._along[1] - 1]) < 0: self._course = (1, -1) else: self._course = (1, 1) def _onChannelChanged(self, new): if (new - self._channel) < 0: self._course = (2, -1) else: self._course = (2, 1) self._channel = new def _onTimeChanged(self, new): if (new - self._time) < 0: self._course = (0, -1) else: self._course = (0, 1) self._time = new
class RectZoomMoveView(QChartView): """ Filter data to be displayed in rectangular body """ rangeSig = pyqtSignal(list) def __init__(self, parent=None): super(RectZoomMoveView, self).__init__(parent) self.setChart(QChart()) self.chart().setMargins(QMargins(5, 5, 5, 5)) self.chart().setContentsMargins(-10, -10, -10, -10) self.chart().setTitle(" ") self.relationState = True # Define two rectangles for background and drawing respectively self.parentRect = QGraphicsRectItem(self.chart()) self.parentRect.setFlag(QGraphicsItem.ItemClipsChildrenToShape, True) self.parentRect.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.RangeItem = RectRangeItem(parent=self.parentRect) self.RangeItem.setZValue(998) pen = QPen(Qt.gray) pen.setWidth(1) self.parentRect.setPen(pen) self.parentRect.setZValue(997) self.scene().addItem(self.parentRect) self.scene().addItem(self.RangeItem) self.RangeItem.hide() self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.dataLength = 0 self.RangeItem.selectedChange.connect(self.changeFromRectItem) self.BtnsWidget = ViewButtonsWidget(self) self.BtnsWidget.refreshBtn.clicked.connect(self.updateView) self.BtnsWidget.RelationSig.connect(self.setRelationState) self.BtnsWidget.dateRangeEdit.dateRangeSig.connect(self.changDateRect) def changDateRect(self, daterange): if self.chartTypes == "Bar": v = 3 else: v = 2 l = len(self.RangeItem.rangePoints) if l > 2: try: num = self.mintimeData.date().daysTo(daterange[0]) left = self.RangeItem.rangePoints[num] num = self.mintimeData.date().daysTo(daterange[1]) right = self.RangeItem.rangePoints[num] rect = self.chart().plotArea() rect.setLeft(left) rect.setRight(right) except: rect = self.chart().plotArea() self.RangeItem.setRect(rect) self.RangeItem.updateHandlesPos() else: try: num = self.mintimeData.date().daysTo(daterange[0]) left = self.RangeItem.rangePoints[num] num = self.mintimeData.date().daysTo(daterange[0]) right = self.RangeItem.rangePoints[num] rect = self.chart().plotArea() rect.setLeft(left) rect.setRight(right) except: rect = self.chart().plotArea() self.RangeItem.setRect(rect) self.RangeItem.updateHandlesPos() def lineSpace(self, start, end, num): res = [] if self.chartTypes == "Bar": step = (end - start) / (num) for i in range(num + 2): res.append(start + i * step) else: step = (end - start) / (num - 1) for i in range(num + 1): res.append(start + i * step) return res def getRangePoints(self): count = self.zoomSeries.count() rect = self.chart().plotArea() left = rect.left() right = rect.right() if count == 0: self.RangeItem.rangePoints = [left, right] else: # Get coordinate position for each node self.RangeItem.rangePoints = self.lineSpace(left, right, count) def setRangeColor(self, color): self.RangeItem.setRangeColor(color) def setRelationState(self, state): self.relationState = state def initSeries(self, chartTypes="Bar"): self.chartTypes = chartTypes axisX = QDateTimeAxis() axisX.setFormat("yyyy-MM-dd") self.zoomSeries = QLineSeries(self.chart()) self.chart().addSeries(self.zoomSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) self.chart().setAxisX(axisX, self.zoomSeries) self.initView() def clearAll(self): # Clear all series and axes self.chart().removeAllSeries() axess = self.chart().axes() for axes in axess: self.chart().removeAxis(axes) def setData(self, timeData, valueData, chartTypes="Bar"): axisX = QDateTimeAxis() axisX.setFormat("yyyy-MM-dd") if self.chartTypes == "Bar": # Clear all series self.clearAll() self.zoomSeries = QLineSeries(self.chart()) barSeries = QBarSeries(self.chart()) barset = QBarSet("data") barSeries.setBarWidth(0.8) barSeries.append(barset) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) barset.append(valueData) self.zoomSeries.hide() self.chart().addSeries(self.zoomSeries) self.chart().addSeries(barSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes == "Scatter": # Clear all series self.clearAll() self.zoomSeries = QLineSeries(self.chart()) scattSeries = QScatterSeries(self.chart()) scattSeries.setMarkerSize(8) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) scattSeries.append(td.toMSecsSinceEpoch(), vd) self.zoomSeries.hide() self.chart().addSeries(self.zoomSeries) self.chart().addSeries(scattSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes in ["Line", "PLine"]: self.clearAll() if self.chartTypes == "Line": self.zoomSeries = QLineSeries(self.chart()) else: self.zoomSeries = QSplineSeries(self.chart()) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) self.chart().addSeries(self.zoomSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes == "Area": self.clearAll() self.zoomSeries = QLineSeries() self.zoomSeries.setColor(QColor("#666666")) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) areaSeries = QAreaSeries(self.zoomSeries, None) self.chart().addSeries(self.zoomSeries) self.chart().addSeries(areaSeries) self.chart().setAxisY(QValueAxis(), areaSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, areaSeries) self.zoomSeries.hide() self.mintimeData = min(timeData) self.maxtimeData = max(timeData) self.BtnsWidget.dateRangeEdit.setDateRange([ self.mintimeData.toString("yyyy-MM-dd"), self.maxtimeData.toString("yyyy-MM-dd"), ]) self.updateView() def resetView(self): rect = self.chart().plotArea() self.parentRect.setRect(rect) topRight = self.chart().plotArea().topRight() x = int(topRight.x()) y = int(topRight.y()) self.BtnsWidget.setGeometry(QRect(x - 420, 0, 420, 23)) self.RangeItem.setRect(rect) self.RangeItem.show() self.save_current_rubber_band() self.RangeItem.updateHandlesPos() self.apply_nice_numbers() self.getRangePoints() self.sendRang() def initView(self): self.RangeItem.hide() # Hide y-axis if self.chart().axisY(): self.chart().axisY().setVisible(False) if self.chart().axisX(): self.chart().axisX().setGridLineVisible(False) self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.getRangePoints() def updateView(self): self.RangeItem.hide() # Hide y-axis if self.chart().axisY(): self.chart().axisY().setVisible(False) if self.chart().axisX(): self.chart().axisX().setGridLineVisible(False) self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.resetView() # Map points to chart def point_to_chart(self, pnt): scene_point = self.mapToScene(pnt) chart_point = self.chart().mapToValue(scene_point) return chart_point # Map chart to points def chart_to_view_point(self, char_coord): scene_point = self.chart().mapToPosition(char_coord) view_point = self.mapFromScene(scene_point) return view_point # Save positions of rectangles def save_current_rubber_band(self): rect = self.RangeItem.rect() chart_top_left = self.point_to_chart(rect.topLeft().toPoint()) self.m_chartRectF.setTopLeft(chart_top_left) chart_bottom_right = self.point_to_chart(rect.bottomRight().toPoint()) self.m_chartRectF.setBottomRight(chart_bottom_right) # Respond to change in positions of rectangles def changeFromRectItem(self, rectIndex): self.save_current_rubber_band() self.sendRang(rectIndex) def sendRang(self, rectIndex=[]): if self.RangeItem.rect() != self.parentRect.rect(): self.BtnsWidget.setPalActive() else: self.BtnsWidget.setPalDisActive() if self.chartTypes == "Bar": v = 3 else: v = 2 if rectIndex == []: maxData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(len(self.RangeItem.rangePoints) - v).x()) minData = QDateTime.fromMSecsSinceEpoch(self.zoomSeries.at(0).x()) else: minData = max(rectIndex[0], 0) maxData = min(rectIndex[1], len(self.RangeItem.rangePoints) - v) minData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(minData).x()) maxData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(maxData).x()) if minData > maxData: if self.RangeItem.handleSelected is None: self.resetView() else: self.BtnsWidget.dateRangeEdit.setDate([ minData.toString("yyyy-MM-dd"), maxData.toString("yyyy-MM-dd") ]) if self.relationState: self.rangeSig.emit([ minData.toString("yyyy-MM-dd HH:mm:ss"), maxData.toString("yyyy-MM-dd HH:mm:ss"), ]) # Change positions of rectangles in scaling def resizeEvent(self, event): super().resizeEvent(event) rect = self.chart().plotArea() self.parentRect.setRect(rect) self.getRangePoints() topRight = self.chart().plotArea().topRight() x = int(topRight.x()) y = int(topRight.y()) self.BtnsWidget.setGeometry(QRect(x - 420, 0, 420, 23)) if self.RangeItem.isVisible(): self.restore_rubber_band() self.save_current_rubber_band() self.RangeItem.updateHandlesPos() else: self.RangeItem.setRect(self.parentRect.rect()) self.RangeItem.show() self.RangeItem.setRect(self.parentRect.rect()) self.save_current_rubber_band() self.RangeItem.updateHandlesPos() self.apply_nice_numbers() # Restore to original positions of rectangles def restore_rubber_band(self): view_top_left = self.chart_to_view_point(self.m_chartRectF.topLeft()) view_bottom_right = self.chart_to_view_point( self.m_chartRectF.bottomRight()) self.m_rubberBandOrigin = view_top_left height = self.chart().plotArea().height() rect = QRectF() rect.setTopLeft(view_top_left) rect.setBottomRight(view_bottom_right) rect.setHeight(height) self.RangeItem.setRect(rect) # Adjust display coordinates of axes automatically def apply_nice_numbers(self): axes_list = self.chart().axes() for value_axis in axes_list: if value_axis: pass
class AerialWareWidget(QWidget): """Core of AerialWare. Place it into your application, and you're good to go. Constructor args: getResultsAfterCompletion -- signifies if it's needed to get results. Already set to True if you use AerialWare as a module. Signals: done -- emitted when user is done with the program. """ done = pyqtSignal() def __init__(self, getResultsAfterCompletion=False): super().__init__() # Set flag to return results after user is done with the program. self.getResultsAfterCompletion = getResultsAfterCompletion # Load UI from file loadUi(programPath + "/ui/form.ui", self) # Create scene self.scene = _QCustomScene() self.Image.setScene(self.scene) # Use antialiasing self.Image.setRenderHint(QPainter.Antialiasing) # Values from step 4. We need to set up something to change languages. self.maxHorizontal = self.maxVertical = 0 # Set validators v = QDoubleValidator() v.setBottom(0.00000001) v.setNotation(QDoubleValidator.StandardNotation) v2 = QDoubleValidator() v2.setNotation(QDoubleValidator.StandardNotation) for k in self.__dict__: obj = self.__dict__[k] tp = type(obj) if tp == QLineEdit: obj.setValidator(v2) # And set font size of labels elif tp == QLabel: font = obj.font() font.setPointSize(11) obj.setFont(font) self.editZoom.setValidator(v) self.editRes.setValidator(v) self.editHeight.setValidator(v) self.xDelimiter.setValidator(v) self.yDelimiter.setValidator(v) # Connect events self.btnOpenImage.clicked.connect(self.__loadImage) self.btnNext.clicked.connect(self.__stepTwo) self.btnIncreaseZoom.clicked.connect(self.__increaseZoom) self.btnDecreaseZoom.clicked.connect(self.__decreaseZoom) self.editZoom.textEdited.connect(self.__setZoom) # Load languages langDir = programPath + "/lang/" self.lastLang = langDir + ".LastLang" for lang in os.listdir(langDir): ext = os.path.splitext(lang) if os.path.isfile(langDir + lang) and ext[1] == ".py": spec = importlib.util.spec_from_file_location( "lang", langDir + lang) lang = importlib.util.module_from_spec(spec) spec.loader.exec_module(lang) name = lang.name self.comboLang.addItem(name, lang) self.comboLang.currentIndexChanged.connect(self.__changeLanguage) # Try to use previously selected language or English. # English is a fallback language. If it's not found, display error message and exit. index = self.comboLang.findText("English") if index == -1: QMessageBox( QMessageBox.Critical, "AerialWare - Error", "English localization not found. It should be in file " + langDir + "english.py. AerialWare uses it as base localization. Please, download AerialWare again." ).exec() exit() self.comboLang.setCurrentIndex(index) self.lang = _LanguageChanger( self.comboLang.currentData()) # Create language changer try: file = open(self.lastLang, "r") index = self.comboLang.findText(file.readline()) file.close() if index != -1: self.comboLang.setCurrentIndex(index) except: pass self.__changeLanguage() # Initialize step 1 self.__stepOne() ################## def __stepOne(self): """Step 1 -- loading image. """ self.__disableItems() # If user opened image with this program try: self.__loadImage(argv[1]) except IndexError: self.__loadImage(programPath + "/ui/img/logo.png") def loadImageFromFile(self, path: str): """Loads image from given path and jumps to Step 2 """ img = QPixmap(path) self.__loadQPixmap(img) self.__stepTwo() def loadImageFromQImage(self, image: QImage): """Loads image from given QImage and jumps to Step 2 """ self.__loadQPixmap(QPixmap.fromImage(image)) self.__stepTwo() def loadImageFromQPixmap(self, pixmap: QPixmap): """Loads image from given QPixmap and jumps to Step 2 """ self.__loadQPixmap(pixmap) self.__stepTwo() def __loadImage(self, path=None): # Open file dialog and get path or try to use user's path if path == None or not path: file = QFileDialog.getOpenFileName( self, self.lang.openImage, "", self.lang.images + " (*.jpg *.jpeg *.png *.bmp);;" + self.lang.allFiles + " (*)")[0] else: file = path # Create a pixmap from file img = QPixmap(file) self.__loadQPixmap(img) def __loadQPixmap(self, img: QPixmap): """Draws QPixmap or raises an error message """ if not img.isNull(): self.scene.clear() item = QGraphicsPixmapItem(img) self.height = img.height() self.width = img.width() self.scene.addItem(item) self.__enableItems() elif file != "": self.__disableItems() msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowTitle(self.lang.invalidImageTitle) msg.setText(self.lang.invalidImage) msg.exec_() ################## def __stepTwo(self): """Step 2 -- Needed only for first click. Step 3 does all the job. """ self.__turnPage() self.btnNext.disconnect() self.btnNext.clicked.connect(self.__stepThree) ################## def __stepThree(self): """Step 3 -- Do basically everything. """ # Validate data def setError(e): """Displays string e. """ self.lblDataError.setText("<html><head/><body><div>" + self.lang.errData + e + "</div></body></html>") # Try to read data. We need to create fields, because this will be used later in other functions. self.xTL = self.__sfloat(self.xTopLeft.text()) self.xTR = self.__sfloat(self.xTopRight.text()) self.xBL = self.__sfloat(self.xBottomLeft.text()) self.xBR = self.__sfloat(self.xBottomRight.text()) self.yTL = self.__sfloat(self.yTopLeft.text()) self.yTR = self.__sfloat(self.yTopRight.text()) self.yBL = self.__sfloat(self.yBottomLeft.text()) self.yBR = self.__sfloat(self.yBottomRight.text()) self.xD = self.__sfloat(self.xDelimiter.text()) self.yD = self.__sfloat(self.yDelimiter.text()) # Convert delimiters to pixels. # Value of another coordinate doesn't matter. # If you'll draw a random line and draw crossing lines parallel to one of axes with same distance between these lines by another axis, you'll split your random line into equal pieces. # See for yourself: # Y # ^ ___\_____ # | ____\____ # | _____\___ # | \ # |-----------> X # You can try it on paper or prove this "theorem" doing some math. try: self.xDTop = self.width / abs(self.xTL - self.xTR) * self.xD self.xDBottom = self.width / abs(self.xBL - self.xBR) * self.xD self.yDLeft = self.height / abs(self.yTL - self.yBL) * self.yD self.yDRight = self.height / abs(self.yTR - self.yBR) * self.yD except ZeroDivisionError: setError(self.lang.errCorners) return # Now do the same stuff, but find how much pixels in one degree. # We won't find absolute value for subtraction because axes of image in geographic system may be not codirectional to axes of image in Qt system. self.topX = (self.xTR - self.xTL) / self.width self.bottomX = (self.xBR - self.xBL) / self.width self.leftY = (self.yTL - self.yBL) / self.height self.rightY = (self.yTR - self.yBR) / self.height # Sides of image in geographic system self.top = QLineF(self.xTL, self.yTL, self.xTR, self.yTR) self.bottom = QLineF(self.xBL, self.yBL, self.xBR, self.yBR) self.left = QLineF(self.xTL, self.yTL, self.xBL, self.yBL) self.right = QLineF(self.xTR, self.yTR, self.xBR, self.yBR) errors = "" if self.xDTop > self.width: errors += self.lang.errLongTop + "<br>" if self.xDBottom > self.width: errors += self.lang.errLongBottom + "<br>" if self.yDLeft > self.height: errors += self.lang.errLatLeft + "<br>" if self.yDRight > self.height: errors += self.lang.errLatRight + "<br>" if errors != "": setError(self.lang.errSides + "<br>" + errors) return # Check if given coordinates form 8-shaped figure if (self.xTL > self.xTR and self.xBL < self.xBR) or ( self.xTL < self.xTR and self.xBL > self.xBR) or ( self.yTL > self.yBL and self.yTR < self.yBR) or (self.yTL < self.yBL and self.yTR > self.yTL): choice = QMessageBox(QMessageBox.Warning, "AerialWare", self.lang.warningCoordinates, QMessageBox.Yes | QMessageBox.No).exec() if choice == QMessageBox.No: return # Draw grid # Set points for grid # Points will look like: # [point, point, ...], # [point, point, ...], ... pointRows = [] # Let x1, y1; x2, y2 be vertical line # and x3, y3; x4, y4 be horizontal line. # Thus, y1 and y2 should be always on top and bottom; # x3, x4 -- on left and right y1, y2, x3, x4 = 0, self.height, 0, self.width # So we have to change: # for vertical line: x1, x2 # for horizontal line: y3, y4 y3 = y4 = 0 # Initial values for horizontal line # Move horizontal line while y3 <= self.height + self.yDLeft / 2 or y4 <= self.height + self.yDRight / 2: x1 = x2 = 0 # Initial values for vertical line # Move vertical line points = [] while x1 <= self.width + self.xDTop / 2 or x2 <= self.width + self.xDBottom / 2: point = QPointF() QLineF.intersect(QLineF(x1, y1, x2, y2), QLineF(x3, y3, x4, y4), point) points.append(point) x1 += self.xDTop x2 += self.xDBottom if points != []: pointRows.append(points) y3 += self.yDLeft y4 += self.yDRight # If nothing has been added if points == []: setError(self.lang.errPoints) return # Get scene geometry to preserve scene expanding rect = self.scene.sceneRect() # And add bounds for the grid self.bounds = QGraphicsRectItem(rect) self.bounds.setFlag(self.bounds.ItemClipsChildrenToShape) self.scene.addItem(self.bounds) # Create polygons from points # We'll recheck previous item i = 1 # Rows while i < len(pointRows): j = 1 # Points while j < len(pointRows[i]): # Add points in following order: top left, top right, bottom right, bottom left points = [ pointRows[i - 1][j - 1], pointRows[i - 1][j], pointRows[i][j], pointRows[i][j - 1] ] # We're assigning self.bounds as parent implicitly, so we shouldn't add polygon to scene by ourselves. poly = _QCustomGraphicsPolygonItem(QPolygonF(points), self.bounds) poly.setRowCol(i - 1, j - 1) j += 1 i += 1 # Restore scene geometry self.scene.setSceneRect(rect) self.__turnPage() self.btnNext.disconnect() self.btnNext.clicked.connect(self.__stepFour) ################## def __stepFour(self): """Step 4 -- Do hardware calculations. """ # If nothing has been selected, display error message. if self.scene.selectedItems() == []: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("AerialWare") msg.setText(self.lang.errEmptySelection) msg.exec_() return self.__turnPage() self.scene.setEnabled(False) # Find biggest vertical and horizontal lines of bounding rect of every polygon for square in self.scene.selectedItems(): pointsOrig = square.polygon() pointsConv = [] for point in pointsOrig: pointsConv.append(self.pxToDeg(point.x(), point.y())) points = QPolygonF(QPolygonF(pointsConv).boundingRect()) # Because top = bottom and left = right we can use only one of each side for comparison. # Points of polygon goes clockwise from top left corner. lenTop = self.__lenMeters(points[0], points[1]) lenRight = self.__lenMeters(points[1], points[2]) self.maxHorizontal = max(self.maxHorizontal, lenTop) self.maxVertical = max(self.maxVertical, lenRight) self.__changeLanguage( ) # Need to change language because we need to insert max values into caption # Connect slots for calculations self.editRes.textEdited.connect(self.__calculateResolution) self.editHeight.textEdited.connect(self.__calculateFocalLength) # Call this slots to fill values with zeros self.__calculateResolution() self.__calculateFocalLength() # Change text on 'Next' button and connect it to 'save' method. if self.getResultsAfterCompletion: nextText = self.lang.done else: nextText = self.lang.save self.btnNext.setText(nextText) self.btnNext.disconnect() self.btnNext.clicked.connect(self.__save) def __calculateResolution(self): """Calculates camera resolution based on given deg/px ratio. """ self.camRatio = self.__sfloat(self.editRes.text()) try: self.camWidth = int(self.maxHorizontal / self.camRatio) self.camHeight = int(self.maxVertical / self.camRatio) except ZeroDivisionError: self.camWidth = 0 self.camHeight = 0 self.lblCamRes.setText(f"{self.camWidth}x{self.camHeight}") def __calculateFocalLength(self): """Calculates focal length based on given flight height. """ self.flightHeight = self.__sfloat(self.editHeight.text()) try: self.focalLength = self.flightHeight / (self.camRatio * 1000) except ZeroDivisionError: self.focalLength = 0 self.lblFocalResult.setText(str(self.focalLength)) ################## def __save(self): """If AerialWare has been used as a module, emits signal 'done'. Saves user results into SVG and makes report otherwise. """ # Variables for report self.pointsMeridian = "" self.pointsHorizontal = "" # Total lengths of paths. Used in report and methods. self.lenMeridian = 0 self.lenMeridianWithTurns = 0 self.lenHorizontal = 0 self.lenHorizontalWithTurns = 0 # Fields for methods self.pathMeridianPointsPx = [] self.pathMeridianPointsDeg = [] self.pathMeridianLinesPx = [] self.pathMeridianLinesWithTurnsPx = [] self.pathMeridianLinesDeg = [] self.pathMeridianLinesWithTurnsDeg = [] self.pathHorizontalPointsPx = [] self.pathHorizontalPointsDeg = [] self.pathHorizontalLinesPx = [] self.pathHorizontalLinesWithTurnsPx = [] self.pathHorizontalLinesDeg = [] self.pathHorizontalLinesWithTurnsDeg = [] # Process each line def processLines(lines, isMeridian=False): i = 2 # Counter for report isEven = False # If current line is even we must swap it's points. for line in lines: linePx = line.line() p1 = linePx.p1() p2 = linePx.p2() p1Deg = self.pxToDeg(p1.x(), p1.y()) p2Deg = self.pxToDeg(p2.x(), p2.y()) lineDeg = QLineF(p1Deg, p2Deg) lineLength = self.__lenMeters(p1Deg, p2Deg) if isMeridian: self.pathMeridianLinesWithTurnsPx.append(linePx) self.pathMeridianLinesWithTurnsDeg.append(lineDeg) self.lenMeridianWithTurns += lineLength else: self.pathHorizontalLinesWithTurnsPx.append(linePx) self.pathHorizontalLinesWithTurnsDeg.append(lineDeg) self.lenHorizontalWithTurns += lineLength if line.pen().style( ) == Qt.SolidLine: # Check if current line doesn't represent turn if isEven: p1, p2, p1Deg, p2Deg = p2, p1, p2Deg, p1Deg point = f'"{i - 1}","{p1Deg.x()}","{p1Deg.y()}"\n' + f'"{i}","{p2Deg.x()}","{p2Deg.y()}"\n' if isMeridian: self.pointsMeridian += point self.pathMeridianPointsPx.extend([p1, p2]) self.pathMeridianPointsDeg.extend([p1Deg, p2Deg]) self.pathMeridianLinesPx.append(linePx) self.pathMeridianLinesDeg.append(lineDeg) self.lenMeridian += lineLength else: self.pointsHorizontal += point self.pathHorizontalPointsPx.extend([p1, p2]) self.pathHorizontalPointsDeg.extend([p1Deg, p2Deg]) self.pathHorizontalLinesPx.append(linePx) self.pathHorizontalLinesDeg.append(lineDeg) self.lenHorizontal += lineLength isEven = not isEven i += 2 processLines(self.scene.getMeridianLines(), True) processLines(self.scene.getHorizontalLines()) if self.getResultsAfterCompletion: self.done.emit() return self.__disableItems() # Make report pointHeader = f'"{self.lang.repPoint}","{self.lang.lblLatitude}","{self.lang.lblLongitude}"\n' if self.lenHorizontalWithTurns > self.lenMeridianWithTurns: directionWithTurns = self.lang.repFlyMeridians elif self.lenHorizontalWithTurns < self.lenMeridianWithTurns: directionWithTurns = self.lang.repFlyHorizontals else: directionWithTurns = self.lang.repFlyEqual if self.lenHorizontal > self.lenMeridian: directionWithoutTurns = self.lang.repFlyMeridians elif self.lenHorizontal < self.lenMeridian: directionWithoutTurns = self.lang.repFlyHorizontals else: directionWithoutTurns = self.lang.repFlyEqual report = ( f'"{self.lang.repCornersDescription}"\n' f'"{self.lang.lblCorner}","{self.lang.lblLongitude}","{self.lang.lblLatitude}"\n' f'"{self.lang.lblTopLeft}","{self.xTL}","{self.yTL}"\n' f'"{self.lang.lblTopRight}","{self.xTR}","{self.yTR}"\n' f'"{self.lang.lblBottomLeft}","{self.xBL}","{self.yBL}"\n' f'"{self.lang.lblBottomRight}","{self.xBR}","{self.yBR}"\n' f'"{self.lang.lblDelimiters}","{self.xD}","{self.yD}"\n\n' f'"{self.lang.repTotalWithTurns}"\n' f'"{self.lang.repByMeridians}","{self.lenMeridianWithTurns}"\n' f'"{self.lang.repByHorizontals}","{self.lenHorizontalWithTurns}"\n' f'"{self.lang.repBetterFlyBy}","{directionWithTurns}"\n\n' f'"{self.lang.repTotalWithoutTurns}"\n' f'"{self.lang.repByMeridians}","{self.lenMeridian}"\n' f'"{self.lang.repByHorizontals}","{self.lenHorizontal}"\n' f'"{self.lang.repBetterFlyBy}","{directionWithoutTurns}"\n\n' f'"{self.lang.repAerialParams}"\n' f'"{self.lang.repArea}","{self.maxHorizontal}","x","{self.maxVertical}"\n' f'"{self.lang.lblDesiredRes}","{self.camRatio}"\n' f'"{self.lang.lblRes}","{self.camWidth}","x","{self.camHeight}"\n' f'"{self.lang.lblHeight}","{self.flightHeight}"\n' f'"{self.lang.lblFocal}","{self.focalLength}"\n\n' f'"{self.lang.repMeridianPoints}"\n' + pointHeader + self.pointsMeridian + f'\n"{self.lang.repHorizontalPoints}"\n' + pointHeader + self.pointsHorizontal) # Save image self.__disableItems() file = QFileDialog.getSaveFileName(self, self.lang.saveFile, "", self.lang.vectorImage + " (*.svg)")[0] if file == "": self.__enableItems() return # And choose where to save report reportName = "" while reportName == "": reportName = QFileDialog.getSaveFileName( self, self.lang.saveFile, "", self.lang.table + " (*.csv)")[0] if reportName == "": msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText(self.lang.errSaveBoth) msg.exec_() rect = self.scene.sceneRect() gen = QSvgGenerator() gen.setFileName(file) gen.setSize(rect.size().toSize()) gen.setViewBox(rect) gen.setTitle("Flight paths generated by AerialWare") gen.setDescription(gen.title()) # Notice: painting will temporarily freeze application because QGraphicsScene::render is not thread safe. # Don't try putting this into python's threads and QThread, doesn't work, I've tried, trust me XD painter = QPainter(gen) self.scene.render(painter) painter.end() # Save report reportFile = open(reportName, "w") reportFile.write(report) reportFile.close() self.__enableItems() ################## # API def getPathByMeridiansPointsPx(self): """Returns list with points in pixels representing flight path by meridians. Please note: all points are sorted as a plane should fly. """ return self.pathMeridianPointsPx def getPathByMeridiansPointsDeg(self): """Returns list with points in geographic coordinate system representing flight path by meridians. Please note: all points are sorted as a plane should fly. Looks like: [QPointF(long, lat), QPointF(long, lat), ...] """ return self.pathMeridianPointsDeg def getPathByMeridiansLinesPx(self): """Returns list with lines in pixels coordinates without turns representing flight path by meridians. Please note: all points of lines are sorted as a plane should fly. """ return self.pathMeridianLinesPx def getPathByMeridiansLinesWithTurnsPx(self): """Returns list with lines in pixels with turns representing flight path by meridians. Please note: points of lines are NOT sorted as a plane should fly. You can use even lines, as they're representing turns, to sort things out. Or just get path without turns. """ return self.pathMeridianLinesWithTurnsPx def getPathByMeridiansLinesDeg(self): """Returns list with lines in degrees without turns representing flight path by meridians. Please note: all points of lines are sorted as a plane should fly. """ return self.pathMeridianLinesDeg def getPathByMeridiansLinesWithTurnsDeg(self): """Returns list with lines in degrees with turns representing flight path by meridians. Please note: points of lines are NOT sorted as a plane should fly. You can use even lines, as they're representing turns, to sort things out. Or just get path without turns. """ return self.pathMeridianLinesWithTurnsDeg # Horizontals def getPathByHorizontalsPointsPx(self): """Returns list with points in pixels representing flight path by horizontals. """ return self.pathHorizontalPointsPx def getPathByHorizontalsPointsDeg(self): """Returns list with points in geographic coordinate system representing flight path by horizontals. Looks like: [QPointF(long, lat), QPointF(long, lat), ...] """ return self.pathHorizontalPointsDeg def getPathByHorizontalsLinesPx(self): """Returns list with lines in pixels coordinates without turns representing flight path by horizontals. """ return self.pathHorizontalLinesPx def getPathByHorizontalsLinesWithTurnsPx(self): """Returns list with lines in pixels with turns representing flight path by horizontals. """ return self.pathHorizontalLinesWithTurnsPx def getPathByHorizontalsLinesDeg(self): """Returns list with lines in degrees without turns representing flight path by horizontals. """ return self.pathHorizontalLinesDeg def getPathByHorizontalsLinesWithTurnsDeg(self): """Returns list with lines in degrees with turns representing flight path by horizontals. """ return self.pathHorizontalLinesWithTurnsDeg # Lengths def getPathLengthByMeridians(self): """Returns length of path by meridians without turns in meters. """ return self.lenMeridian def getPathLengthByMeridiansWithTurns(self): """Returns length of path by meridians with turns in meters. This value is approximate. """ return self.lenMeridianWithTurns def getPathLengthByHorizontals(self): """Returns length of path by horizontal without turns in meters. """ return self.lenHorizontal def getPathLengthByHorizontalsWithTurns(self): """Returns length of path by horizontal with turns in meters. This value is approximate. """ return self.lenHorizontalWithTurns # Aerial parameters def getMaxArea(self): """Returns maximum area in meters to be captured in dict: {"w": width, "h": height} """ return {"w": self.maxHorizontal, "h": self.maxVertical} def getCameraRatio(self): """Returns m/px ratio entered by user. """ return self.camRatio def getCameraResolution(self): """Returns camera resolution in dict: {"w": width, "h": height} """ return {"w": self.camWidth, "h": self.camHeight} def getFlightHeight(self): """Returns flight height in meters. """ return self.flightHeight def getFocalLength(self): """Returns focal length in mm. """ return self.focalLength def pxToDeg(self, x, y): """Transforms pixel coordinates of point to Geographic coordinate system. Args: x, y -- X and Y coordinates of point to process. Returns: QPointF(long, lat) -- longitude and latitude coordinates of given point """ # Please check comments in __stepThree() where we converting step in degrees to pixels in order to understand what we're doing here. # Convert given coordinates to degrees relative to the sides. topX = self.xTL + self.topX * x bottomX = self.xBL + self.bottomX * x # We subtract because Y and lat are not codirectional by default leftY = self.yTL - self.leftY * y rightY = self.yTR - self.rightY * y # Find another coordinate for each side by drawing straight line with calculated coordinate for both points. # Intersection of this line and side will give needed point. # Looks like this: # Y # ^ # | \ <- This is side # | \ # |---*----- <- This line is crossing point relative to the side # | \ # | \ # |------------------> X # Containers for points and result. top, bottom, left, right, res = QPointF(), QPointF(), QPointF( ), QPointF(), QPointF() QLineF.intersect(QLineF(topX, 0, topX, 1), self.top, top) QLineF.intersect(QLineF(bottomX, 0, bottomX, 1), self.bottom, bottom) QLineF.intersect(QLineF(0, leftY, 1, leftY), self.left, left) QLineF.intersect(QLineF(0, rightY, 1, rightY), self.right, right) # We've got coordinates for each side where given point should lie. # Let's draw lines throgh them like this: # ________________________ # | \ | # |_____\__________________| # | \ | # |_______\________________| # Lines are drawn in geographic system, not in pixels. # Their intersection will return given point in geographic system. QLineF.intersect(QLineF(top, bottom), QLineF(left, right), res) return res def __lenMeters(self, p1, p2): """Calculates length in meters of line in geographic system using haversine formula. Args: QPointF p1, p2 -- points of line. """ f1, f2 = math.radians(p1.y()), math.radians(p2.y()) df = f2 - f1 dl = math.radians(p2.x() - p1.x()) a = math.sin( df / 2)**2 + math.cos(f1) * math.cos(f2) * math.sin(dl / 2)**2 # First value is radius of Earth return 6371000 * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) ################## # Misc stuff def __enableItems(self): """Enables controls """ self.scene.setEnabled(True) self.editZoom.setEnabled(True) self.btnDecreaseZoom.setEnabled(True) self.btnIncreaseZoom.setEnabled(True) self.btnNext.setEnabled(True) def __disableItems(self): """Disables controls """ self.scene.setEnabled(False) self.editZoom.setEnabled(False) self.btnDecreaseZoom.setEnabled(False) self.btnIncreaseZoom.setEnabled(False) self.btnNext.setEnabled(False) def __changeLanguage(self): """Changes app language. Uses _LanguageChanger, check it's description for more. """ # Get language self.lang.setLanguage(self.comboLang.currentData()) langName = self.comboLang.currentText() # Save this language to file file = open(self.lastLang, "w") file.write(langName) file.close() # Change text of labels and buttons self.lblZoom.setText(self.lang.lblZoom) self.lblCorner.setText(self.lang.lblCorner) self.lblLongitude.setText(self.lang.lblLongitude) self.lblLatitude.setText(self.lang.lblLatitude) self.lblTopLeft.setText(self.lang.lblTopLeft) self.lblTopRight.setText(self.lang.lblTopRight) self.lblBottomLeft.setText(self.lang.lblBottomLeft) self.lblBottomRight.setText(self.lang.lblBottomRight) self.lblDelimiters.setText(self.lang.lblDelimiters) self.lblRes.setText(self.lang.lblRes) self.lblDesiredRes.setText(self.lang.lblDesiredRes) self.lblHeight.setText(self.lang.lblHeight) self.lblFocal.setText(self.lang.lblFocal) self.btnOpenImage.setText(self.lang.btnOpenImage) # Change text of task labels start = "<html><head/><body>" end = "</body></html>" self.lblTask1.setText(start + f""" <p style='text-align: center;'><b>{self.lang.heading}</b></p> <p><b>{self.lang.headingAbout}</b></p> <p>{self.lang.about1}</p> <p>{self.lang.about2}</p> <p><b>{self.lang.workingTitle}</b></p> <p>{self.lang.working}</p> <p><b>{self.lang.thisStepBold}</b>{self.lang.thisStep}</p> <p><b>{self.lang.noteStep1Bold}</b>{self.lang.noteStep1}</p> """ + end) self.lblTask2.setText(start + f""" <p><b>{self.lang.setTitle}</b></p> <ul> <li>{self.lang.coordinates}</li> <li>{self.lang.delimiters}</li> </ul> """ + end) self.lblTask3.setText(start + f""" <p>{self.lang.intro}<b>{self.lang.clickBold}</b></p> <p>{self.lang.path}</p> <p><b>{self.lang.legendTitle}</b></p> <ul> <li><span style="color: red;">{self.lang.red}</span>{self.lang.line1}<span style="color: green;">{self.lang.green}</span>{self.lang.line2}</li> <li><b>{self.lang.dashedBold}</b>{self.lang.line3}</li> </ul> """ + end) if self.getResultsAfterCompletion: btnText = self.lang.done s4Text = self.lang.s4Done else: btnText = self.lang.save s4Text = start + f""" <p>{self.lang.s4Save}</p> <p><b>{self.lang.noteStep4Bold}</b>{self.lang.noteStep4} ¯\_(ツ)_/¯</p> """ + end if self.Steps.currentIndex() == self.Steps.count(): self.btnNext.setText(btnText) else: self.btnNext.setText(self.lang.btnNext) self.lblTask4_1.setText( f"{self.lang.s4Text1P1}{self.maxHorizontal}x{self.maxVertical}{self.lang.s4Text1P2}" ) self.lblTask4_2.setText(self.lang.s4Text2) self.lblTask4_3.setText(s4Text) # On Step 3 there are dynamically outputed errors. In order to update it we can manually reset it or just kill two birds with one stone by recalling this step in cost of couple milliseconds. if self.Steps.currentIndex() == 1 and self.lblDataError.text() != "": self.__stepThree() def __turnPage(self): """Turns page of "Steps" """ self.Steps.setCurrentIndex(self.Steps.currentIndex() + 1) def __increaseZoom(self): """Zooms image in """ value = round(self.__sfloat(self.editZoom.text()) + 10, 2) self.editZoom.setText(str(value)) self.__setZoom() def __decreaseZoom(self): """Zooms image out """ value = self.__sfloat(self.editZoom.text()) if value <= 10: value = round(value / 2, 2) elif value <= 0: return else: value -= 10 self.editZoom.setText(str(value)) self.__setZoom() def __setZoom(self): """Sets zoom """ value = self.__sfloat(self.editZoom.text()) * 0.01 self.Image.resetTransform() self.Image.scale(value, value) def __sfloat(self, s): """Converts string to float. If can't convert will return 0.0. Used as shorthand. """ try: ss = float(s) except: return 0.0 return ss
class CameraView(QGraphicsObject): font: QFont = QFont("monospace", 16) stick_link_requested = pyqtSignal(StickWidget) stick_context_menu = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') stick_widgets_out_of_sync = pyqtSignal('PyQt_PyObject') visibility_toggled = pyqtSignal() synchronize_clicked = pyqtSignal('PyQt_PyObject') previous_photo_clicked = pyqtSignal('PyQt_PyObject') next_photo_clicked = pyqtSignal('PyQt_PyObject') sync_confirm_clicked = pyqtSignal('PyQt_PyObject') sync_cancel_clicked = pyqtSignal('PyQt_PyObject') first_photo_clicked = pyqtSignal('PyQt_PyObject') enter_pressed = pyqtSignal() def __init__(self, scale: float, parent: Optional[QGraphicsItem] = None): QGraphicsObject.__init__(self, parent) self.current_highlight_color = QColor(0, 0, 0, 0) self.current_timer = -1 self.scaling = scale self.pixmap = QGraphicsPixmapItem(self) self.stick_widgets: List[StickWidget] = [] self.link_cam_text = QGraphicsSimpleTextItem("Link camera...", self) self.link_cam_text.setZValue(40) self.link_cam_text.setVisible(False) self.link_cam_text.setFont(CameraView.font) self.link_cam_text.setPos(0, 0) self.link_cam_text.setPen(QPen(QColor(255, 255, 255, 255))) self.link_cam_text.setBrush(QBrush(QColor(255, 255, 255, 255))) self.show_add_buttons = False self.camera = None self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.show_stick_widgets = False self.setAcceptHoverEvents(True) self.stick_edit_mode = False self.original_pixmap = self.pixmap.pixmap() self.hovered = False self.mode = 0 # TODO make enum Mode self.click_handler = None self.double_click_handler: Callable[[int, int], None] = None self.stick_widget_mode = StickMode.Display self.highlight_animation = QPropertyAnimation(self, b"highlight_color") self.highlight_animation.setEasingCurve(QEasingCurve.Linear) self.highlight_animation.valueChanged.connect( self.handle_highlight_color_changed) self.highlight_rect = QGraphicsRectItem(self) self.highlight_rect.setZValue(4) self.highlight_rect.setPen(QPen(QColor(0, 0, 0, 0))) self.title_btn = Button('btn_title', '', parent=self) self.title_btn.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) self.title_btn.setZValue(5) self.title_btn.setVisible(False) self.sticks_without_width: List[Stick] = [] self.current_image_name: str = '' self.control_widget = ControlWidget(parent=self) self.control_widget.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) self.control_widget.setVisible(True) self._connect_control_buttons() self.image_available = True self.blur_eff = QGraphicsBlurEffect() self.blur_eff.setBlurRadius(5.0) self.blur_eff.setEnabled(False) self.pixmap.setGraphicsEffect(self.blur_eff) self.overlay_message = QGraphicsSimpleTextItem('not available', parent=self) font = self.title_btn.font font.setPointSize(48) self.overlay_message.setFont(font) self.overlay_message.setBrush(QBrush(QColor(200, 200, 200, 200))) self.overlay_message.setPen(QPen(QColor(0, 0, 0, 200), 2.0)) self.overlay_message.setVisible(False) self.overlay_message.setZValue(6) self.stick_box = QGraphicsRectItem(parent=self) self.stick_box.setFlag(QGraphicsItem.ItemIsMovable, True) self.stick_box.setVisible(False) self.stick_box_start_pos = QPoint() def _connect_control_buttons(self): self.control_widget.synchronize_btn.clicked.connect( lambda: self.synchronize_clicked.emit(self)) self.control_widget.prev_photo_btn.clicked.connect( lambda: self.previous_photo_clicked.emit(self)) self.control_widget.next_photo_btn.clicked.connect( lambda: self.next_photo_clicked.emit(self)) self.control_widget.accept_btn.clicked.connect( lambda: self.sync_confirm_clicked.emit(self)) self.control_widget.cancel_btn.clicked.connect( lambda: self.sync_cancel_clicked.emit(self)) self.control_widget.first_photo_btn.clicked.connect( lambda: self.first_photo_clicked.emit(self)) def paint(self, painter: QPainter, option: PyQt5.QtWidgets.QStyleOptionGraphicsItem, widget: QWidget): if self.pixmap.pixmap().isNull(): return painter.setRenderHint(QPainter.Antialiasing, True) if self.show_stick_widgets: brush = QBrush(QColor(255, 255, 255, 100)) painter.fillRect(self.boundingRect(), brush) if self.mode and self.hovered: pen = QPen(QColor(0, 125, 200, 255)) pen.setWidth(4) painter.setPen(pen) def boundingRect(self) -> PyQt5.QtCore.QRectF: return self.pixmap.boundingRect().united( self.title_btn.boundingRect().translated(self.title_btn.pos())) def initialise_with(self, camera: Camera): if self.camera is not None: self.camera.stick_added.disconnect(self.handle_stick_created) self.camera.sticks_added.disconnect(self.handle_sticks_added) self.camera.stick_removed.disconnect(self.handle_stick_removed) self.camera.sticks_removed.disconnect(self.handle_sticks_removed) self.camera.stick_changed.disconnect(self.handle_stick_changed) self.camera = camera self.prepareGeometryChange() self.set_image(camera.rep_image, Path(camera.rep_image_path).name) self.title_btn.set_label(self.camera.folder.name) self.title_btn.set_height(46) self.title_btn.fit_to_contents() self.title_btn.set_width(int(self.boundingRect().width())) self.title_btn.setPos(0, self.boundingRect().height()) self.control_widget.title_btn.set_label(self.camera.folder.name) self.camera.stick_added.connect(self.handle_stick_created) self.camera.sticks_added.connect(self.handle_sticks_added) self.camera.stick_removed.connect(self.handle_stick_removed) self.camera.sticks_removed.connect(self.handle_sticks_removed) self.camera.stick_changed.connect(self.handle_stick_changed) self.control_widget.set_font_height(32) self.control_widget.set_widget_height( self.title_btn.boundingRect().height()) self.control_widget.set_widget_width(int(self.boundingRect().width())) self.control_widget.setPos(0, self.pixmap.boundingRect().height() ) #self.boundingRect().height()) self.control_widget.set_mode('view') self.update_stick_widgets() def set_image(self, img: Optional[np.ndarray] = None, image_name: Optional[str] = None): if img is None: self.show_overlay_message('not available') return self.show_overlay_message(None) self.prepareGeometryChange() barray = QByteArray(img.tobytes()) image = QImage(barray, img.shape[1], img.shape[0], QImage.Format_BGR888) self.original_pixmap = QPixmap.fromImage(image) self.pixmap.setPixmap(self.original_pixmap) self.highlight_rect.setRect(self.boundingRect()) self.current_image_name = image_name def update_stick_widgets(self): stick_length = 60 for stick in self.camera.sticks: sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) stick_length = stick.length_cm self.update_stick_box() self.scene().update() def scale_item(self, factor: float): self.prepareGeometryChange() pixmap = self.original_pixmap.scaledToHeight( int(self.original_pixmap.height() * factor)) self.pixmap.setPixmap(pixmap) self.__update_title() def set_show_stick_widgets(self, value: bool): for sw in self.stick_widgets: sw.setVisible(value) self.scene().update() def hoverEnterEvent(self, e: QGraphicsSceneHoverEvent): self.hovered = True self.scene().update(self.sceneBoundingRect()) def hoverLeaveEvent(self, e: QGraphicsSceneHoverEvent): self.hovered = False self.scene().update(self.sceneBoundingRect()) def mousePressEvent(self, e: QGraphicsSceneMouseEvent): super().mousePressEvent(e) def mouseReleaseEvent(self, e: QGraphicsSceneMouseEvent): if self.mode == 1: self.click_handler(self.camera) def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent): if self.stick_widget_mode == StickMode.EditDelete: x = event.pos().toPoint().x() y = event.pos().toPoint().y() stick = self.camera.create_new_sticks( [(np.array([[x, y - 50], [x, y + 50]]), 3)], self.current_image_name)[ 0] #self.dataset.create_new_stick(self.camera) self.sticks_without_width.append(stick) def set_button_mode(self, click_handler: Callable[[Camera], None], data: str): self.mode = 1 # TODO make a proper ENUM self.click_handler = lambda c: click_handler(c, data) def set_display_mode(self): self.mode = 0 # TODO make a proper ENUM self.click_handler = None def _remove_stick_widgets(self): for sw in self.stick_widgets: sw.setParentItem(None) self.scene().removeItem(sw) sw.deleteLater() self.stick_widgets.clear() def handle_stick_created(self, stick: Stick): if stick.camera_id != self.camera.id: return sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) self.stick_widgets_out_of_sync.emit(self) self.update() def handle_stick_removed(self, stick: Stick): if stick.camera_id != self.camera.id: return stick_widget = next( filter(lambda sw: sw.stick.id == stick.id, self.stick_widgets)) self.disconnect_stick_widget_signals(stick_widget) self.stick_widgets.remove(stick_widget) stick_widget.setParentItem(None) self.scene().removeItem(stick_widget) stick_widget.deleteLater() self.update() def handle_sticks_removed(self, sticks: List[Stick]): if sticks[0].camera_id != self.camera.id: return for stick in sticks: to_remove: StickWidget = None for sw in self.stick_widgets: if sw.stick.id == stick.id: to_remove = sw break self.stick_widgets.remove(to_remove) to_remove.setParentItem(None) if self.scene() is not None: self.scene().removeItem(to_remove) to_remove.deleteLater() self.update() def handle_sticks_added(self, sticks: List[Stick]): if len(sticks) == 0: return if sticks[0].camera_id != self.camera.id: return for stick in sticks: sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) self.update_stick_box() self.stick_widgets_out_of_sync.emit(self) self.update() def connect_stick_widget_signals(self, stick_widget: StickWidget): stick_widget.delete_clicked.connect( self.handle_stick_widget_delete_clicked) stick_widget.stick_changed.connect(self.handle_stick_widget_changed) stick_widget.link_initiated.connect(self.handle_stick_link_initiated) stick_widget.right_clicked.connect( self.handle_stick_widget_context_menu) def disconnect_stick_widget_signals(self, stick_widget: StickWidget): stick_widget.delete_clicked.disconnect( self.handle_stick_widget_delete_clicked) stick_widget.stick_changed.disconnect(self.handle_stick_widget_changed) stick_widget.link_initiated.disconnect( self.handle_stick_link_initiated) stick_widget.right_clicked.disconnect( self.handle_stick_widget_context_menu) def handle_stick_widget_delete_clicked(self, stick: Stick): self.camera.remove_stick(stick) def set_stick_widgets_mode(self, mode: StickMode): self.stick_widget_mode = mode for sw in self.stick_widgets: sw.set_mode(mode) self.set_stick_edit_mode(mode == StickMode.Edit) def handle_stick_widget_changed(self, stick_widget: StickWidget): self.camera.stick_changed.emit(stick_widget.stick) def handle_stick_changed(self, stick: Stick): if stick.camera_id != self.camera.id: return sw = next( filter(lambda _sw: _sw.stick.id == stick.id, self.stick_widgets)) sw.adjust_line() sw.update_tooltip() def handle_stick_link_initiated(self, stick_widget: StickWidget): self.stick_link_requested.emit(stick_widget) def get_top_left(self) -> QPointF: return self.sceneBoundingRect().topLeft() def get_top_right(self) -> QPointF: return self.sceneBoundingRect().topRight() def highlight(self, color: Optional[QColor]): if color is None: self.highlight_animation.stop() self.highlight_rect.setVisible(False) return alpha = color.alpha() color.setAlpha(0) self.highlight_animation.setStartValue(color) self.highlight_animation.setEndValue(color) color.setAlpha(alpha) self.highlight_animation.setKeyValueAt(0.5, color) self.highlight_animation.setDuration(2000) self.highlight_animation.setLoopCount(-1) self.highlight_rect.setPen(QPen(color)) self.highlight_rect.setVisible(True) self.highlight_animation.start() @pyqtProperty(QColor) def highlight_color(self) -> QColor: return self.current_highlight_color @highlight_color.setter def highlight_color(self, color: QColor): self.current_highlight_color = color def handle_highlight_color_changed(self, color: QColor): self.highlight_rect.setBrush(QBrush(color)) self.update() def handle_stick_widget_context_menu(self, sender: Dict[str, StickWidget]): self.stick_context_menu.emit(sender['stick_widget'], self) def show_overlay_message(self, msg: Optional[str]): if msg is None: self.overlay_message.setVisible(False) self.blur_eff.setEnabled(False) return self.overlay_message.setText(msg) self.overlay_message.setPos( self.pixmap.boundingRect().center() - QPointF(0.5 * self.overlay_message.boundingRect().width(), 0.5 * self.overlay_message.boundingRect().height())) self.overlay_message.setVisible(True) self.blur_eff.setEnabled(True) def show_status_message(self, msg: Optional[str]): if msg is None: self.control_widget.set_title_text(self.camera.folder.name) else: self.control_widget.set_title_text(msg) def update_stick_box(self): left = 9000 right = 0 top = 9000 bottom = -1 for stick in self.camera.sticks: left = min(left, min(stick.top[0], stick.bottom[0])) right = max(right, max(stick.top[0], stick.bottom[0])) top = min(top, min(stick.top[1], stick.bottom[1])) bottom = max(bottom, max(stick.top[1], stick.bottom[1])) left -= 100 right += 100 top -= 100 bottom += 100 self.stick_box.setRect(left, top, right - left, bottom - top) pen = QPen(QColor(0, 100, 200, 200)) pen.setWidth(2) pen.setStyle(Qt.DashLine) self.stick_box.setPen(pen) def set_stick_edit_mode(self, is_edit: bool): if is_edit: self.update_stick_box() self.stick_box_start_pos = self.stick_box.pos() for sw in self.stick_widgets: sw.setParentItem(self.stick_box) else: offset = self.stick_box.pos() - self.stick_box_start_pos for sw in self.stick_widgets: stick = sw.stick stick.translate(np.array([int(offset.x()), int(offset.y())])) sw.setParentItem(self) sw.set_stick(stick) self.stick_box.setParentItem(None) self.stick_box = QGraphicsRectItem(self) self.stick_box.setFlag(QGraphicsItem.ItemIsMovable, True) self.stick_box.setVisible(False) self.stick_box.setVisible(is_edit) def keyPressEvent(self, event: QKeyEvent) -> None: pass def keyReleaseEvent(self, event: QKeyEvent) -> None: if event.key() in [Qt.Key_Right, Qt.Key_Tab, Qt.Key_Space]: self.control_widget.next_photo_btn.click_button(True) elif event.key() in [Qt.Key_Left]: self.control_widget.prev_photo_btn.click_button(True) elif event.key() == Qt.Key_S: self.enter_pressed.emit()
def __init__(self, part_instance: ObjectInstance, viewroot: GridRootItemT): """Summary Args: part_instance: ``ObjectInstance`` of the ``Part`` viewroot: ``GridRootItem`` parent: Default is ``None`` """ super(GridNucleicAcidPartItem, self).__init__( part_instance, viewroot) self._getActiveTool = viewroot.manager.activeToolGetter m_p = self._model_part self._controller = NucleicAcidPartItemController(self, m_p) self.scale_factor: float = self._RADIUS / m_p.radius() self.active_virtual_helix_item: GridVirtualHelixItem = None self.prexover_manager = PreXoverManager(self) self.hide() # hide while until after attemptResize() to avoid flicker # set this to a token value self._rect: QRectF = QRectF(0., 0., 1000., 1000.) self.boundRectToModel() self.setPen(getNoPen()) self.setRect(self._rect) self.setAcceptHoverEvents(True) # Cache of VHs that were active as of last call to activeGridChanged # If None, all grids will be redrawn and the cache will be filled. # Connect destructor. This is for removing a part from scenes. # initialize the NucleicAcidPartItem with an empty set of old coords self.setZValue(styles.ZPARTITEM) outline = QGraphicsRectItem(self) self.outline: QGraphicsRectItem = outline o_rect = self._configureOutline(outline) outline.setFlag(QGraphicsItem.ItemStacksBehindParent) outline.setZValue(styles.ZDESELECTOR) model_color = m_p.getColor() outline.setPen(getPenObj(model_color, _DEFAULT_WIDTH)) GC_SIZE = 10 self.grab_cornerTL: GrabCornerItem = GrabCornerItem(GC_SIZE, model_color, True, self) self.grab_cornerTL.setTopLeft(o_rect.topLeft()) self.grab_cornerBR: GrabCornerItem = GrabCornerItem(GC_SIZE, model_color, True, self) self.grab_cornerBR.setBottomRight(o_rect.bottomRight()) self.griditem: GridItem = GridItem(self, self._model_props['grid_type']) self.griditem.setZValue(1) self.grab_cornerTL.setZValue(2) self.grab_cornerBR.setZValue(2) # select upon creation for part in m_p.document().children(): if part is m_p: part.setSelected(True) else: part.setSelected(False) self.show()
class SelectAreaDialog(QDialog): finish_selecting_area = pyqtSignal(TrimmingData) def __init__(self): super().__init__() self.ui = Ui_SelectAreaDialog() self.ui.setupUi(self) self.width = 200 self.height = 200 self.h, self.w = None, None self.select_area = None self.original_image_scene = None self.size_flag = True self.select_area_label = None self.select_area_label_proxy = None self.start_position = None self.get_ng_sample_image_path() if self.h <= self.height and self.w <= self.width: self.size_flag = False if self.size_flag: self.show_select_area_at_default_position() else: self.ui.notation_label.setText('この画像サイズは十分小さいため, 画像全体でトレーニングを行います.' '\nこのままトレーニング開始ボタンを押してください.') pass self.ui.ok_button.clicked.connect(self.on_clicked_ok_button) self.ui.cancel_button.clicked.connect(self.on_clicked_cancel_button) def get_ng_sample_image_path(self): test_ng_path = str(Dataset.images_path(Dataset.Category.TEST_NG)) test_ng_images = os.listdir(test_ng_path) test_ng_images = [ img for img in test_ng_images if Path(img).suffix in ['.jpg', '.jpeg', '.png', '.gif', '.bmp'] ] if not test_ng_images: return original_image_path = os.path.join(test_ng_path, test_ng_images[0]) original_image = cv2.imread(original_image_path) h, w, c = original_image.shape self.h, self.w = h, w original_image_shape = QSize(w + 2, h + 10) original_image_item = QGraphicsPixmapItem(QPixmap(original_image_path)) original_image_item.setZValue(0) self.original_image_scene = QGraphicsScene() self.original_image_scene.addItem(original_image_item) self.ui.original_image_view.setScene(self.original_image_scene) self.ui.original_image_view.setBaseSize(original_image_shape) self.ui.original_image_view.setMaximumSize(original_image_shape) self.resize(self.w + 32, self.h + 72) def show_select_area_at_default_position(self): trimming_data = Project.latest_trimming_data() if trimming_data.position: self.start_position = QPoint(trimming_data.position[0], trimming_data.position[1]) rect = QRectF(trimming_data.position[0], trimming_data.position[1], self.width, self.height) else: self.start_position = QPoint((self.w - self.width) // 2, (self.h - self.height) // 2) rect = QRectF((self.w - self.width) // 2, (self.h - self.height) // 2, self.width, self.height) self.select_area = QGraphicsRectItem(rect) self.select_area.setZValue(1) pen = QPen(QColor('#ffa00e')) pen.setWidth(4) pen.setJoinStyle(Qt.RoundJoin) self.select_area.setPen(pen) self.select_area.setFlag(QGraphicsItem.ItemIsMovable, True) self.original_image_scene.addItem(self.select_area) self.select_area_label_proxy = QGraphicsProxyWidget(self.select_area) self.select_area_label = SelectAreaLabel() self.select_area_label.set_label() self.select_area_label_proxy.setWidget(self.select_area_label) self.select_area_label_proxy.setPos( self.select_area.boundingRect().left() + 2, self.select_area.boundingRect().bottom() - self.select_area_label.height() - 2) def on_clicked_ok_button(self): if not self.size_flag: trimming_data = TrimmingData(position=(0, 0), size=(self.w, self.h), needs_trimming=False) self.finish_selecting_area.emit(trimming_data) self.close() else: rel_position = self.select_area.pos() position = (self.start_position.x() + rel_position.x(), self.start_position.y() + rel_position.y()) if position[0] < 0 or position[ 0] > self.w - self.width - 1 or position[ 1] < 0 or position[1] > self.h - self.height - 1: print('Error: Please set area contained in the image.') self.ui.notation_label.setText('エラー: 切り取る領域は画像内に収まるようにしてください.') else: trimming_data = TrimmingData(position=position, size=(self.width, self.height), needs_trimming=True) self.finish_selecting_area.emit(trimming_data) self.close() def on_clicked_cancel_button(self): self.close() def closeEvent(self, QCloseEvent): self.close()