コード例 #1
0
class TreeVisualizer(QWidget):
    def __init__(self):
        """Initial configuration."""
        super().__init__()

        # GLOBAL VARIABLES
        # graph variables
        self.graph: Graph = Graph()
        self.selected_node: Node = None

        self.selected_vertex: Tuple[Node, Node] = None

        # offset of the mouse from the position of the currently dragged node
        self.mouse_drag_offset: Vector = None

        # position of the mouse; is updated when the mouse moves
        self.mouse_position: Vector = Vector(-1, -1)

        # variables for visualizing the graph
        self.node_radius: float = 20
        self.weight_rectangle_size: float = self.node_radius / 3

        self.arrowhead_size: float = 8
        self.arrow_separation: float = pi / 7

        self.selected_color = Qt.red
        self.regular_node_color = Qt.white
        self.regular_vertex_weight_color = Qt.black

        # limit the displayed length of labels for each node
        self.node_label_limit: int = 10

        # UI variables
        self.font_family: str = "Times New Roman"
        self.font_size: int = 18

        self.layout_margins: float = 8
        self.layout_item_spacing: float = 2 * self.layout_margins

        # canvas positioning (scale and translation)
        self.scale: float = 1
        self.scale_coefficient: float = 2  # by how much the scale changes on scroll
        self.translation: float = Vector(0, 0)

        # by how much the rotation of the nodes changes
        self.node_rotation_coefficient: float = 0.7

        # TIMERS
        # timer that runs the simulation (60 times a second... once every ~= 16ms)
        self.simulation_timer = QTimer(
            interval=16, timeout=self.perform_simulation_iteration)

        # WIDGETS
        self.canvas = QFrame(self, minimumSize=QSize(0, 400))
        self.canvas_size: Vector = None
        self.canvas.resizeEvent = self.adjust_canvas_translation

        # toggles between directed/undirected graphs
        self.directed_toggle_button = QPushButton(
            text="undirected", clicked=self.toggle_directed_graph)

        # for showing the labels of the nodes
        self.labels_checkbox = QCheckBox(text="labels")

        # sets, whether the graph is weighted or not
        self.weighted_checkbox = QCheckBox(text="weighted",
                                           clicked=self.set_weighted_graph)

        # enables/disables forces (True by default - they're fun!)
        # self.forces_checkbox = QCheckBox(text="forces", checked=False)

        # input of the labels and vertex weights
        self.input_line_edit = QLineEdit(
            enabled=self.labels_checkbox.isChecked(),
            textChanged=self.input_line_edit_changed,
        )

        # displays information about the app
        self.about_button = QPushButton(
            text="?",
            clicked=self.show_help,
            sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed),
        )

        # for creating complements of the graph
        self.complement_button = QPushButton(
            text="complement",
            clicked=self.graph.complement,
        )

        # imports/exports the current graph
        self.import_graph_button = QPushButton(text="import",
                                               clicked=self.import_graph)
        self.export_graph_button = QPushButton(text="export",
                                               clicked=self.export_graph)

        # WIDGET LAYOUT
        self.main_v_layout = QVBoxLayout(self, margin=0)
        self.main_v_layout.addWidget(self.canvas)

        self.option_h_layout = QHBoxLayout(self, margin=self.layout_margins)
        self.option_h_layout.addWidget(self.directed_toggle_button)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.weighted_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.labels_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        # self.option_h_layout.addWidget(self.forces_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.input_line_edit)

        self.bottom_h_layout = QHBoxLayout(self, margin=self.layout_margins)
        self.bottom_h_layout.addWidget(self.complement_button)
        self.bottom_h_layout.addSpacing(self.layout_item_spacing)
        self.bottom_h_layout.addWidget(self.import_graph_button)
        self.bottom_h_layout.addSpacing(self.layout_item_spacing)
        self.bottom_h_layout.addWidget(self.export_graph_button)
        self.bottom_h_layout.addSpacing(self.layout_item_spacing)
        self.bottom_h_layout.addWidget(self.about_button)

        self.main_v_layout.addLayout(self.option_h_layout)
        self.main_v_layout.addSpacing(-self.layout_margins)
        self.main_v_layout.addLayout(self.bottom_h_layout)

        self.setLayout(self.main_v_layout)

        # WINDOW SETTINGS
        self.setWindowTitle("Graph Visualizer")
        self.setFont(QFont(self.font_family, self.font_size))
        self.setWindowIcon(QIcon("icon.ico"))
        self.show()

        # start the simulation
        self.simulation_timer.start()

    def set_weighted_graph(self):
        """Is called when the weighted checkbox is pressed; sets, whether the graph is 
        weighted or not."""
        self.graph.set_weighted(self.weighted_checkbox.isChecked())

    def adjust_canvas_translation(self, event):
        """Is called when the canvas widget is resized; changes translation so the 
        center stays in the center."""
        size = Vector(event.size().width(), event.size().height())

        if self.canvas_size is not None:
            self.translation += self.scale * (size - self.canvas_size) / 2

        self.canvas_size = size

    def repulsion_force(self, distance: float) -> float:
        """Calculates the strength of the repulsion force at the specified distance."""
        # return 1 / distance * 10 if self.forces_checkbox.isChecked() else 0
        return 0

    def attraction_force(self, distance: float, leash_length=80) -> float:
        """Calculates the strength of the attraction force at the specified distance 
        and leash length."""
        # return (
        #     -(distance - leash_length) / 10 if self.forces_checkbox.isChecked() else 0
        # )
        return 0

    def import_graph(self):
        """Is called when the import button is clicked; imports a graph from a file."""
        path = QFileDialog.getOpenFileName()[0]

        if path != "":
            try:
                with open(path, "r") as file:
                    # a list of vertices of the graph
                    data = [line.strip() for line in file.read().splitlines()]

                    # set the properties of the graph by its first vertex
                    sample = data[0].split(" ")

                    directed = True if sample[1] in ["->", "<-", "<>"
                                                     ] else False
                    weighted = (False if len(sample) == 2
                                or directed and len(sample) == 3 else True)

                    graph = Graph(directed=directed, weighted=weighted)

                    node_dictionary = {}

                    # add each of the nodes of the vertex to the graph
                    for vertex in data:
                        vertex_components = vertex.split(" ")

                        # the formats are either 'A B' or 'A <something> B'
                        nodes = [
                            vertex_components[0],
                            vertex_components[2]
                            if directed else vertex_components[1],
                        ]

                        # if weights are present, the formats are:
                        # - 'A B num' for undirected graphs
                        # - 'A <something> B num (num)' for directed graphs
                        weights_strings = (None if not weighted else [
                            vertex_components[2]
                            if not directed else vertex_components[3],
                            None
                            if not directed or vertex_components[1] != "<>"
                            else vertex_components[4],
                        ])

                        for node in nodes:
                            if node not in node_dictionary:
                                # slightly randomize the coordinates, so the graph
                                # doesn't stay in one place
                                x = self.canvas.width() / 2 + (random() - 0.5)
                                y = self.canvas.height() / 2 + (random() - 0.5)

                                # add it to graph with default values
                                node_dictionary[node] = graph.add_node(
                                    Vector(x, y), self.node_radius, node)

                        # get the node objects from the names
                        n1, n2 = node_dictionary[nodes[0]], node_dictionary[
                            nodes[1]]

                        graph.add_vertex(
                            n2 if vertex_components[1] == "<-" else n1,
                            n1 if vertex_components[1] == "<-" else n2,
                            0 if not weighted else ast.literal_eval(
                                weights_strings[0]),
                        )

                        # possibly add the other way
                        if vertex_components[1] == "<>":
                            graph.add_vertex(
                                n2,
                                n1,
                                0 if not weighted else ast.literal_eval(
                                    weights_strings[1]),
                            )

                # if everything was successful, override the current graph
                self.graph = graph

            except UnicodeDecodeError:
                QMessageBox.critical(self, "Error!",
                                     "Can't read binary files!")
            except ValueError:
                QMessageBox.critical(
                    self, "Error!",
                    "The weights of the graph are not numbers!")
            except Exception:
                QMessageBox.critical(
                    self,
                    "Error!",
                    "An error occurred when importing the graph. Make sure that the "
                    +
                    "file is in the correct format and that it isn't currently being "
                    + "used!",
                )

            # make sure that the UI is in order
            self.deselect_node()
            self.deselect_vertex()
            self.set_checkbox_values()

    def set_checkbox_values(self):
        """Sets the values of the checkboxes from the graph."""
        self.weighted_checkbox.setChecked(self.graph.is_weighted())
        self.update_directed_toggle_button_text()

    def export_graph(self):
        """Is called when the export button is clicked; exports a graph to a file."""
        path = QFileDialog.getSaveFileName()[0]

        if path != "":
            try:
                with open(path, "w") as file:
                    # look at every pair of nodes and examine the vertices
                    for i, n1 in enumerate(self.graph.get_nodes()):
                        for j, n2 in enumerate(self.graph.get_nodes()[i + 1:]):
                            # information about vertices and weights
                            v1_exists = self.graph.does_vertex_exist(n1, n2)
                            v2_exists = self.graph.does_vertex_exist(n2, n1)

                            if not v1_exists and v2_exists:
                                continue

                            w1_value = self.graph.get_weight(n1, n2)
                            w1 = ("" if not self.graph.is_weighted()
                                  or w1_value is None else str(w1_value))

                            # undirected graphs
                            if not self.graph.is_directed() and v1_exists:
                                file.write(
                                    f"{n1.get_label()} {n2.get_label()} {w1}\n"
                                )
                            else:
                                w2_value = self.graph.get_weight(n2, n1)
                                w2 = ("" if not self.graph.is_weighted()
                                      or w2_value is None else str(w2_value))

                                symbol = ("<>" if v1_exists and v2_exists else
                                          "->" if v1_exists else "<-")

                                vertex = f"{n1.get_label()} {symbol} {n2.get_label()}"

                                if w1 != "":
                                    vertex += f" {w1}"
                                if w2 != "":
                                    vertex += f" {w2}"

                                file.write(vertex + "\n")
            except Exception:
                QMessageBox.critical(
                    self,
                    "Error!",
                    "An error occurred when exporting the graph. Make sure that you "
                    "have permission to write to the specified file and try again!",
                )

    def show_help(self):
        """Is called when the help button is clicked; displays basic information about 
        the application."""
        message = """
            <p>Welcome to <strong>Graph Visualizer</strong>.</p>
            <p>The app aims to help with creating, visualizing and exporting graphs. 
            It is powered by PyQt5 &ndash; a set of Python bindings for the C++ library Qt.</p>
            <hr />
            <p>The controls are as follows:</p>
            <ul>
            <li><em>Left Mouse Button</em> &ndash; selects nodes and moves them</li>
            <li><em>Right Mouse Button</em> &ndash; creates/removes nodes and vertices</li>
            <li><em>Mouse Wheel</em> &ndash; zooms in/out</li>
            <li><em>Shift + Left Mouse Button</em> &ndash; moves connected nodes</li>
            <li><em>Shift + Mouse Wheel</em> &ndash; rotates nodes around the selected node</li>
            </ul>
            <hr />
            <p>If you spot an issue, or would like to check out the source code, see the app's 
            <a href="https://github.com/xiaoxiae/GraphVisualizer">GitHub repository</a>.</p>
        """

        QMessageBox.information(self, "About", message)

    def toggle_directed_graph(self):
        """Is called when the directed checkbox changes; toggles between directed and 
        undirected graphs."""
        self.graph.set_directed(not self.graph.is_directed())
        self.update_directed_toggle_button_text()

    def update_directed_toggle_button_text(self):
        """Changes the text of the directed toggle button, according to whether the 
        graph is directer or not."""
        self.directed_toggle_button.setText(
            "directed" if self.graph.is_directed() else "undirected")

    def input_line_edit_changed(self, text: str):
        """Is called when the input line edit changes; changes either the label of the 
        node selected node, or the value of the selected vertex."""
        palette = self.input_line_edit.palette()
        text = text.strip()

        if self.selected_node is not None:
            # text is restricted for rendering and graph export purposes
            if 0 < len(text) < self.node_label_limit and " " not in text:
                self.selected_node.set_label(text)
                palette.setColor(self.input_line_edit.backgroundRole(),
                                 Qt.white)
            else:
                palette.setColor(self.input_line_edit.backgroundRole(), Qt.red)
        elif self.selected_vertex is not None:
            # try to parse the input text either as an integer, or as a float
            weight = None
            try:
                weight = int(text)
            except ValueError:
                try:
                    weight = float(text)
                except ValueError:
                    pass

            # if the parsing was unsuccessful, set the input line edit background to
            # red to indicate this
            if weight is None:
                palette.setColor(self.input_line_edit.backgroundRole(), Qt.red)
            else:
                self.graph.add_vertex(self.selected_vertex[0],
                                      self.selected_vertex[1], weight)
                palette.setColor(self.input_line_edit.backgroundRole(),
                                 Qt.white)

        self.input_line_edit.setPalette(palette)

    def select_node(self, node: Node):
        """Sets the selected node to the specified node, sets the input line edit to 
        its label and enables it."""
        self.selected_node = node

        self.input_line_edit.setText(node.get_label())
        self.input_line_edit.setEnabled(True)
        self.input_line_edit.setFocus()

    def deselect_node(self):
        """Sets the selected node to None and disables the input line edit."""
        self.selected_node = None
        self.input_line_edit.setEnabled(False)

    def select_vertex(self, vertex):
        """Sets the selected vertex to the specified vertex, sets the input line edit to
        its weight and enables it."""
        self.selected_vertex = vertex

        self.input_line_edit.setText(str(self.graph.get_weight(*vertex)))
        self.input_line_edit.setEnabled(True)
        self.input_line_edit.setFocus()

    def deselect_vertex(self):
        """Sets the selected vertex to None and disables the input line edit."""
        self.selected_vertex = None
        self.input_line_edit.setEnabled(False)

    def mousePressEvent(self, event):
        """Is called when a mouse button is pressed; creates and moves 
        nodes/vertices."""
        pos = self.get_mouse_position(event)

        # if we are not on canvas, don't do anything
        if pos is None:
            return

        # sets the focus to the window (for the keypresses to register)
        self.setFocus()

        # (potentially) find a node that has been pressed
        pressed_node = None
        for node in self.graph.get_nodes():
            if distance(pos, node.get_position()) <= node.get_radius():
                pressed_node = node

        # (potentially) find a vertex that has been pressed
        pressed_vertex = None
        if self.graph.is_weighted():
            for n1 in self.graph.get_nodes():
                for n2, weight in n1.get_neighbours().items():

                    if self.graph.is_directed() or id(n1) < id(n2):
                        weight = self.graph.get_weight(n1, n2)

                        # the bounding box of this weight
                        weight_rect = self.get_vertex_weight_rect(
                            n1, n2, weight)
                        if weight_rect.contains(QPointF(*pos)):
                            pressed_vertex = (n1, n2)

        if event.button() == Qt.LeftButton:
            # nodes have the priority in selection over vertices
            if pressed_node is not None:
                self.deselect_vertex()
                self.select_node(pressed_node)

                self.mouse_drag_offset = pos - self.selected_node.get_position(
                )
                self.mouse_position = pos

            elif pressed_vertex is not None:
                self.deselect_node()
                self.select_vertex(pressed_vertex)

            else:
                self.deselect_node()
                self.deselect_vertex()

        elif event.button() == Qt.RightButton:
            if pressed_node is not None:
                if self.selected_node is not None:
                    self.graph.toggle_vertex(self.selected_node, pressed_node)
                else:
                    self.graph.remove_node(pressed_node)
                    self.deselect_node()

            elif pressed_vertex is not None:
                self.graph.remove_vertex(*pressed_vertex)
                self.deselect_vertex()

            else:
                node = self.graph.add_node(pos, self.node_radius)

                # if a selected node exists, connect it to the newly created node
                if self.selected_node is not None:
                    self.graph.add_vertex(self.selected_node, node)

                self.deselect_vertex()
                self.select_node(node)

    def mouseReleaseEvent(self, event):
        """Is called when a mouse button is released; stops node drag."""
        self.mouse_drag_offset = None

    def mouseMoveEvent(self, event):
        """Is called when the mouse is moved across the window; updates mouse 
        coordinates."""
        self.mouse_position = self.get_mouse_position(event, scale_down=True)

    def wheelEvent(self, event):
        """Is called when the mouse wheel is moved; node rotation and zoom."""
        # positive/negative for scrolling away from/towards the user
        scroll_distance = radians(event.angleDelta().y() / 8)

        if QApplication.keyboardModifiers() == Qt.ShiftModifier:
            if self.selected_node is not None:
                self.rotate_nodes_around(
                    self.selected_node.get_position(),
                    scroll_distance * self.node_rotation_coefficient,
                )
        else:
            mouse_coordinates = self.get_mouse_position(event)

            # only do something, if we're working on canvas
            if mouse_coordinates is None:
                return

            prev_scale = self.scale
            self.scale *= 2**(scroll_distance)

            # adjust translation so the x and y of the mouse stay in the same spot
            self.translation -= mouse_coordinates * (self.scale - prev_scale)

    def rotate_nodes_around(self, point: Vector, angle: float):
        """Rotates coordinates of all of the nodes in the same component as the selected 
        node by a certain angle (in radians) around it."""
        for node in self.graph.get_nodes():
            if self.graph.share_component(node, self.selected_node):
                node.set_position((node.position - point).rotated(angle) +
                                  point)

    def get_mouse_position(self, event, scale_down=False) -> Vector:
        """Returns mouse coordinates if they are within the canvas and None if they are 
        not. If scale_down is True, the function will scale down the coordinates to be 
        within the canvas (useful for dragging) and return them instead."""
        x = event.pos().x()
        y = event.pos().y()

        x_on_canvas = 0 <= x <= self.canvas.width()
        y_on_canvas = 0 <= y <= self.canvas.height()

        # scale down the coordinates if scale_down is True, or return None if we are
        # not on canvas
        if scale_down:
            x = x if x_on_canvas else 0 if x <= 0 else self.canvas.width()
            y = y if y_on_canvas else 0 if y <= 0 else self.canvas.height()
        elif not x_on_canvas or not y_on_canvas:
            return None

        # return the mouse coordinates, accounting for canvas translation and scale
        return (Vector(x, y) - self.translation) / self.scale

    def perform_simulation_iteration(self):
        """Performs one iteration of the simulation."""
        # evaluate forces that act upon each pair of nodes
        for i, n1 in enumerate(self.graph.get_nodes()):
            for j, n2 in enumerate(self.graph.get_nodes()[i + 1:]):
                # if they are not in the same component, no forces act on them
                if not self.graph.share_component(n1, n2):
                    continue

                # if the nodes are right on top of each other, no forces act on them
                d = distance(n1.get_position(), n2.get_position())
                if n1.get_position() == n2.get_position():
                    continue

                uv = (n2.get_position() - n1.get_position()).unit()

                # the size of the repel force between the two nodes
                #fr = self.repulsion_force(d)

                # add a repel force to each of the nodes, in the opposite directions
            # n1.add_force(-uv * fr)
            # n2.add_force(uv * fr)

            # if they are also connected, add the attraction force
            #if self.graph.does_vertex_exist(n1, n2, ignore_direction=True):
            #    fa = self.attraction_force(d)

            #    n1.add_force(-uv * fa)
            #    n2.add_force(uv * fa)

            # since this node will not be visited again, we can evaluate the forces
            # n1.evaluate_forces()

        # drag the selected node
        if self.selected_node is not None and self.mouse_drag_offset is not None:
            prev_node_position = self.selected_node.get_position()

            self.selected_node.set_position(self.mouse_position -
                                            self.mouse_drag_offset)

            # move the rest of the nodes that are connected to the selected node if
            # shift is pressed
            if QApplication.keyboardModifiers() == Qt.ShiftModifier:
                pos_delta = self.selected_node.get_position(
                ) - prev_node_position

                for node in self.graph.get_nodes():
                    if node is not self.selected_node and self.graph.share_component(
                            node, self.selected_node):
                        node.set_position(node.get_position() + pos_delta)

        self.update()

    def paintEvent(self, event):
        """Paints the board."""
        painter = QPainter(self)

        painter.setRenderHint(QPainter.Antialiasing, True)

        painter.setPen(QPen(Qt.black, Qt.SolidLine))
        painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))

        painter.setClipRect(0, 0, self.canvas.width(), self.canvas.height())

        # background
        painter.drawRect(0, 0, self.canvas.width(), self.canvas.height())

        painter.translate(*self.translation)
        painter.scale(self.scale, self.scale)

        # draw vertexes
        for n1 in self.graph.get_nodes():
            for n2, weight in n1.get_neighbours().items():
                self.draw_vertex(n1, n2, weight, painter)

        # draw nodes
        for node in self.graph.get_nodes():
            self.draw_node(node, painter)

    def draw_node(self, node: Node, painter):
        """Draw the specified node."""
        painter.setBrush(
            QBrush(
                self.selected_color
                if node is self.selected_node else self.regular_node_color,
                Qt.SolidPattern,
            ))

        node_position = node.get_position()
        node_radius = Vector(node.get_radius()).repeat(2)

        painter.drawEllipse(QPointF(*node_position), *node_radius)

        if self.labels_checkbox.isChecked():
            label = node.get_label()

            # scale font down, depending on the length of the label of the node
            painter.setFont(
                QFont(self.font_family, self.font_size / len(label)))

            # draw the node label within the node dimensions
            painter.drawText(
                QRectF(*(node_position - node_radius), *(2 * node_radius)),
                Qt.AlignCenter,
                label,
            )

    def draw_vertex(self, n1: Node, n2: Node, weight: float, painter):
        """Draw the specified vertex."""
        # special case for a node pointing to itself
        if n1 is n2:
            r = n1.get_radius()
            x, y = n1.get_position()

            painter.setPen(QPen(Qt.black, Qt.SolidLine))
            painter.setBrush(QBrush(Qt.black, Qt.NoBrush))

            painter.drawEllipse(QPointF(x - r / 2, y - r), r / 2, r / 2)

            head = Vector(x, y) - Vector(0, r)
            uv = Vector(0, 1)

            painter.setBrush(QBrush(Qt.black, Qt.SolidPattern))
            painter.drawPolygon(
                QPointF(*head),
                QPointF(*(head +
                          (-uv).rotated(radians(10)) * self.arrowhead_size)),
                QPointF(*(head +
                          (-uv).rotated(radians(-50)) * self.arrowhead_size)),
            )
        else:
            start, end = self.get_vertex_position(n1, n2)

            # draw the head of a directed arrow, which is an equilateral triangle
            if self.graph.is_directed():
                uv = (end - start).unit()

                painter.setBrush(QBrush(Qt.black, Qt.SolidPattern))
                painter.drawPolygon(
                    QPointF(*end),
                    QPointF(
                        *(end +
                          (-uv).rotated(radians(30)) * self.arrowhead_size)),
                    QPointF(
                        *(end +
                          (-uv).rotated(radians(-30)) * self.arrowhead_size)),
                )

            painter.setPen(QPen(Qt.black, Qt.SolidLine))
            painter.drawLine(QPointF(*start), QPointF(*end))

        if self.graph.is_weighted():
            # set color according to whether the vertex is selected or not
            painter.setBrush(
                QBrush(
                    self.selected_color if self.selected_vertex is not None and
                    ((n1 is self.selected_vertex[0]
                      and n2 is self.selected_vertex[1]) or
                     (not self.graph.is_directed()
                      and n2 is self.selected_vertex[0]
                      and n1 is self.selected_vertex[1])) else
                    self.regular_vertex_weight_color,
                    Qt.SolidPattern,
                ))

            weight_rectangle = self.get_vertex_weight_rect(n1, n2, weight)
            painter.drawRect(weight_rectangle)

            painter.setFont(QFont(self.font_family, int(self.font_size / 4)))

            painter.setPen(QPen(Qt.white, Qt.SolidLine))
            painter.drawText(weight_rectangle, Qt.AlignCenter, str(weight))
            painter.setPen(QPen(Qt.black, Qt.SolidLine))

    def get_vertex_position(self, n1: Node, n2: Node) -> Tuple[Vector, Vector]:
        """Return the position of the vertex on the screen."""
        # positions of the nodes
        n1_p = Vector(*n1.get_position())
        n2_p = Vector(*n2.get_position())

        # unit vector from n1 to n2
        uv = (n2_p - n1_p).unit()

        # start and end of the vertex to be drawn
        start = n1_p + uv * n1.get_radius()
        end = n2_p - uv * n2.get_radius()

        if self.graph.is_directed():
            # if the graph is directed and a vertex exists that goes the other way, we
            # have to move the start end end so the vertexes don't overlap
            if self.graph.does_vertex_exist(n2, n1):
                start = start.rotated(self.arrow_separation, n1_p)
                end = end.rotated(-self.arrow_separation, n2_p)

        return start, end

    def get_vertex_weight_rect(self, n1: Node, n2: Node, weight: float):
        """Get a RectF surrounding the weight of the node."""
        r = self.weight_rectangle_size

        # width adjusted to number of chars in weight label
        adjusted_width = len(str(weight)) / 3 * r + r / 3
        weight_vector = Vector(r if adjusted_width <= r else adjusted_width, r)

        if n1 is n2:
            # special case for a vertex pointing to itself
            mid = n1.get_position() - Vector(r * 3, r * 4)
        else:
            start, end = self.get_vertex_position(n1, n2)
            mid = (start + end) / 2

        return QRectF(*(mid - weight_vector), *(2 * weight_vector))
