From 694c7386b48801b3acdaecd78072fe948a6915de Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Thu, 20 Jul 2023 00:56:16 +0200 Subject: [PATCH] Visualisation graph is now separate. Maybe other changes, idk --- birdvisu/annotations/layout.py | 136 +++++++---------------------- birdvisu/graphics_items.py | 95 ++++++++++---------- birdvisu/topo_v3.py | 32 ++++++- poor_mans_visualisation.py | 154 +++++++++++++++++++++++---------- 4 files changed, 219 insertions(+), 198 deletions(-) diff --git a/birdvisu/annotations/layout.py b/birdvisu/annotations/layout.py index 213c985..4086409 100644 --- a/birdvisu/annotations/layout.py +++ b/birdvisu/annotations/layout.py @@ -65,11 +65,11 @@ class PlaceVerticesFromFile(StyleAnnotator): # TODO: cope with multiple presets in one file? for d, chl in positions: - if d.startswith('visualisation '): + if d.startswith('visualisation'): self.add_positions(chl) break return self.result - def add_positions(chl) -> None: + def add_positions(self, chl) -> None: # level-2 are standard directives like from BIRD. For networks, level-3 may contain details (dr / address, router for stubnets maybe?) for directive, details in chl: tag, *det = directive.split() @@ -77,7 +77,7 @@ class PlaceVerticesFromFile(StyleAnnotator): elif tag in ['router', 'xrouter']: router_id = _parse_router_id(det[0]) vtxid = VertexID(router_id=router_id, is_router=True, address=None, family=None, dr_id=None, discriminator=None) - annot = _parse_details(det) + annot = _parse_details(details) self.result.for_vertex[vtxid] = annot elif tag == 'vlink': raise ValueError('Being a virtual link is not a vertex attribute.') @@ -97,8 +97,8 @@ class PlaceVerticesFromFile(StyleAnnotator): print(f'Bad number of candidates for {directive}') continue # Place with other way rid = cand.pop().router_id - vtxid = VertexID(router_id=rid, is_router=False, address=addr, family=family, dr_if=None, discriminator=None) - annot = _parse_details(det) + vtxid = VertexID(router_id=rid, is_router=False, address=addr, family=family, dr_id=None, discriminator=None) + annot = _parse_details(details) self.result.for_vertex[vtxid] = annot elif tag == 'network': # We do not know the OSPF version, which is sad. Let us guess @@ -136,7 +136,7 @@ class PlaceVerticesFromFile(StyleAnnotator): continue # Place with other way rid = cand.pop().dr_id vtxid = VertexID(dr_id=rid, discriminator=None, is_router=False, address=addr, family=AF_INET, router_id=None) - annot = _parse_details(det) + annot = _parse_details(details) self.result.for_vertex[vtxid] = annot else: raise ValueError(f'Unknown directive: {directive}') @@ -160,6 +160,11 @@ class PlaceUnplacedVertices(StyleAnnotator): best = self.previous already_placed: set[VertexID] = set(topo.annotations[best].for_vertex.keys()) if best is not None else set() + bad_vertices = set() + for v in already_placed: + if v not in topo.topology.vertices.keys(): + bad_vertices.add(v) + already_placed -= bad_vertices to_be_placed: set[VertexID] = topo.topology.vertices.keys() - already_placed # Let's say that a proximity is any distance shorter than 10% of the current diameter @@ -172,7 +177,7 @@ class PlaceUnplacedVertices(StyleAnnotator): result = Annotation() - if best is not None: + if len(already_placed) > 0: for vtxid in already_placed: queue.append(( vtxid, @@ -226,7 +231,7 @@ class PlaceUnplacedVertices(StyleAnnotator): def _default_width_for_cost(cost): # Maybe use exponential decay? - if cost > 0: return 10/cost + if cost > 0: return 100/cost # As thin as possible for no-weight edges (e.g. network → router) return 0 class EdgeWidthByCost(StyleAnnotator): @@ -263,108 +268,25 @@ class HighlightTopoDiff(StyleAnnotator): }[v]} return result -class HighlightCurrent(StyleAnnotator): - idempotent = False - def __init__(self, what): - self.what = what +class HighlightSPDAG(StyleAnnotator): + idempotent = True + def __init__(self, vtxid): + self.vtxid = vtxid def annotate(self, topo): + spd_annot = AnnotatorID(ShortestPathTree, vtxid) + topo.run_annotator(spd_annot) + annotation = topo.annotations[spd_annot] result = Annotation() - if self.what is None: - # Not going to guess. - result.for_topology = False - return result - # We got an annotator, make sure it has run - topo.run_annotator(self.what) - current = topo.annotations[self.what] - if False: 'alignment' - elif self.what.annotator == analysis.ShortestPathTree: - # Highlight edges - result.for_edge = {e: {'highlight_colour': (200, 200, 0, 128)} for e in current.for_edge.keys()} - return result - # Add highlights for other tools here. - -class MegaStyler(StyleAnnotator): - """This Annotator summarizes various previous StyleAnnotators into styles of graphics items. - - Either it gets a Sequence of annotators to consider as the parameter, or we - try to utilize most of the existing Annotations. The notable exception is - the ShortestPathTree, since that is only relevant when it is explicitly selected. - - We depend on StyleAnnotators' Annotations being the correct dictionaries. - - Also, there are not enough Annotators currently shipped with Birdvisu. - Therefore, we do not implement any priority-based colouring, but rather - just apply styles in order, overriding the previous ones. While not - future-proof, it is simple enough to be implemented now and substituted in the - future. + result.for_edge = {e: {'highlight_colour': (200, 200, 0, 128)} for e in annotation.for_edge.keys()} + return result - It is almost like a MetaStyler, but sounds much cooler with a G!""" #lol - idempotent = False +class HighlightShortestPath(StyleAnnotator): + idempotent = True def __init__(self, param): - # The boolean determines whether we should run this annotator when it has not been run previously - self.annotator_order: Sequence[tuple[StyleAnnotator, bool]] = [ - (PlaceVerticesFromFile, False), - (PlaceUnplacedVertices, True), - (EdgeWidthByCost, True), - (HighlightTopoDiff, True), - (HighlightCurrent, True), - ] - if param is not None: - self.detect = False - self.relevant_annotators = param - else: - self.detect = True - + self.start, self.end = param def annotate(self, topo): - # First, set some base styles - edge_style = defaultdict(lambda: { - 'width': 1, - 'colour': (0,0,0,255), - 'highlight_colour': (0,0,0,0), # "None" - }) - vertex_style = defaultdict(lambda: { - 'position': (0,0), # Fallback to have consistent output, PLEASE OVERRIDE! - 'highlight_colour': (0,0,0,0), # "None" - }) - - # Walk the annotators and collect the annotations - if not self.detect: - relevant_annotators = self.relevant_annotators - else: - # We will be iterating over this often, so this time it is not a set. - relevant_annotators = list(filter( - lambda ann_id: ann_id.annotator != analysis.ShortestPathTree, - topo.annotations.keys())) - - for ann_cls, should_run in self.annotator_order: - # Could this code be better? - runs = list(filter(lambda ann_id: ann_id.annotator == ann_cls, relevant_annotators)) - if len(runs) > 0: - annot = runs[0] # If there is not only one, we lack a hint what to choose. - elif len(runs) == 0 and should_run: - param = None - # This is sooo ugly: PlaceUnplacedVertices should get a - # parameter if a relevant placement annotator has been run. - # HighlightCurrent needs to know the right current tool. - if False: 'alignment' - elif ann_cls == PlaceUnplacedVertices: - param = next(filter(lambda ann_id: ann_id.annotator == PlaceVerticesFromFile, relevant_annotators), None) - elif ann_cls == HighlightCurrent: - # We hope that relevant_annotators contain a single ShortestPathTree: - param = next(filter(lambda ann_id: ann_id.annotator == analysis.ShortestPathTree, relevant_annotators), None) - annot = AnnotatorID(annotator=ann_cls, param=param) - topo.run_annotator(annot) - else: continue - data = topo.annotations[annot] - - for k, v in data.for_vertex.items(): - vertex_style[k] |= v - for k, v in data.for_edge.items(): - edge_style[k] |= v - + spd_annot = AnnotatorID(ShortestPathTree, vtxid) + topo.run_annotator(spd_annot) + annotation = topo.annotations[spd_annot] result = Annotation() - for v in topo.topology.vertices.keys(): - result.for_vertex[v] = vertex_style[v] - for e in topo.topology.edges: - result.for_edge[e] = edge_style[e] - return result + ... diff --git a/birdvisu/graphics_items.py b/birdvisu/graphics_items.py index 43af787..ac5ac8b 100644 --- a/birdvisu/graphics_items.py +++ b/birdvisu/graphics_items.py @@ -16,7 +16,10 @@ def _addrs_as_str(addrs): # TODO: Do not duplicate so much code! # TODO: tooltips? class RouterGraphicsItem(QGraphicsItem): - def __init__(self, vtxid, style, window, parent=None): + default_style = { + 'highlight_colour': (0,0,0,0), + } + def __init__(self, vtxid, window, parent=None): super().__init__(parent) self.vertex_id = vtxid self.window = window @@ -40,17 +43,13 @@ class RouterGraphicsItem(QGraphicsItem): p.setStyle(Qt.PenStyle.NoPen) self.highlight.setPen(p) self.setFlag(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) - self.apply_position(style) - self.apply_style(style) + self.apply_style({}) self.setZValue(200) - - def apply_position(self, style): - x, y = style['position'] - self.setPos(x, y) def apply_style(self, style): + full_style = self.default_style | style # Highlight: - r,g,b,a = style['highlight_colour'] + r,g,b,a = full_style['highlight_colour'] color = QColor(r,g,b,a) br = self.highlight.brush() br.setStyle(Qt.BrushStyle.SolidPattern) @@ -65,9 +64,10 @@ class RouterGraphicsItem(QGraphicsItem): 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 + vtxid = qgri.vertex_id + neighs = self.window.visu_graph[0][vtxid] + neigh_edges = {tuple(sorted((vtxid, n))) for n in neighs} + all_edges |= neigh_edges for edge in all_edges: self.window.graphicsitems[edge].update_line() super().mouseMoveEvent(evt) @@ -79,7 +79,10 @@ class RouterGraphicsItem(QGraphicsItem): self.icon.paint(painter, option, widget) class NetworkGraphicsItem(QGraphicsItem): - def __init__(self, vtxid, style, window, parent=None): + default_style = { + 'highlight_colour': (0,0,0,0), + } + def __init__(self, vtxid, window, parent=None): super().__init__(parent) self.vertex_id = vtxid self.window = window @@ -104,17 +107,13 @@ class NetworkGraphicsItem(QGraphicsItem): p.setStyle(Qt.PenStyle.NoPen) self.highlight.setPen(p) self.setFlag(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) - self.apply_position(style) - self.apply_style(style) + self.apply_style({}) self.setZValue(200) - def apply_position(self, style): - x, y = style['position'] - self.setPos(x, y) - def apply_style(self, style): + full_style = self.default_style | style # Highlight: - r,g,b,a = style['highlight_colour'] + r,g,b,a = full_style['highlight_colour'] color = QColor(r,g,b,a) br = self.highlight.brush() br.setColor(color) @@ -129,9 +128,10 @@ class NetworkGraphicsItem(QGraphicsItem): 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 + vtxid = qgri.vertex_id + neighs = self.window.visu_graph[0][vtxid] + neigh_edges = {tuple(sorted((vtxid, n))) for n in neighs} + all_edges |= neigh_edges for edge in all_edges: self.window.graphicsitems[edge].update_line() super().mouseMoveEvent(evt) @@ -143,15 +143,20 @@ class NetworkGraphicsItem(QGraphicsItem): self.icon.paint(painter, option, widget) class EdgeGraphicsItem(QGraphicsItem): - def __init__(self, edge, style, window, parent=None): + default_style = { + 'highlight_colour': (0,0,0,0), + 'width': 2, + 'colour': (40,40,40,255), + } + def __init__(self, edge, 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] + self.e_source = self.window.graphicsitems[edge[0]] + self.e_target = self.window.graphicsitems[edge[1]] 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) @@ -159,7 +164,7 @@ class EdgeGraphicsItem(QGraphicsItem): self.bottom_line = QGraphicsLineItem(qlinef, parent=self.top_line) self.bottom_line.setZValue(90) - self.apply_style(style) + self.apply_style({}) self.setZValue(100) def update_line(self): @@ -168,25 +173,25 @@ class EdgeGraphicsItem(QGraphicsItem): 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) + full_style = self.default_style | style + p = self.top_line.pen() + p.setWidth(full_style['width']) + self.top_line.setPen(p) + p = QPen(p) + p.setWidth(1.5*full_style['width']) + self.bottom_line.setPen(p) + + r,g,b,a = full_style['colour'] + col = QColor(r,g,b,a) + p = self.top_line.pen() + p.setColor(col) + self.top_line.setPen(p) + + r,g,b,a = full_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() diff --git a/birdvisu/topo_v3.py b/birdvisu/topo_v3.py index e40cf0d..7ba3e08 100644 --- a/birdvisu/topo_v3.py +++ b/birdvisu/topo_v3.py @@ -9,6 +9,7 @@ from enum import IntEnum from socket import AddressFamily from collections import defaultdict from ipaddress import IPv4Network, IPv6Network +from functools import total_ordering class TopologyV3: """This class represents the topology, that is the graph itself. @@ -120,7 +121,8 @@ class VertexType(IntEnum): ExtraAreaNet = 301 StubNet = 302 -@dataclass(frozen=True) +@total_ordering +@dataclass(frozen=True, eq=True) class VertexID: """An identifier of a particular vertex. @@ -192,6 +194,34 @@ class VertexID: @property def is_network(self): return not self.is_router + # We need to be able to order vertices in visualisation. No particular + # order required, as long as its linear. + def __lt__(self, other): + if self.is_router and other.is_network: return True + if self.is_network and other.is_router: return False + if self.is_router: + return self.router_id < other.router_id + if self.dr_id is None and other.dr_id is not None: return True + if self.dr_id is not None and other.dr_id is None: return False + if self.dr_id is None: + #external, xnetwork or stubnet + return (self.address, self.router_id if self.router_id is not None else hash(None)) < (other.address, other.router_id if other.router_id is not None else hash(None)) + # network + if self.dr_id != other.dr_id: + return self.dr_id < other.dr_id + if self.discriminator != other.discriminator: + return (self.discriminator or '') < (other.discrininator or '') + # Only tiebreaker: addresses. + if self.address is None and other.address is not None: return True + if self.address is not None and other.address is None: return False + if self.family != other.family: + return self.family < other.family + if isinstance(self.address, tuple) != isinstance(other.address, tuple): + return isinstance(self.address, tuple) < isinstance(other.address, tuple) + # Both tuples and IP addresses are comparable directly. + return self.address < other.address + # Phew! + @dataclass class Vertex: """Holds the details about a vertex that are specific to this topology. diff --git a/poor_mans_visualisation.py b/poor_mans_visualisation.py index bd7110c..e5fc760 100755 --- a/poor_mans_visualisation.py +++ b/poor_mans_visualisation.py @@ -2,7 +2,7 @@ from birdvisu.annotations import AnnotatedTopology, AnnotatorID from birdvisu.annotations.analysis import TopologyDifference, ShortestPathTree -from birdvisu.annotations.layout import MegaStyler +from birdvisu.annotations.layout import PlaceVerticesFromFile, PlaceUnplacedVertices, EdgeWidthByCost, HighlightTopoDiff, HighlightSPDAG, HighlightShortestPath from birdvisu.ospfsock import BirdSocketConnection from birdvisu.providers import BirdSocketTopologyProvider, OspfFileTopologyProvider, OspfFileParseError from birdvisu.topo_v3 import TopologyV3, VertexID @@ -78,9 +78,11 @@ class BirdTopologyLoader(QtWidgets.QDialog): class MainWindow(QtWidgets.QMainWindow): - class Tool(Enum): - MoveTopology = auto() + class Mode(Enum): ShortestPath = auto() + ShortestPathDAG = auto() + TopologyDifference = auto() + EdgeWeight = auto() def create_menus(self): print('Creating menus…') @@ -112,14 +114,21 @@ class MainWindow(QtWidgets.QMainWindow): refresh_act.triggered.connect(self.refreshTopologies) topo_menu.addAction(refresh_act) + positions = self.menubar.addMenu('&Positions') + loadp = QtGui.QAction('Load from file', self) + loadp.triggered.connect(self.load_positions) + positions.addAction(loadp) + def __init__(self, *a, **kwa): super().__init__(*a, **kwa) self.ref_topo_provider = None self.cur_topo_provider = None - self.set_initial_annotators() self.annot_topo = None + # The actual graph to show: list of neighbours and of edges + self.visu_graph: tuple[dict[VertexID, set[VertexID]], set[VertexID, VertexID]] = None self.highlighter = None - self.tool = self.Tool.MoveTopology + self.mode = None + self.positions: dict[VertexID, tuple[float, float]] = dict() self.scene = QtWidgets.QGraphicsScene() self.view = QtWidgets.QGraphicsView(self.scene) self.view.setDragMode(self.view.DragMode.ScrollHandDrag) @@ -127,6 +136,7 @@ class MainWindow(QtWidgets.QMainWindow): self.statusbar = self.statusBar() self.statusbar.showMessage('Hello!') self.create_menus() + self.edgeWeightMode() self.autoLoad() #Hack @@ -137,27 +147,76 @@ class MainWindow(QtWidgets.QMainWindow): self.cur_topo_provider = BirdSocketTopologyProvider(instance='gennet4', area=1, version=2) self.refreshTopologies() + @Slot() + def load_positions(self): + filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open vertex positions', '.', 'OSPF files(*.visu);;All files(*)')[0] + if filename == '': return # Do nothing + self.positions_from_file(filename) + + def positions_from_file(self, fn): + if self.visu_graph is None: return # nothing to do yet. + # Let's be frank: this used to be StyleAnnotators and it is not re-implemented. + pvff = AnnotatorID(PlaceVerticesFromFile, fn) + self.annot_topo.run_annotator(pvff) + placements = PlaceUnplacedVertices(pvff).annotate(self.annot_topo) + self.positions = {v: d['position'] for v, d in placements.for_vertex.items()} + # Apply the positions: + for v, pos in self.positions.items(): + x, y = pos + self.graphicsitems[v].setPos(x, y) + # Fix all the edges probably + for e in self.visu_graph[1]: + self.graphicsitems[e].update_line() + + + @Slot() + def savePositions(self): + ... + + @Slot() + def apply_styles(self): + if self.visu_graph is None: return # First need graph. + styles_ant = self.highlighter.annotate(self.annot_topo) + for_vertex = styles_ant.for_vertex + for_edge_ant = styles_ant.for_edge + # We must resolve conflicts + for_edge: dict[tuple[VertexID, VertexID], tuple[Edge, dict]] = dict() # → edge, styling dict. + for e, sty in for_edge_ant.items(): + a, b = tuple(sorted((e.source, e.target))) + if (a,b) not in for_edge: + for_edge[(a,b)] = (e, sty) + if e.cost == 0: continue # A collision that we do not care about + oe, _sty = for_edge[(a,b)] + if oe.cost == 0 or oe.cost > e.cost: + for_edge[(a,b)] = (e, sty) + # Actually apply the style: + for v, sty in for_vertex.items(): + self.graphicsitems[v].apply_style(sty) + for e, tup in for_edge.items(): + self.graphicsitems[e].apply_style(tup[1]) + + @Slot() + def dagMode(self): + self.mode = self.Mode.ShortestPathDAG + self.highlighter = HighlightSPDAG(...) + self.apply_styles() - def set_initial_annotators(self): - # We have three kinds of annotators: - # - The essential analytic ones (TopologyDifference) - # - The one that describes the current tool (when that is Annotator backed, like for ShortestPathTree) - # - The styling ones, that actually help visualise stuff (MegaStyler) - self.essential_annotators = [AnnotatorID(TopologyDifference)] - self.current_annotators = [AnnotatorID(ShortestPathTree, (VertexID( - family=None, - is_router=True, - address=None, - router_id=int(IPv4Address('172.23.100.10')), - dr_id=None, - discriminator=None - ), 'current')), - ] - self.styling_annotators = [AnnotatorID(MegaStyler, tuple(self.essential_annotators + self.current_annotators))] + @Slot() + def topoDiffMode(self): + self.mode = self.Mode.TopologyDifference + self.highlighter = HighlightTopoDiff(None) + self.apply_styles() + @Slot() + def edgeWeightMode(self): + self.mode = self.Mode.EdgeWeight + self.highlighter = EdgeWidthByCost(None) + self.apply_styles() + @Slot() def shortestPathMode(self): - self.tool = self.Tool.ShortestPath + self.mode = self.Mode.ShortestPath + ... @Slot() def openRefTopology(self): @@ -226,40 +285,45 @@ class MainWindow(QtWidgets.QMainWindow): combined_topology = TopologyV3.combine_topologies(reference=ref_topo, current=cur_topo) combined_topology.freeze() self.annot_topo = AnnotatedTopology(combined_topology) - for ann_id in self.essential_annotators + self.current_annotators + self.styling_annotators: - self.annot_topo.run_annotator(ann_id) - # Draw it + self.topo_to_graph() self.draw_visu() + def topo_to_graph(self): + """Converts an AnnotatedTopology to a graph""" + # We actually do not care about the annotations + topo = self.annot_topo.topology + neighbours = {v: set() for v in topo.vertices.keys()} # Neighbour lists + edges: set[tuple[VertexID, VertexID]] = set() + for e in topo.edges: + neighbours[e.source].add(e.target) + neighbours[e.target].add(e.source) + first = min([e.source, e.target]) + second = max([e.source, e.target]) + edges.add((first, second)) + self.visu_graph = (neighbours, edges) + def draw_visu(self): - # just take the result of the MegaStyler and create vertices according to the style. - megastyler = self.styling_annotators[-1] - assert megastyler.annotator == MegaStyler - msann = self.annot_topo.annotations[megastyler] - # These two dictionaries are used from outside, so that QGraphicsItems - # can resolve graph relations (e.g. to draw the line between the - # correct Vertices) - self.graphicsitems: dict[VertexID, QGraphicsItem] = dict() - self.topologyitems: dict[QGraphicsItem, VertexID|Edge] = dict() - for vtxid, style in msann.for_vertex.items(): - gritem = self.create_vertex(vtxid, style) + self.graphicsitems: dict[VertexID|tuple[VertexID, VertexID], QGraphicsItem] = dict() + for vtxid in self.visu_graph[0].keys(): + gritem = self.create_vertex(vtxid) self.graphicsitems[vtxid] = gritem - self.topologyitems[gritem] = vtxid self.scene.addItem(gritem) - for edge, style in msann.for_edge.items(): - gritem = self.create_edge(edge, style) + for edge in self.visu_graph[1]: + gritem = self.create_edge(edge) self.graphicsitems[edge] = gritem - self.topologyitems[gritem] = edge self.scene.addItem(gritem) + self.positions_from_file(None) + self.apply_styles() - def create_vertex(self, vtxid, style): + def create_vertex(self, vtxid): if vtxid.is_router: - return RouterGraphicsItem(vtxid, style, self) + return RouterGraphicsItem(vtxid, self) else: - return NetworkGraphicsItem(vtxid, style, self) + return NetworkGraphicsItem(vtxid, self) + + def create_edge(self, edge): + return EdgeGraphicsItem(edge, self) - def create_edge(self, edge, style): - return EdgeGraphicsItem(edge, style, self) main_window = MainWindow() main_window.show()