diff --git a/birdvisu/visualisation/annotators.py b/birdvisu/visualisation/annotators.py new file mode 100644 index 0000000..55e3f3f --- /dev/null +++ b/birdvisu/visualisation/annotators.py @@ -0,0 +1,185 @@ +"""Annotators for visualising. + +Many of these should be somewhere else, because they make assumptions not +related to visualisation. Also, only the currently needed annotators were +written.""" + +from enum import Enum, auto +from dataclasses import dataclass +import re +import random +from PySide6 import QtCore, QtGui, QtWidgets + +# Classification + +class DifferenceStatus: + """Describes differences between two topologies""" + NORMAL = auto() + MISSING = auto() + EXTRA = auto() + DISCREPANCY = auto() + +def difference_annotator(at, reference_src='reference', actual_src='actual'): + """Adds DifferenceStatuses according to which sources provided which part + of topology""" + + topo = at.topology + for data, annot in [ + (topo.routers, at.router_annotations), + (topo.networks, at.network_annotations), + (topo.links, at.link_annotations), + ]: + for k, v in data.items(): + verdict = DifferenceStatus.NORMAL + if actual_src not in v.sources: + verdict = DifferenceStatus.MISSING + if reference_src not in v.sources: + verdict = DifferenceStatus.EXTRA + # Nodes currently cannot have discrepant information (provided the + # Topology.is_valid()) + if data is topo.links: + if k.metric < 0: + verdict = DifferenceStatus.DISCREPANCY + + # TODO: We should probably disallow mutating previous AnnotatedTopologies. + annot[k].append(verdict) + return at + +# TODO: annotator for routing trees + + + +# Layouting + +@dataclass +class Position: + x: float + y: float + +def extract_positions(at, directive='visualisation default'): + topo = at.topology + for data, annot in [ + (topo.routers, at.router_annotations), + (topo.networks, at.network_annotations), + ]: + for k, v in data.items(): + visu_directive = None + for details in k.all_details: + visu_directives = list(filter(lambda x: x[0] == directive, details)) + if len(visu_directives) > 0: + visu_directive = visu_directives[-1] # Use the last one found. + position = None + if visu_directive is not None: + for pos in visu_directive[1]: + m = re.match(r'position \[([0-9.]+) ([0-9.]+)\]', pos) + if pos is not None: + x, y = m.groups() + position = Position(x = float(x), y = float(y)) + if position is not None: + annot[k].append(position) + return at + +def random_posiiton(at): + # Fallback when no position could be extracted + # TODO: this should use some kind of heuristic or existing layout engine. + topo = at.topology + for data, annot in [ + (topo.routers, at.router_annotations), + (topo.networks, at.network_annotations), + ]: + for k, v in data.items(): + if len(annot[k]) > 0 and not isinstance(annot[k][-1], Position): + annot[k].append(Position( + # This is really last resort, so expecting the canvas to be + # FullHD is as good assumption as any. + x = float(random.randint(0, 1920)), + y = float(random.randint(0, 1080)), + )) + return at + + +# Rendering + +def assign_brushes(at): + for annot in [ + at.router_annotations, + at.network_annotations, + at.link_annotations, + ]: + for tags in annot.values(): + statuses = list(filter(lambda x: isinstance(x, DifferenceStatus), tags)) + status = statuses[-1] if len(statuses) > 0 else DifferenceStatus.DISCREPANCY # should always have something. + color = { + DifferenceStatus.NORMAL: 'black', + DifferenceStatus.EXTRA: 'green', + DifferenceStatus.MISSING: 'red', + DifferenceStatus.DISCREPANCY: 'blue', + }[status] + tags.append(QtGui.QBrush(QtGui.QColor(color))) + return at + +def create_qgritems(at): + # qgritem = QGraphicsItem + # TODO: reasonable visualisation, not just squares :-) + # - this probably involves creatin custom qgritems + # - We will need own qgritems anyway to handle interaction (e.g. + # resolving to Router objects) + topo = at.topology + for rk, r in topo.routers.items(): + size = 30 + brush = None + for tag in at.router_annotations[rk][-1::-1]: + if isinstance(tag, QtGui.QBrush): + brush = tag + break + pos = None + for tag in at.router_annotations[rk][-1::-1]: + if isinstance(tag, Position): + pos = tag + break + x = pos.x + y = pos.y + + shape = QtWidgets.QGraphicsRectItem(x, y, size, size) + shape.setBrush(brush) + label = QtWidgets.QGraphicsSimpleTextItem(rk, parent=shape) + 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 = QtWidgets.QGraphicsRectItem(x, y, size, size) + shape.setBrush(brush) + label = QtWidgets.QGraphicsSimpleTextItem(nk, parent=shape) + 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, Position): + rpos = tag + break + npos = None + for tag in at.network_annotations[nid][-1::-1]: + if isinstance(tag, Position): + npos = tag + break + + line = QtWidgets.QGraphicsLineItem(rpos.x, rpos.y, npos.x, npos.y) + at.link_annotations[lk].append(line) +