Esempio n. 1
0
class Node(QGraphicsEllipseItem):
    Type = QGraphicsItem.UserType + 1

    def __init__(self, index, label=None):
        super().__init__(-RADIUS, -RADIUS, 2 * RADIUS, 2 * RADIUS)

        self._edges = set()
        self._pie = []

        self._font = QApplication.font()
        self._text_color = QColor()
        self._pixmap = QPixmap()
        self._stock_polygon = NodePolygon.Circle
        self._node_polygon = QPolygonF()
        self._overlay_brush: QBrush = QBrush()

        self.id = index
        if label is None:
            self.setLabel(str(index + 1))
        else:
            self.setLabel(label)

        self.setFlags(QGraphicsItem.ItemIsMovable
                      | QGraphicsItem.ItemIsSelectable
                      | QGraphicsItem.ItemSendsScenePositionChanges)
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)

        self.setBrush(Qt.lightGray)
        self.setPen(QPen(Qt.black, 1))
        self.setZValue(10)

    def invalidateShape(self):
        # TODO: Can't find a good way to update shape
        self.prepareGeometryChange()
        rect = self.rect()
        self.setRect(QRectF())
        self.setRect(rect)

    def updateLabelRect(self):
        fm = QFontMetrics(self.font())
        width = fm.width(self.label())
        height = fm.height()
        # noinspection PyAttributeOutsideInit
        self._label_rect = QRectF(-width / 2, -height / 2, width, height)

        self.invalidateShape()

    def index(self) -> int:
        return self.id

    def radius(self) -> int:
        return int(self.rect().width() / 2)

    def setRadius(self, radius: int):
        self.prepareGeometryChange()
        self.setRect(QRectF(-radius, -radius, 2 * radius, 2 * radius))
        self.scalePolygon()

    def font(self) -> QFont:
        return self._font

    def setFont(self, font: QFont):
        self._font = font
        self.updateLabelRect()

    def textColor(self) -> QColor:
        return self._text_color

    def setTextColor(self, color: QColor):
        self._text_color = color

    # noinspection PyMethodOverriding
    def setBrush(self, brush: QBrush, autoTextColor: bool = True):
        super().setBrush(brush)

        if autoTextColor:
            # Calculate the perceptive luminance (aka luma) - human eye favors green color...
            # See https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
            color = QBrush(brush).color()
            if color.alpha() < 128:
                self._text_color = QColor(Qt.black)
            else:
                luma = (0.299 * color.red() + 0.587 * color.green() +
                        0.114 * color.blue()) / 255
                self._text_color = QColor(Qt.black) if luma > 0.5 else QColor(
                    Qt.white)

    def overlayBrush(self) -> QBrush:
        return self._overlay_brush

    def setOverlayBrush(self, brush: QBrush):
        self._overlay_brush = brush

    def label(self) -> str:
        return self._label

    def setLabel(self, label: str):
        # noinspection PyAttributeOutsideInit
        self._label = label
        self.updateLabelRect()

    def pie(self) -> list:
        return self._pie

    def setPie(self, values: list):
        if values is not None:
            sum_ = sum(values)
            values = [v / sum_ for v in values] if sum_ > 0 else []
            self._pie = values
        else:
            self._pie = []
        self.update()

    def pixmap(self):
        return self._pixmap

    def setPixmap(self, pixmap: QPixmap):
        self._pixmap = pixmap

    def setPixmapFromSmiles(self, smiles: str, size: QSize = QSize(300, 300)):
        self._pixmap = SmilesToPixmap(smiles, size)

    def setPixmapFromInchi(self, smiles: str, size: QSize = QSize(300, 300)):
        self._pixmap = InchiToPixmap(smiles, size)

    def setPixmapFromBase64(self, b64: bytes) -> None:
        pixmap = QPixmap()
        pixmap.loadFromData(base64.b64decode(b64))
        self._pixmap = pixmap

    def setPixmapFromSvg(self, svg: bytes, size: QSize = QSize(300, 300)):
        self._pixmap = SvgToPixmap(svg, size)

    def scalePolygon(self):
        rect_size = max(self.rect().width(), self.rect().height())
        polygon_size = max(self._node_polygon.boundingRect().width(),
                           self._node_polygon.boundingRect().height())
        scale = rect_size / polygon_size if polygon_size > 0. else 1.
        self._node_polygon = QTransform().scale(scale,
                                                scale).map(self._node_polygon)

    def polygon(self) -> NodePolygon:
        return self._stock_polygon

    def setPolygon(self, id: Union[NodePolygon, int]):
        if isinstance(id, int):
            id = NodePolygon(id)
        self.setCustomPolygon(NODE_POLYGON_MAP.get(id, QPolygonF()))
        self._stock_polygon = id

    def customPolygon(self) -> QPolygonF:
        return self._node_polygon

    def setCustomPolygon(self, polygon: QPolygonF):
        self.prepareGeometryChange()
        self._stock_polygon = NodePolygon.Custom
        self._node_polygon = polygon
        self.scalePolygon()
        self.invalidateShape()

    def addEdge(self, edge: Edge):
        self._edges.add(edge)

    def removeEdge(self, edge: Edge):
        self._edges.remove(edge)

    def edges(self) -> Set[Edge]:
        return self._edges

    def updateStyle(self, style: NetworkStyle, old: NetworkStyle = None):
        if old is None or self.brush().color() == old.nodeBrush().color():
            self.setBrush(style.nodeBrush(), autoTextColor=False)
            self.setTextColor(style.nodeTextColor())
        self.setPen(style.nodePen())
        self.setFont(style.nodeFont())
        self.invalidateShape()

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemScenePositionHasChanged:
            for edge in self._edges:
                edge.adjust()
        elif change == QGraphicsItem.ItemSelectedChange:
            self.setZValue(20 if value else 10)  # Bring item to front
            self.setCacheMode(self.cacheMode())  # Force redraw
        return super().itemChange(change, value)

    def mousePressEvent(self, event):
        self.update()
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.update()
        super().mouseReleaseEvent(event)

    def shape(self):
        if self._stock_polygon == NodePolygon.Circle:
            path = super().shape()
        else:
            path = QPainterPath()
            path.addPolygon(self._node_polygon)

        label_path = QPainterPath()
        label_path.addRect(self._label_rect)
        return path.united(label_path)

    # noinspection PyMethodOverriding
    def paint(self, painter, option, widget):
        scene = self.scene()
        if scene is None:
            return

        style = scene.networkStyle()

        # If selected, change brush to yellow
        if option.state & QStyle.State_Selected:
            brush = style.nodeBrush(True)
            text_color = style.nodeTextColor(True)
            if brush is None or not brush.color().isValid():
                brush = self.brush()
                text_color = self.textColor()
            painter.setBrush(brush)
            painter.setPen(style.nodePen(True))
        else:
            painter.setBrush(self.brush())
            painter.setPen(self.pen())
            text_color = self.textColor()

        # Get level of detail
        lod = option.levelOfDetailFromTransform(painter.worldTransform())
        rect = self.rect()

        if lod < 0.1:
            painter.fillRect(rect, painter.brush())
            return

        if self._stock_polygon == NodePolygon.Circle:
            # Draw ellipse
            if self.spanAngle() != 0 and abs(
                    self.spanAngle()) % (360 * 16) == 0:
                painter.drawEllipse(rect)
                if self._overlay_brush.style() != Qt.NoBrush:
                    painter.setBrush(self._overlay_brush)
                    painter.drawEllipse(rect)
            else:
                painter.drawPie(rect, self.startAngle(), self.spanAngle())
                if self._overlay_brush.style() != Qt.NoBrush:
                    painter.setBrush(self._overlay_brush)
                    painter.drawPie(rect, self.startAngle(), self.spanAngle())

        else:
            # Draw polygon
            painter.drawPolygon(self._node_polygon)

            if self._overlay_brush.style() != Qt.NoBrush:
                painter.setBrush(self._overlay_brush)
                painter.drawPolygon(self._node_polygon)

        # Draw pies if any
        if scene.pieChartsVisibility() and len(self._pie) > 0:
            start = 0.
            colors = self.scene().pieColors()
            painter.setPen(QPen(Qt.NoPen))

            if self._stock_polygon == NodePolygon.Circle:
                rect = QTransform().scale(.85, .85).mapRect(rect)
                for v, color in zip(self._pie, colors):
                    painter.setBrush(color)
                    painter.drawPie(rect, int(start * 5760), int(v * 5760))
                    start += v
            else:
                if self._stock_polygon == NodePolygon.Square:
                    rect = QTransform().scale(1.2, 1.2).mapRect(rect)

                # Set clip path for pies
                clip_path = QPainterPath()
                clip_path.addPolygon(QTransform().scale(.8, .8).map(
                    self._node_polygon))

                for v, color in zip(self._pie, colors):
                    painter.setBrush(color)
                    pie_path = QPainterPath()
                    pie_path.arcTo(rect, start * 360, v * 360)
                    painter.drawPath(clip_path.intersected(pie_path))
                    start += v

        # Draw text
        if lod > 0.4:
            bounding_rect = self.boundingRect()
            painter.setClipping(False)
            painter.setFont(self.font())
            painter.setPen(QPen(text_color, 0))
            painter.drawText(bounding_rect, Qt.AlignCenter, self._label)
            if scene.pixmapVisibility() and not self._pixmap.isNull():
                painter.drawPixmap(bounding_rect.toRect(), self._pixmap,
                                   self._pixmap.rect())