コード例 #2
0
ファイル: filterDlg.py プロジェクト: wllacer/dana-cube
class filterDialog(QDialog):
    """
        FIXME existe duda si en guias jerarquicas filtra con toda la amplitud
    """
    def __init__(self,recordStructure,currentData,title,parent=None,driver=None):
        super().__init__(parent)
        # cargando parametros de defecto
        self.record = recordStructure
        self.campos = [ elem['name'] for elem in recordStructure ]
        self.formatos =[ elem['format'] for elem in recordStructure ]
        self.driver = driver
        self.context = []
        self.context.append(('campo','formato','condicion','valores'))
        self.context.append((None,WComboBox,None,self.campos))
        self.context.append((None,QLineEdit,{'setEnabled':False},None))
        self.context.append(('=',WComboBox,None,tuple(LOGICAL_OPERATOR)))
        self.context.append((None,QLineEdit,None,None))
        self.data = []
        
        self.sheet = WDataSheet(self.context,len(recordStructure))
        cabeceras = [ item  for item in self.context[0] ]
        self.sheet.verticalHeader().hide()
        self.sheet.setHorizontalHeaderLabels(cabeceras)
                
        self.sheet.initialize()
        self.load(currentData)
        for i in range(self.sheet.rowCount()):
            #for j in range(2):
            self.sheet.item(i,1).setBackground(QColor(Qt.gray))
        #for k in range(len(self.record)):
            #self.addRow(k)
        self.sheet.resizeRowsToContents()
        self.sheet.horizontalHeader().setStretchLastSection(True)
        
        self.origMsg = 'Recuerde: en SQL el separador decimal es el punto "."'
        # super(filterDialog,self).__init__('Defina el filtro',self.context,len(self.data),self.data,parent=parent) 

        InicioLabel = QLabel(title)
        #
        
        self.mensaje = QLineEdit('')
        self.mensaje.setReadOnly(True)
        
        freeSqlLbl = QLabel('Texto Libre')
        self.freeSql  = QLineEdit() #QPlainTextEdit()

        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,
                                     Qt.Horizontal)

        #formLayout = QHBoxLayout()
        #self.meatLayout = QVBoxLayout()
        self.meatLayout = QVBoxLayout() #QGridLayout()
        buttonLayout = QHBoxLayout()
        formLayout = QVBoxLayout()
       
        self.meatLayout.addWidget(InicioLabel) #,0,0)
        self.meatLayout.addWidget(self.sheet) #,1,0,6,5)
        self.meatLayout.addWidget(freeSqlLbl) #,8,0)
        self.meatLayout.addWidget(self.freeSql) #,8,1,1,4)
        self.meatLayout.addWidget(self.mensaje) #,10,0,1,4)
        
        buttonLayout.addWidget(buttonBox)
        
        formLayout.addLayout(self.meatLayout)        
        formLayout.addLayout(buttonLayout)
        
        self.setLayout(formLayout)
        self.setMinimumSize(QSize(480,480))
        
        self.sheet.itemChanged.connect(self.cambioCampo)
        buttonBox.accepted.connect(self.accept)
        buttonBox.rejected.connect(self.reject)

        self.setWindowTitle(title)
        

 
 
        #--- end super
        self.setMinimumSize(QSize(800,480))
        
        self.mensaje.setText(self.origMsg)
        self.defaultBackground = self.mensaje.backgroundRole()
        
        #for k in range(4):
            #self.sheet.resizeColumnToContents(k)
        
    def load(self,currentData):
        data = []
        if not currentData:
            return
        for item in currentData:
            if item[0]:
                #FIXME y si el campo no existe porque es calculado ¿? editable, etc ...
                linea = item[:]
                if not linea[1]:
                    linea[1] = self.formatos[self.campos.index(item[0])]
                if not linea[2]:
                    linea[2] = '='
                data.append(linea)
            else:
                continue
        self.sheet.loadData(data)
        
    def cambioCampo(self,item):
        if item.column() != 0:
            return 
        if item.text() == '':
            return
        pos = self.campos.index(item.text())
        self.sheet.setData(item.row(),1,self.formatos[pos])
        
    def accept(self):
        self.mensaje.setText(self.origMsg)
        fallo = False
        errorTxt = ''
        self.queryArray = []
        values = self.sheet.unloadData()
        for pos,item in enumerate(values):
            opcode = item[2]
            values = item[3]
            if opcode in ('is null','is not null'): #TODO, esto no es así
                self.queryArray.append((item[0],
                                    opcode.upper(),
                                    None,None))
                continue
            if not values: # or item[3] == '':  #Existe  de datos
                continue
            aslist = norm2List(values)
            #primero comprobamos la cardinalidad. Ojo en sentencias separadas o el elif no funciona bien
            if opcode in ('between','not between'): 
                if len(aslist) != 2:
                    errorTxt = 'La operacion between exige exactamente dos valores'
                    fallo = True
            elif opcode not in ('in','not in') :
                if len(aslist) != 1:
                    errorTxt = ' La operacion elegida exige un único valor'
                    fallo = True
            
            if not fallo:
                testElem = aslist[0].lower().strip()
                formato = item[1]
                if formato in ('numerico','entero') and not is_number(testElem):
                    # vago. no distingo entre ambos tipos numericos FIXME
                    errorTxt = 'No contiene un valor numerico aceptable'
                    fallo = True
                #elif formato in ('texto','binario'):
                    #pass
                elif formato in ('booleano',) and testElem not in ('true','false'):
                    errorTxt = 'Solo admitimos como booleanos: True y False'
                    fallo = True
                elif formato in ('fecha','fechahora','hora') and not isDate(testElem):
                    errorTxt = 'Formato o fecha incorrecta. Verifique que es del tipo AAAA-MM-DD HH:mm:SS'
                    fallo = True
                else:
                    pass

            if fallo:
                self.mensaje.setText('ERROR @{}: {}'.format(item[0],errorTxt))
                #self.sheet.cellWidget(pos,3).selectAll()  FIXME ¿que hay para combos ?
                self.sheet.setCurrentCell(pos,3)
                self.sheet.setFocus()
                return
            qfmt = 't'     
            if formato in ('entero','numerico'):
                qfmt = 'n'
            elif formato in ('fecha','fechahora','hora'):
                qfmt = 'f'
            elif formato in ('booleano'):
                qfmt = 'n' #me parece 
                
            self.queryArray.append((item[0],
                                opcode.upper(),
                                aslist[0] if len(aslist) == 1 else aslist,
                                qfmt))

        self.result = mergeStrings('AND',
                                    searchConstructor('where',where=self.queryArray,driver=self.driver),
                                    self.freeSql.text(),
                                    spaced=True)
        self.data = self.sheet.values()
        QDialog.accept(self)
