コード例 #1
0
class TestMapCommand(unittest.TestCase):
    def setUp(self):
        polygons = [
            [1, 2, 1, '1,2'],
        ]
        layers = [
            [[1, 6, 'desc'], ],
        ]
        polygon_table = create_dao_polygon_table(polygons, layers)

        self.helper = DbHelper()
        self.helper.polygon_table = polygon_table
        self.cmd = MapCommand(self.helper)

    def test_execute_exception(self):
        self.assertRaises(CommandNotValidException, self.cmd.execute, 'goto')
        self.assertRaises(CommandNotValidException, self.cmd.execute, 'goto shape 1 1')
        self.assertRaises(CommandNotValidException, self.cmd.execute, 'goto shap 1')
        self.assertRaises(CommandNotValidException, self.cmd.execute, 'del shape -1')

    def test_undo_redo(self):
        with patch.object(MapCommand, 'execute', wraps=self.cmd.execute) as fake_execute:
            with patch.object(MapCommand, 'execute_commands', wraps=self.cmd.execute_commands) as fake_commands:
                with patch.object(MapCommand, 'execute_single_command', wraps=self.cmd.execute_single_command) \
                        as fake_single:
                    self.assertEqual(0, fake_execute.call_count)
                    self.assertEqual(0, fake_commands.call_count)
                    self.assertEqual(0, fake_single.call_count)
                    self.cmd.execute('goto shape 1')
                    self.assertEqual(1, fake_execute.call_count)
                    self.assertEqual(1, fake_commands.call_count)
                    self.assertEqual(1, fake_single.call_count)
                    self.cmd.execute(['goto shape 1', 'goto shape 1'])
                    self.assertEqual(2, fake_execute.call_count)
                    self.assertEqual(2, fake_commands.call_count)
                    self.assertEqual(3, fake_single.call_count)
                    self.assertEqual(2, len(self.cmd.command_history))

                    # 撤销后的次数
                    self.cmd.undo()
                    self.assertEqual(1, len(self.cmd.command_history))
                    self.cmd.redo()
                    self.assertEqual(2, len(self.cmd.command_history))
                    self.cmd.redo()
                    self.assertEqual(2, len(self.cmd.command_history))

                    self.cmd.undo()
                    self.assertEqual(1, len(self.cmd.command_history))
                    self.cmd.undo()
                    self.assertEqual(0, len(self.cmd.command_history))
                    self.cmd.undo()
                    self.assertEqual(0, len(self.cmd.command_history))
                    self.cmd.redo()
                    self.assertEqual(1, len(self.cmd.command_history))

    def test_get_spare_id(self):
        self.assertEqual(2, self.cmd.get_spare_id(1))
        self.assertEqual(3, self.cmd.get_spare_id(3))

    def test_add_shape(self):
        with patch.object(MapCommand, 'execute_add_polygon', wraps=self.cmd.execute_add_polygon) as fake_add:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_add.call_count)
            self.cmd.execute('add shape 2 1 0 1')
            self.assertEqual(1, fake_add.call_count)
            self.cmd.execute('add shape 3 0 name')
            self.assertEqual(2, fake_add.call_count)
            self.assertEqual([2, 1], self.helper.polygon_table[1].traversal_post_order())
            self.assertEqual([3,], self.helper.polygon_table[3].traversal_post_order())

    def test_add_point(self):
        with patch.object(MapCommand, 'execute_add_point', wraps=self.cmd.execute_add_point) as fake_add:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_add.call_count)
            self.cmd.execute('add pt 1 0.0 0.5')
            self.assertEqual(1, fake_add.call_count)
            vertices = [[v.x, v.y] for v in self.helper.polygon_table[1].vertices]
            self.assertEqual([[1, 2], [0.0, 0.5]], vertices)
            self.assertRaises(CommandNotValidException, self.cmd.execute, 'add pt 2 0.0 0.5')

    def test_delete_shape(self):
        with patch.object(MapCommand, 'execute_remove_polygon', wraps=self.cmd.execute_remove_polygon) as fake_rm:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_rm.call_count)
            self.cmd.execute('del shape 1')
            self.assertEqual(1, fake_rm.call_count)
            self.assertEqual(0, len(self.helper.polygon_table))
            self.assertRaises(CommandNotValidException, self.cmd.execute, 'del shape 2')

    def test_move_point(self):
        with patch.object(MapCommand, 'execute_move_point', wraps=self.cmd.execute_move_point) as fake_move:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_move.call_count)
            self.cmd.execute('mov pt 1 0 1.0 1.5')
            self.assertEqual(1, fake_move.call_count)
            vertices = [[v.x, v.y] for v in self.helper.polygon_table[1].vertices]
            self.assertEqual([[2.0, 3.5], ], vertices)
            self.assertRaises(CommandNotValidException, self.cmd.execute, 'mov pt 2 4 1.0 1.5')

    def test_move_shape(self):
        with patch.object(MapCommand, 'execute_move_polygon', wraps=self.cmd.execute_move_polygon) as fake_move:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_move.call_count)
            self.cmd.execute('mov shape 1 1.0 1.5')
            self.assertEqual(1, fake_move.call_count)
            vertices = [[v.x, v.y] for v in self.helper.polygon_table[1].vertices]
            self.assertEqual([[2.0, 3.5], ], vertices)
            self.assertRaises(CommandNotValidException, self.cmd.execute, 'mov shape 2 1.0 1.5')

    def test_set_point(self):
        with patch.object(MapCommand, 'execute_set_point', wraps=self.cmd.execute_set_point) as fake_set:
            self.cmd.command_tree = self.cmd.init_command_tree()
            self.assertEqual(0, fake_set.call_count)
            self.cmd.execute('set pt 1 0 1.0 1.5')
            self.assertEqual(1, fake_set.call_count)
            vertices = [[v.x, v.y] for v in self.helper.polygon_table[1].vertices]
            self.assertEqual([[1.0, 1.5], ], vertices)
            self.assertRaises(CommandNotValidException, self.cmd.execute, 'set pt 2 0 1.0 1.5')
