Example #1
0
 def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
     """
     create an edge between source node and destination node
     """
     edge = CanvasEdge(self.app, src)
     edge.complete(dst)
     return edge
Example #2
0
    def click_press(self, event: tk.Event):
        """
        Start drawing an edge if mouse click is on a node
        """
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return

        self.cursor = x, y
        selected = self.get_selected(event)
        logging.debug("click press(%s): %s", self.cursor, selected)
        x_check = self.cursor[0] - self.offset[0]
        y_check = self.cursor[1] - self.offset[1]
        logging.debug("click press offset(%s, %s)", x_check, y_check)
        is_node = selected in self.nodes
        if self.mode == GraphMode.EDGE and is_node:
            pos = self.coords(selected)
            self.drawing_edge = CanvasEdge(self, selected, pos, pos)

        if self.mode == GraphMode.ANNOTATION:
            if is_marker(self.annotation_type):
                r = self.app.toolbar.marker_tool.radius
                self.create_oval(
                    x - r,
                    y - r,
                    x + r,
                    y + r,
                    fill=self.app.toolbar.marker_tool.color,
                    outline="",
                    tags=(tags.MARKER, tags.ANNOTATION),
                    state=self.show_annotations.state(),
                )
                return
            if selected is None:
                shape = Shape(self.app, self, self.annotation_type, x, y)
                self.selected = shape.id
                self.shape_drawing = True
                self.shapes[shape.id] = shape

        if selected is not None:
            if selected not in self.selection:
                if selected in self.shapes:
                    shape = self.shapes[selected]
                    self.select_object(shape.id)
                    self.selected = selected
                elif selected in self.nodes:
                    node = self.nodes[selected]
                    self.select_object(node.id)
                    self.selected = selected
                    logging.debug(
                        "selected node(%s), coords: (%s, %s)",
                        node.core_node.name,
                        node.core_node.position.x,
                        node.core_node.position.y,
                    )
        else:
            if self.mode == GraphMode.SELECT:
                shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
                self.select_box = shape
            self.clear_selection()
Example #3
0
 def create_link(self, edge: CanvasEdge, canvas_src_node: CanvasNode,
                 canvas_dst_node: CanvasNode) -> None:
     """
     Create core link for a pair of canvas nodes, with token referencing
     the canvas edge.
     """
     src_node = canvas_src_node.core_node
     dst_node = canvas_dst_node.core_node
     self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
     src_iface = None
     if NodeUtils.is_container_node(src_node.type):
         src_iface = self.create_iface(canvas_src_node)
         self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token
         edge.src_iface = src_iface
         canvas_src_node.ifaces[src_iface.id] = src_iface
     dst_iface = None
     if NodeUtils.is_container_node(dst_node.type):
         dst_iface = self.create_iface(canvas_dst_node)
         self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
         edge.dst_iface = dst_iface
         canvas_dst_node.ifaces[dst_iface.id] = dst_iface
     link = Link(
         type=LinkType.WIRED,
         node1_id=src_node.id,
         node2_id=dst_node.id,
         iface1=src_iface,
         iface2=dst_iface,
     )
     edge.set_link(link)
     self.links[edge.token] = edge
     logging.info("added link between %s and %s", src_node.name,
                  dst_node.name)
Example #4
0
    def create_link(
        self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
    ):
        """
        Create core link for a pair of canvas nodes, with token referencing
        the canvas edge.
        """
        src_node = canvas_src_node.core_node
        dst_node = canvas_dst_node.core_node

        # determine subnet
        self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)

        src_interface = None
        if NodeUtils.is_container_node(src_node.type):
            src_interface = self.create_interface(canvas_src_node)
            edge.src_interface = src_interface
            self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token

        dst_interface = None
        if NodeUtils.is_container_node(dst_node.type):
            dst_interface = self.create_interface(canvas_dst_node)
            edge.dst_interface = dst_interface
            self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token

        link = core_pb2.Link(
            type=core_pb2.LinkType.WIRED,
            node_one_id=src_node.id,
            node_two_id=dst_node.id,
            interface_one=src_interface,
            interface_two=dst_interface,
        )
        edge.set_link(link)
        self.links[edge.token] = edge
        logging.info("Add link between %s and %s", src_node.name, dst_node.name)
Example #5
0
 def add_wired_edge(self, src: CanvasNode, dst: CanvasNode,
                    link: Link) -> None:
     token = create_edge_token(src.id, dst.id)
     if token in self.edges and link.options.unidirectional:
         edge = self.edges[token]
         edge.asymmetric_link = link
     elif token not in self.edges:
         node1 = src.core_node
         node2 = dst.core_node
         src_pos = (node1.position.x, node1.position.y)
         dst_pos = (node2.position.x, node2.position.y)
         edge = CanvasEdge(self, src.id, src_pos, dst_pos)
         edge.token = token
         edge.dst = dst.id
         edge.set_link(link)
         edge.check_wireless()
         src.edges.add(edge)
         dst.edges.add(edge)
         self.edges[edge.token] = edge
         self.core.links[edge.token] = edge
         if link.iface1:
             iface1 = link.iface1
             self.core.iface_to_edge[(node1.id, iface1.id)] = token
             src.ifaces[iface1.id] = iface1
             edge.src_iface = iface1
         if link.iface2:
             iface2 = link.iface2
             self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
             dst.ifaces[iface2.id] = iface2
             edge.dst_iface = iface2
Example #6
0
 def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
     token = create_edge_token(link)
     if token in self.edges and link.options.unidirectional:
         edge = self.edges[token]
         edge.asymmetric_link = link
     elif token not in self.edges:
         edge = CanvasEdge(self.app, src, dst)
         edge.complete(dst, link)
Example #7
0
 def create_edge(self, source: CanvasNode, dest: CanvasNode):
     """
     create an edge between source node and destination node
     """
     token = create_edge_token(source.id, dest.id)
     if token not in self.edges:
         pos = (source.core_node.position.x, source.core_node.position.y)
         edge = CanvasEdge(self, source.id, pos, pos)
         edge.complete(dest.id)
         self.edges[edge.token] = edge
         self.nodes[source.id].edges.add(edge)
         self.nodes[dest.id].edges.add(edge)
         self.core.create_link(edge, source, dest)
Example #8
0
 def create_edge(self, source: CanvasNode, dest: CanvasNode):
     """
     create an edge between source node and destination node
     """
     if (source.id, dest.id) not in self.edges:
         pos0 = source.core_node.position
         x0 = pos0.x
         y0 = pos0.y
         edge = CanvasEdge(x0, y0, x0, y0, source.id, self)
         edge.complete(dest.id)
         self.edges[edge.token] = edge
         self.nodes[source.id].edges.add(edge)
         self.nodes[dest.id].edges.add(edge)
         self.core.create_link(edge, source, dest)
Example #9
0
 def complete_edge(
     self,
     src: CanvasNode,
     dst: CanvasNode,
     edge: CanvasEdge,
     link: Optional[Link] = None,
 ) -> None:
     linked_wireless = self.is_linked_wireless(src.id, dst.id)
     edge.complete(dst.id, linked_wireless)
     if link is None:
         link = self.core.create_link(edge, src, dst)
     edge.link = link
     if link.iface1:
         iface1 = link.iface1
         src.ifaces[iface1.id] = iface1
     if link.iface2:
         iface2 = link.iface2
         dst.ifaces[iface2.id] = iface2
     src.edges.add(edge)
     dst.edges.add(edge)
     edge.token = create_edge_token(edge.link)
     self.arc_common_edges(edge)
     edge.draw_labels()
     edge.check_options()
     self.edges[edge.token] = edge
     self.core.save_edge(edge, src, dst)
Example #10
0
 def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
     """
     create an edge between source node and destination node
     """
     pos = (src.core_node.position.x, src.core_node.position.y)
     edge = CanvasEdge(self, src.id, pos, pos)
     self.complete_edge(src, dst, edge)
     return edge
Example #11
0
 def delete_edge(self, edge: CanvasEdge) -> None:
     edge.delete()
     del self.edges[edge.token]
     src_node = self.nodes[edge.src]
     src_node.edges.discard(edge)
     if edge.src_iface:
         del src_node.ifaces[edge.src_iface.id]
     dst_node = self.nodes[edge.dst]
     dst_node.edges.discard(edge)
     if edge.dst_iface:
         del dst_node.ifaces[edge.dst_iface.id]
     src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
     if src_wireless:
         dst_node.delete_antenna()
     dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
     if dst_wireless:
         src_node.delete_antenna()
     self.core.deleted_canvas_edges([edge])
Example #12
0
 def delete_edge(self, edge: CanvasEdge):
     edge.delete()
     del self.edges[edge.token]
     src_node = self.nodes[edge.src]
     src_node.edges.discard(edge)
     if edge.src_interface in src_node.interfaces:
         src_node.interfaces.remove(edge.src_interface)
     dst_node = self.nodes[edge.dst]
     dst_node.edges.discard(edge)
     if edge.dst_interface in dst_node.interfaces:
         dst_node.interfaces.remove(edge.dst_interface)
     src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
     if src_wireless:
         dst_node.delete_antenna()
     dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
     if dst_wireless:
         src_node.delete_antenna()
     self.core.deleted_graph_edges([edge])
