From 4eeb79202045b089534a3f4d1ead694e8c3675a6 Mon Sep 17 00:00:00 2001
From: Oscar Gustafsson <oscar.gustafsson@gmail.com>
Date: Thu, 23 Feb 2023 10:26:49 +0100
Subject: [PATCH] Fix moving operations in y-direction

---
 b_asic/schedule.py                      | 74 ++++++++++++++++++++++++-
 b_asic/scheduler_gui/main_window.py     |  4 ++
 b_asic/scheduler_gui/scheduler_event.py | 27 +++++----
 b_asic/scheduler_gui/scheduler_item.py  | 15 +++--
 4 files changed, 103 insertions(+), 17 deletions(-)

diff --git a/b_asic/schedule.py b/b_asic/schedule.py
index 2b725089..6eaa7111 100644
--- a/b_asic/schedule.py
+++ b/b_asic/schedule.py
@@ -31,7 +31,7 @@ from b_asic._preferences import (
 from b_asic.graph_component import GraphID
 from b_asic.operation import Operation
 from b_asic.port import InputPort, OutputPort
-from b_asic.process import MemoryVariable, Process
+from b_asic.process import MemoryVariable
 from b_asic.resources import ProcessCollection
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
@@ -244,6 +244,7 @@ class Schedule:
         return self.backward_slack(graph_id), self.forward_slack(graph_id)
 
     def print_slacks(self) -> None:
+        """Print the slack times for all operations in the schedule."""
         raise NotImplementedError
 
     def set_schedule_time(self, time: int) -> "Schedule":
@@ -281,10 +282,12 @@ class Schedule:
 
     @property
     def schedule_time(self) -> int:
+        """The schedule time of the current schedule."""
         return self._schedule_time
 
     @property
     def cyclic(self) -> bool:
+        """If the current schedule is cyclic."""
         return self._cyclic
 
     def increase_time_resolution(self, factor: int) -> "Schedule":
@@ -360,6 +363,75 @@ class Schedule:
         self._schedule_time = self._schedule_time // factor
         return self
 
+    def move_y_location(
+        self, graph_id: GraphID, new_y: int, insert: bool = False
+    ) -> None:
+        """
+        Move operation in y-direction and remove any empty rows.
+
+        Parameters
+        ----------
+        graph_id : GraphID
+            The GraphID of the operation to move.
+        new_y : int
+           The new y-position of the operation.
+        insert : bool, optional
+            If True, all operations on that y-position will be moved one position.
+            The default is False.
+
+        """
+        if insert:
+            for gid, y_location in self._y_locations.items():
+                if y_location >= new_y:
+                    self._y_locations[gid] += 1
+        self._y_locations[graph_id] = new_y
+        used_locations = {*self._y_locations.values()}
+        possible_locations = set(range(max(used_locations) + 1))
+        if not possible_locations - used_locations:
+            return
+        remapping = {}
+        offset = 0
+        for loc in possible_locations:
+            if loc in used_locations:
+                remapping[loc] = loc - offset
+            else:
+                offset += 1
+
+        for gid, y_location in self._y_locations.items():
+            self._y_locations[gid] = remapping[self._y_locations[gid]]
+
+    def get_y_location(self, graph_id: GraphID) -> int:
+        """
+        Get the y-position of the Operation with GraphID *graph_id*.
+
+        Parameters
+        ----------
+        graph_id : GraphID
+            The GraphID of the operation.
+
+        Returns
+        -------
+        int
+            The y-position of the operation.
+
+        """
+        return self._y_locations[graph_id]
+
+    def set_y_location(self, graph_id: GraphID, y_location: int) -> None:
+        """
+        Set the y-position of the Operation with GraphID *graph_id* to *y_location*.
+
+
+        Parameters
+        ----------
+        graph_id : GraphID
+            The GraphID of the operation to move.
+        y_location : int
+           The new y-position of the operation.
+
+        """
+        self._y_locations[graph_id] = y_location
+
     def move_operation(self, graph_id: GraphID, time: int) -> "Schedule":
         """
         Move an operation in the schedule.
diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py
index 8d56996e..28c6e550 100644
--- a/b_asic/scheduler_gui/main_window.py
+++ b/b_asic/scheduler_gui/main_window.py
@@ -482,9 +482,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         self._graph._signals.schedule_time_changed.connect(
             self.info_table_update_schedule
         )
+        self._graph._signals.redraw_all.connect(self._redraw_all)
         self.info_table_fill_schedule(self._schedule)
         self.update_statusbar(self.tr("Schedule loaded successfully"))
 
+    def _redraw_all(self) -> None:
+        self._graph._redraw_all()
+
     def update_statusbar(self, msg: str) -> None:
         """
         Write *msg* to the statusbar with temporarily policy.
diff --git a/b_asic/scheduler_gui/scheduler_event.py b/b_asic/scheduler_gui/scheduler_event.py
index 37add599..026d90fa 100644
--- a/b_asic/scheduler_gui/scheduler_event.py
+++ b/b_asic/scheduler_gui/scheduler_event.py
@@ -44,12 +44,14 @@ class SchedulerEvent:  # PyQt5
         component_selected = Signal(str)
         schedule_time_changed = Signal()
         component_moved = Signal(str)
+        redraw_all = Signal()
 
     _axes: Optional[AxesItem]
     _current_pos: QPointF
     _delta_time: int
     _signals: Signals  # PyQt5
     _schedule: Schedule
+    _old_op_position: int = -1
 
     def __init__(self, parent: Optional[QGraphicsItem] = None):  # PyQt5
         super().__init__(parent=parent)
@@ -202,7 +204,10 @@ class SchedulerEvent:  # PyQt5
                 self._current_pos.setX(self._current_pos.x() + dx)
                 self._current_pos.setY(self._current_pos.y() + dy)
                 self._redraw_lines(operation_item)
-                self._schedule._y_locations[operation_item.operation.graph_id] += dy
+                gid = operation_item.operation.graph_id
+                self._schedule.set_y_location(
+                    gid, dy + self._schedule.get_y_location(gid)
+                )
 
         item: OperationItem = self.scene().mouseGrabberItem()
         delta_x = (item.mapToParent(event.pos()) - self._current_pos).x()
@@ -224,7 +229,7 @@ class SchedulerEvent:  # PyQt5
         allows the item to receive future move, release and double-click events.
         """
         item: OperationItem = self.scene().mouseGrabberItem()
-        self._old_op_position = self._schedule._y_locations[item.operation.graph_id]
+        self._old_op_position = self._schedule.get_y_location(item.operation.graph_id)
         self._signals.component_selected.emit(item.graph_id)
         self._current_pos = item.mapToParent(event.pos())
         self.set_item_active(item)
@@ -243,18 +248,20 @@ class SchedulerEvent:  # PyQt5
         if pos_x > self._schedule.schedule_time:
             pos_x = pos_x % self._schedule.schedule_time
             redraw = True
-        if self._schedule._y_locations[item.operation.graph_id] % 1:
-            # TODO: move other operations
-            self._schedule._y_locations[item.operation.graph_id] = math.ceil(
-                self._schedule._y_locations[item.operation.graph_id]
+        pos_y = self._schedule.get_y_location(item.operation.graph_id)
+        # Check move in y-direction
+        if pos_y != self._old_op_position:
+            self._schedule.move_y_location(
+                item.operation.graph_id,
+                math.ceil(pos_y),
+                (pos_y % 1) != 0,
             )
-            pos_y = item.y() + (OPERATION_GAP + OPERATION_HEIGHT) / 2
-            item.setY(pos_y)
-            redraw = True
+            self._signals.redraw_all.emit()
+        # Operation has been moved in x-direction
         if redraw:
             item.setX(pos_x)
             self._redraw_lines(item)
-        self._signals.component_moved.emit(item.graph_id)
+            self._signals.component_moved.emit(item.graph_id)
 
     def operation_mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
         ...
diff --git a/b_asic/scheduler_gui/scheduler_item.py b/b_asic/scheduler_gui/scheduler_item.py
index 0e8a0443..eceb35d2 100644
--- a/b_asic/scheduler_gui/scheduler_item.py
+++ b/b_asic/scheduler_gui/scheduler_item.py
@@ -224,15 +224,18 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup):  # PySide2 / PyQt5
 
     def _redraw_from_start(self) -> None:
         self.schedule._reset_y_locations()
-        for graph_id in {
-            k: v
-            for k, v in sorted(
-                self.schedule.start_times.items(), key=lambda item: item[1]
-            )
-        }:
+        for graph_id in dict(
+            sorted(self.schedule.start_times.items(), key=lambda item: item[1])
+        ):
             self._set_position(graph_id)
         self._redraw_all_lines()
 
+    def _redraw_all(self) -> None:
+        for graph_id in self._operation_items:
+            self._set_position(graph_id)
+        self._redraw_all_lines()
+        self._update_axes()
+
     def _update_axes(self, build=False) -> None:
         # build axes
         schedule_time = self.schedule.schedule_time
-- 
GitLab