コード例 #3
0
class demowind(QWidget):
    #def datad_function(self, parent=None):
    #    dw.scratchpad2.setText("datad_function called")
    #    mylog()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)

        faulthandler.enable()

        self.setGeometry(200, 200, 780, 650)
        self.setWindowTitle('Weather Station')

        myfont = PyQt5.QtGui.QFont('SansSerif', 13)

        yellowpalette = PyQt5.QtGui.QPalette()
        #yellowpalette.setColor(PyQt5.QtGui.QPalette.Text, QColor(255,165,0))
        yellowpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.red)

        cyanpalette = PyQt5.QtGui.QPalette()
        cyanpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.blue)

        redpalette = PyQt5.QtGui.QPalette()
        redpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.red)

        bluepalette = PyQt5.QtGui.QPalette()
        bluepalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.blue)

        greenpalette = PyQt5.QtGui.QPalette()
        greenpalette.setColor(PyQt5.QtGui.QPalette.Text, PyQt5.QtCore.Qt.green)

        datad = QPushButton('data\r\ndump', self)
        datad.setFont(myfont)
        datad.setGeometry(10, 10, 90, 620)
        datad.clicked.connect(self.datad_function)

        self.textfield = QLineEdit(self)
        self.textfield.setFont(myfont)
        self.textfield.setGeometry(120, 10, 630, 50)
        self.textfield.setText("time:")

        self.identfield2 = QLineEdit(self)
        self.identfield2.setFont(myfont)
        self.identfield2.setGeometry(120, 80, 310, 50)
        self.identfield2.setText("IP: 192.168.1.121")

        self.tempfield2 = QLineEdit(self)
        self.tempfield2.setAutoFillBackground(True)
        redpalette.setColor(self.tempfield2.backgroundRole(),
                            PyQt5.QtCore.Qt.white)
        self.tempfield2.setPalette(redpalette)
        self.tempfield2.setFont(myfont)
        self.tempfield2.setGeometry(120, 140, 310, 50)
        self.tempfield2.setText("inside temperature:")

        self.humidfield2 = QLineEdit(self)
        self.humidfield2.setAutoFillBackground(True)
        bluepalette.setColor(self.humidfield2.backgroundRole(),
                             PyQt5.QtCore.Qt.white)
        self.humidfield2.setPalette(bluepalette)
        self.humidfield2.setFont(myfont)
        self.humidfield2.setGeometry(120, 200, 310, 50)
        self.humidfield2.setText("inside humidity:")

        self.pressfield2 = QLineEdit(self)
        self.pressfield2.setAutoFillBackground(True)
        greenpalette.setColor(self.pressfield2.backgroundRole(),
                              PyQt5.QtCore.Qt.white)
        self.pressfield2.setPalette(greenpalette)
        self.pressfield2.setFont(myfont)
        self.pressfield2.setGeometry(440, 140, 310, 50)
        self.pressfield2.setText("inside atm pressure:")

        self.battfield2 = QLineEdit(self)
        self.battfield2.setFont(myfont)
        self.battfield2.setGeometry(440, 200, 310, 50)
        self.battfield2.setText("battery voltage:")

        self.identfield3 = QLineEdit(self)
        self.identfield3.setFont(myfont)
        self.identfield3.setGeometry(120, 300, 310, 50)
        self.identfield3.setText("IP: 192.168.1.113")

        self.tempfield3 = QLineEdit(self)
        self.tempfield3.setAutoFillBackground(True)
        yellowpalette.setColor(self.tempfield3.backgroundRole(),
                               PyQt5.QtCore.Qt.white)
        self.tempfield3.setPalette(redpalette)
        self.tempfield3.setFont(myfont)
        self.tempfield3.setGeometry(120, 360, 310, 50)
        self.tempfield3.setText("outside temperature:")

        self.humidfield3 = QLineEdit(self)
        self.humidfield3.setAutoFillBackground(True)
        cyanpalette.setColor(self.humidfield3.backgroundRole(),
                             PyQt5.QtCore.Qt.white)
        self.humidfield3.setPalette(bluepalette)
        self.humidfield3.setFont(myfont)
        self.humidfield3.setGeometry(120, 420, 310, 50)
        self.humidfield3.setText("outside humidity:")

        self.pressfield3 = QLineEdit(self)
        self.pressfield3.setAutoFillBackground(True)
        #greenpalette.setColor(self.pressfield3.backgroundRole(), PyQt5.QtCore.Qt.black)
        self.pressfield3.setPalette(greenpalette)
        self.pressfield3.setFont(myfont)
        self.pressfield3.setGeometry(440, 360, 310, 50)
        self.pressfield3.setText("outside atm pressure:")

        self.battfield3 = QLineEdit(self)
        self.battfield3.setFont(myfont)
        self.battfield3.setGeometry(440, 420, 310, 50)
        self.battfield3.setText("battery voltage:")

        self.scratchpad2 = QLineEdit(self)
        self.scratchpad2.setFont(myfont)
        self.scratchpad2.setGeometry(120, 510, 630, 50)
        self.scratchpad2.setText("scratchpad2")

        self.scratchpad3 = QLineEdit(self)
        self.scratchpad3.setFont(myfont)
        self.scratchpad3.setGeometry(120, 580, 630, 50)
        self.scratchpad3.setText("scratchpad3")

        self.temp_array2 = numpy.zeros(1000)
        self.humid_array2 = numpy.zeros(1000)
        self.press_array2 = 29.1 + numpy.zeros(1000)
        self.batt_array2 = numpy.zeros(1000)
        self.x2 = numpy.zeros(1000)

        self.temp_array3 = numpy.zeros(1000)
        self.humid_array3 = numpy.zeros(1000)
        self.press_array3 = 29.1 + numpy.zeros(1000)
        self.batt_array3 = numpy.zeros(1000)
        self.x3 = numpy.zeros(1000)

        self.time_array = numpy.zeros(1000)

        self.temp_plt = pg.plot()
        self.press_plt = pg.plot()

        pg.setConfigOptions(antialias=True)
        #view = pg.GraphicsView()
        #layout = pg.GraphicsLayout()
        #view.setCentralItem(layout)
        #view.resize(2000,400)

        #self.scratchpad2.setText("sizing")
        #self.scratchpad3.setText("sizing")

        self.temp_plt.setBackground('w')
        self.temp_plt.resize(1000, 850)

        self.press_plt.setBackground('w')
        self.press_plt.resize(1000, 700)

        #self.scratchpad2.setText("sized")
        #self.scratchpad3.setText("sized")

        self.p12 = self.temp_plt.plotItem
        self.p12.showAxis('right')
        self.p12.setYRange(0, 100)

        self.p13 = self.press_plt.plotItem
        self.p13.showAxis('right')
        self.p13.setYRange(29, 30.5)

        self.p22 = pg.ViewBox()
        self.p32 = pg.ViewBox()
        self.p42 = pg.ViewBox()

        self.p23 = pg.ViewBox()

        self.timeplot = pg.ViewBox()

        self.p12.scene().addItem(self.p22)
        self.p12.scene().addItem(self.p32)
        self.p12.scene().addItem(self.p42)

        self.p13.scene().addItem(self.p23)

        self.p12.scene().addItem(self.timeplot)

        self.p12.getAxis('right').linkToView(self.p22)
        self.p12.getAxis('right').linkToView(self.p32)
        self.p12.getAxis('right').linkToView(self.p42)
        #self.p12.getAxis('left').linkToView(self.p32)

        self.p13.getAxis('right').linkToView(self.p23)
        #self.p13.getAxis('left').linkToView(self.p33)

        self.p12.getAxis('left').linkToView(self.timeplot)

        self.p22.setYRange(0, 100)
        self.p32.setYRange(0, 100)
        self.p42.setYRange(0, 100)

        self.p23.setYRange(29, 30.5)

        self.timeplot.setYRange(0, 100)

        self.p22.setXLink(self.p12)
        self.p32.setXLink(self.p12)
        self.p42.setXLink(self.p12)

        self.p23.setXLink(self.p13)

        self.timeplot.setXLink(self.p12)

        self.p12.setGeometry(self.p12.vb.sceneBoundingRect())
        self.p22.setGeometry(self.p12.vb.sceneBoundingRect())
        self.p32.setGeometry(self.p12.vb.sceneBoundingRect())
        self.p42.setGeometry(self.p12.vb.sceneBoundingRect())

        self.p13.setGeometry(self.p13.vb.sceneBoundingRect())
        self.p23.setGeometry(self.p13.vb.sceneBoundingRect())

        self.timeplot.setGeometry(self.p13.vb.sceneBoundingRect())

        #self.mylegend = self.plt.addLegend((100,100),(50,50))
        self.my_temp_legend = self.temp_plt.addLegend()
        self.my_press_legend = self.press_plt.addLegend()

        #self.legend2 = pg.LegendItem()
        #self.legend3 = pg.LegendItem()
        #self.p2.addItem(self.legend2)
        #self.p3.addItem(self.legend3)

        #self.curve13=self.plt.plot(x=[] , y=[], pen = pg.mkPen(color=(255,165,0), width=4), name="Outside temperature", style=PyQt5.QtCore.Qt.DotLine)
        self.curve12 = self.temp_plt.plot(x=[],
                                          y=[],
                                          pen=pg.mkPen('r', width=6),
                                          name="inside temperature",
                                          style=PyQt5.QtCore.Qt.DotLine)
        self.curve13 = self.press_plt.plot(x=[],
                                           y=[],
                                           pen=pg.mkPen('g', width=6),
                                           name="inside air pressure",
                                           style=PyQt5.QtCore.Qt.DotLine)

        self.curve22 = pg.PlotCurveItem(pen=pg.mkPen('r', width=3),
                                        name="outside temperature",
                                        style=PyQt5.QtCore.Qt.DotLine)
        self.curve32 = pg.PlotCurveItem(pen=pg.mkPen('b', width=6),
                                        name="inside humidity",
                                        style=PyQt5.QtCore.Qt.DotLine)
        self.curve42 = pg.PlotCurveItem(pen=pg.mkPen('b', width=3),
                                        name="outside humidity",
                                        style=PyQt5.QtCore.Qt.DotLine)

        self.curve23 = pg.PlotCurveItem(pen=pg.mkPen('g', width=3),
                                        name="outside air pressure")

        self.curve_tp = pg.PlotCurveItem(pen=pg.mkPen(QColor(128, 128, 128),
                                                      width=4),
                                         name="time")

        #self.my_temp_legend.addItem(self.curve12, name = self.curve12.opts['name'])
        self.my_temp_legend.addItem(self.curve22,
                                    name=self.curve22.opts['name'])
        self.my_temp_legend.addItem(self.curve32,
                                    name=self.curve32.opts['name'])
        self.my_temp_legend.addItem(self.curve42,
                                    name=self.curve42.opts['name'])

        #self.my_press_legend.addItem(self.curve13, name = self.curve13.opts['name'])
        self.my_press_legend.addItem(self.curve23,
                                     name=self.curve23.opts['name'])

        #self.p12.addItem(self.curve12)
        self.p22.addItem(self.curve22)
        self.p32.addItem(self.curve32)
        self.p42.addItem(self.curve42)

        #self.p13.addItem(self.curve13)
        self.p23.addItem(self.curve23)

        self.timeplot.addItem(self.curve_tp)

        #self.plt_temp = self.plt.plot(self.x, self.temp_array, title = "my plot", pen = pg.mkPen('r', width=4))
        #self.plt_humid = self.plt.plot(self.x, self.humid_array, title = "my plot", pen = pg.mkPen('g', width=4))
        #self.plt_press = self.plt.plot(self.x, self.press_array, title = "my plot", pen = pg.mkPen('b', width=4))

        #threading.Timer(10.0, data_display).start()
        #threading.Timer(10.0, datad_function).start()

        #self.scratchpad2.setText("about to start QTimer")

        self.timer = QTimer()
        self.timer.setInterval(120000)
        self.timer.timeout.connect(self.datad_function)
        self.timer.start()

        #self.scratchpad2.setText("QTimer should have started")

    def test_function(self, parent=None):
        dw.scratchpad2.setText("test_function called")

    def datad_function(self, parent=None):
        dw.scratchpad2.setText("datad_function called")
        self.mylog()

    def repeat(self):

        #
        #   Arduino 2:  192.168.1.121
        #

        #dw.scratchpad2.setText("starting socket opps")

        global temperature_2
        global temperature_3

        global temperature_num_2
        global temperature_num_3

        global humidity_num_2
        global humidity_num_3

        global pressure_num_2
        global pressure_num_3

        global V_num_2
        global V_num_3

        global I_num_2
        global I_num_3

        #update_temp = pyqtSignal(str)
        #update_temp.emit(temperature_2)

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        #s.setblocking(0)
        HOST = '192.168.1.121'
        PORT = 80
        s.connect((HOST, PORT))  #connect to 192.168.1.121, Arduino 2
        s.sendto("**dump2".encode(), (HOST, PORT))
        message = s.recv(60)

        temperature_2 = message.decode('utf-8')
        s.close()

        temperature_string = temperature_2[0:4:1]

        #dw.scratchpad2.setText(temperature_2)

        #ts = time.gmtime()
        #time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts)
        #dw.textfield.setText(time_str)
        #dw.textfield.setText("current time is %f" % time.time())

        now = datetime.now()
        s_s_m = (now - now.replace(hour=0, minute=0, second=0,
                                   microsecond=0)).total_seconds()

        d = 50 - (50 * math.cos(s_s_m * 6.28318 / 86400))

        if self.isfloat(temperature_string):
            temperature_num_2 = (float(temperature_string)) / 100
            temperature_num_2 = (temperature_num_2 * 1.8) + 32
            #dw.tempfield2.setText("temperature: %0.2f deg F" % temperature_num_2)
            #dw.scratchpad.setText("True")
            i = 0
            while i < 999:
                dw.temp_array2[i] = dw.temp_array2[i + 1]
                dw.time_array[i] = dw.time_array[i + 1]
                i += 1
            dw.temp_array2[999] = temperature_num_2
            dw.time_array[999] = d
        #else:
        #   dw.scratchpad2.setText("temp 2 error: %s" % time_str)

        index = temperature_2.find('HM')
        humidity_string = temperature_2[index + 2:index + 6:1]

        #dw.scratchpad2.setText(humidity_string)

        if self.isfloat(humidity_string):
            humidity_num_2 = (float(humidity_string)) / 100
            #dw.humidfield2.setText("humidity: %0.2f %%" % humidity_num_2)
            i = 0
            while i < 999:
                dw.humid_array2[i] = dw.humid_array2[i + 1]
                i += 1
            dw.humid_array2[999] = humidity_num_2
        #else:
        #   dw.scratchpad2.setText("humidity 2 error: %s" % time_str)

        index = temperature_2.find('PS')
        pressure_string = temperature_2[index + 2:index + 6:1]

        if self.isfloat(pressure_string):
            pressure_num_2 = (float(pressure_string)) * 0.02953
            #dw.pressfield2.setText("atm pressure: %0.2f inHg" % pressure_num_2)
            i = 0
            while i < 999:
                dw.press_array2[i] = dw.press_array2[i + 1]
                i += 1
            dw.press_array2[999] = 0.2 * (
                pressure_num_2 + dw.press_array2[998] + dw.press_array2[997] +
                dw.press_array2[996] + dw.press_array2[995])
            #dw.scratchpad.setText("atm pressure: %d" % dw.press_array2[359])
        #else:
        #   dw.scratchpad2.setText("atm pressure 2 error: %s" % time_str)

        indexV1 = temperature_2.find('LV')
        #indexV2=temperature_2.find('CU')
        V_string = temperature_2[indexV1 + 2:indexV1 + 6:1]
        indexI1 = temperature_2.find('CU')
        #indexI2=len(temperature_2)
        I_string = temperature_2[indexI1 + 2:indexI1 + 7:1]

        if (self.isfloat(V_string) and self.isfloat(I_string)):
            V_num_2 = (float(V_string))
            I_num_2 = (float(I_string))
            #dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_2, V_num_2))
            #i=0
            #while i<359:
            #    dw.batt_array2[i]=dw.batt_array2[i+1]
            #    i+=1
            #dw.batt_array2[359]=V_num
        #else:
        #   dw.scratchpad2.setText("I or V, 2 error: %s" % time_str)

        #s.close()

        #
        #   Arduino 3:  192.168.1.113
        #

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        #s.setblocking(0)
        HOST = '192.168.1.113'
        PORT = 80
        s.connect((HOST, PORT))  #connect to 192.168.1.113, Arduino 3
        s.sendto("**dump".encode(), (HOST, PORT))
        message = s.recv(60)

        temperature_3 = message.decode('utf-8')
        s.close()

        temperature_string = temperature_3[0:4:1]

        #dw.scratchpad3.setText(temperature_3)

        if self.isfloat(temperature_string):
            temperature_num_3 = (float(temperature_string)) / 100
            temperature_num_3 = (temperature_num_3 * 1.8) + 32
            #dw.tempfield3.setText("temperature: %0.2f deg F" % temperature_num_3)
            #dw.scratchpad.setText("True")
            i = 0
            while i < 999:
                dw.temp_array3[i] = dw.temp_array3[i + 1]
                i += 1
            dw.temp_array3[999] = temperature_num_3
        #else:
        #   dw.scratchpad3.setText("temp 3 error: %s" % time_str)

        index = temperature_3.find('HM')
        humidity_string = temperature_3[index + 2:index + 6:1]

        #dw.scratchpad3.setText(humidity_string)

        if self.isfloat(humidity_string):
            humidity_num_3 = (float(humidity_string)) / 100
            #dw.humidfield3.setText("humidity: %0.2f %%" % humidity_num_3)
            i = 0
            while i < 999:
                dw.humid_array3[i] = dw.humid_array3[i + 1]
                i += 1
            dw.humid_array3[999] = humidity_num_3
        #else:
        #   dw.scratchpad3.setText("humidity 3 error: %s" % time_str)

        index = temperature_3.find('PS')
        pressure_string = temperature_3[index + 2:index + 6:1]

        if self.isfloat(pressure_string):
            pressure_num_3 = (float(pressure_string)) * 0.02953
            #pressure_num_3 = pressure_num_3 + 0.01
            #dw.pressfield3.setText("atm pressure: %0.2f inHg" % pressure_num_3)
            i = 0
            while i < 999:
                dw.press_array3[i] = dw.press_array3[i + 1]
                i += 1
            dw.press_array3[999] = 0.2 * (
                pressure_num_3 + dw.press_array3[998] + dw.press_array3[997] +
                dw.press_array3[996] + dw.press_array3[995])
            #dw.scratchpad.setText("atm pressure: %d" % dw.press_array3[359])
        #else:
        #   dw.scratchpad3.setText("atm pressure 3 error: %s" % time_str)

        indexV1 = temperature_3.find('LV')
        #indexV2=temperature_3.find('CU')
        V_string = temperature_3[indexV1 + 2:indexV1 + 6:1]
        indexI1 = temperature_3.find('CU')
        #indexI2=len(temperature_3)
        I_string = temperature_3[indexI1 + 2:indexI1 + 7:1]

        if (self.isfloat(V_string) and self.isfloat(I_string)):
            V_num_3 = (float(V_string))
            I_num_3 = (float(I_string))
            #dw.battfield3.setText("battery: %0.2fmA@%0.2fV" % (I_num_3, V_num_3))
            #i=0
            #while i<359:
            #    dw.batt_array3[i]=dw.batt_array3[i+1]
            #    i+=1
            #dw.batt_array3[359]=V_num
        #else:
        #   dw.scratchpad3.setText("I or V, 3 error: %s" % time_str)

        #s.close()

    def mylog(self):

        #dw.scratchpad2.setText("mylog called")

        dw.p22.setGeometry(dw.p12.vb.sceneBoundingRect())
        dw.p32.setGeometry(dw.p12.vb.sceneBoundingRect())
        dw.p42.setGeometry(dw.p12.vb.sceneBoundingRect())

        dw.p23.setGeometry(dw.p13.vb.sceneBoundingRect())

        dw.timeplot.setGeometry(dw.p13.vb.sceneBoundingRect())

        self.repeat()

        ts = time.localtime()
        time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts)
        dw.textfield.setText(time_str)

        dw.scratchpad2.setText(temperature_2)
        dw.scratchpad3.setText(temperature_3)

        dw.tempfield2.setText("inside temperature: %0.2f deg F" %
                              temperature_num_2)
        dw.tempfield3.setText("outside temperature: %0.2f deg F" %
                              temperature_num_3)

        dw.humidfield2.setText("inside humidity: %0.2f %%" % humidity_num_2)
        dw.humidfield3.setText("outside humidity: %0.2f %%" % humidity_num_3)

        dw.pressfield2.setText("inside atm pressure: %0.2f inHg" %
                               pressure_num_2)
        dw.pressfield3.setText("outside atm pressure: %0.2f inHg" %
                               pressure_num_3)

        dw.battfield2.setText("inside battery: %0.2fmA@%0.2fV" %
                              (I_num_2, V_num_2))
        dw.battfield3.setText("outside battery: %0.2fmA@%0.2fV" %
                              (I_num_3, V_num_3))

        #dw.p1.plot(dw.x, dw.temp_array)
        #dw.curve.setData(dw.x, dw.temp_array)
        #dw.plt_humid.setData(dw.x, dw.humid_array)

        dw.curve12.setData(dw.temp_array2)
        dw.curve22.setData(dw.temp_array3)
        dw.curve32.setData(dw.humid_array2)
        dw.curve42.setData(dw.humid_array3)

        dw.curve13.setData(dw.press_array2)
        dw.curve23.setData(dw.press_array3)

        dw.curve_tp.setData(dw.time_array)

        #dw.curve.setData(dw.x, dw.temp_array)
        #dw.curve2.setData(dw.x, dw.humid_array)

        dw.p22.setGeometry(dw.p12.vb.sceneBoundingRect())
        dw.p32.setGeometry(dw.p12.vb.sceneBoundingRect())
        dw.p42.setGeometry(dw.p12.vb.sceneBoundingRect())

        dw.p23.setGeometry(dw.p13.vb.sceneBoundingRect())

        #dw.scratchpad.setText("resizing")

        #dw.plt.resize(1801,850)
        #dw.plt.resize(1800,850)

        #dw.scratchpad.setText("resized")

        pg.QtGui.QGuiApplication.processEvents()
        #threading.Timer(60.0, self.mylog).start()

    def data_display():

        ts = time.localtime()
        time_str = time.strftime("time: %Y-%m-%d %H-%M-%S", ts)
        dw.textfield.setText(time_str)

        dw.scratchpad2.setText(temperature_2)
        dw.scratchpad3.setText(temperature_3)

        dw.tempfield2.setText("temperature: %0.2f deg F" % temperature_num_2)
        dw.tempfield3.setText("temperature: %0.2f deg F" % temperature_num_3)

        dw.humidfield2.setText("humidity: %0.2f %%" % humidity_num_2)
        dw.humidfield3.setText("humidity: %0.2f %%" % humidity_num_3)

        dw.pressfield2.setText("atm pressure: %0.2f inHg" % pressure_num_2)
        dw.pressfield3.setText("atm pressure: %0.2f inHg" % pressure_num_3)

        dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_2, V_num_2))
        dw.battfield2.setText("battery: %0.2fmA@%0.2fV" % (I_num_3, V_num_3))

        #threading.Timer(10.0, data_display).start()

    def isfloat(self, s):
        try:
            float(s)
            return True
        except ValueError:
            return False