Example #13
0
    def create_link(
        self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
    ) -> None:
        """
        Create core link for a pair of canvas nodes, with token referencing
        the canvas edge.
        """
        src_node = canvas_src_node.core_node
        dst_node = canvas_dst_node.core_node

        # determine subnet
        self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)

        src_iface = None
        if NodeUtils.is_container_node(src_node.type):
            src_iface = self.create_iface(canvas_src_node)
            self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token

        dst_iface = None
        if NodeUtils.is_container_node(dst_node.type):
            dst_iface = self.create_iface(canvas_dst_node)
            self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token

        link = Link(
            type=LinkType.WIRED,
            node1_id=src_node.id,
            node2_id=dst_node.id,
            iface1=src_iface,
            iface2=dst_iface,
        )
        # assign after creating link proto, since interfaces are copied
        if src_iface:
            iface1 = link.iface1
            edge.src_iface = iface1
            canvas_src_node.ifaces[iface1.id] = iface1
        if dst_iface:
            iface2 = link.iface2
            edge.dst_iface = iface2
            canvas_dst_node.ifaces[iface2.id] = iface2
        edge.set_link(link)
        self.links[edge.token] = edge
        logging.info("Add link between %s and %s", src_node.name, dst_node.name)
Example #14
0
 def add_wired_edge(self, src: CanvasNode, dst: CanvasNode,
                    link: Link) -> None:
     token = create_edge_token(link)
     if token in self.edges and link.options.unidirectional:
         edge = self.edges[token]
         edge.asymmetric_link = link
     elif token not in self.edges:
         node1 = src.core_node
         node2 = dst.core_node
         src_pos = (node1.position.x, node1.position.y)
         dst_pos = (node2.position.x, node2.position.y)
         edge = CanvasEdge(self, src.id, src_pos, dst_pos)
         self.complete_edge(src, dst, edge, link)
Example #15
0
 def click_link(self, node: "CanvasNode") -> None:
     edge = CanvasEdge(self.app, self, node)
     edge.complete(node)
Example #16
0
 def click_unlink(self, edge: CanvasEdge) -> None:
     edge.delete()
     self.app.default_info()
