core-extra/daemon/core/gui/graph/edges.py

390 lines
13 KiB
Python

import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Any, Tuple
from core.api.grpc import core_pb2
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
TEXT_DISTANCE = 0.30
EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933"
ARC_DISTANCE = 50
def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
values = [src, dst]
if network is not None:
values.append(network)
return tuple(sorted(values))
def arc_edges(edges) -> None:
if not edges:
return
mid_index = len(edges) // 2
if mid_index == 0:
arc_step = ARC_DISTANCE
else:
arc_step = ARC_DISTANCE / mid_index
# below edges
arc = 0
for edge in edges[:mid_index]:
arc -= arc_step
edge.arc = arc
edge.redraw()
# mid edge
if len(edges) % 2 != 0:
arc = 0
edge = edges[mid_index]
edge.arc = arc
edge.redraw()
mid_index += 1
# above edges
arc = 0
for edge in edges[mid_index:]:
arc += arc_step
edge.arc = arc
edge.redraw()
class Edge:
tag = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
self.id = None
self.src = src
self.dst = dst
self.arc = 0
self.token = None
self.src_label = None
self.middle_label = None
self.dst_label = None
self.color = EDGE_COLOR
self.width = EDGE_WIDTH
@classmethod
def create_token(cls, src: int, dst: int) -> Tuple[int, ...]:
return tuple(sorted([src, dst]))
def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale
def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
) -> Tuple[float, float]:
src_x, src_y = src_pos
dst_x, dst_y = dst_pos
mp_x = (src_x + dst_x) / 2
mp_y = (src_y + dst_y) / 2
slope_denominator = src_x - dst_x
slope_numerator = src_y - dst_y
# vertical line
if slope_denominator == 0:
return mp_x + self.arc, mp_y
# horizontal line
if slope_numerator == 0:
return mp_x, mp_y + self.arc
# everything else
m = slope_numerator / slope_denominator
perp_m = -1 / m
b = mp_y - (perp_m * mp_x)
# get arc x and y
offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2)))
arc_x = mp_x
if self.arc >= 0:
arc_x += offset
else:
arc_x -= offset
arc_y = (perp_m * arc_x) + b
return arc_x, arc_y
def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line(
*src_pos,
*arc_pos,
*dst_pos,
smooth=True,
tags=self.tag,
width=self.scaled_width(),
fill=self.color,
)
def redraw(self):
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
self.move_src(src_pos)
def middle_label_pos(self) -> Tuple[float, float]:
_, _, x, y, _, _ = self.canvas.coords(self.id)
return x, y
def middle_label_text(self, text: str) -> None:
if self.middle_label is None:
x, y = self.middle_label_pos()
self.middle_label = self.canvas.create_text(
x,
y,
font=self.canvas.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.middle_label, text=text)
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v1 = dst_x - src_x
v2 = dst_y - src_y
ux = TEXT_DISTANCE * v1
uy = TEXT_DISTANCE * v2
src_x = src_x + ux
src_y = src_y + uy
dst_x = dst_x - ux
dst_y = dst_y - uy
return (src_x, src_y), (dst_x, dst_y)
def src_label_text(self, text: str) -> None:
if self.src_label is None:
src_pos, _ = self.node_label_positions()
self.src_label = self.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.src_label, text=text)
def dst_label_text(self, text: str) -> None:
if self.dst_label is None:
_, dst_pos = self.node_label_positions()
self.dst_label = self.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.dst_label, text=text)
def move_node(self, node_id: int, pos: Tuple[float, float]) -> None:
if self.src == node_id:
self.move_src(pos)
else:
self.move_dst(pos)
def move_dst(self, dst_pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
def move_src(self, src_pos: Tuple[float, float]) -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id)
dst_pos = dst_x, dst_y
self.moved(src_pos, dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions()
if self.src_label:
self.canvas.coords(self.src_label, *src_pos)
if self.dst_label:
self.canvas.coords(self.dst_label, *dst_pos)
def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.src_label)
self.canvas.delete(self.middle_label)
self.canvas.delete(self.dst_label)
self.id = None
self.src_label = None
self.middle_label = None
self.dst_label = None
class CanvasWirelessEdge(Edge):
tag = tags.WIRELESS_EDGE
def __init__(
self,
canvas: "CanvasGraph",
src: int,
dst: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
token: Tuple[Any, ...],
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
self.token = token
self.width = WIRELESS_WIDTH
self.color = WIRELESS_COLOR
self.draw(src_pos, dst_pos)
class CanvasEdge(Edge):
"""
Canvas edge class
"""
def __init__(
self,
canvas: "CanvasGraph",
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
) -> None:
"""
Create an instance of canvas edge object
"""
super().__init__(canvas, src)
self.src_interface = None
self.dst_interface = None
self.text_src = None
self.text_dst = None
self.link = None
self.asymmetric_link = None
self.throughput = None
self.draw(src_pos, dst_pos)
self.set_binding()
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.create_context)
def set_link(self, link) -> None:
self.link = link
self.draw_labels()
def interface_label(self, interface: core_pb2.Interface) -> str:
label = ""
if interface.name and self.canvas.show_interface_names.get():
label = f"{interface.name}"
if interface.ip4 and self.canvas.show_ip4s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip4}/{interface.ip4mask}"
if interface.ip6 and self.canvas.show_ip6s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip6}/{interface.ip6mask}"
return label
def create_node_labels(self) -> Tuple[str, str]:
label_one = None
if self.link.HasField("interface_one"):
label_one = self.interface_label(self.link.interface_one)
label_two = None
if self.link.HasField("interface_two"):
label_two = self.interface_label(self.link.interface_two)
return label_one, label_two
def draw_labels(self) -> None:
src_text, dst_text = self.create_node_labels()
self.src_label_text(src_text)
self.dst_label_text(dst_text)
def redraw(self) -> None:
super().redraw()
self.draw_labels()
def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput
text = f"{throughput:.3f} kbps"
self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold:
color = self.canvas.throughput_color
width = self.canvas.throughput_width
else:
color = self.color
width = self.scaled_width()
self.canvas.itemconfig(self.id, fill=color, width=width)
def complete(self, dst: int) -> None:
self.dst = dst
self.token = create_edge_token(self.src, self.dst)
dst_pos = self.canvas.coords(self.dst)
self.move_dst(dst_pos)
self.check_wireless()
self.canvas.tag_raise(self.src)
self.canvas.tag_raise(self.dst)
logging.debug("Draw wired link from node %s to node %s", self.src, dst)
def is_wireless(self) -> bool:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
# update the wlan/EMANE network
wlan_network = self.canvas.wireless_network
if is_src_wireless and not is_dst_wireless:
if self.src not in wlan_network:
wlan_network[self.src] = set()
wlan_network[self.src].add(self.dst)
elif not is_src_wireless and is_dst_wireless:
if self.dst not in wlan_network:
wlan_network[self.dst] = set()
wlan_network[self.dst].add(self.src)
return is_src_wireless or is_dst_wireless
def check_wireless(self) -> None:
if self.is_wireless():
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self._check_antenna()
def _check_antenna(self) -> None:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
if is_src_wireless or is_dst_wireless:
if is_src_wireless and not is_dst_wireless:
dst_node.add_antenna()
elif not is_src_wireless and is_dst_wireless:
src_node.add_antenna()
else:
src_node.add_antenna()
def reset(self) -> None:
self.canvas.delete(self.middle_label)
self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
def create_context(self, event: tk.Event) -> None:
context = tk.Menu(self.canvas)
themes.style_menu(context)
context.add_command(label="Configure", command=self.configure)
context.add_command(label="Delete", command=self.click_delete)
if self.canvas.app.core.is_runtime():
context.entryconfigure(1, state="disabled")
context.post(event.x_root, event.y_root)
def click_delete(self):
self.canvas.delete_edge(self)
def configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self)
dialog.show()