コード例 #4
0
class TreeVisualizer(QWidget):
    def __init__(self):
        """Initial configuration."""
        super().__init__()

        # GLOBAL VARIABLES
        # graph variables
        self.graph = Graph()
        self.selected_node = None

        self.vertex_positions = []
        self.selected_vertex = None

        # offset of the mouse from the position of the currently dragged node
        self.mouse_drag_offset = None

        # position of the mouse; is updated when the mouse moves
        self.mouse_x = -1
        self.mouse_y = -1

        # variables for visualizing the graph
        self.node_radius = 20
        self.weight_rectangle_size = self.node_radius / 3

        self.arrowhead_size = 4
        self.arrow_separation = pi / 7

        self.selected_color = Qt.red
        self.regular_node_color = Qt.white
        self.regular_vertex_weight_color = Qt.black

        # limit the displayed length of labels for each node
        self.node_label_limit = 10

        # UI variables
        self.font_family = "Times New Roman"
        self.font_size = 18

        self.layout_margins = 8
        self.layout_item_spacing = 2 * self.layout_margins

        # canvas positioning - scale and translation
        self.scale = 1
        self.scale_coefficient = 1.1  # by how much the scale changes on scroll
        self.translation = [0, 0]

        # angle (in degrees) by which all of the nodes rotate
        self.node_rotation_angle = 15

        # TIMERS
        # runs the simulation 60 times a second (1000/60 ~= 16ms)
        self.simulation_timer = QTimer(
            interval=16, timeout=self.perform_simulation_iteration)

        # WIDGETS
        self.canvas = QFrame(self, minimumSize=QSize(0, 400))
        self.canvas_size = None
        self.canvas.resizeEvent = self.adjust_canvas_translation

        # toggles between directed/undirected graphs
        self.directed_toggle_button = QPushButton(
            text="undirected", clicked=self.toggle_directed_graph)

        # for showing the labels of the nodes
        self.labels_checkbox = QCheckBox(text="labels")

        # sets, whether the graph is weighted or not
        self.weighted_checkbox = QCheckBox(text="weighted",
                                           clicked=self.set_weighted_graph)

        # enables/disables forces (True by default - they're fun!)
        self.forces_checkbox = QCheckBox(text="forces", checked=True)

        # input of the labels and vertex weights
        self.input_line_edit = QLineEdit(
            enabled=self.labels_checkbox.isChecked(),
            textChanged=self.input_line_edit_changed)

        # displays information about the app
        self.about_button = QPushButton(text="?",
                                        clicked=self.show_help,
                                        sizePolicy=QSizePolicy(
                                            QSizePolicy.Fixed,
                                            QSizePolicy.Fixed))

        # imports/exports the current graph
        self.import_graph_button = QPushButton(text="import",
                                               clicked=self.import_graph)
        self.export_graph_button = QPushButton(text="export",
                                               clicked=self.export_graph)

        # WIDGET LAYOUT
        self.main_v_layout = QVBoxLayout(self, margin=0)
        self.main_v_layout.addWidget(self.canvas)

        self.option_h_layout = QHBoxLayout(self, margin=self.layout_margins)
        self.option_h_layout.addWidget(self.directed_toggle_button)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.weighted_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.labels_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.forces_checkbox)
        self.option_h_layout.addSpacing(self.layout_item_spacing)
        self.option_h_layout.addWidget(self.input_line_edit)

        self.io_h_layout = QHBoxLayout(self, margin=self.layout_margins)
        self.io_h_layout.addWidget(self.import_graph_button)
        self.io_h_layout.addSpacing(self.layout_item_spacing)
        self.io_h_layout.addWidget(self.export_graph_button)
        self.io_h_layout.addSpacing(self.layout_item_spacing)
        self.io_h_layout.addWidget(self.about_button)

        self.main_v_layout.addLayout(self.option_h_layout)
        self.main_v_layout.addSpacing(-self.layout_margins)
        self.main_v_layout.addLayout(self.io_h_layout)

        self.setLayout(self.main_v_layout)

        # WINDOW SETTINGS
        self.setWindowTitle('Graph Visualizer')
        self.setFont(QFont(self.font_family, self.font_size))
        self.setWindowIcon(QIcon("icon.ico"))
        self.show()

        # start the simulation
        self.simulation_timer.start()

    def set_weighted_graph(self):
        """Is called when the weighted checkbox is pressed; sets, whether the graph is weighted or not."""
        self.graph.set_weighted(self.weighted_checkbox.isChecked())

    def adjust_canvas_translation(self, event):
        """Is called when the canvas widget is resized; changes translation so the center stays in the center."""
        size = (event.size().width(), event.size().height())

        # don't translate on the initial resize
        if self.canvas_size is not None:
            self.translation[0] += self.scale * (size[0] -
                                                 self.canvas_size[0]) / 2
            self.translation[1] += self.scale * (size[1] -
                                                 self.canvas_size[1]) / 2

        self.canvas_size = size

    def repulsion_force(self, distance):
        """Calculates the strength of the repulsion force at the specified distance."""
        return 1 / distance * 10 if self.forces_checkbox.isChecked() else 0

    def attraction_force(self, distance, leash_length=80):
        """Calculates the strength of the attraction force at the specified distance and leash length."""
        return -(distance -
                 leash_length) / 10 if self.forces_checkbox.isChecked() else 0

    def import_graph(self):
        """Is called when the import button is clicked; imports a graph from a file."""
        path = QFileDialog.getOpenFileName()[0]

        if path != "":
            try:
                with open(path, "r") as file:
                    # a list of vertices of the graph
                    data = [line.strip() for line in file.read().splitlines()]

                    sample = data[0].split(" ")
                    vertex_types = ["->", "<", "<>"]

                    # whether the graph is directed and weighted; done by looking at a sample input vertex
                    directed = True if sample[1] in vertex_types else False
                    weighted = False if len(
                        sample) == 2 or directed and len(sample) == 3 else True

                    graph = Graph(directed=directed, weighted=weighted)

                    # to remember the created nodes and to connect them later
                    node_dictionary = {}

                    # add each of the nodes of the vertex to the graph
                    for vertex in data:
                        vertex_components = vertex.split(" ")

                        # the formats are either 'A B' or 'A ... B', where ... is one of the vertex types
                        nodes = [
                            vertex_components[0], vertex_components[2]
                            if directed else vertex_components[1]
                        ]

                        # if weights are present, the formats are either 'A B num' or 'A ... B num (num)'
                        weights_strings = None if not weighted else [
                            vertex_components[2] if not directed else
                            vertex_components[3], None if not directed
                            or vertex_components[1] != vertex_types[2] else
                            vertex_components[4]
                        ]

                        for node in nodes:
                            if node not in node_dictionary:
                                # slightly randomize the coordinates so the graph doesn't stay in one place
                                x = self.canvas.width() / 2 + (random() - 0.5)
                                y = self.canvas.height() / 2 + (random() - 0.5)

                                # add it to graph with default values
                                node_dictionary[node] = graph.add_node(
                                    x, y, self.node_radius, node)

                        # get the node objects from the names
                        n1, n2 = node_dictionary[nodes[0]], node_dictionary[
                            nodes[1]]

                        # export the graph, according to the direction of the vertex, and whether it's weighted or not
                        if not directed or vertex_components[1] == "->":
                            if weighted:
                                graph.add_vertex(
                                    n1, n2,
                                    self._convert_string_to_number(
                                        weights_strings[0]))
                            else:
                                graph.add_vertex(n1, n2)
                        elif vertex_components[1] == "<-":
                            if weighted:
                                graph.add_vertex(
                                    n2, n1,
                                    self._convert_string_to_number(
                                        weights_strings[0]))
                            else:
                                graph.add_vertex(n2, n1)
                        else:
                            if weighted:
                                graph.add_vertex(
                                    n1, n2,
                                    self._convert_string_to_number(
                                        weights_strings[0]))
                                graph.add_vertex(
                                    n2, n1,
                                    self._convert_string_to_number(
                                        weights_strings[1]))
                            else:
                                graph.add_vertex(n1, n2)
                                graph.add_vertex(n2, n1)

                self.graph = graph

            except UnicodeDecodeError:
                QMessageBox.critical(self, "Error!",
                                     "Can't read binary files!")
            except ValueError:
                QMessageBox.critical(
                    self, "Error!",
                    "The weights of the graph are not numbers!")
            except Exception:
                QMessageBox.critical(
                    self, "Error!",
                    "An error occurred when importing the graph. Make sure that the "
                    "file is in the correct format!")

            # make sure that the UI is in order
            self.deselect_node()
            self.deselect_vertex()
            self.set_checkbox_values()

    def _convert_string_to_number(self, str):
        """Attempts to convert the specified string to a number. Throws error if it fails to do so!"""
        try:
            return int(str)
        except ValueError:
            return float(str)

    def set_checkbox_values(self):
        """Sets the values of the checkboxes from the graph."""
        self.weighted_checkbox.setChecked(self.graph.is_weighted())
        self.update_directed_toggle_button_text()

    def export_graph(self):
        """Is called when the export button is clicked; exports a graph to a file."""
        path = QFileDialog.getSaveFileName()[0]

        if path != "":
            try:
                with open(path, "w") as file:
                    # look at every pair of nodes and examine the vertices
                    for i in range(len(self.graph.get_nodes())):
                        n1 = self.graph.get_nodes()[i]
                        for j in range(i + 1, len(self.graph.get_nodes())):
                            n2 = self.graph.get_nodes()[j]

                            v1_exists = self.graph.does_vertex_exist(n1, n2)

                            w1_value = self.graph.get_weight(n1, n2)
                            w1 = "" if not self.graph.is_weighted(
                            ) or w1_value is None else str(w1_value)

                            if not self.graph.is_directed() and v1_exists:
                                # for undirected graphs, no direction symbols are necessary
                                file.write(n1.get_label() + " " +
                                           n2.get_label() + " " + w1 + "\n")
                            else:
                                v2_exists = self.graph.does_vertex_exist(
                                    n2, n1)

                                w2_value = self.graph.get_weight(n2, n1)
                                w2 = "" if not self.graph.is_weighted(
                                ) or w2_value is None else str(w2_value)

                                # node that w1 and w2 might be empty strings, so we can combine
                                # directed weighted and unweighted graphs in one command
                                if v1_exists and v2_exists:
                                    file.write("%s <> %s %s %s\n" %
                                               (n1.get_label(), n2.get_label(),
                                                str(w1), str(w2)))
                                elif v1_exists:
                                    file.write("%s -> %s %s\n" %
                                               (n1.get_label(), n2.get_label(),
                                                str(w1)))
                                elif v2_exists:
                                    file.write("%s <- %s %s\n" %
                                               (n1.get_label(), n2.get_label(),
                                                str(w2)))
            except Exception:
                QMessageBox.critical(
                    self, "Error!",
                    "An error occurred when exporting the graph. Make sure that you "
                    "have permission to write to the specified file and try again!"
                )

    def show_help(self):
        """Is called when the help button is clicked; displays basic information about the application."""
        message = """
            <p>Welcome to <strong>Graph Visualizer</strong>.</p>
            <p>The app aims to help with creating, visualizing and exporting graphs. 
            It is powered by PyQt5 &ndash; a set of Python bindings for the C++ library Qt.</p>
            <hr />
            <p>The controls are as follows:</p>
            <ul>
            <li><em>Left Mouse Button</em> &ndash; selects nodes and moves them</li>
            <li><em>Right Mouse Button</em> &ndash; creates/removes nodes and vertices</li>
            <li><em>Mouse Wheel</em> &ndash; zooms in/out</li>
            <li><em>Shift + Left Mouse Button</em> &ndash; moves connected nodes</li>
            <li><em>Shift + Mouse Wheel</em> &ndash; rotates nodes around the selected node</li>
            </ul>
            <hr />
            <p>If you spot an issue, or would like to check out the source code, see the app's 
            <a href="https://github.com/xiaoxiae/GraphVisualizer">GitHub repository</a>.</p>
        """

        QMessageBox.information(self, "About", message)

    def toggle_directed_graph(self):
        """Is called when the directed checkbox changes; toggles between directed and undirected graphs."""
        self.graph.set_directed(not self.graph.is_directed())
        self.update_directed_toggle_button_text()

    def update_directed_toggle_button_text(self):
        """Changes the text of the directed toggle button, according to whether the graph is directer or not."""
        self.directed_toggle_button.setText(
            "directed" if self.graph.is_directed() else "undirected")

    def input_line_edit_changed(self, text):
        """Is called when the input line edit changes; changes either the label of the node selected node, or the value
        of the selected vertex."""
        palette = self.input_line_edit.palette()

        if self.selected_node is not None:
            # the text has to be non-zero and not contain spaces, for the import/export language to work properly
            # the text length is also restricted, for rendering purposes
            if 0 < len(text) < self.node_label_limit and " " not in text:
                self.selected_node.set_label(text)
                palette.setColor(self.input_line_edit.backgroundRole(),
                                 Qt.white)
            else:
                palette.setColor(self.input_line_edit.backgroundRole(), Qt.red)
        elif self.selected_vertex is not None:
            # try to parse the input text either as an integer, or as a float
            weight = None
            try:
                weight = int(text)
            except ValueError:
                try:
                    weight = float(text)
                except ValueError:
                    pass

            # if the parsing was unsuccessful, set the input line edit background to red to indicate this fact
            # if it worked, set the weight by adding a new vertex with the input weight
            if weight is None:
                palette.setColor(self.input_line_edit.backgroundRole(), Qt.red)
            else:
                self.graph.add_vertex(self.selected_vertex[0],
                                      self.selected_vertex[1], weight)
                palette.setColor(self.input_line_edit.backgroundRole(),
                                 Qt.white)

        self.input_line_edit.setPalette(palette)

    def select_node(self, node):
        """Sets the selected node to the specified node, sets the input line edit to its label and enables it."""
        self.selected_node = node

        self.input_line_edit.setText(node.get_label())
        self.input_line_edit.setEnabled(True)
        self.input_line_edit.setFocus()

    def deselect_node(self):
        """Sets the selected node to None and disables the input line edit."""
        self.selected_node = None
        self.input_line_edit.setEnabled(False)

    def select_vertex(self, vertex):
        """Sets the selected vertex to the specified vertex, sets the input line edit to its weight and enables it."""
        self.selected_vertex = vertex

        self.input_line_edit.setText(str(self.graph.get_weight(*vertex)))
        self.input_line_edit.setEnabled(True)
        self.input_line_edit.setFocus()

    def deselect_vertex(self):
        """Sets the selected vertex to None and disables the input line edit."""
        self.selected_vertex = None
        self.input_line_edit.setEnabled(False)

    def mousePressEvent(self, event):
        """Is called when a mouse button is pressed; creates and moves nodes/vertices."""
        mouse_coordinates = self.get_mouse_coordinates(event)

        # if we are not on canvas, don't do anything
        if mouse_coordinates is None:
            return

        # sets the focus to the entire window, for the keypresses to register
        self.setFocus()

        x = mouse_coordinates[0]
        y = mouse_coordinates[1]

        # (potentially) find a node that has been pressed
        pressed_node = None
        for node in self.graph.get_nodes():
            if self.distance(x, y, node.get_x(),
                             node.get_y()) <= node.get_radius():
                pressed_node = node

        # (potentially) find a vertex that has been pressed
        pressed_vertex = None
        for vertex in self.vertex_positions:
            # vertex position items have the structure [x, y, (n1, n2)]
            if abs(vertex[0] - x) < self.weight_rectangle_size and abs(
                    vertex[1] - y) < self.weight_rectangle_size:
                pressed_vertex = vertex[2]

        # select on left click
        # create/connect on right click
        if event.button() == Qt.LeftButton:
            # nodes have the priority in selection before vertices
            if pressed_node is not None:
                self.deselect_vertex()
                self.select_node(pressed_node)

                self.mouse_drag_offset = (x - self.selected_node.get_x(),
                                          y - self.selected_node.get_y())
                self.mouse_x = x
                self.mouse_y = y
            elif pressed_vertex is not None:
                self.deselect_node()
                self.select_vertex(pressed_vertex)
            else:
                self.deselect_node()
                self.deselect_vertex()
        elif event.button() == Qt.RightButton:
            # either make/remove a connection if we right clicked a node, or create a new node if we haven't
            if pressed_node is not None:
                if self.selected_node is not None and pressed_node is not self.selected_node:
                    # if a connection does not exist between the nodes, create it; otherwise remove it
                    if self.graph.does_vertex_exist(self.selected_node,
                                                    pressed_node):
                        self.graph.remove_vertex(self.selected_node,
                                                 pressed_node)
                    else:
                        self.graph.add_vertex(self.selected_node, pressed_node)
                else:
                    self.graph.remove_node(pressed_node)
                    self.deselect_node()
            elif pressed_vertex is not None:
                self.graph.remove_vertex(*pressed_vertex)
                self.deselect_vertex()
            else:
                node = self.graph.add_node(x, y, self.node_radius)

                # if a selected node exists, connect it to the newly created node
                if self.selected_node is not None:
                    self.graph.add_vertex(self.selected_node, node)

                self.select_node(node)
                self.deselect_vertex()

    def mouseReleaseEvent(self, event):
        """Is called when a mouse button is released; stops node drag."""
        self.mouse_drag_offset = None

    def mouseMoveEvent(self, event):
        """Is called when the mouse is moved across the window; updates mouse coordinates."""
        mouse_coordinates = self.get_mouse_coordinates(event, scale_down=True)

        self.mouse_x = mouse_coordinates[0]
        self.mouse_y = mouse_coordinates[1]

    def wheelEvent(self, event):
        """Is called when the mouse wheel is moved; node rotation and zoom."""
        if QApplication.keyboardModifiers() == Qt.ShiftModifier:
            if self.selected_node is not None:
                # positive/negative for scrolling away from/towards the user
                angle = self.node_rotation_angle if event.angleDelta().y(
                ) > 0 else -self.node_rotation_angle

                self.rotate_nodes_around(self.selected_node.get_x(),
                                         self.selected_node.get_y(), angle)
        else:
            mouse_coordinates = self.get_mouse_coordinates(event)

            # only do something, if we're working on canvas
            if mouse_coordinates is None:
                return

            x, y = mouse_coordinates[0], mouse_coordinates[1]
            prev_scale = self.scale

            # adjust the canvas scale, depending on the scroll direction
            # if angleDelta.y() is positive, scroll away (zoom out) from the user (and vice versa)
            if event.angleDelta().y() > 0:
                self.scale *= self.scale_coefficient
            else:
                self.scale /= self.scale_coefficient

            # adjust translation so the x and y of the mouse stay in the same spot
            scale_delta = self.scale - prev_scale
            self.translation[0] += -(x * scale_delta)
            self.translation[1] += -(y * scale_delta)

    def rotate_nodes_around(self, x, y, angle):
        """Rotates coordinates of all of the points by a certain angle (in degrees) around the specified point."""
        angle = radians(angle)

        for node in self.graph.get_nodes():
            # only rotate points that are in the same continuity set
            if self.graph.share_continuity_set(node, self.selected_node):
                # translate the coordinates to origin for the rotation to work
                node_x, node_y = node.get_x() - x, node.get_y() - y

                # rotate and translate the coordinates of the node
                node.set_x(node_x * cos(angle) - node_y * sin(angle) + x)
                node.set_y(node_x * sin(angle) + node_y * cos(angle) + y)

    def get_mouse_coordinates(self, event, scale_down=False):
        """Returns mouse coordinates if they are within the canvas and None if they are not.
        If scale_down is True, the function will scale down the coordinates to be within the canvas (useful for
        dragging) and return them instead."""
        x = event.pos().x()
        y = event.pos().y()

        # booleans for whether the coordinate components are on canvas
        x_on_canvas = 0 <= x <= self.canvas.width()
        y_on_canvas = 0 <= y <= self.canvas.height()

        # scale down the coordinates if scale_down is True, or return none if we are not on canvas
        if scale_down:
            x = x if x_on_canvas else 0 if x <= 0 else self.canvas.width()
            y = y if y_on_canvas else 0 if y <= 0 else self.canvas.height()
        elif not x_on_canvas or not y_on_canvas:
            return None

        # return the mouse coordinates, accounting for the translation and scale of the canvas
        return ((x - self.translation[0]) / self.scale,
                (y - self.translation[1]) / self.scale)

    def perform_simulation_iteration(self):
        """Performs one iteration of the simulation."""
        # evaluate forces that act upon each pair of nodes
        for i in range(len(self.graph.get_nodes())):
            n1 = self.graph.get_nodes()[i]
            for j in range(i + 1, len(self.graph.get_nodes())):
                n2 = self.graph.get_nodes()[j]

                # if they are not in the same continuity set, no forces act on them
                if not self.graph.share_continuity_set(n1, n2):
                    continue

                # calculate the distance of the nodes and a unit vector from the first to the second
                d = self.distance(n1.get_x(), n1.get_y(), n2.get_x(),
                                  n2.get_y())

                # if the nodes are right on top of each other, the force can't be calculated
                if d == 0:
                    continue

                ux, uy = (n2.get_x() - n1.get_x()) / d, (n2.get_y() -
                                                         n1.get_y()) / d

                # the size of the repel force between the two nodes
                fr = self.repulsion_force(d)

                # add a repel force to each of the nodes, in the opposite directions
                n1.add_force((-ux * fr, -uy * fr))
                n2.add_force((ux * fr, uy * fr))

                # if they are connected, add the leash force, regardless of whether the graph is directed or not
                if self.graph.does_vertex_exist(n1, n2, ignore_direction=True):
                    # the size of the attraction force between the two nodes
                    fa = self.attraction_force(d)

                    # add the repel force to each of the nodes, in the opposite directions
                    n1.add_force((-ux * fa, -uy * fa))
                    n2.add_force((ux * fa, uy * fa))

            # since this node will not be visited again, evaluate the forces
            n1.evaluate_forces()

        # drag the selected node, after all of the forces have been applied, so it doesn't move anymore
        if self.selected_node is not None and self.mouse_drag_offset is not None:
            prev_x = self.selected_node.get_x()
            prev_y = self.selected_node.get_y()

            self.selected_node.set_x(self.mouse_x - self.mouse_drag_offset[0])
            self.selected_node.set_y(self.mouse_y - self.mouse_drag_offset[1])

            # move the rest of the nodes that are connected to the selected node, if shift is pressed
            if QApplication.keyboardModifiers() == Qt.ShiftModifier:
                x_delta = self.selected_node.get_x() - prev_x
                y_delta = self.selected_node.get_y() - prev_y

                for node in self.graph.get_nodes():
                    if node is not self.selected_node and self.graph.share_continuity_set(
                            node, self.selected_node):
                        node.set_x(node.get_x() + x_delta)
                        node.set_y(node.get_y() + y_delta)

        self.update()

    def paintEvent(self, event):
        """Paints the board."""
        painter = QPainter(self)

        painter.setRenderHint(QPainter.Antialiasing, True)

        painter.setPen(QPen(Qt.black, Qt.SolidLine))
        painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))

        # bound the canvas area to not draw outside of it
        painter.setClipRect(0, 0, self.canvas.width(), self.canvas.height())

        # draw the background
        painter.drawRect(0, 0, self.canvas.width(), self.canvas.height())

        # transform and scale the painter accordingly
        painter.translate(self.translation[0], self.translation[1])
        painter.scale(self.scale, self.scale)

        # if the graph is weighted, reset the positions, since they will be re-drawn later on
        if self.graph.is_weighted():
            self.vertex_positions = []

        # draw vertices; has to be drawn before nodes, so they aren't drawn on top of them
        for node in self.graph.get_nodes():
            for neighbour, weight in node.get_neighbours().items():
                x1, y1, x2, y2 = node.get_x(), node.get_y(), neighbour.get_x(
                ), neighbour.get_y()

                # create a unit vector from the first to the second graph
                d = self.distance(x1, y1, x2, y2)
                ux, uy = (x2 - x1) / d, (y2 - y1) / d
                r = neighbour.get_radius()

                # if it's directed, draw the head of the arrow
                if self.graph.is_directed():
                    # in case there is a vertex going the other way, we will move the line up the circles by an angle,
                    # so there is separation between the vertices
                    if self.graph.does_vertex_exist(neighbour, node):
                        nx = -uy * r * sin(self.arrow_separation) + ux * r * (
                            1 - cos(self.arrow_separation))
                        ny = ux * r * sin(self.arrow_separation) + uy * r * (
                            1 - cos(self.arrow_separation))

                        x1, x2, y1, y2 = x1 + nx, x2 + nx, y1 + ny, y2 + ny

                    # the position of the head of the arrow
                    xa, ya = x1 + ux * (d - r), y1 + uy * (d - r)

                    # calculate the two remaining points of the arrow
                    # this is done the same way as the previous calculation (shift by vector)
                    d = self.distance(x1, y1, xa, ya)
                    ux_arrow, uy_arrow = (xa - x1) / d, (ya - y1) / d

                    # position of the base of the arrow
                    x, y = x1 + ux_arrow * (
                        d - self.arrowhead_size * 2), y1 + uy_arrow * (
                            d - self.arrowhead_size * 2)

                    # the normal vectors to the unit vector of the arrow head
                    nx_arrow, ny_arrow = -uy_arrow, ux_arrow

                    # draw the tip of the arrow, as the triangle
                    painter.setBrush(QBrush(Qt.black, Qt.SolidPattern))
                    painter.drawPolygon(
                        QPointF(xa, ya),
                        QPointF(x + nx_arrow * self.arrowhead_size,
                                y + ny_arrow * self.arrowhead_size),
                        QPointF(x - nx_arrow * self.arrowhead_size,
                                y - ny_arrow * self.arrowhead_size))

                # draw only one of the two vertices, if the graph is undirected
                if self.graph.is_directed() or id(node) < id(neighbour):
                    painter.drawLine(QPointF(x1, y1), QPointF(x2, y2))

                    if self.graph.is_weighted():
                        x_middle, y_middle = (x2 + x1) / 2, (y2 + y1) / 2

                        # if the graph is directed, the vertices are offset (so they aren't draw on top of each other),
                        # so we need to shift them back to be at the midpoint between the nodes
                        if self.graph.is_directed():
                            x_middle -= ux * r * (1 -
                                                  cos(self.arrow_separation))
                            y_middle -= uy * r * (1 -
                                                  cos(self.arrow_separation))

                        r = self.weight_rectangle_size

                        self.vertex_positions.append(
                            (x_middle, y_middle, (node, neighbour)))

                        # make the selected vertex rectangle background different, if it's selected (for aesthetics)
                        if self.selected_vertex is not None and node is self.selected_vertex[0] and neighbour is \
                                self.selected_vertex[1]:
                            painter.setBrush(
                                QBrush(self.selected_color, Qt.SolidPattern))
                        else:
                            painter.setBrush(
                                QBrush(self.regular_vertex_weight_color,
                                       Qt.SolidPattern))

                        # draw the square
                        square_rectangle = QRectF(x_middle - r, y_middle - r,
                                                  2 * r, 2 * r)
                        painter.drawRect(square_rectangle)

                        # adjust the length of the weight string, so the minus sign doesn't make the number smaller
                        length = len(str(weight)) - (1 if weight < 0 else 0)

                        painter.setFont(
                            QFont(self.font_family,
                                  self.font_size / (length * 3)))

                        # draw the value of the vertex (in white, so it's visible against the background)
                        painter.setPen(QPen(Qt.white, Qt.SolidLine))
                        painter.drawText(square_rectangle, Qt.AlignCenter,
                                         str(weight))
                        painter.setPen(QPen(Qt.black, Qt.SolidLine))

        # draw nodes
        for node in self.graph.get_nodes():
            # selected nodes are red to make them distinct; others are white
            if node is self.selected_node:
                painter.setBrush(QBrush(self.selected_color, Qt.SolidPattern))
            else:
                painter.setBrush(
                    QBrush(self.regular_node_color, Qt.SolidPattern))

            x, y, r = node.get_x(), node.get_y(), node.get_radius()

            painter.drawEllipse(QPointF(x, y), r, r)

            # only draw labels if the label checkbox is checked
            if self.labels_checkbox.isChecked():
                label = node.get_label()

                # scale font down, depending on the length of the label of the node
                painter.setFont(
                    QFont(self.font_family, self.font_size / len(label)))

                # draw the node label within the node dimensions
                painter.drawText(QRectF(x - r, y - r, 2 * r, 2 * r),
                                 Qt.AlignCenter, label)

    def distance(self, x1, y1, x2, y2):
        """Returns the distance of two points in space."""
        return sqrt((x1 - x2)**2 + (y1 - y2)**2)