Example #17
0
class CanvasGraph(tk.Canvas):
    def __init__(
        self,
        master: tk.BaseWidget,
        app: "Application",
        manager: "CanvasManager",
        core: "CoreClient",
        _id: int,
        dimensions: Tuple[int, int],
    ) -> None:
        super().__init__(master,
                         highlightthickness=0,
                         background=BACKGROUND_COLOR)
        self.id: int = _id
        self.app: "Application" = app
        self.manager: "CanvasManager" = manager
        self.core: "CoreClient" = core
        self.selection: Dict[int, int] = {}
        self.select_box: Optional[Shape] = None
        self.selected: Optional[int] = None
        self.nodes: Dict[int, CanvasNode] = {}
        self.shadow_nodes: Dict[int, ShadowNode] = {}
        self.shapes: Dict[int, Shape] = {}
        self.shadow_core_nodes: Dict[int, ShadowNode] = {}

        # map wireless/EMANE node to the set of MDRs connected to that node
        self.wireless_network: Dict[int, Set[int]] = {}

        self.drawing_edge: Optional[CanvasEdge] = None
        self.rect: Optional[int] = None
        self.shape_drawing: bool = False
        self.current_dimensions: Tuple[int, int] = dimensions
        self.ratio: float = 1.0
        self.offset: Tuple[int, int] = (0, 0)
        self.cursor: Tuple[int, int] = (0, 0)
        self.to_copy: List[CanvasNode] = []

        # background related
        self.wallpaper_id: Optional[int] = None
        self.wallpaper: Optional[Image.Image] = None
        self.wallpaper_drawn: Optional[PhotoImage] = None
        self.wallpaper_file: str = ""
        self.scale_option: tk.IntVar = tk.IntVar(value=1)
        self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)

        # bindings
        self.setup_bindings()

        # draw base canvas
        self.draw_canvas()
        self.draw_grid()

    def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None:
        if self.rect is not None:
            self.delete(self.rect)
        if not dimensions:
            dimensions = self.manager.default_dimensions
        self.current_dimensions = dimensions
        self.rect = self.create_rectangle(
            0,
            0,
            *dimensions,
            outline="#000000",
            fill="#ffffff",
            width=1,
            tags="rectangle",
        )
        self.configure(scrollregion=self.bbox(tk.ALL))

    def setup_bindings(self) -> None:
        """
        Bind any mouse events or hot keys to the matching action
        """
        self.bind("<Control-c>", self.copy_selected)
        self.bind("<Control-v>", self.paste_selected)
        self.bind("<Control-x>", self.cut_selected)
        self.bind("<Control-d>", self.delete_selected)
        self.bind("<Control-h>", self.hide_selected)
        self.bind("<ButtonPress-1>", self.click_press)
        self.bind("<ButtonRelease-1>", self.click_release)
        self.bind("<B1-Motion>", self.click_motion)
        self.bind("<Delete>", self.delete_selected)
        self.bind("<Control-1>", self.ctrl_click)
        self.bind("<Double-Button-1>", self.double_click)
        self.bind("<MouseWheel>", self.zoom)
        self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN))
        self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT))
        self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
        self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))

    def get_shadow(self, node: CanvasNode) -> ShadowNode:
        shadow_node = self.shadow_core_nodes.get(node.core_node.id)
        if not shadow_node:
            shadow_node = ShadowNode(self.app, self, node)
        return shadow_node

    def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
        actual_x = (x - self.offset[0]) / self.ratio
        actual_y = (y - self.offset[1]) / self.ratio
        return actual_x, actual_y

    def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]:
        scaled_x = (x * self.ratio) + self.offset[0]
        scaled_y = (y * self.ratio) + self.offset[1]
        return scaled_x, scaled_y

    def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]:
        x1, y1, x2, y2 = self.bbox(self.rect)
        valid_x = x1 <= x <= x2
        valid_y = y1 <= y <= y2
        return valid_x and valid_y

    def valid_position(self, x1: int, y1: int, x2: int,
                       y2: int) -> Tuple[bool, bool]:
        valid_topleft = self.inside_canvas(x1, y1)
        valid_bottomright = self.inside_canvas(x2, y2)
        return valid_topleft and valid_bottomright

    def draw_grid(self) -> None:
        """
        Create grid.
        """
        width, height = self.width_and_height()
        width = int(width)
        height = int(height)
        for i in range(0, width, 27):
            self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE)
        for i in range(0, height, 27):
            self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
        self.tag_lower(tags.GRIDLINE)
        self.tag_lower(self.rect)

    def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
        """
        Convert window coordinate to canvas coordinate
        """
        x = self.canvasx(event.x)
        y = self.canvasy(event.y)
        return x, y

    def get_selected(self, event: tk.Event) -> int:
        """
        Retrieve the item id that is on the mouse position
        """
        x, y = self.canvas_xy(event)
        overlapping = self.find_overlapping(x, y, x, y)
        selected = None
        for _id in overlapping:
            if self.drawing_edge and self.drawing_edge.id == _id:
                continue
            elif _id in self.nodes:
                selected = _id
            elif _id in self.shapes:
                selected = _id
            elif _id in self.shadow_nodes:
                selected = _id
        return selected

    def click_release(self, event: tk.Event) -> None:
        """
        Draw a node or finish drawing an edge according to the current graph mode
        """
        logger.debug("click release")
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return
        if self.manager.mode == GraphMode.ANNOTATION:
            self.focus_set()
            if self.shape_drawing:
                shape = self.shapes[self.selected]
                shape.shape_complete(x, y)
                self.shape_drawing = False
        elif self.manager.mode == GraphMode.SELECT:
            self.focus_set()
            if self.select_box:
                x0, y0, x1, y1 = self.coords(self.select_box.id)
                inside = [
                    x for x in self.find_enclosed(x0, y0, x1, y1)
                    if "node" in self.gettags(x) or "shape" in self.gettags(x)
                ]
                for i in inside:
                    self.select_object(i, True)
                self.select_box.disappear()
                self.select_box = None
        else:
            self.focus_set()
            self.selected = self.get_selected(event)
            logger.debug("click release selected(%s) mode(%s)", self.selected,
                         self.manager.mode)
            if self.manager.mode == GraphMode.EDGE:
                self.handle_edge_release(event)
            elif self.manager.mode == GraphMode.NODE:
                self.add_node(x, y)
            elif self.manager.mode == GraphMode.PICKNODE:
                self.manager.mode = GraphMode.NODE
        self.selected = None

    def handle_edge_release(self, _event: tk.Event) -> None:
        # not drawing edge return
        if not self.drawing_edge:
            return
        edge = self.drawing_edge
        self.drawing_edge = None
        # edge dst must be a node
        logger.debug("current selected: %s", self.selected)
        dst_node = self.nodes.get(self.selected)
        if not dst_node:
            edge.delete()
            return
        # check if node can be linked
        if not edge.src.is_linkable(dst_node):
            edge.delete()
            return
        # finalize edge creation
        edge.drawing(dst_node.position())
        edge.complete(dst_node)

    def select_object(self,
                      object_id: int,
                      choose_multiple: bool = False) -> None:
        """
        create a bounding box when a node is selected
        """
        if not choose_multiple:
            self.clear_selection()

        # draw a bounding box if node hasn't been selected yet
        if object_id not in self.selection:
            x0, y0, x1, y1 = self.bbox(object_id)
            selection_id = self.create_rectangle(
                (x0 - 6, y0 - 6, x1 + 6, y1 + 6),
                activedash=True,
                dash="-",
                tags=tags.SELECTION,
            )
            self.selection[object_id] = selection_id
        else:
            selection_id = self.selection.pop(object_id)
            self.delete(selection_id)

    def clear_selection(self) -> None:
        """
        Clear current selection boxes.
        """
        for _id in self.selection.values():
            self.delete(_id)
        self.selection.clear()

    def move_selection(self, object_id: int, x_offset: float,
                       y_offset: float) -> None:
        select_id = self.selection.get(object_id)
        if select_id is not None:
            self.move(select_id, x_offset, y_offset)

    def delete_selected_objects(self, _event: tk.Event = None) -> None:
        edges = set()
        nodes = []
        for object_id in self.selection:
            #  delete selection box
            selection_id = self.selection[object_id]
            self.delete(selection_id)

            # delete node and related edges
            if object_id in self.nodes:
                canvas_node = self.nodes.pop(object_id)
                # delete related edges
                while canvas_node.edges:
                    edge = canvas_node.edges.pop()
                    if edge in edges:
                        continue
                    edges.add(edge)
                    edge.delete()
                # delete node
                canvas_node.delete()
                nodes.append(canvas_node)

            # delete shape
            if object_id in self.shapes:
                shape = self.shapes.pop(object_id)
                shape.delete()

        self.selection.clear()
        self.core.deleted_canvas_nodes(nodes)

    def hide_selected(self, _event: tk.Event = None) -> None:
        for object_id in self.selection:
            #  delete selection box
            selection_id = self.selection[object_id]
            self.delete(selection_id)
            # hide node and related edges
            if object_id in self.nodes:
                canvas_node = self.nodes[object_id]
                canvas_node.hide()

    def show_hidden(self) -> None:
        for node in self.nodes.values():
            if node.hidden:
                node.show()

    def zoom(self, event: tk.Event, factor: float = None) -> None:
        if not factor:
            factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
        event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
        self.scale(tk.ALL, event.x, event.y, factor, factor)
        self.configure(scrollregion=self.bbox(tk.ALL))
        self.ratio *= float(factor)
        self.offset = (
            self.offset[0] * factor + event.x * (1 - factor),
            self.offset[1] * factor + event.y * (1 - factor),
        )
        logger.debug("ratio: %s", self.ratio)
        logger.debug("offset: %s", self.offset)
        self.app.statusbar.set_zoom(self.ratio)
        if self.wallpaper:
            self.redraw_wallpaper()

    def click_press(self, event: tk.Event) -> None:
        """
        Start drawing an edge if mouse click is on a node
        """
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return

        self.cursor = x, y
        selected = self.get_selected(event)
        logger.debug("click press(%s): %s", self.cursor, selected)
        x_check = self.cursor[0] - self.offset[0]
        y_check = self.cursor[1] - self.offset[1]
        logger.debug("click press offset(%s, %s)", x_check, y_check)
        is_node = selected in self.nodes
        if self.manager.mode == GraphMode.EDGE and is_node:
            node = self.nodes[selected]
            self.drawing_edge = CanvasEdge(self.app, node)
            self.organize()

        if self.manager.mode == GraphMode.ANNOTATION:
            if is_marker(self.manager.annotation_type):
                r = self.app.toolbar.marker_frame.size.get()
                self.create_oval(
                    x - r,
                    y - r,
                    x + r,
                    y + r,
                    fill=self.app.toolbar.marker_frame.color,
                    outline="",
                    tags=(tags.MARKER, tags.ANNOTATION),
                    state=self.manager.show_annotations.state(),
                )
                return
            if selected is None:
                shape = Shape(self.app, self, self.manager.annotation_type, x,
                              y)
                self.selected = shape.id
                self.shape_drawing = True
                self.shapes[shape.id] = shape

        if selected is not None:
            if selected not in self.selection:
                if selected in self.shapes:
                    shape = self.shapes[selected]
                    self.select_object(shape.id)
                    self.selected = selected
                elif selected in self.nodes:
                    node = self.nodes[selected]
                    self.select_object(node.id)
                    self.selected = selected
                    logger.debug(
                        "selected node(%s), coords: (%s, %s)",
                        node.core_node.name,
                        node.core_node.position.x,
                        node.core_node.position.y,
                    )
                elif selected in self.shadow_nodes:
                    shadow_node = self.shadow_nodes[selected]
                    self.select_object(shadow_node.id)
                    self.selected = selected
                    logger.debug(
                        "selected shadow node(%s), coords: (%s, %s)",
                        shadow_node.node.core_node.name,
                        shadow_node.node.core_node.position.x,
                        shadow_node.node.core_node.position.y,
                    )
        else:
            if self.manager.mode == GraphMode.SELECT:
                shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
                self.select_box = shape
            self.clear_selection()

    def ctrl_click(self, event: tk.Event) -> None:
        # update cursor location
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return

        self.cursor = x, y

        # handle multiple selections
        logger.debug("control left click: %s", event)
        selected = self.get_selected(event)
        if (selected not in self.selection and selected in self.shapes
                or selected in self.nodes):
            self.select_object(selected, choose_multiple=True)

    def click_motion(self, event: tk.Event) -> None:
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            if self.select_box:
                self.select_box.delete()
                self.select_box = None
            if is_draw_shape(
                    self.manager.annotation_type) and self.shape_drawing:
                shape = self.shapes.pop(self.selected)
                shape.delete()
                self.shape_drawing = False
            return

        x_offset = x - self.cursor[0]
        y_offset = y - self.cursor[1]
        self.cursor = x, y

        if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None:
            self.drawing_edge.drawing(self.cursor)
        if self.manager.mode == GraphMode.ANNOTATION:
            if is_draw_shape(
                    self.manager.annotation_type) and self.shape_drawing:
                shape = self.shapes[self.selected]
                shape.shape_motion(x, y)
                return
            elif is_marker(self.manager.annotation_type):
                r = self.app.toolbar.marker_frame.size.get()
                self.create_oval(
                    x - r,
                    y - r,
                    x + r,
                    y + r,
                    fill=self.app.toolbar.marker_frame.color,
                    outline="",
                    tags=(tags.MARKER, tags.ANNOTATION),
                )
                return

        if self.manager.mode == GraphMode.EDGE:
            return

        # move selected objects
        if self.selection:
            for selected_id in self.selection:
                if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
                    shape = self.shapes[selected_id]
                    shape.motion(x_offset, y_offset)
                elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes:
                    node = self.nodes[selected_id]
                    node.motion(x_offset,
                                y_offset,
                                update=self.core.is_runtime())
                elif (self.manager.mode in MOVE_NODE_MODES
                      and selected_id in self.shadow_nodes):
                    shadow_node = self.shadow_nodes[selected_id]
                    shadow_node.motion(x_offset, y_offset)
        else:
            if self.select_box and self.manager.mode == GraphMode.SELECT:
                self.select_box.shape_motion(x, y)

    def double_click(self, event: tk.Event) -> None:
        selected = self.get_selected(event)
        if selected is not None and selected in self.shapes:
            shape = self.shapes[selected]
            dialog = ShapeDialog(self.app, shape)
            dialog.show()

    def add_node(self, x: float, y: float) -> None:
        if self.selected is not None and self.selected not in self.shapes:
            return
        actual_x, actual_y = self.get_actual_coords(x, y)
        core_node = self.core.create_node(
            actual_x,
            actual_y,
            self.manager.node_draw.node_type,
            self.manager.node_draw.model,
        )
        if not core_node:
            return
        core_node.canvas = self.id
        node = CanvasNode(self.app, self, x, y, core_node,
                          self.manager.node_draw.image)
        self.nodes[node.id] = node
        self.core.set_canvas_node(core_node, node)

    def width_and_height(self) -> Tuple[int, int]:
        """
        retrieve canvas width and height in pixels
        """
        x0, y0, x1, y1 = self.coords(self.rect)
        canvas_w = abs(x0 - x1)
        canvas_h = abs(y0 - y1)
        return canvas_w, canvas_h

    def get_wallpaper_image(self) -> Image.Image:
        width = int(self.wallpaper.width * self.ratio)
        height = int(self.wallpaper.height * self.ratio)
        image = self.wallpaper.resize((width, height), Image.ANTIALIAS)
        return image

    def draw_wallpaper(self,
                       image: PhotoImage,
                       x: float = None,
                       y: float = None) -> None:
        if x is None and y is None:
            x1, y1, x2, y2 = self.bbox(self.rect)
            x = (x1 + x2) / 2
            y = (y1 + y2) / 2
        self.wallpaper_id = self.create_image((x, y),
                                              image=image,
                                              tags=tags.WALLPAPER)
        self.wallpaper_drawn = image

    def wallpaper_upper_left(self) -> None:
        self.delete(self.wallpaper_id)

        # create new scaled image, cropped if needed
        width, height = self.width_and_height()
        image = self.get_wallpaper_image()
        cropx = image.width
        cropy = image.height
        if image.width > width:
            cropx = image.width
        if image.height > height:
            cropy = image.height
        cropped = image.crop((0, 0, cropx, cropy))
        image = PhotoImage(cropped)

        # draw on canvas
        x1, y1, _, _ = self.bbox(self.rect)
        x = (cropx / 2) + x1
        y = (cropy / 2) + y1
        self.draw_wallpaper(image, x, y)

    def wallpaper_center(self) -> None:
        """
        place the image at the center of canvas
        """
        self.delete(self.wallpaper_id)

        # dimension of the cropped image
        width, height = self.width_and_height()
        image = self.get_wallpaper_image()
        cropx = 0
        if image.width > width:
            cropx = (image.width - width) / 2
        cropy = 0
        if image.height > height:
            cropy = (image.height - height) / 2
        x1 = 0 + cropx
        y1 = 0 + cropy
        x2 = image.width - cropx
        y2 = image.height - cropy
        cropped = image.crop((x1, y1, x2, y2))
        image = PhotoImage(cropped)
        self.draw_wallpaper(image)

    def wallpaper_scaled(self) -> None:
        """
        scale image based on canvas dimension
        """
        self.delete(self.wallpaper_id)
        canvas_w, canvas_h = self.width_and_height()
        image = self.wallpaper.resize((int(canvas_w), int(canvas_h)),
                                      Image.ANTIALIAS)
        image = PhotoImage(image)
        self.draw_wallpaper(image)

    def resize_to_wallpaper(self) -> None:
        self.delete(self.wallpaper_id)
        image = PhotoImage(self.wallpaper)
        self.redraw_canvas((image.width(), image.height()))
        self.draw_wallpaper(image)

    def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None:
        logger.debug("redrawing canvas to dimensions: %s", dimensions)

        # reset scale and move back to original position
        logger.debug("resetting scaling: %s %s", self.ratio, self.offset)
        factor = 1 / self.ratio
        self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
        self.move(tk.ALL, -self.offset[0], -self.offset[1])

        # reset ratio and offset
        self.ratio = 1.0
        self.offset = (0, 0)

        # redraw canvas rectangle
        self.draw_canvas(dimensions)

        # redraw gridlines to new canvas size
        self.delete(tags.GRIDLINE)
        self.draw_grid()
        self.app.manager.show_grid.click_handler()

    def redraw_wallpaper(self) -> None:
        if self.adjust_to_dim.get():
            logger.debug("drawing wallpaper to canvas dimensions")
            self.resize_to_wallpaper()
        else:
            option = ScaleOption(self.scale_option.get())
            logger.debug("drawing canvas using scaling option: %s", option)
            if option == ScaleOption.UPPER_LEFT:
                self.wallpaper_upper_left()
            elif option == ScaleOption.CENTERED:
                self.wallpaper_center()
            elif option == ScaleOption.SCALED:
                self.wallpaper_scaled()
            elif option == ScaleOption.TILED:
                logger.warning("tiled background not implemented yet")
        self.organize()

    def organize(self) -> None:
        for tag in tags.ORGANIZE_TAGS:
            self.tag_raise(tag)

    def set_wallpaper(self, filename: Optional[str]) -> None:
        logger.info("setting canvas(%s) background: %s", self.id, filename)
        if filename:
            img = Image.open(filename)
            self.wallpaper = img
            self.wallpaper_file = filename
            self.redraw_wallpaper()
        else:
            if self.wallpaper_id is not None:
                self.delete(self.wallpaper_id)
            self.wallpaper = None
            self.wallpaper_file = None

    def is_selection_mode(self) -> bool:
        return self.manager.mode == GraphMode.SELECT

    def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
        """
        create an edge between source node and destination node
        """
        edge = CanvasEdge(self.app, src)
        edge.complete(dst)
        return edge

    def copy_selected(self, _event: tk.Event = None) -> None:
        if self.core.is_runtime():
            logger.debug("copy is disabled during runtime state")
            return
        if self.selection:
            logger.debug("to copy nodes: %s", self.selection)
            self.to_copy.clear()
            for node_id in self.selection.keys():
                canvas_node = self.nodes[node_id]
                self.to_copy.append(canvas_node)

    def cut_selected(self, _event: tk.Event = None) -> None:
        if self.core.is_runtime():
            logger.debug("cut is disabled during runtime state")
            return
        self.copy_selected()
        self.delete_selected()

    def delete_selected(self, _event: tk.Event = None) -> None:
        """
        delete selected nodes and any data that relates to it
        """
        logger.debug("press delete key")
        if self.core.is_runtime():
            logger.debug("node deletion is disabled during runtime state")
            return
        self.delete_selected_objects()
        self.app.default_info()

    def paste_selected(self, _event: tk.Event = None) -> None:
        if self.core.is_runtime():
            logger.debug("paste is disabled during runtime state")
            return
        # maps original node canvas id to copy node canvas id
        copy_map = {}
        # the edges that will be copy over
        to_copy_edges = set()
        to_copy_ids = {x.id for x in self.to_copy}
        for canvas_node in self.to_copy:
            core_node = canvas_node.core_node
            actual_x = core_node.position.x + 50
            actual_y = core_node.position.y + 50
            scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y)
            copy = self.core.create_node(actual_x, actual_y, core_node.type,
                                         core_node.model)
            if not copy:
                continue
            node = CanvasNode(self.app, self, scaled_x, scaled_y, copy,
                              canvas_node.image)
            # copy configurations and services
            node.core_node.services = core_node.services.copy()
            node.core_node.config_services = core_node.config_services.copy()
            node.core_node.emane_model_configs = deepcopy(
                core_node.emane_model_configs)
            node.core_node.wlan_config = deepcopy(core_node.wlan_config)
            node.core_node.mobility_config = deepcopy(
                core_node.mobility_config)
            node.core_node.service_configs = deepcopy(
                core_node.service_configs)
            node.core_node.service_file_configs = deepcopy(
                core_node.service_file_configs)
            node.core_node.config_service_configs = deepcopy(
                core_node.config_service_configs)

            copy_map[canvas_node.id] = node.id
            self.nodes[node.id] = node
            self.core.set_canvas_node(copy, node)
            for edge in canvas_node.edges:
                if edge.src not in to_copy_ids or edge.dst not in to_copy_ids:
                    if canvas_node.id == edge.src:
                        dst_node = self.nodes[edge.dst]
                        copy_edge = self.create_edge(node, dst_node)
                    elif canvas_node.id == edge.dst:
                        src_node = self.nodes[edge.src]
                        copy_edge = self.create_edge(src_node, node)
                    else:
                        continue
                    copy_link = copy_edge.link
                    iface1_id = copy_link.iface1.id if copy_link.iface1 else None
                    iface2_id = copy_link.iface2.id if copy_link.iface2 else None
                    options = edge.link.options
                    if options:
                        copy_edge.link.options = deepcopy(options)
                    if options and options.unidirectional:
                        asym_iface1 = None
                        if iface1_id is not None:
                            asym_iface1 = Interface(id=iface1_id)
                        asym_iface2 = None
                        if iface2_id is not None:
                            asym_iface2 = Interface(id=iface2_id)
                        copy_edge.asymmetric_link = Link(
                            node1_id=copy_link.node2_id,
                            node2_id=copy_link.node1_id,
                            iface1=asym_iface2,
                            iface2=asym_iface1,
                            options=deepcopy(edge.asymmetric_link.options),
                        )
                    copy_edge.redraw()
                else:
                    to_copy_edges.add(edge)

        # copy link and link config
        for edge in to_copy_edges:
            src_node_id = copy_map[edge.src]
            dst_node_id = copy_map[edge.dst]
            src_node_copy = self.nodes[src_node_id]
            dst_node_copy = self.nodes[dst_node_id]
            copy_edge = self.create_edge(src_node_copy, dst_node_copy)
            copy_link = copy_edge.link
            iface1_id = copy_link.iface1.id if copy_link.iface1 else None
            iface2_id = copy_link.iface2.id if copy_link.iface2 else None
            options = edge.link.options
            if options:
                copy_link.options = deepcopy(options)
            if options and options.unidirectional:
                asym_iface1 = None
                if iface1_id is not None:
                    asym_iface1 = Interface(id=iface1_id)
                asym_iface2 = None
                if iface2_id is not None:
                    asym_iface2 = Interface(id=iface2_id)
                copy_edge.asymmetric_link = Link(
                    node1_id=copy_link.node2_id,
                    node2_id=copy_link.node1_id,
                    iface1=asym_iface2,
                    iface2=asym_iface1,
                    options=deepcopy(edge.asymmetric_link.options),
                )
            copy_edge.redraw()
            self.itemconfig(
                copy_edge.id,
                width=self.itemcget(edge.id, "width"),
                fill=self.itemcget(edge.id, "fill"),
            )
        self.tag_raise(tags.NODE)

    def scale_graph(self) -> None:
        for node_id, canvas_node in self.nodes.items():
            image = nutils.get_icon(canvas_node.core_node, self.app)
            self.itemconfig(node_id, image=image)
            canvas_node.image = image
            canvas_node.scale_text()
            canvas_node.scale_antennas()
        for edge_id in self.find_withtag(tags.EDGE):
            self.itemconfig(edge_id,
                            width=int(EDGE_WIDTH * self.app.app_scale))

    def get_metadata(self) -> Dict[str, Any]:
        wallpaper_path = None
        if self.wallpaper_file:
            wallpaper = Path(self.wallpaper_file)
            if appconfig.BACKGROUNDS_PATH == wallpaper.parent:
                wallpaper_path = wallpaper.name
            else:
                wallpaper_path = str(wallpaper)
        return dict(
            id=self.id,
            wallpaper=wallpaper_path,
            wallpaper_style=self.scale_option.get(),
            fit_image=self.adjust_to_dim.get(),
            dimensions=self.current_dimensions,
        )

    def parse_metadata(self, config: Dict[str, Any]) -> None:
        fit_image = config.get("fit_image", False)
        self.adjust_to_dim.set(fit_image)
        wallpaper_style = config.get("wallpaper_style", 1)
        self.scale_option.set(wallpaper_style)
        dimensions = config.get("dimensions")
        if dimensions:
            self.redraw_canvas(dimensions)
        wallpaper = config.get("wallpaper")
        if wallpaper:
            wallpaper = Path(wallpaper)
            if not wallpaper.is_file():
                wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)
            logger.info("canvas(%s), wallpaper: %s", self.id, wallpaper)
            if wallpaper.is_file():
                self.set_wallpaper(str(wallpaper))
            else:
                self.app.show_error("Background Error",
                                    f"background file not found: {wallpaper}")
