Visualisation graph is now separate.

Maybe other changes, idk
topo-mov
LEdoian 1 year ago
parent 09e654a016
commit 694c7386b4

@ -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
...

@ -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()

@ -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.

@ -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()

Loading…
Cancel
Save