コード例 #2
0
ファイル: main_window.py プロジェクト: bssthu/L5MapEditor
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__()
        config_loader.load_all()
        # ui
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.graphics_view.setScene(QGraphicsScene())
        self.view = self.ui.graphics_view
        self.scene = self.ui.graphics_view.scene()
        self.ui.polygon_table_widget.setColumnCount(2)
        self.ui.polygon_table_widget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.ui.polygon_table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.ui.polygon_table_widget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.ui.second_table_widget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.ui.second_table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.ui.second_table_widget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.ui.insert_layer_combo_box.addItems(config_loader.get_layer_names())
        self.ui.graphics_view.scale(1, -1)  # invert y
        # data
        self.db = DbHelper()
        self.command_handler = MapCommand(self.db)
        self.path = None
        # fsm
        self.__init_fsm()
        # other signals/slots
        self.command_handler.gotoPolygon.connect(self.goto_polygon)
        self.ui.polygon_table_widget.itemSelectionChanged.connect(self.polygon_selection_changed)
        self.ui.polygon_table_widget.itemClicked.connect(self.polygon_selection_clicked)
        self.ui.polygon_table_widget.polygonActivated.connect(self.goto_polygon)
        self.ui.second_table_widget.itemSelectionChanged.connect(self.second_selection_changed)
        self.ui.second_table_widget.polygonActivated.connect(self.goto_polygon)
        self.ui.scale_slider.valueChanged.connect(self.scale_slider_changed)
        self.ui.graphics_view.polygonCreated.connect(self.add_polygon)
        self.ui.graphics_view.polygonUpdated.connect(self.update_polygon)
        self.ui.graphics_view.pointsUpdated.connect(self.update_points)
        log.logger.onLog.connect(self.print_to_output)
        # open default database
        self.setAcceptDrops(True)
        self.open("default.sqlite", True)

    def __init_fsm(self):
        """初始化 UI 状态机"""
        self.fsm_mgr = FsmMgr()
        self.fsm_mgr.changeState.connect(self.change_state)
        self.fsm_mgr.get_fsm("insert").enterState.connect(self.ui.graphics_view.begin_insert)
        self.fsm_mgr.get_fsm("insert").exitState.connect(self.ui.graphics_view.end_insert)
        self.fsm_mgr.get_fsm("normal").transferToState.connect(
            lambda name: self.ui.graphics_view.begin_move() if (name == "move") else None
        )
        self.fsm_mgr.get_fsm("normal").transferToState.connect(
            lambda name: self.ui.graphics_view.begin_move() if (name == "move_point") else None
        )
        self.fsm_mgr.get_fsm("move").transferToState.connect(
            lambda name: self.ui.graphics_view.end_move() if (name == "normal") else None
        )
        self.fsm_mgr.get_fsm("move_point").transferToState.connect(
            lambda name: self.ui.graphics_view.end_move() if (name == "normal") else None
        )
        self.change_state(self.fsm_mgr.get_current_state())

    # slots
    @pyqtSlot(QObject)
    def change_state(self, new_state):
        """UI 状态转移"""
        if new_state == self.fsm_mgr.get_fsm("empty"):
            self.ui.save_action.setEnabled(False)
            self.ui.undo_action.setEnabled(False)
            self.ui.redo_action.setEnabled(False)
            self.ui.insert_action.setEnabled(False)
            self.ui.delete_action.setEnabled(False)
            self.ui.move_action.setEnabled(False)
            self.ui.graphics_view.setCursor(QCursor(Qt.ForbiddenCursor))
            self.ui.polygon_table_widget.setEnabled(False)
            self.ui.list2_type_label.setText("")
        if new_state == self.fsm_mgr.get_fsm("normal"):
            self.ui.save_action.setEnabled(True)
            self.ui.undo_action.setEnabled(True)
            self.ui.redo_action.setEnabled(True)
            self.ui.insert_action.setEnabled(True)
            self.ui.delete_action.setEnabled(True)
            self.ui.move_action.setEnabled(True)
            self.ui.graphics_view.setCursor(QCursor(Qt.ArrowCursor))
            self.ui.polygon_table_widget.setEnabled(True)
            self.ui.list2_type_label.setText("children")
        elif new_state == self.fsm_mgr.get_fsm("insert"):
            self.ui.undo_action.setEnabled(False)
            self.ui.redo_action.setEnabled(False)
            self.ui.insert_action.setEnabled(True)
            self.ui.delete_action.setEnabled(False)
            self.ui.move_action.setEnabled(False)
            self.ui.graphics_view.setCursor(QCursor(Qt.CrossCursor))
            self.ui.polygon_table_widget.setEnabled(True)
            self.ui.list2_type_label.setText("children")
        elif new_state == self.fsm_mgr.get_fsm("move"):
            self.ui.undo_action.setEnabled(False)
            self.ui.redo_action.setEnabled(False)
            self.ui.insert_action.setEnabled(False)
            self.ui.delete_action.setEnabled(False)
            self.ui.move_action.setEnabled(True)
            self.ui.graphics_view.setCursor(QCursor(Qt.DragMoveCursor))
            self.ui.polygon_table_widget.setEnabled(False)
            self.ui.list2_type_label.setText("points")
        elif new_state == self.fsm_mgr.get_fsm("move_point"):
            self.ui.undo_action.setEnabled(False)
            self.ui.redo_action.setEnabled(False)
            self.ui.insert_action.setEnabled(False)
            self.ui.delete_action.setEnabled(False)
            self.ui.move_action.setEnabled(True)
            self.ui.graphics_view.setCursor(QCursor(Qt.DragMoveCursor))
            self.ui.polygon_table_widget.setEnabled(False)
            self.ui.list2_type_label.setText("points")

    @pyqtSlot()
    def on_open_action_triggered(self):
        """点击“打开”按钮"""
        path = QFileDialog.getOpenFileName(self, "载入数据", ".", "数据库文档(*.sqlite)")[0]
        if path:
            self.open(path)

    @pyqtSlot()
    def on_save_action_triggered(self):
        """点击“保存”按钮"""
        if self.path is not None:
            self.save(self.path)

    @pyqtSlot()
    def on_undo_action_triggered(self):
        """点击“撤销”按钮"""
        try:
            self.command_handler.undo()
            self.update_polygon_list()
        except Exception as e:
            log.error("撤销操作出错: %s" % repr(e))
            return False
        else:
            return True

    @pyqtSlot()
    def on_redo_action_triggered(self):
        """点击“重做”按钮"""
        try:
            self.command_handler.redo()
            self.update_polygon_list()
        except Exception as e:
            log.error("重做操作出错: %s" % repr(e))
            return False
        else:
            return True

    @pyqtSlot()
    def on_insert_action_triggered(self):
        """点击“插入”按钮"""
        if self.ui.insert_action.isChecked():
            if not self.fsm_mgr.change_fsm("normal", "insert"):
                self.ui.insert_action.setChecked(False)
        else:
            if not self.fsm_mgr.change_fsm("insert", "normal"):
                self.ui.insert_action.setChecked(True)

    @pyqtSlot()
    def on_delete_action_triggered(self):
        """点击“删除”按钮"""
        _id = self.selected_id()
        if _id >= 0:
            row = self.ui.polygon_table_widget.currentRow()
            self.execute("del shape %d" % _id)
            self.ui.polygon_table_widget.setCurrentCell(row, 0)

    @pyqtSlot()
    def on_about_action_triggered(self):
        """点击“关于”按钮"""
        info = "L5MapEditor by bssthu\n\n" "https://github.com/bssthu/L5MapEditor"
        QMessageBox.about(self, "关于", info)

    @pyqtSlot()
    def on_exit_action_triggered(self):
        """点击“退出”按钮"""
        exit()

    @pyqtSlot()
    def on_move_action_triggered(self):
        """点击“移动”按钮"""
        state_name = "move" if not self.ui.move_point_action.isChecked() else "move_point"
        if self.ui.move_action.isChecked():
            if not self.fsm_mgr.change_fsm("normal", state_name):
                self.ui.move_action.setChecked(False)
        else:
            if not self.fsm_mgr.change_fsm(state_name, "normal"):
                self.ui.move_action.setChecked(True)

    @pyqtSlot()
    def on_move_point_action_triggered(self):
        """点击“拾取点”按钮"""
        self.ui.graphics_view.move_point(self.ui.move_point_action.isChecked())
        if self.ui.move_point_action.isChecked():
            self.fsm_mgr.change_fsm("move", "move_point")
        else:
            self.fsm_mgr.change_fsm("move_point", "move")

    @pyqtSlot()
    def on_closed_polygon_action_triggered(self):
        """点击“绘制封闭多边形”按钮"""
        self.ui.graphics_view.draw_closed_polygon(self.ui.closed_polygon_action.isChecked())

    @pyqtSlot()
    def on_highlight_action_triggered(self):
        """点击“突出显示图形”按钮"""
        self.ui.graphics_view.highlight_selection(self.ui.highlight_action.isChecked())

    @pyqtSlot()
    def on_grid_action_triggered(self):
        """点击“显示网格”按钮"""
        pass

    @pyqtSlot()
    def on_mark_points_action_triggered(self):
        """点击“标出点”按钮"""
        self.ui.graphics_view.mark_points(self.ui.mark_points_action.isChecked())

    @pyqtSlot()
    def on_command_edit_returnPressed(self):
        """输入命令后按下回车"""
        commands = self.ui.command_edit.text().strip()
        if commands != "":
            # 执行命令
            self.execute(commands)
        self.ui.command_edit.setText("")

    def lock_ui(self):
        """锁定 UI"""
        self.ui.tool_bar.setEnabled(False)

    @pyqtSlot()
    def unlock_ui(self):
        """解锁 UI"""
        self.ui.tool_bar.setEnabled(True)
        self.ui.graphics_view.scene().update()

    @pyqtSlot(list)
    def update_child_list(self, polygon_table):
        """更新 children 列表

        Args:
            polygon_table: 多边形表
        """
        self.ui.second_table_widget.fill_with_polygons(polygon_table)

    @pyqtSlot(QTableWidgetItem)
    def polygon_selection_clicked(self, item):
        self.polygon_selection_changed()

    @pyqtSlot()
    def polygon_selection_changed(self):
        """在多边形列表中选择了多边形"""
        _id = self.selected_id()
        if _id >= 0:
            # draw polygon
            polygon = self.db.get_polygon_by_id(_id)
            # list children
            child_list = self.db.get_children_table_by_id(_id)
        else:
            # 选中了非法的多边形
            polygon = None
            child_list = {}
        self.ui.graphics_view.set_selected_polygon(polygon)
        self.update_child_list(child_list)
        return

    @pyqtSlot()
    def second_selection_changed(self):
        """在第二列选中"""
        if self.ui.move_action.isChecked():
            self.ui.graphics_view.select_point(self.ui.second_table_widget.currentRow())
        if self.ui.second_table_widget.is_polygon:
            _id = self.ui.second_table_widget.get_selected_id()
            polygon = self.db.get_polygon_by_id(_id)
            if polygon is not None:
                self.ui.graphics_view.set_selected_polygon(polygon)

    @pyqtSlot()
    def scale_slider_changed(self):
        """修改地图缩放比例"""
        scale = math.exp(self.ui.scale_slider.value() / 10)
        self.ui.graphics_view.resetTransform()
        self.ui.graphics_view.scale(scale, -scale)

    @pyqtSlot(list)
    def add_polygon(self, vertices):
        """插入多边形

        Args:
            vertices (list[list[float]]): 多边形顶点 list, [[x1,y1], [x2,y2], ..., [xn,yn]]
        """
        parent_id = self.selected_id()
        _id = self.command_handler.get_spare_id(parent_id)
        layer = self.ui.insert_layer_combo_box.currentIndex()
        additional = 0
        if layer == 0:
            commands = ["add shape %d %d %s" % (_id, layer, str(additional))]
        else:
            commands = ["add shape %d %d %s %d" % (_id, layer, str(additional), parent_id)]
        for vertex in vertices:
            commands.append("add pt %d %f %f" % (_id, vertex[0], vertex[1]))
        self.execute(commands)

    @pyqtSlot("PyQt_PyObject")
    def goto_polygon(self, _id):
        """视角聚焦到多边形中心

        Args:
            _id (int): 目标多边形 id
        """
        self.ui.graphics_view.center_on_polygon(self.db.get_polygon_by_id(_id))

    @pyqtSlot(list)
    def update_polygon(self, vertices):
        """修改当前选中的多边形的顶点坐标

        Args:
            vertices (list[list[float]]): 多边形顶点 list, [[x1,y1], [x2,y2], ..., [xn,yn]]
        """
        _id = self.selected_id()
        commands = []
        for pt_id in range(0, len(vertices)):
            x = vertices[pt_id][0]
            y = vertices[pt_id][1]
            commands.append("set pt %d %d %f %f" % (_id, pt_id, x, y))
        self.execute(commands)

    @pyqtSlot(list)
    def update_points(self, points):
        """更新第二列显示的点

        编辑模式下,第二列显示当前图形的点的坐标。本方法用于更新第二列的显示。

        Args:
            points (list[QPointF]): 多边形顶点 list, [qpoint1, qpoint2, ..., qpointn]
        """
        row = self.ui.second_table_widget.currentRow()
        self.ui.second_table_widget.fill_with_points(points)
        row_count = self.ui.second_table_widget.rowCount()
        if row_count > 0:
            row = min(row_count - 1, max(0, row))
            self.ui.second_table_widget.setCurrentCell(row, 0)

    @pyqtSlot(str, QColor)
    def print_to_output(self, msg, color):
        """打印到输出窗口

        Args:
            msg (str): 输出内容
            color (QColor): 输出颜色
        """
        print(msg)
        self.ui.output_browser.setTextColor(color)
        self.ui.output_browser.append(msg)

    def execute(self, commands):
        """执行命令

        Args:
            commands (str): 待执行命令
        """
        log.debug(commands)
        try:
            self.command_handler.execute(commands)
            self.update_polygon_list()
        except Exception as e:
            log.error("执行命令出错: %s" % repr(e))
            return False
        else:
            return True

    def update_polygon_list(self):
        """更新多边形列表"""
        polygon_table = self.db.polygon_table
        _id = self.selected_id()
        self.ui.polygon_table_widget.fill_with_polygons(polygon_table)
        self.ui.graphics_view.set_polygons(polygon_table, len(config_loader.get_layer_names()))
        if len(polygon_table) > 0:
            if not self.select_row_by_id(_id):
                self.ui.polygon_table_widget.setCurrentCell(0, 0)

    def open(self, path, quiet=False):
        """打开 sqlite 数据库文件

        Args:
            path (str): 文件路径
            quiet (bool): 报错不弹框
        """
        if os.path.exists(path):
            try:
                self.db.load_tables(path)
                self.command_handler.reset_backup_data()
                self.update_polygon_list()
                self.path = path
                self.fsm_mgr.change_fsm("empty", "normal")
                log.debug('Open "%s".' % path)
            except sqlite3.Error as error:
                log.error('Failed to open "%s".' % path)
                log.error(repr(error))
                if not quiet:
                    self.show_message(repr(error))
        else:
            log.error("File %s not exists." % path)
            if not quiet:
                self.show_message("File %s not exists." % path)

    def save(self, path):
        """保存 sqlite 数据库文件

        Args:
            path (str): 文件路径
        """
        try:
            self.db.write_to_file(path)
            self.command_handler.reset_backup_data()
            log.debug('Save "%s".' % path)
        except sqlite3.Error as error:
            log.error('Failed to save "%s".' % path)
            log.error(repr(error))
            self.show_message(repr(error))

    def selected_id(self):
        """选中的多边形的 id"""
        return self.ui.polygon_table_widget.get_selected_id()

    def select_row_by_id(self, polygon_id):
        """根据多边形的 id 选中行

        Args:
            polygon_id (int): 多边形 id
        """
        return self.ui.polygon_table_widget.select_id(polygon_id)

    def show_message(self, msg, title="L5MapEditor"):
        QMessageBox.information(self, title, msg)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        url = event.mimeData().urls()[0]
        if url.isLocalFile():
            path = url.toLocalFile()
            if os.path.isfile(path):
                self.open(path)