Example #18
0
    def draw_session(self, session):
        """
        Draw existing session.

        :return: nothing
        """
        # draw existing nodes
        for core_node in session.nodes:
            # peer to peer node is not drawn on the GUI
            if NodeUtils.is_ignore_node(core_node.type):
                continue

            # draw nodes on the canvas
            logging.info("drawing core node: %s", core_node)
            image = NodeUtils.node_icon(core_node.type, core_node.model)
            if core_node.icon:
                try:
                    image = Images.create(core_node.icon, nodeutils.ICON_SIZE)
                except OSError:
                    logging.error("invalid icon: %s", core_node.icon)

            x = core_node.position.x
            y = core_node.position.y
            node = CanvasNode(self.master, x, y, core_node, image)
            self.nodes[node.id] = node
            self.core.canvas_nodes[core_node.id] = node

        # draw existing links
        for link in session.links:
            logging.info("drawing link: %s", link)
            canvas_node_one = self.core.canvas_nodes[link.node_one_id]
            node_one = canvas_node_one.core_node
            canvas_node_two = self.core.canvas_nodes[link.node_two_id]
            node_two = canvas_node_two.core_node
            token = tuple(sorted((canvas_node_one.id, canvas_node_two.id)))

            if link.type == core_pb2.LinkType.WIRELESS:
                self.add_wireless_edge(canvas_node_one, canvas_node_two)
            else:
                if token not in self.edges:
                    edge = CanvasEdge(
                        node_one.position.x,
                        node_one.position.y,
                        node_two.position.x,
                        node_two.position.y,
                        canvas_node_one.id,
                        self,
                    )
                    edge.token = token
                    edge.dst = canvas_node_two.id
                    edge.set_link(link)
                    edge.check_wireless()
                    canvas_node_one.edges.add(edge)
                    canvas_node_two.edges.add(edge)
                    self.edges[edge.token] = edge
                    self.core.links[edge.token] = edge
                    if link.HasField("interface_one"):
                        canvas_node_one.interfaces.append(link.interface_one)
                    if link.HasField("interface_two"):
                        canvas_node_two.interfaces.append(link.interface_two)
                elif link.options.unidirectional:
                    edge = self.edges[token]
                    edge.asymmetric_link = link
                else:
                    logging.error("duplicate link received: %s", link)

        # raise the nodes so they on top of the links
        self.tag_raise(tags.NODE)
