From 12b0c6578eac8381905a3e16f7b5dc07a90526f8 Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Wed, 19 Jul 2023 21:13:30 +0200 Subject: [PATCH] Add new visualisation stuff. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Does apx. right thing, but the other right… --- birdvisu/graphics_items.py | 195 +++++++++++++++++++++++++++ birdvisu/visualisation/annotators.py | 60 --------- birdvisu/visualisation/qt_widgets.py | 23 ---- poor_mans_visualisation.py | 69 +--------- 4 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 birdvisu/graphics_items.py delete mode 100644 birdvisu/visualisation/annotators.py delete mode 100644 birdvisu/visualisation/qt_widgets.py diff --git a/birdvisu/graphics_items.py b/birdvisu/graphics_items.py new file mode 100644 index 0000000..43af787 --- /dev/null +++ b/birdvisu/graphics_items.py @@ -0,0 +1,195 @@ +from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem, QGraphicsSimpleTextItem, QGraphicsLineItem +from PySide6.QtCore import QLineF, Qt +from PySide6.QtGui import QColor, QBrush, QPen + +def _rid_to_str(rid): + from ipaddress import IPv4Address + return str(IPv4Address(rid)) + +def _addrs_as_str(addrs): + if addrs is None: return '' + from ipaddress import IPv4Network, IPv6Network + if isinstance(addrs, IPv4Network) or isinstance(addrs, IPv6Network): return str(addrs) + return '\n'.join(str(a) for a in addrs) + +# TODO: we MAY WISH TO use QGraphicsItemGroup. +# TODO: Do not duplicate so much code! +# TODO: tooltips? +class RouterGraphicsItem(QGraphicsItem): + def __init__(self, vtxid, style, window, parent=None): + super().__init__(parent) + self.vertex_id = vtxid + self.window = window + + size = 30 + self.icon = QGraphicsRectItem(-size/2, -size/2, size, size, parent=self) + self.icon.setZValue(200) + self.icon.setBrush(QBrush(QColor('yellow'))) + self.label = QGraphicsSimpleTextItem(parent=self.icon) + self.label.setY(size * 0.8) + self.label.setText(_rid_to_str(vtxid.router_id)) + # Center text + text_width = self.label.boundingRect().width() + self.label.setX(-text_width/2) + + br = self.icon.boundingRect() + br.adjust(-5, -5, 5, 5) + self.highlight = QGraphicsRectItem(br, parent=self) + self.highlight.setZValue(190) + p = self.highlight.pen() + p.setStyle(Qt.PenStyle.NoPen) + self.highlight.setPen(p) + self.setFlag(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) + self.apply_position(style) + self.apply_style(style) + self.setZValue(200) + + def apply_position(self, style): + x, y = style['position'] + self.setPos(x, y) + + def apply_style(self, style): + # Highlight: + r,g,b,a = style['highlight_colour'] + color = QColor(r,g,b,a) + br = self.highlight.brush() + br.setStyle(Qt.BrushStyle.SolidPattern) + br.setColor(color) + self.highlight.setBrush(br) + + # Vertex moving + def mouseMoveEvent(self, evt): + # Other selected graphics items do not receive the event, so we need to + # move everything. + selected_items = self.window.scene.selectedItems() + all_edges = set() + for qgri in selected_items: + # It is a vertex. + vtxid = self.window.topologyitems[qgri] + vtx = self.window.annot_topo.topology.vertices[vtxid] + all_edges |= vtx.incoming_edges | vtx.outgoing_edges + for edge in all_edges: + self.window.graphicsitems[edge].update_line() + super().mouseMoveEvent(evt) + + # Admit we are basically only a wrapper of the icon. + def boundingRect(self): + return self.icon.boundingRect() + def paint(self, painter, option, widget): + self.icon.paint(painter, option, widget) + +class NetworkGraphicsItem(QGraphicsItem): + def __init__(self, vtxid, style, window, parent=None): + super().__init__(parent) + self.vertex_id = vtxid + self.window = window + + size = 40 + self.icon = QGraphicsRectItem(-size/2, -size/2, size, size, parent=self) + self.icon.setZValue(200) + self.icon.setBrush(QBrush(QColor('gray'))) + self.label = QGraphicsSimpleTextItem(parent=self.icon) + self.label.setText(_addrs_as_str(vtxid.address)) + # Center text + text_width = self.label.boundingRect().width() + self.label.setX(-text_width/2) + text_height = self.label.boundingRect().height() + self.label.setY(-text_height/2) + + br = self.icon.boundingRect() + br.adjust(-5, -5, 5, 5) + self.highlight = QGraphicsRectItem(br, parent=self) + self.highlight.setZValue(190) + p = self.highlight.pen() + p.setStyle(Qt.PenStyle.NoPen) + self.highlight.setPen(p) + self.setFlag(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) + self.apply_position(style) + self.apply_style(style) + self.setZValue(200) + + def apply_position(self, style): + x, y = style['position'] + self.setPos(x, y) + + def apply_style(self, style): + # Highlight: + r,g,b,a = style['highlight_colour'] + color = QColor(r,g,b,a) + br = self.highlight.brush() + br.setColor(color) + br.setStyle(Qt.BrushStyle.SolidPattern) + self.highlight.setBrush(br) + + # Vertex moving + def mouseMoveEvent(self, evt): + # Other selected graphics items do not receive the event, so we need to + # move everything. + selected_items = self.window.scene.selectedItems() + all_edges = set() + for qgri in selected_items: + # It is a vertex. + vtxid = self.window.topologyitems[qgri] + vtx = self.window.annot_topo.topology.vertices[vtxid] + all_edges |= vtx.incoming_edges | vtx.outgoing_edges + for edge in all_edges: + self.window.graphicsitems[edge].update_line() + super().mouseMoveEvent(evt) + + # Admit we are basically only a wrapper of the icon. + def boundingRect(self): + return self.icon.boundingRect() + def paint(self, painter, option, widget): + self.icon.paint(painter, option, widget) + +class EdgeGraphicsItem(QGraphicsItem): + def __init__(self, edge, style, window, parent=None): + super().__init__(parent) + self.edge = edge + self.window = window + + # Cache the two related objects. + # NOTE: this is only possible because the main window first creates vertices and then edges. + self.e_source = self.window.graphicsitems[edge.source] + self.e_target = self.window.graphicsitems[edge.target] + + qlinef = QLineF(self.e_source.x(), self.e_source.y(), self.e_target.x(), self.e_target.y()) + self.top_line = QGraphicsLineItem(qlinef, parent=self) + self.top_line.setZValue(100) + + self.bottom_line = QGraphicsLineItem(qlinef, parent=self.top_line) + self.bottom_line.setZValue(90) + self.apply_style(style) + self.setZValue(100) + + def update_line(self): + qlinef = QLineF(self.e_source.x(), self.e_source.y(), self.e_target.x(), self.e_target.y()) + self.top_line.setLine(qlinef) + self.bottom_line.setLine(qlinef) + + def apply_style(self, style): + if 'width' in style: + p = self.top_line.pen() + p.setWidth(style['width']) + self.top_line.setPen(p) + p = QPen(p) + p.setWidth(1.5*style['width']) + self.bottom_line.setPen(p) + if 'colour' in style: + r,g,b,a = style['colour'] + col = QColor(r,g,b,a) + p = self.top_line.pen() + p.setColor(col) + self.top_line.setPen(p) + if 'highlight_colour' in style: + r,g,b,a = style['highlight_colour'] + col = QColor(r,g,b,a) + p = self.bottom_line.pen() + p.setColor(col) + self.bottom_line.setPen(p) + + def boundingRect(self): + return self.top_line.boundingRect() + def paint(self, painter, option, widget): + return self.top_line.paint(painter, option, widget) + diff --git a/birdvisu/visualisation/annotators.py b/birdvisu/visualisation/annotators.py deleted file mode 100644 index 2039476..0000000 --- a/birdvisu/visualisation/annotators.py +++ /dev/null @@ -1,60 +0,0 @@ -def create_qgritems(at): - shape = MyGraphicsRectItem(-size/2, -size/2, size, size) - shape.setBrush(brush) - shape.setPos(x, y) - shape.setToolTip(rk) - label = QtWidgets.QGraphicsSimpleTextItem(rk, parent=shape) - label.setY(size*0.8) - # Centering: - text_width = label.boundingRect().width() - label.setX(-text_width/2) - shape.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable) - at.router_annotations[rk].append(shape) - - for nk, n in topo.networks.items(): - size = 10 - brush = None - for tag in at.network_annotations[nk][-1::-1]: - if isinstance(tag, QtGui.QBrush): - brush = tag - break - pos = None - for tag in at.network_annotations[nk][-1::-1]: - if isinstance(tag, Position): - pos = tag - break - x = pos.x - y = pos.y - - shape = MyGraphicsRectItem(-size/2, -size/2, size, size) - shape.setBrush(brush) - shape.setPos(x, y) - shape.setToolTip(nk) - label = QtWidgets.QGraphicsSimpleTextItem(nk, parent=shape) - label.setY(size*0.8) - # Centering: - text_width = label.boundingRect().width() - label.setX(-text_width/2) - shape.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable) - at.network_annotations[nk].append(shape) - - for lk, l in topo.links.items(): - rid = l.router.ident - nid = l.network.ident - - rpos = None - for tag in at.router_annotations[rid][-1::-1]: - if isinstance(tag, QtWidgets.QGraphicsRectItem): - ritem = tag - if isinstance(tag, Position): - rpos = tag - break - npos = None - for tag in at.network_annotations[nid][-1::-1]: - if isinstance(tag, QtWidgets.QGraphicsRectItem): - nitem = tag - if isinstance(tag, Position): - npos = tag - break - - diff --git a/birdvisu/visualisation/qt_widgets.py b/birdvisu/visualisation/qt_widgets.py deleted file mode 100644 index 486a274..0000000 --- a/birdvisu/visualisation/qt_widgets.py +++ /dev/null @@ -1,23 +0,0 @@ -from PySide6.QtWidgets import QGraphicsRectItem -from PySide6.QtCore import QLineF - -class MyGraphicsRectItem(QGraphicsRectItem): - #def __init__(self, *a, **kwa): - # return super().__init__(*a, **kwa) - #def itemChange(self, change, val): - # return super().itemChange(change, val) - def mouseMoveEvent(self, evt): - print(f'Moving: {self}') - line = self.data(0) - other_end = line.data(0) - if other_end == self: - other_end = line.data(1) - new_qlinef = QLineF( - self.x(), - self.y(), - other_end.x(), - other_end.y(), - ) - line.setLine(new_qlinef) - return super().mouseMoveEvent(evt) - diff --git a/poor_mans_visualisation.py b/poor_mans_visualisation.py index 19ffff7..eb3102d 100755 --- a/poor_mans_visualisation.py +++ b/poor_mans_visualisation.py @@ -6,37 +6,18 @@ from birdvisu.annotations.layout import MegaStyler from birdvisu.ospfsock import BirdSocketConnection from birdvisu.providers import BirdSocketTopologyProvider, OspfFileTopologyProvider, OspfFileParseError from birdvisu.topo_v3 import TopologyV3, VertexID +from birdvisu.graphics_items import RouterGraphicsItem, NetworkGraphicsItem, EdgeGraphicsItem from collections import defaultdict from enum import Enum, auto from ipaddress import IPv4Address from PySide6 import QtCore, QtGui, QtWidgets -from PySide6.QtCore import Slot +from PySide6.QtCore import Slot, QObject from random import randint import sys app = QtWidgets.QApplication([]) - -class MyGraphicsRectItem(QtWidgets.QGraphicsRectItem): - def __init__(self, nei, shapes, *a, **kwa): - self.nei = nei - self.shapes = shapes - return super().__init__(*a, **kwa) - #def itemChange(self, change, val): - # return super().itemChange(change, val) - def mouseMoveEvent(self, evt): - vtxid = self.data(0) - for e in self.nei[vtxid]: - x1 = self.x() - y1 = self.y() - other = e.source if e.source != vtxid else e.target - x2 = self.shapes[other].x() - y2 = self.shapes[other].y() - qlinef = QtCore.QLineF(x1, y1, x2, y2) - self.shapes[e].setLine(qlinef) - return super().mouseMoveEvent(evt) - class BirdTopologyLoader(QtWidgets.QDialog): def __init__(self, *a, **kwa): super().__init__(*a, **kwa) @@ -268,50 +249,14 @@ class MainWindow(QtWidgets.QMainWindow): self.scene.addItem(gritem) def create_vertex(self, vtxid, style): - ... + if vtxid.is_router: + return RouterGraphicsItem(vtxid, style, self) + else: + return NetworkGraphicsItem(vtxid, style, self) def create_edge(self, edge, style): - ... + return EdgeGraphicsItem(edge, style, self) - def ad_hoc_draw_visu(self): - shapes = dict() - self.nei = defaultdict(lambda: []) - for k, v in self.annot_topo.topology.vertices.items(): - size = 30 if k.is_router else 10 - x, y = randint(0, 1920), randint(0, 1080) - shape = MyGraphicsRectItem(self.nei, shapes, -size/2, -size/2, size, size) - shape.setPos(x,y) - # TODO:brush - label_text = str(IPv4Address(k.router_id)) if k.is_router else str(k.address) # Surprisingly works for all the possible addresses. - label = QtWidgets.QGraphicsSimpleTextItem(label_text, parent=shape) - label.setY(size*0.8) - text_width = label.boundingRect().width() - label.setX(-text_width/2) - shape.setData(0, k) - shape.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable) - shapes[k] = shape - - for e in self.annot_topo.topology.edges: - start = shapes[e.source].pos() - end = shapes[e.target].pos() - qlinef = QtCore.QLineF(start, end) - line = QtWidgets.QGraphicsLineItem(qlinef) - line.setData(0, e) - self.nei[e.source].append(e) - self.nei[e.target].append(e) - shapes[e] = line - if self.highlighter is not None and self.highlighter in self.annot_topo.annotations: - ann = self.annot_topo.annotations[self.highlighter] - for shk in ann.for_vertex.keys() | ann.for_edge.keys(): - pen = QtGui.QPen(QtGui.QColor('blue')) - pen.setWidth(pen.width() * 3) - c = pen.color() - c.setAlpha(128) - pen.setColor(c) - shapes[shk].setPen(pen) - # FIXME: also color edges in opposite direction - for sh in shapes.values(): self.scene.addItem(sh) - main_window = MainWindow() main_window.show() app.exec()