Example #19
0
    def draw_session(self, session: core_pb2.Session):
        """
        Draw existing session.
        """
        # draw existing nodes
        for core_node in session.nodes:
            logging.debug("drawing node %s", core_node)
            # peer to peer node is not drawn on the GUI
            if NodeUtils.is_ignore_node(core_node.type):
                continue
            image = NodeUtils.node_image(core_node, self.app.guiconfig,
                                         self.app.app_scale)
            # if the gui can't find node's image, default to the "edit-node" image
            if not image:
                image = Images.get(ImageEnum.EDITNODE,
                                   int(ICON_SIZE * self.app.app_scale))
            x = core_node.position.x
            y = core_node.position.y
            node = CanvasNode(self.app, x, y, core_node, image)
            self.nodes[node.id] = node
            self.core.canvas_nodes[core_node.id] = node

        # draw existing links
        for link in session.links:
            logging.debug("drawing link: %s", link)
            canvas_node_one = self.core.canvas_nodes[link.node_one_id]
            node_one = canvas_node_one.core_node
            canvas_node_two = self.core.canvas_nodes[link.node_two_id]
            node_two = canvas_node_two.core_node
            token = create_edge_token(canvas_node_one.id, canvas_node_two.id)

            if link.type == core_pb2.LinkType.WIRELESS:
                self.add_wireless_edge(canvas_node_one, canvas_node_two, link)
            else:
                if token not in self.edges:
                    src_pos = (node_one.position.x, node_one.position.y)
                    dst_pos = (node_two.position.x, node_two.position.y)
                    edge = CanvasEdge(self, canvas_node_one.id, src_pos,
                                      dst_pos)
                    edge.token = token
                    edge.dst = canvas_node_two.id
                    edge.set_link(link)
                    edge.check_wireless()
                    canvas_node_one.edges.add(edge)
                    canvas_node_two.edges.add(edge)
                    self.edges[edge.token] = edge
                    self.core.links[edge.token] = edge
                    if link.HasField("interface_one"):
                        canvas_node_one.interfaces.append(link.interface_one)
                        edge.src_interface = link.interface_one
                    if link.HasField("interface_two"):
                        canvas_node_two.interfaces.append(link.interface_two)
                        edge.dst_interface = link.interface_two
                elif link.options.unidirectional:
                    edge = self.edges[token]
                    edge.asymmetric_link = link
                else:
                    logging.error("duplicate link received: %s", link)

        # raise the nodes so they on top of the links
        self.tag_raise(tags.NODE)
Example #20
0
class CanvasGraph(tk.Canvas):
    def __init__(
        self,
        master: tk.Widget,
        app: "Application",
        core: "CoreClient",
        width: int,
        height: int,
    ):
        super().__init__(master, highlightthickness=0, background="#cccccc")
        self.app = app
        self.core = core
        self.mode = GraphMode.SELECT
        self.annotation_type = None
        self.selection = {}
        self.select_box = None
        self.selected = None
        self.node_draw = None
        self.nodes = {}
        self.edges = {}
        self.shapes = {}
        self.wireless_edges = {}

        # map wireless/EMANE node to the set of MDRs connected to that node
        self.wireless_network = {}

        self.drawing_edge = None
        self.rect = None
        self.shape_drawing = False
        self.default_dimensions = (width, height)
        self.current_dimensions = self.default_dimensions
        self.ratio = 1.0
        self.offset = (0, 0)
        self.cursor = (0, 0)
        self.marker_tool = None
        self.to_copy = []

        # background related
        self.wallpaper_id = None
        self.wallpaper = None
        self.wallpaper_drawn = None
        self.wallpaper_file = ""
        self.scale_option = tk.IntVar(value=1)
        self.adjust_to_dim = tk.BooleanVar(value=False)

        # throughput related
        self.throughput_threshold = 250.0
        self.throughput_width = 10
        self.throughput_color = "#FF0000"

        # drawing related
        self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True)
        self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True)
        self.show_grid = ShowVar(self, tags.GRIDLINE, value=True)
        self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True)
        self.show_interface_names = BooleanVar(value=False)
        self.show_ip4s = BooleanVar(value=True)
        self.show_ip6s = BooleanVar(value=True)

        # bindings
        self.setup_bindings()

        # draw base canvas
        self.draw_canvas()
        self.draw_grid()

    def draw_canvas(self, dimensions: Tuple[int, int] = None):
        if self.rect is not None:
            self.delete(self.rect)
        if not dimensions:
            dimensions = self.default_dimensions
        self.current_dimensions = dimensions
        self.rect = self.create_rectangle(
            0,
            0,
            *dimensions,
            outline="#000000",
            fill="#ffffff",
            width=1,
            tags="rectangle",
        )
        self.configure(scrollregion=self.bbox(tk.ALL))

    def reset_and_redraw(self, session: core_pb2.Session):
        """
        Reset the private variables CanvasGraph object, redraw nodes given the new grpc
        client.
        :param session: session to draw
        """
        # reset view options to default state
        self.show_node_labels.set(True)
        self.show_link_labels.set(True)
        self.show_grid.set(True)
        self.show_annotations.set(True)
        self.show_interface_names.set(False)
        self.show_ip4s.set(True)
        self.show_ip6s.set(True)

        # delete any existing drawn items
        for tag in tags.COMPONENT_TAGS:
            self.delete(tag)

        # set the private variables to default value
        self.mode = GraphMode.SELECT
        self.annotation_type = None
        self.node_draw = None
        self.selected = None
        self.nodes.clear()
        self.edges.clear()
        self.shapes.clear()
        self.wireless_edges.clear()
        self.wireless_network.clear()
        self.drawing_edge = None
        self.draw_session(session)

    def setup_bindings(self):
        """
        Bind any mouse events or hot keys to the matching action
        """
        self.bind("<ButtonPress-1>", self.click_press)
        self.bind("<ButtonRelease-1>", self.click_release)
        self.bind("<B1-Motion>", self.click_motion)
        self.bind("<Delete>", self.press_delete)
        self.bind("<Control-1>", self.ctrl_click)
        self.bind("<Double-Button-1>", self.double_click)
        self.bind("<MouseWheel>", self.zoom)
        self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN))
        self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT))
        self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
        self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))

    def get_actual_coords(self, x: float, y: float) -> [float, float]:
        actual_x = (x - self.offset[0]) / self.ratio
        actual_y = (y - self.offset[1]) / self.ratio
        return actual_x, actual_y

    def get_scaled_coords(self, x: float, y: float) -> [float, float]:
        scaled_x = (x * self.ratio) + self.offset[0]
        scaled_y = (y * self.ratio) + self.offset[1]
        return scaled_x, scaled_y

    def inside_canvas(self, x: float, y: float) -> [bool, bool]:
        x1, y1, x2, y2 = self.bbox(self.rect)
        valid_x = x1 <= x <= x2
        valid_y = y1 <= y <= y2
        return valid_x and valid_y

    def valid_position(self, x1: int, y1: int, x2: int,
                       y2: int) -> [bool, bool]:
        valid_topleft = self.inside_canvas(x1, y1)
        valid_bottomright = self.inside_canvas(x2, y2)
        return valid_topleft and valid_bottomright

    def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent):
        for interface_throughput in throughputs_event.interface_throughputs:
            node_id = interface_throughput.node_id
            interface_id = interface_throughput.interface_id
            throughput = interface_throughput.throughput
            interface_to_edge_id = (node_id, interface_id)
            token = self.core.interface_to_edge.get(interface_to_edge_id)
            if not token:
                continue
            edge = self.edges.get(token)
            if edge:
                edge.set_throughput(throughput)
            else:
                del self.core.interface_to_edge[interface_to_edge_id]

    def draw_grid(self):
        """
        Create grid.
        """
        width, height = self.width_and_height()
        width = int(width)
        height = int(height)
        for i in range(0, width, 27):
            self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE)
        for i in range(0, height, 27):
            self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
        self.tag_lower(tags.GRIDLINE)
        self.tag_lower(self.rect)

    def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode,
                          link: core_pb2.Link) -> None:
        network_id = link.network_id if link.network_id else None
        token = create_edge_token(src.id, dst.id, network_id)
        if token in self.wireless_edges:
            logging.warning("ignoring link that already exists: %s", link)
            return
        src_pos = self.coords(src.id)
        dst_pos = self.coords(dst.id)
        edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos,
                                  token)
        if link.label:
            edge.middle_label_text(link.label)
        if link.color:
            edge.color = link.color
        self.wireless_edges[token] = edge
        src.wireless_edges.add(edge)
        dst.wireless_edges.add(edge)
        self.tag_raise(src.id)
        self.tag_raise(dst.id)
        # update arcs when there are multiple links
        common_edges = list(src.wireless_edges & dst.wireless_edges)
        arc_edges(common_edges)

    def delete_wireless_edge(self, src: CanvasNode, dst: CanvasNode,
                             link: core_pb2.Link) -> None:
        network_id = link.network_id if link.network_id else None
        token = create_edge_token(src.id, dst.id, network_id)
        if token not in self.wireless_edges:
            return
        edge = self.wireless_edges.pop(token)
        edge.delete()
        src.wireless_edges.remove(edge)
        dst.wireless_edges.remove(edge)
        # update arcs when there are multiple links
        common_edges = list(src.wireless_edges & dst.wireless_edges)
        arc_edges(common_edges)

    def update_wireless_edge(self, src: CanvasNode, dst: CanvasNode,
                             link: core_pb2.Link) -> None:
        if not link.label:
            return
        network_id = link.network_id if link.network_id else None
        token = create_edge_token(src.id, dst.id, network_id)
        if token not in self.wireless_edges:
            self.add_wireless_edge(src, dst, link)
        else:
            edge = self.wireless_edges[token]
            edge.middle_label_text(link.label)

    def draw_session(self, session: core_pb2.Session):
        """
        Draw existing session.
        """
        # draw existing nodes
        for core_node in session.nodes:
            logging.debug("drawing node %s", core_node)
            # peer to peer node is not drawn on the GUI
            if NodeUtils.is_ignore_node(core_node.type):
                continue
            image = NodeUtils.node_image(core_node, self.app.guiconfig,
                                         self.app.app_scale)
            # if the gui can't find node's image, default to the "edit-node" image
            if not image:
                image = Images.get(ImageEnum.EDITNODE,
                                   int(ICON_SIZE * self.app.app_scale))
            x = core_node.position.x
            y = core_node.position.y
            node = CanvasNode(self.app, x, y, core_node, image)
            self.nodes[node.id] = node
            self.core.canvas_nodes[core_node.id] = node

        # draw existing links
        for link in session.links:
            logging.debug("drawing link: %s", link)
            canvas_node_one = self.core.canvas_nodes[link.node_one_id]
            node_one = canvas_node_one.core_node
            canvas_node_two = self.core.canvas_nodes[link.node_two_id]
            node_two = canvas_node_two.core_node
            token = create_edge_token(canvas_node_one.id, canvas_node_two.id)

            if link.type == core_pb2.LinkType.WIRELESS:
                self.add_wireless_edge(canvas_node_one, canvas_node_two, link)
            else:
                if token not in self.edges:
                    src_pos = (node_one.position.x, node_one.position.y)
                    dst_pos = (node_two.position.x, node_two.position.y)
                    edge = CanvasEdge(self, canvas_node_one.id, src_pos,
                                      dst_pos)
                    edge.token = token
                    edge.dst = canvas_node_two.id
                    edge.set_link(link)
                    edge.check_wireless()
                    canvas_node_one.edges.add(edge)
                    canvas_node_two.edges.add(edge)
                    self.edges[edge.token] = edge
                    self.core.links[edge.token] = edge
                    if link.HasField("interface_one"):
                        canvas_node_one.interfaces.append(link.interface_one)
                        edge.src_interface = link.interface_one
                    if link.HasField("interface_two"):
                        canvas_node_two.interfaces.append(link.interface_two)
                        edge.dst_interface = link.interface_two
                elif link.options.unidirectional:
                    edge = self.edges[token]
                    edge.asymmetric_link = link
                else:
                    logging.error("duplicate link received: %s", link)

        # raise the nodes so they on top of the links
        self.tag_raise(tags.NODE)

    def stopped_session(self):
        # clear wireless edges
        for edge in self.wireless_edges.values():
            edge.delete()
            src_node = self.nodes[edge.src]
            src_node.wireless_edges.remove(edge)
            dst_node = self.nodes[edge.dst]
            dst_node.wireless_edges.remove(edge)
        self.wireless_edges.clear()

        # clear all middle edge labels
        for edge in self.edges.values():
            edge.reset()

    def canvas_xy(self, event: tk.Event) -> [float, float]:
        """
        Convert window coordinate to canvas coordinate
        """
        x = self.canvasx(event.x)
        y = self.canvasy(event.y)
        return x, y

    def get_selected(self, event: tk.Event) -> int:
        """
        Retrieve the item id that is on the mouse position
        """
        x, y = self.canvas_xy(event)
        overlapping = self.find_overlapping(x, y, x, y)
        selected = None
        for _id in overlapping:
            if self.drawing_edge and self.drawing_edge.id == _id:
                continue

            if _id in self.nodes:
                selected = _id
                break

            if _id in self.shapes:
                selected = _id

        return selected

    def click_release(self, event: tk.Event):
        """
        Draw a node or finish drawing an edge according to the current graph mode
        """
        logging.debug("click release")
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return
        if self.mode == GraphMode.ANNOTATION:
            self.focus_set()
            if self.shape_drawing:
                shape = self.shapes[self.selected]
                shape.shape_complete(x, y)
                self.shape_drawing = False
        elif self.mode == GraphMode.SELECT:
            self.focus_set()
            if self.select_box:
                x0, y0, x1, y1 = self.coords(self.select_box.id)
                inside = [
                    x for x in self.find_enclosed(x0, y0, x1, y1)
                    if "node" in self.gettags(x) or "shape" in self.gettags(x)
                ]
                for i in inside:
                    self.select_object(i, True)
                self.select_box.disappear()
                self.select_box = None
        else:
            self.focus_set()
            self.selected = self.get_selected(event)
            logging.debug(
                f"click release selected({self.selected}) mode({self.mode})")
            if self.mode == GraphMode.EDGE:
                self.handle_edge_release(event)
            elif self.mode == GraphMode.NODE:
                self.add_node(x, y)
            elif self.mode == GraphMode.PICKNODE:
                self.mode = GraphMode.NODE
        self.selected = None

    def handle_edge_release(self, _event: tk.Event):
        edge = self.drawing_edge
        self.drawing_edge = None

        # not drawing edge return
        if edge is None:
            return

        # edge dst must be a node
        logging.debug("current selected: %s", self.selected)
        dst_node = self.nodes.get(self.selected)
        if not dst_node:
            edge.delete()
            return

        # edge dst is same as src, delete edge
        if edge.src == self.selected:
            edge.delete()
            return

        # ignore repeated edges
        token = create_edge_token(edge.src, self.selected)
        if token in self.edges:
            edge.delete()
            return

        # set dst node and snap edge to center
        edge.complete(self.selected)

        self.edges[edge.token] = edge
        node_src = self.nodes[edge.src]
        node_src.edges.add(edge)
        node_dst = self.nodes[edge.dst]
        node_dst.edges.add(edge)
        self.core.create_link(edge, node_src, node_dst)

    def select_object(self, object_id: int, choose_multiple: bool = False):
        """
        create a bounding box when a node is selected
        """
        if not choose_multiple:
            self.clear_selection()

        # draw a bounding box if node hasn't been selected yet
        if object_id not in self.selection:
            x0, y0, x1, y1 = self.bbox(object_id)
            selection_id = self.create_rectangle(
                (x0 - 6, y0 - 6, x1 + 6, y1 + 6),
                activedash=True,
                dash="-",
                tags=tags.SELECTION,
            )
            self.selection[object_id] = selection_id
        else:
            selection_id = self.selection.pop(object_id)
            self.delete(selection_id)

    def clear_selection(self):
        """
        Clear current selection boxes.
        """
        for _id in self.selection.values():
            self.delete(_id)
        self.selection.clear()

    def move_selection(self, object_id: int, x_offset: float, y_offset: float):
        select_id = self.selection.get(object_id)
        if select_id is not None:
            self.move(select_id, x_offset, y_offset)

    def delete_selected_objects(self) -> None:
        edges = set()
        nodes = []
        for object_id in self.selection:
            #  delete selection box
            selection_id = self.selection[object_id]
            self.delete(selection_id)

            # delete node and related edges
            if object_id in self.nodes:
                canvas_node = self.nodes.pop(object_id)
                canvas_node.delete()
                nodes.append(canvas_node)
                is_wireless = NodeUtils.is_wireless_node(
                    canvas_node.core_node.type)
                # delete related edges
                for edge in canvas_node.edges:
                    if edge in edges:
                        continue
                    edges.add(edge)
                    del self.edges[edge.token]
                    edge.delete()
                    # update node connected to edge being deleted
                    other_id = edge.src
                    other_interface = edge.src_interface
                    if edge.src == object_id:
                        other_id = edge.dst
                        other_interface = edge.dst_interface
                    other_node = self.nodes[other_id]
                    other_node.edges.remove(edge)
                    if other_interface in other_node.interfaces:
                        other_node.interfaces.remove(other_interface)
                    if is_wireless:
                        other_node.delete_antenna()

            # delete shape
            if object_id in self.shapes:
                shape = self.shapes.pop(object_id)
                shape.delete()

        self.selection.clear()
        self.core.deleted_graph_nodes(nodes)
        self.core.deleted_graph_edges(edges)

    def delete_edge(self, edge: CanvasEdge):
        edge.delete()
        del self.edges[edge.token]
        src_node = self.nodes[edge.src]
        src_node.edges.discard(edge)
        if edge.src_interface in src_node.interfaces:
            src_node.interfaces.remove(edge.src_interface)
        dst_node = self.nodes[edge.dst]
        dst_node.edges.discard(edge)
        if edge.dst_interface in dst_node.interfaces:
            dst_node.interfaces.remove(edge.dst_interface)
        src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
        if src_wireless:
            dst_node.delete_antenna()
        dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
        if dst_wireless:
            src_node.delete_antenna()
        self.core.deleted_graph_edges([edge])

    def zoom(self, event: tk.Event, factor: float = None):
        if not factor:
            factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
        event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
        self.scale(tk.ALL, event.x, event.y, factor, factor)
        self.configure(scrollregion=self.bbox(tk.ALL))
        self.ratio *= float(factor)
        self.offset = (
            self.offset[0] * factor + event.x * (1 - factor),
            self.offset[1] * factor + event.y * (1 - factor),
        )
        logging.info("ratio: %s", self.ratio)
        logging.info("offset: %s", self.offset)
        self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) +
                                       "%")

        if self.wallpaper:
            self.redraw_wallpaper()

    def click_press(self, event: tk.Event):
        """
        Start drawing an edge if mouse click is on a node
        """
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return

        self.cursor = x, y
        selected = self.get_selected(event)
        logging.debug("click press(%s): %s", self.cursor, selected)
        x_check = self.cursor[0] - self.offset[0]
        y_check = self.cursor[1] - self.offset[1]
        logging.debug("click press offset(%s, %s)", x_check, y_check)
        is_node = selected in self.nodes
        if self.mode == GraphMode.EDGE and is_node:
            pos = self.coords(selected)
            self.drawing_edge = CanvasEdge(self, selected, pos, pos)

        if self.mode == GraphMode.ANNOTATION:
            if is_marker(self.annotation_type):
                r = self.app.toolbar.marker_tool.radius
                self.create_oval(
                    x - r,
                    y - r,
                    x + r,
                    y + r,
                    fill=self.app.toolbar.marker_tool.color,
                    outline="",
                    tags=(tags.MARKER, tags.ANNOTATION),
                    state=self.show_annotations.state(),
                )
                return
            if selected is None:
                shape = Shape(self.app, self, self.annotation_type, x, y)
                self.selected = shape.id
                self.shape_drawing = True
                self.shapes[shape.id] = shape

        if selected is not None:
            if selected not in self.selection:
                if selected in self.shapes:
                    shape = self.shapes[selected]
                    self.select_object(shape.id)
                    self.selected = selected
                elif selected in self.nodes:
                    node = self.nodes[selected]
                    self.select_object(node.id)
                    self.selected = selected
                    logging.debug(
                        "selected node(%s), coords: (%s, %s)",
                        node.core_node.name,
                        node.core_node.position.x,
                        node.core_node.position.y,
                    )
        else:
            if self.mode == GraphMode.SELECT:
                shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
                self.select_box = shape
            self.clear_selection()

    def ctrl_click(self, event: tk.Event):
        # update cursor location
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            return

        self.cursor = x, y

        # handle multiple selections
        logging.debug("control left click: %s", event)
        selected = self.get_selected(event)
        if (selected not in self.selection and selected in self.shapes
                or selected in self.nodes):
            self.select_object(selected, choose_multiple=True)

    def click_motion(self, event: tk.Event):
        """
        Redraw drawing edge according to the current position of the mouse
        """
        x, y = self.canvas_xy(event)
        if not self.inside_canvas(x, y):
            if self.select_box:
                self.select_box.delete()
                self.select_box = None
            if is_draw_shape(self.annotation_type) and self.shape_drawing:
                shape = self.shapes.pop(self.selected)
                shape.delete()
                self.shape_drawing = False
            return

        x_offset = x - self.cursor[0]
        y_offset = y - self.cursor[1]
        self.cursor = x, y

        if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
            self.drawing_edge.move_dst(self.cursor)
        if self.mode == GraphMode.ANNOTATION:
            if is_draw_shape(self.annotation_type) and self.shape_drawing:
                shape = self.shapes[self.selected]
                shape.shape_motion(x, y)
            elif is_marker(self.annotation_type):
                r = self.app.toolbar.marker_tool.radius
                self.create_oval(
                    x - r,
                    y - r,
                    x + r,
                    y + r,
                    fill=self.app.toolbar.marker_tool.color,
                    outline="",
                    tags=(tags.MARKER, tags.ANNOTATION),
                )
            return

        if self.mode == GraphMode.EDGE:
            return

        # move selected objects
        if self.selection:
            for selected_id in self.selection:
                if selected_id in self.shapes:
                    shape = self.shapes[selected_id]
                    shape.motion(x_offset, y_offset)

                if selected_id in self.nodes:
                    node = self.nodes[selected_id]
                    node.motion(x_offset,
                                y_offset,
                                update=self.core.is_runtime())
        else:
            if self.select_box and self.mode == GraphMode.SELECT:
                self.select_box.shape_motion(x, y)

    def press_delete(self, _event: tk.Event):
        """
        delete selected nodes and any data that relates to it
        """
        logging.debug("press delete key")
        if not self.app.core.is_runtime():
            self.delete_selected_objects()
        else:
            logging.info("node deletion is disabled during runtime state")

    def double_click(self, event: tk.Event):
        selected = self.get_selected(event)
        if selected is not None and selected in self.shapes:
            shape = self.shapes[selected]
            dialog = ShapeDialog(self.app, shape)
            dialog.show()

    def add_node(self, x: float, y: float) -> None:
        if self.selected is not None and self.selected not in self.shapes:
            return
        actual_x, actual_y = self.get_actual_coords(x, y)
        core_node = self.core.create_node(actual_x, actual_y,
                                          self.node_draw.node_type,
                                          self.node_draw.model)
        if not core_node:
            return
        try:
            self.node_draw.image = Images.get(
                self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale))
        except AttributeError:
            self.node_draw.image = Images.get_custom(
                self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale))
        node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
        self.core.canvas_nodes[core_node.id] = node
        self.nodes[node.id] = node

    def width_and_height(self):
        """
        retrieve canvas width and height in pixels
        """
        x0, y0, x1, y1 = self.coords(self.rect)
        canvas_w = abs(x0 - x1)
        canvas_h = abs(y0 - y1)
        return canvas_w, canvas_h

    def get_wallpaper_image(self) -> Image.Image:
        width = int(self.wallpaper.width * self.ratio)
        height = int(self.wallpaper.height * self.ratio)
        image = self.wallpaper.resize((width, height), Image.ANTIALIAS)
        return image

    def draw_wallpaper(self,
                       image: ImageTk.PhotoImage,
                       x: float = None,
                       y: float = None):
        if x is None and y is None:
            x1, y1, x2, y2 = self.bbox(self.rect)
            x = (x1 + x2) / 2
            y = (y1 + y2) / 2
        self.wallpaper_id = self.create_image((x, y),
                                              image=image,
                                              tags=tags.WALLPAPER)
        self.wallpaper_drawn = image

    def wallpaper_upper_left(self):
        self.delete(self.wallpaper_id)

        # create new scaled image, cropped if needed
        width, height = self.width_and_height()
        image = self.get_wallpaper_image()
        cropx = image.width
        cropy = image.height
        if image.width > width:
            cropx = image.width
        if image.height > height:
            cropy = image.height
        cropped = image.crop((0, 0, cropx, cropy))
        image = ImageTk.PhotoImage(cropped)

        # draw on canvas
        x1, y1, _, _ = self.bbox(self.rect)
        x = (cropx / 2) + x1
        y = (cropy / 2) + y1
        self.draw_wallpaper(image, x, y)

    def wallpaper_center(self):
        """
        place the image at the center of canvas
        """
        self.delete(self.wallpaper_id)

        # dimension of the cropped image
        width, height = self.width_and_height()
        image = self.get_wallpaper_image()
        cropx = 0
        if image.width > width:
            cropx = (image.width - width) / 2
        cropy = 0
        if image.height > height:
            cropy = (image.height - height) / 2
        x1 = 0 + cropx
        y1 = 0 + cropy
        x2 = image.width - cropx
        y2 = image.height - cropy
        cropped = image.crop((x1, y1, x2, y2))
        image = ImageTk.PhotoImage(cropped)
        self.draw_wallpaper(image)

    def wallpaper_scaled(self):
        """
        scale image based on canvas dimension
        """
        self.delete(self.wallpaper_id)
        canvas_w, canvas_h = self.width_and_height()
        image = self.wallpaper.resize((int(canvas_w), int(canvas_h)),
                                      Image.ANTIALIAS)
        image = ImageTk.PhotoImage(image)
        self.draw_wallpaper(image)

    def resize_to_wallpaper(self):
        self.delete(self.wallpaper_id)
        image = ImageTk.PhotoImage(self.wallpaper)
        self.redraw_canvas((image.width(), image.height()))
        self.draw_wallpaper(image)

    def redraw_canvas(self, dimensions: Tuple[int, int] = None):
        logging.info("redrawing canvas to dimensions: %s", dimensions)

        # reset scale and move back to original position
        logging.info("resetting scaling: %s %s", self.ratio, self.offset)
        factor = 1 / self.ratio
        self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
        self.move(tk.ALL, -self.offset[0], -self.offset[1])

        # reset ratio and offset
        self.ratio = 1.0
        self.offset = (0, 0)

        # redraw canvas rectangle
        self.draw_canvas(dimensions)

        # redraw gridlines to new canvas size
        self.delete(tags.GRIDLINE)
        self.draw_grid()
        self.app.canvas.show_grid.click_handler()

    def redraw_wallpaper(self):
        if self.adjust_to_dim.get():
            logging.info("drawing wallpaper to canvas dimensions")
            self.resize_to_wallpaper()
        else:
            option = ScaleOption(self.scale_option.get())
            logging.info("drawing canvas using scaling option: %s", option)
            if option == ScaleOption.UPPER_LEFT:
                self.wallpaper_upper_left()
            elif option == ScaleOption.CENTERED:
                self.wallpaper_center()
            elif option == ScaleOption.SCALED:
                self.wallpaper_scaled()
            elif option == ScaleOption.TILED:
                logging.warning("tiled background not implemented yet")

        # raise items above wallpaper
        for component in tags.ABOVE_WALLPAPER_TAGS:
            self.tag_raise(component)

    def set_wallpaper(self, filename: str):
        logging.debug("setting wallpaper: %s", filename)
        if filename:
            img = Image.open(filename)
            self.wallpaper = img
            self.wallpaper_file = filename
            self.redraw_wallpaper()
        else:
            if self.wallpaper_id is not None:
                self.delete(self.wallpaper_id)
            self.wallpaper = None
            self.wallpaper_file = None

    def is_selection_mode(self) -> bool:
        return self.mode == GraphMode.SELECT

    def create_edge(self, source: CanvasNode, dest: CanvasNode):
        """
        create an edge between source node and destination node
        """
        token = create_edge_token(source.id, dest.id)
        if token not in self.edges:
            pos = (source.core_node.position.x, source.core_node.position.y)
            edge = CanvasEdge(self, source.id, pos, pos)
            edge.complete(dest.id)
            self.edges[edge.token] = edge
            self.nodes[source.id].edges.add(edge)
            self.nodes[dest.id].edges.add(edge)
            self.core.create_link(edge, source, dest)

    def copy(self):
        if self.core.is_runtime():
            logging.info("copy is disabled during runtime state")
            return
        if self.selection:
            logging.info("to copy nodes: %s", self.selection)
            self.to_copy.clear()
            for node_id in self.selection.keys():
                canvas_node = self.nodes[node_id]
                self.to_copy.append(canvas_node)

    def paste(self):
        if self.core.is_runtime():
            logging.info("paste is disabled during runtime state")
            return
        # maps original node canvas id to copy node canvas id
        copy_map = {}
        # the edges that will be copy over
        to_copy_edges = []
        for canvas_node in self.to_copy:
            core_node = canvas_node.core_node
            actual_x = core_node.position.x + 50
            actual_y = core_node.position.y + 50
            scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y)
            copy = self.core.create_node(actual_x, actual_y, core_node.type,
                                         core_node.model)
            if not copy:
                continue
            node = CanvasNode(self.app, scaled_x, scaled_y, copy,
                              canvas_node.image)

            # copy configurations and services
            node.core_node.services[:] = canvas_node.core_node.services
            node.core_node.config_services[:] = canvas_node.core_node.config_services
            node.emane_model_configs = deepcopy(
                canvas_node.emane_model_configs)
            node.wlan_config = deepcopy(canvas_node.wlan_config)
            node.mobility_config = deepcopy(canvas_node.mobility_config)
            node.service_configs = deepcopy(canvas_node.service_configs)
            node.service_file_configs = deepcopy(
                canvas_node.service_file_configs)
            node.config_service_configs = deepcopy(
                canvas_node.config_service_configs)

            copy_map[canvas_node.id] = node.id
            self.core.canvas_nodes[copy.id] = node
            self.nodes[node.id] = node
            for edge in canvas_node.edges:
                if edge.src not in self.to_copy or edge.dst not in self.to_copy:
                    if canvas_node.id == edge.src:
                        dst_node = self.nodes[edge.dst]
                        self.create_edge(node, dst_node)
                    elif canvas_node.id == edge.dst:
                        src_node = self.nodes[edge.src]
                        self.create_edge(src_node, node)
                else:
                    to_copy_edges.append(edge)

        # copy link and link config
        for edge in to_copy_edges:
            src_node_id = copy_map[edge.token[0]]
            dst_node_id = copy_map[edge.token[1]]
            src_node_copy = self.nodes[src_node_id]
            dst_node_copy = self.nodes[dst_node_id]
            self.create_edge(src_node_copy, dst_node_copy)
            token = create_edge_token(src_node_copy.id, dst_node_copy.id)
            copy_edge = self.edges[token]
            copy_link = copy_edge.link
            options = edge.link.options
            copy_link.options.CopyFrom(options)
            interface_one = None
            if copy_link.HasField("interface_one"):
                interface_one = copy_link.interface_one.id
            interface_two = None
            if copy_link.HasField("interface_two"):
                interface_two = copy_link.interface_two.id
            if not options.unidirectional:
                copy_edge.asymmetric_link = None
            else:
                asym_interface_one = None
                if interface_one:
                    asym_interface_one = core_pb2.Interface(id=interface_one)
                asym_interface_two = None
                if interface_two:
                    asym_interface_two = core_pb2.Interface(id=interface_two)
                copy_edge.asymmetric_link = core_pb2.Link(
                    node_one_id=copy_link.node_two_id,
                    node_two_id=copy_link.node_one_id,
                    interface_one=asym_interface_one,
                    interface_two=asym_interface_two,
                    options=edge.asymmetric_link.options,
                )
            self.itemconfig(
                copy_edge.id,
                width=self.itemcget(edge.id, "width"),
                fill=self.itemcget(edge.id, "fill"),
            )
        self.tag_raise(tags.NODE)

    def scale_graph(self):
        for nid, canvas_node in self.nodes.items():
            img = None
            if NodeUtils.is_custom(canvas_node.core_node.type,
                                   canvas_node.core_node.model):
                for custom_node in self.app.guiconfig.nodes:
                    if custom_node.name == canvas_node.core_node.model:
                        img = Images.get_custom(
                            custom_node.image,
                            int(ICON_SIZE * self.app.app_scale))
            else:
                image_enum = TypeToImage.get(canvas_node.core_node.type,
                                             canvas_node.core_node.model)
                img = Images.get(image_enum,
                                 int(ICON_SIZE * self.app.app_scale))

            self.itemconfig(nid, image=img)
            canvas_node.image = img
            canvas_node.scale_text()
            canvas_node.scale_antennas()

            for edge_id in self.find_withtag(tags.EDGE):
                self.itemconfig(edge_id,
                                width=int(EDGE_WIDTH * self.app.app_scale))