예제 #1
0
    def __init__(
        self,
        collections: Mapping[str, Any],
        get_collection: Callable[[], Mapping[str, Any]],
        project_no_save: Callable[[], None],
        show_ticks: int,
        monochrome: bool,
        parent: QWidget
    ):
        """We put the 'collections' (from iteration widget) reference here."""
        super(CollectionsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint
                            & ~Qt.WindowContextHelpButtonHint)
        self.collections = dict(collections)
        self.get_collection = get_collection
        self.project_no_save = project_no_save

        # Current profile name
        self.name = ""
        self.params = {}
        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        self.preview_canvas.set_show_ticks(show_ticks)
        self.preview_canvas.set_monochrome_mode(monochrome)
        self.common_list.addItems(all_collections())
        self.collections_list.addItems(self.collections)

        # Splitter
        self.main_splitter.setSizes([200, 200])
        self.sub_splitter.setSizes([100, 200])

        self.__has_collection()
        self.__can_open()
예제 #2
0
    def __init__(self, parent: MainWindowBase) -> None:
        """Reference names:

        + Iteration collections.
        + Result data.
        + Main window function references.
        """
        super(DimensionalSynthesis, self).__init__(parent)
        self.setupUi(self)
        self.mech: Dict[str, Any] = {}
        self.path: Dict[int, List[_Coord]] = {}
        # Some reference of 'collections'
        self.collections = parent.collections.configure_widget.collections
        self.get_collection = parent.get_configure
        self.input_from = parent.input_from
        self.output_to = parent.output_to
        self.save_reply_box = parent.save_reply_box
        self.project_no_save = parent.project_no_save
        self.merge_result = parent.merge_result
        self.update_ranges = parent.main_canvas.update_ranges
        self.set_solving_path = parent.main_canvas.set_solving_path
        self.get_zoom = parent.main_canvas.get_zoom
        self.prefer = parent.prefer
        # Data and functions
        self.mechanism_data: List[Dict[str, Any]] = []
        self.alg_options: Dict[str, Union[int, float]] = {}
        self.alg_options.update(DEFAULT_PARAMS)
        self.alg_options.update(PARAMS[AlgorithmType.DE])
        # Canvas
        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        # Splitter
        self.main_splitter.setStretchFactor(0, 10)
        self.main_splitter.setStretchFactor(1, 100)
        self.up_splitter.setSizes([80, 100])
        self.down_splitter.setSizes([20, 80])
        # Table widget column width
        header = self.parameter_list.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.algorithm_options: Dict[AlgorithmType, QRadioButton] = {}
        for option in PARAMS:
            button = QRadioButton(option.value, self)
            button.clicked.connect(self.__set_algorithm_default)
            self.algorithm_options[option] = button
            self.algorithm_layout.addWidget(button)
        self.clear()
예제 #3
0
    def __init__(self, parent: MainWindowBase):
        """Reference names:

        + Iteration collections.
        + Result data.
        + Main window function references.
        """
        super(Optimizer, self).__init__(parent)
        self.setupUi(self)
        self.mech = {}
        self.path = {}
        # Some reference of 'collections'
        self.collections = parent.collections.configure_widget.collections
        self.get_collection = parent.get_configure
        self.input_from = parent.input_from
        self.output_to = parent.output_to
        self.save_reply_box = parent.save_reply_box
        self.project_no_save = parent.project_no_save
        self.merge_result = parent.merge_result
        self.update_ranges = parent.main_canvas.update_ranges
        self.set_solving_path = parent.main_canvas.set_solving_path
        self.get_zoom = parent.main_canvas.get_zoom
        self.prefer = parent.prefer
        # Data and functions
        self.mechanism_data = []
        self.alg_options = default(AlgorithmType.DE)
        # Canvas
        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        # Splitter
        self.main_splitter.setStretchFactor(0, 10)
        self.main_splitter.setStretchFactor(1, 100)
        self.canvas_splitter.setSizes([80, 100])
        self.down_splitter.setSizes([20, 80])
        # Table widget column width
        header = self.parameter_list.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.algorithm_options = {}
        for opt in AlgorithmType:
            button = QRadioButton(opt.value, self)
            button.clicked.connect(self.__set_algorithm_default)
            self.algorithm_options[opt] = button
            self.algorithm_layout.addWidget(button)
        self.clear()
 def __from_profile(self) -> None:
     """Show up the dialog to load structure data."""
     dlg = CollectionsDialog(
         self.collections,
         self.get_configure,
         self.project_no_save,
         self.prefer.tick_mark_option,
         self.configure_canvas.monochrome,
         self
     )
     dlg.show()
     if not dlg.exec_():
         dlg.deleteLater()
         return
     # Add customize joints
     params = dlg.params
     graph = Graph(params['graph'])
     expression: str = params['expression']
     pos_list = parse_pos(expression)
     cus: Dict[int, int] = params['cus']
     same: Dict[int, int] = params['same']
     for node, ref in sorted(same.items()):
         pos_list.insert(node, pos_list[ref])
     if not self.set_graph(graph, {i: (x, y) for i, (x, y) in enumerate(pos_list)}):
         dlg.deleteLater()
         return
     self.profile_name.setText(dlg.name)
     dlg.deleteLater()
     del dlg
     self.configure_canvas.cus = cus
     self.configure_canvas.same = same
     # Grounded setting
     for row in PreviewCanvas.grounded_detect(set(params['placement']), graph, same):
         self.__set_grounded(row)
     # Driver, Target
     input_list: List[Tuple[Tuple[int, int], Tuple[float, float]]] = params['input']
     self.driver_list.addItems(f"(P{b}, P{d})" for (b, d), _ in input_list)
     self.configure_canvas.set_driver([d for d, _ in input_list])
     _set_warning(self.driver_label, self.driver_list.count() == 0)
     target_list: Dict[int, Sequence[_Coord]] = params['target']
     self.configure_canvas.set_target(sorted(target_list))
     self.target_list.addItems(f"P{n}" for n in target_list)
     _set_warning(self.target_label, self.target_list.count() == 0)
     # Expression
     self.expr_show.setText(params['expression'])
예제 #5
0
class DimensionalSynthesis(QWidget, Ui_Form):
    """Dimensional synthesis widget.

    User can run the dimensional synthesis here.
    """

    def __init__(self, parent: MainWindowBase) -> None:
        """Reference names:

        + Iteration collections.
        + Result data.
        + Main window function references.
        """
        super(DimensionalSynthesis, self).__init__(parent)
        self.setupUi(self)
        self.mech: Dict[str, Any] = {}
        self.path: Dict[int, List[_Coord]] = {}
        # Some reference of 'collections'
        self.collections = parent.collections.configure_widget.collections
        self.get_collection = parent.get_configure
        self.input_from = parent.input_from
        self.output_to = parent.output_to
        self.save_reply_box = parent.save_reply_box
        self.project_no_save = parent.project_no_save
        self.merge_result = parent.merge_result
        self.update_ranges = parent.main_canvas.update_ranges
        self.set_solving_path = parent.main_canvas.set_solving_path
        self.get_zoom = parent.main_canvas.get_zoom
        self.prefer = parent.prefer
        # Data and functions
        self.mechanism_data: List[Dict[str, Any]] = []
        self.alg_options: Dict[str, Union[int, float]] = {}
        self.alg_options.update(DEFAULT_PARAMS)
        self.alg_options.update(PARAMS[AlgorithmType.DE])
        # Canvas
        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        # Splitter
        self.main_splitter.setStretchFactor(0, 10)
        self.main_splitter.setStretchFactor(1, 100)
        self.up_splitter.setSizes([80, 100])
        self.down_splitter.setSizes([20, 80])
        # Table widget column width
        header = self.parameter_list.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.algorithm_options: Dict[AlgorithmType, QRadioButton] = {}
        for option in PARAMS:
            button = QRadioButton(option.value, self)
            button.clicked.connect(self.__set_algorithm_default)
            self.algorithm_options[option] = button
            self.algorithm_layout.addWidget(button)
        self.clear()

    def clear(self) -> None:
        """Clear all sub-widgets."""
        self.mechanism_data.clear()
        self.result_list.clear()
        self.__clear_settings()
        self.__has_result()

    def __clear_settings(self) -> None:
        """Clear sub-widgets that contain the setting."""
        self.clear_path(ask=False)
        self.path.clear()
        self.mech.clear()
        self.preview_canvas.clear()
        self.profile_name.clear()
        self.algorithm_options[AlgorithmType.DE].setChecked(True)
        self.__set_algorithm_default()
        self.parameter_list.setRowCount(0)
        self.target_points.clear()
        self.target_label.setVisible(self.has_target())
        self.expression_string.clear()
        self.update_range()
        self.__able_to_generate()

    def has_target(self) -> bool:
        """Return True if the panel is no target settings."""
        return self.target_points.count() > 0

    @Slot(name='on_clear_button_clicked')
    def __user_clear(self) -> None:
        if not self.profile_name.text():
            return
        if QMessageBox.question(
            self,
            "Clear setting",
            "Do you want to clear the setting?"
        ) == QMessageBox.Yes:
            self.__clear_settings()

    def load_results(self, mechanism_data: Sequence[Dict[str, Any]]) -> None:
        """Append results of project database to memory."""
        for e in mechanism_data:
            self.mechanism_data.append(e)
            self.__add_result(e)

    def __current_path_changed(self) -> None:
        """Call the canvas to update to current target path."""
        self.set_solving_path({n: tuple(p) for n, p in self.path.items()})
        self.__able_to_generate()

    def current_path(self) -> List[_Coord]:
        """Return the pointer of current target path."""
        item = self.target_points.currentItem()
        if item is None:
            return []
        return self.path[int(item.text().replace('P', ''))]

    @Slot(str, name='on_target_points_currentTextChanged')
    def __set_target(self, _=None) -> None:
        """Switch to the current target path."""
        self.path_list.clear()
        for x, y in self.current_path():
            self.path_list.addItem(f"({x:.04f}, {y:.04f})")
        self.__current_path_changed()

    @Slot(name='on_path_clear_clicked')
    def clear_path(self, *, ask: bool = True) -> None:
        """Clear the current target path."""
        if ask:
            if QMessageBox.question(
                self,
                "Clear path",
                "Are you sure to clear the current path?"
            ) != QMessageBox.Yes:
                return
        self.current_path().clear()
        self.path_list.clear()
        self.__current_path_changed()

    @Slot(name='on_path_copy_clicked')
    def __copy_path(self) -> None:
        """Copy the current path coordinates to clipboard."""
        QApplication.clipboard().setText('\n'.join(
            f"[{x}, {y}]," for x, y in self.current_path()
        ))

    @Slot(name='on_path_paste_clicked')
    def __paste_path(self) -> None:
        """Paste path data from clipboard."""
        self.__read_path_from_csv(QApplication.clipboard().text())

    @Slot(name='on_import_csv_button_clicked')
    def __import_csv(self) -> None:
        """Paste path data from a text file."""
        file_name = self.input_from(
            "path data",
            ["Text file (*.txt)", "Comma-Separated Values (*.csv)"]
        )
        if not file_name:
            return
        with open(file_name, 'r', encoding='utf-8', newline='') as f:
            data = f.read()
        self.__read_path_from_csv(data)

    def __read_path_from_csv(self, raw: str) -> None:
        """Turn string to float then add them to current target path."""
        try:
            path = parse_path(raw)
        except LarkError as e:
            QMessageBox.warning(self, "Wrong format", f"{e}")
        else:
            self.set_path(path)

    @Slot(name='on_save_path_button_clicked')
    def __save_path(self):
        """Save current path."""
        path = self.current_path()
        if not path:
            return
        file_name = self.output_to("Path file", ["Text file (*.txt)"])
        if not file_name:
            return
        with open(file_name, 'w+', encoding='utf-8') as f:
            f.write("\n".join(f"{x}, {y}" for x, y in path))
        self.save_reply_box("Path file", file_name)

    @Slot(name='on_import_xlsx_button_clicked')
    def __import_xlsx(self) -> None:
        """Paste path data from a Excel file."""
        file_name = self.input_from(
            "Excel project",
            ["Microsoft Office Excel (*.xlsx *.xlsm *.xltx *.xltm)"]
        )
        if not file_name:
            return
        wb = load_workbook(file_name)
        sheets = wb.get_sheet_names()
        name, ok = QInputDialog.getItem(self, "Sheets", "Select a sheet:", sheets, 0)
        if not ok:
            return

        def get_path(sheet: str) -> Iterator[_Coord]:
            """Keep finding until there is no value"""
            ws = wb.get_sheet_by_name(sheets.index(sheet))
            i = 1
            while True:
                sx = ws.cell(i, 1).value
                sy = ws.cell(i, 2).value
                if None in {sx, sy}:
                    break
                try:
                    yield float(sx), float(sy)
                except Exception as e:
                    QMessageBox.warning(self, "File error", f"{e}")
                    return
                i += 1

        self.set_path(get_path(name))

    @Slot(name='on_edit_path_button_clicked')
    def __adjust_path(self) -> None:
        """Show up path adjust dialog and
        get back the changes of current target path.
        """
        dlg = EditPathDialog(self)
        dlg.show()
        dlg.exec_()
        dlg.deleteLater()
        self.__current_path_changed()

    @Slot(name='on_efd_button_clicked')
    def __efd_path(self) -> None:
        """Elliptical Fourier Descriptors."""
        path = self.current_path()
        n, ok = QInputDialog.getInt(
            self,
            "Elliptical Fourier Descriptors",
            "The number of points:",
            len(path), 3
        )
        if not ok:
            return
        dlg = QProgressDialog("Path transform.", "Cancel", 0, 1, self)
        dlg.setWindowTitle("Elliptical Fourier Descriptors")
        dlg.show()
        self.set_path(efd_fitting(path, n))
        dlg.setValue(1)
        dlg.deleteLater()

    @Slot(name='on_norm_path_button_clicked')
    def __norm_path(self) -> None:
        """Normalize current path."""
        scale, ok = QInputDialog.getDouble(
            self,
            "Scale",
            "Length of unit vector:",
            60, 0.01, 1000, 2)
        if ok:
            self.set_path(norm_path(self.current_path(), scale))

    def add_point(self, x: float, y: float) -> None:
        """Add path data to list widget and current target path."""
        self.current_path().append((x, y))
        self.path_list.addItem(f"({x:.04f}, {y:.04f})")
        self.path_list.setCurrentRow(self.path_list.count() - 1)
        self.__current_path_changed()

    def set_path(self, path: Iterable[_Coord]) -> None:
        """Set the current path."""
        self.clear_path(ask=False)
        for x, y in path:
            self.add_point(x, y)
        self.__current_path_changed()

    @Slot(float, float)
    def set_point(self, x: float, y: float) -> None:
        """Set the coordinate of current target path."""
        if not self.edit_target_point_button.isChecked():
            return
        for i, (cx, cy) in enumerate(self.current_path()):
            if hypot(x - cx, y - cy) < 10 / self.get_zoom():
                index = i
                self.path_list.setCurrentRow(index)
                break
        else:
            return
        self.current_path()[index] = (x, y)
        self.path_list.item(index).setText(f"({x:.04f}, {y:.04f})")
        self.__current_path_changed()

    @Slot(name='on_close_path_clicked')
    def __close_path(self) -> None:
        """Add a the last point same as first point."""
        path = self.current_path()
        if self.path_list.count() > 1 and path[0] != path[-1]:
            self.add_point(*path[0])

    @Slot(name='on_point_up_clicked')
    def __move_up_point(self) -> None:
        """Target point move up."""
        row = self.path_list.currentRow()
        if not (row > 0 and self.path_list.count() > 1):
            return
        path = self.current_path()
        path.insert(row - 1, (path[row][0], path[row][1]))
        path.pop(row + 1)
        c = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row - 1, f"({c[0]}, {c[1]})")
        self.path_list.takeItem(row + 1)
        self.path_list.setCurrentRow(row - 1)
        self.__current_path_changed()

    @Slot(name='on_point_down_clicked')
    def __move_down_point(self) -> None:
        """Target point move down."""
        row = self.path_list.currentRow()
        if not (
            row < self.path_list.count() - 1
            and self.path_list.count() > 1
        ):
            return
        path = self.current_path()
        path.insert(row + 2, (path[row][0], path[row][1]))
        path.pop(row)
        c = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row + 2, f"{c[0]}, {c[1]}")
        self.path_list.takeItem(row)
        self.path_list.setCurrentRow(row + 1)
        self.__current_path_changed()

    @Slot(name='on_point_delete_clicked')
    def __delete_point(self) -> None:
        """Delete a target point."""
        row = self.path_list.currentRow()
        if not row > -1:
            return
        self.current_path().pop(row)
        self.path_list.takeItem(row)
        self.__current_path_changed()

    def __able_to_generate(self) -> None:
        """Set button enable if all the data are already."""
        self.point_num.setText(
            "<p><span style=\"font-size:12pt;"
            f"color:#00aa00;\">{self.path_list.count()}</span></p>"
        )
        n = bool(
            self.mech
            and self.path_list.count() > 2
            and self.expression_string.text()
        )
        for button in (
            self.save_path_button,
            self.edit_path_button,
            self.efd_button,
            self.norm_path_button,
            self.synthesis_button,
        ):
            button.setEnabled(n)

    @Slot(name='on_synthesis_button_clicked')
    def __synthesis(self) -> None:
        """Start synthesis."""
        # Check if the amount of the target points are same.
        length = -1
        for path in self.path.values():
            if length < 0:
                length = len(path)
            if len(path) != length:
                QMessageBox.warning(
                    self,
                    "Target Error",
                    "The length of target paths should be the same."
                )
                return
        # Get the algorithm type
        for option, button in self.algorithm_options.items():
            if button.isChecked():
                algorithm = option
                break
        else:
            raise ValueError("no option")
        mech = deepcopy(self.mech)
        mech['shape_only'] = self.shape_only_option.isChecked()
        mech['wavelet_mode'] = self.wavelet_mode_option.isChecked()
        if mech['shape_only']:
            if QMessageBox.question(
                self,
                "Elliptical Fourier Descriptor",
                "An even distribution will make the comparison more accurate.\n"
                "Do you make sure yet?",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.Yes
            ) == QMessageBox.No:
                return
        mech['expression'] = parse_vpoints(mech.pop('expression', []))
        mech['target'] = deepcopy(self.path)

        def name_in_table(target_name: str) -> int:
            """Find a target_name and return the row from the table."""
            for r in range(self.parameter_list.rowCount()):
                if self.parameter_list.item(r, 0).text() == target_name:
                    return r
            return -1

        placement: Dict[int, Tuple[float, float, float]] = mech['placement']
        for name in placement:
            row = name_in_table(f"P{name}")
            placement[name] = (
                self.parameter_list.cellWidget(row, 2).value(),
                self.parameter_list.cellWidget(row, 3).value(),
                self.parameter_list.cellWidget(row, 4).value(),
            )
        # Start progress dialog
        dlg = ProgressDialog(algorithm, mech, self.alg_options, self)
        dlg.show()
        if not dlg.exec_():
            dlg.deleteLater()
            return
        mechanisms_plot: List[Dict[str, Any]] = []
        for data in dlg.mechanisms:
            mechanisms_plot.append({
                'time_fitness': data.pop('time_fitness'),
                'algorithm': data['algorithm'],
            })
            self.mechanism_data.append(data)
            self.__add_result(data)
        self.__set_time(dlg.time_spend)
        self.project_no_save()
        dlg.deleteLater()
        dlg = ChartDialog("Convergence Data", mechanisms_plot,
                          self.preview_canvas.monochrome, self)
        dlg.show()
        dlg.exec_()
        dlg.deleteLater()

    def __set_time(self, time: float) -> None:
        """Set the time label."""
        self.timeShow.setText(
            f"<html><head/><body><p><span style=\"font-size:16pt\">"
            f"{int(time // 60):02d}min {time % 60:05.02f}s"
            f"</span></p></body></html>"
        )

    def __add_result(self, result: Dict[str, Any]) -> None:
        """Add result items, except add to the list."""
        item = QListWidgetItem(result['algorithm'])
        interrupt = result['interrupted']
        if interrupt == 'False':
            interrupt_icon = "task_completed.png"
        elif interrupt == 'N/A':
            interrupt_icon = "question.png"
        else:
            interrupt_icon = "interrupted.png"
        item.setIcon(QIcon(QPixmap(f":/icons/{interrupt_icon}")))
        if interrupt == 'False':
            interrupt_text = "No interrupt."
        else:
            interrupt_text = f"Interrupt at: {interrupt}"
        text = f"{result['algorithm']} ({interrupt_text})"
        if interrupt == 'N/A':
            text += "\n※Completeness is unknown."
        item.setToolTip(text)
        self.result_list.addItem(item)

    @Slot(name='on_delete_button_clicked')
    def __delete_result(self) -> None:
        """Delete a result."""
        row = self.result_list.currentRow()
        if not row > -1:
            return
        if QMessageBox.question(
            self,
            "Delete",
            "Delete this result from list?"
        ) != QMessageBox.Yes:
            return
        self.mechanism_data.pop(row)
        self.result_list.takeItem(row)
        self.project_no_save()
        self.__has_result()

    @Slot(QModelIndex, name='on_result_list_clicked')
    def __has_result(self, *_) -> None:
        """Set enable if there has any result."""
        enable = self.result_list.currentRow() > -1
        for button in (
            self.merge_button,
            self.delete_button,
            self.result_load_settings,
            self.result_clipboard
        ):
            button.setEnabled(enable)

    @Slot(QModelIndex, name='on_result_list_doubleClicked')
    def __show_result(self, _=None) -> None:
        """Double click result item can show up preview dialog."""
        row = self.result_list.currentRow()
        if not row > -1:
            return
        dlg = PreviewDialog(self.mechanism_data[row], self.__get_path(row),
                            self.preview_canvas.monochrome, self)
        dlg.show()
        dlg.exec_()
        dlg.deleteLater()

    @Slot(name='on_merge_button_clicked')
    def __merge_result(self) -> None:
        """Merge mechanism into main canvas."""
        row = self.result_list.currentRow()
        if not row > -1:
            return
        if QMessageBox.question(
            self,
            "Merge",
            "Add the result expression into storage?"
        ) == QMessageBox.Yes:
            expression: str = self.mechanism_data[row]['expression']
            self.merge_result(expression, self.__get_path(row))

    def __get_path(self, row: int) -> List[List[_Coord]]:
        """Using result data to generate paths of mechanism."""
        result = self.mechanism_data[row]
        expression: str = result['expression']
        same: Dict[int, int] = result['same']
        inputs: List[Tuple[_Pair, _Coord]] = result['input']
        input_list = []
        for (b, d), _ in inputs:
            for i in range(b):
                if i in same:
                    b -= 1
            for i in range(d):
                if i in same:
                    d -= 1
            input_list.append((b, d))
        vpoints = parse_vpoints(expression)
        expr = t_config(vpoints, input_list)
        b, d = input_list[0]
        base_angle = vpoints[b].slope_angle(vpoints[d])
        path: List[List[_Coord]] = [[] for _ in range(len(vpoints))]
        for angle in range(360 + 1):
            try:
                result_list = expr_solving(
                    expr,
                    {i: f"P{i}" for i in range(len(vpoints))},
                    vpoints,
                    [base_angle + angle] + [0] * (len(input_list) - 1)
                )
            except ValueError:
                nan = float('nan')
                for i in range(len(vpoints)):
                    path[i].append((nan, nan))
            else:
                for i in range(len(vpoints)):
                    coord = result_list[i]
                    if type(coord[0]) is tuple:
                        path[i].append(cast(_Coord, coord[1]))
                    else:
                        path[i].append(cast(_Coord, coord))
        return path

    @Slot(name='on_result_clipboard_clicked')
    def __copy_result_text(self) -> None:
        """Copy pretty print result as text."""
        QApplication.clipboard().setText(
            pprint.pformat(self.mechanism_data[self.result_list.currentRow()])
        )

    @Slot(name='on_save_profile_clicked')
    def __save_profile(self) -> None:
        """Save as new profile to collection widget."""
        if not self.mech:
            return
        name, ok = QInputDialog.getText(
            self,
            "Profile name",
            "Please enter the profile name:"
        )
        if not ok:
            return
        i = 0
        while (not name) and (name not in self.collections):
            name = f"Structure_{i}"
            i += 1
        mech = deepcopy(self.mech)
        for key in ('placement', 'target'):
            for mp in mech[key]:
                mech[key][mp] = None
        self.collections[name] = mech
        self.project_no_save()

    @Slot(name='on_load_profile_clicked')
    def __load_profile(self) -> None:
        """Load profile from collections dialog."""
        dlg = CollectionsDialog(
            self.collections,
            self.get_collection,
            self.project_no_save,
            self.prefer.tick_mark_option,
            self.preview_canvas.monochrome,
            self
        )
        dlg.show()
        if not dlg.exec_():
            dlg.deleteLater()
            return
        params = dlg.params
        name = dlg.name
        dlg.deleteLater()
        del dlg
        # Check the profile
        if not (params['target'] and params['input'] and params['placement']):
            QMessageBox.warning(
                self,
                "Invalid profile",
                "The profile is not configured yet."
            )
            return
        self.__set_profile(name, params)

    def __set_profile(self, profile_name: str, params: Dict[str, Any]) -> None:
        """Set profile to sub-widgets."""
        self.__clear_settings()
        self.mech = deepcopy(params)
        expression: str = self.mech['expression']
        self.expression_string.setText(expression)
        target: Dict[int, List[_Coord]] = self.mech['target']
        for p in sorted(target):
            self.target_points.addItem(f"P{p}")
            if target[p]:
                self.path[p] = target[p].copy()
            else:
                self.path[p] = []
        if self.has_target():
            self.target_points.setCurrentRow(0)
        self.target_label.setVisible(self.has_target())
        inputs: List[Tuple[_Pair, List[float]]] = self.mech.get('input', [])
        self.mech['input'] = inputs
        placement: Dict[int, Optional[_Range]] = self.mech.get('placement', {})
        self.mech['placement'] = placement
        # Table settings
        self.parameter_list.setRowCount(0)
        self.parameter_list.setRowCount(len(inputs) + len(placement) + 1)
        row = 0

        def spinbox(
            v: float = 0.,
            *,
            minimum: float = 0.,
            maximum: float = 9999.,
            prefix: bool = False
        ) -> QDoubleSpinBox:
            double_spinbox = QDoubleSpinBox()
            double_spinbox.setMinimum(minimum)
            double_spinbox.setMaximum(maximum)
            double_spinbox.setValue(v)
            if prefix:
                double_spinbox.setPrefix("±")
            return double_spinbox

        def set_angle(index1: int, index2: int) -> Callable[[float], None]:
            """Return a slot function use to set angle value."""
            @Slot(float)
            def func(value: float) -> None:
                inputs[index1][1][index2] = value
            return func

        # Angles
        for i, ((b, d), se) in enumerate(inputs):
            self.parameter_list.setItem(row, 0, QTableWidgetItem(f"P{b}->P{d}"))
            self.parameter_list.setItem(row, 1, QTableWidgetItem('input'))
            s1 = spinbox(se[0], maximum=360.)
            s2 = spinbox(se[1], maximum=360.)
            self.parameter_list.setCellWidget(row, 2, s1)
            self.parameter_list.setCellWidget(row, 3, s2)
            s1.valueChanged.connect(set_angle(i, 0))
            s2.valueChanged.connect(set_angle(i, 1))
            row += 1

        # Grounded joints
        self.preview_canvas.from_profile(self.mech)
        pos_list = parse_pos(expression)
        for node, ref in sorted(self.preview_canvas.same.items()):
            pos_list.insert(node, pos_list[ref])
        for p in sorted(placement):
            coord = placement[p]
            self.parameter_list.setItem(row, 0, QTableWidgetItem(f"P{p}"))
            self.parameter_list.setItem(row, 1, QTableWidgetItem('placement'))
            x, y = self.preview_canvas.pos[p]
            for i, s in enumerate([
                spinbox(coord[0] if coord else x, minimum=-9999.),
                spinbox(coord[1] if coord else y, minimum=-9999.),
                spinbox(coord[2] if coord else 5., prefix=True),
            ]):
                s.valueChanged.connect(self.update_range)
                self.parameter_list.setCellWidget(row, i + 2, s)
            row += 1
        # Default value of upper and lower
        self.mech['upper'] = self.mech.get('upper', 100)
        self.mech['lower'] = self.mech.get('lower', 0)

        def set_link(opt: str) -> Callable[[float], None]:
            """Set link length."""
            @Slot(float)
            def func(value: float) -> None:
                self.mech[opt] = value
            return func

        self.parameter_list.setItem(row, 0, QTableWidgetItem("L"))
        self.parameter_list.setItem(row, 1, QTableWidgetItem('link'))
        for i, (s, tag) in enumerate([(spinbox(), 'upper'), (spinbox(), 'lower')]):
            s.setValue(self.mech[tag])
            s.valueChanged.connect(set_link(tag))
            self.parameter_list.setCellWidget(row, i + 2, s)
        # Update previews
        self.update_range()
        self.profile_name.setText(profile_name)
        # Default value of algorithm option
        if 'settings' in self.mech:
            self.alg_options.update(self.mech['settings'])
        else:
            self.__set_algorithm_default()
        self.__able_to_generate()

    @Slot(name='on_result_load_settings_clicked')
    def __load_result_settings(self) -> None:
        """Load settings from a result."""
        self.__has_result()
        row = self.result_list.currentRow()
        if not row > -1:
            return
        self.__clear_settings()
        result = self.mechanism_data[row]
        for option, button in self.algorithm_options.items():
            if result.get('algorithm', "") == option.value:
                button.setChecked(True)
                break
        else:
            raise ValueError("no option")
        # Copy to mechanism params
        self.__set_profile("External setting", result)
        self.__set_time(result.get('time', 0))
        # Load settings
        self.alg_options.clear()
        self.alg_options.update(result.get('settings', {}))
        self.shape_only_option.setChecked(result.get('shape_only', False))
        self.wavelet_mode_option.setChecked(result.get('wavelet_mode', False))

    @Slot()
    def __set_algorithm_default(self) -> None:
        """Set the algorithm settings to default."""
        self.alg_options.clear()
        self.alg_options.update(DEFAULT_PARAMS)
        for option, button in self.algorithm_options.items():
            if button.isChecked():
                self.alg_options.update(PARAMS[option])

    @Slot(name='on_advance_button_clicked')
    def __show_advance(self) -> None:
        """Get the settings from advance dialog."""
        for option, button in self.algorithm_options.items():
            if button.isChecked():
                algorithm = option
                break
        else:
            raise ValueError("no option")
        dlg = AlgorithmOptionDialog(algorithm, self.alg_options, self)
        dlg.show()
        if not dlg.exec_():
            dlg.deleteLater()
            return
        self.alg_options['report'] = dlg.report.value()
        self.alg_options.pop('max_gen', None)
        self.alg_options.pop('min_fit', None)
        self.alg_options.pop('max_time', None)
        if dlg.max_gen_option.isChecked():
            self.alg_options['max_gen'] = dlg.max_gen.value()
        elif dlg.min_fit_option.isChecked():
            self.alg_options['min_fit'] = dlg.min_fit.value()
        elif dlg.max_time_option.isChecked():
            # Three spinbox value translate to second.
            self.alg_options['max_time'] = (
                dlg.max_time_h.value() * 3600
                + dlg.max_time_m.value() * 60
                + dlg.max_time_s.value()
            )
        else:
            raise ValueError("invalid option")
        pop_size = dlg.pop_size.value()
        if algorithm == AlgorithmType.RGA:
            self.alg_options['nPop'] = pop_size
        elif algorithm == AlgorithmType.Firefly:
            self.alg_options['n'] = pop_size
        elif algorithm == AlgorithmType.DE:
            self.alg_options['NP'] = pop_size
        elif algorithm == AlgorithmType.TLBO:
            self.alg_options['class_size'] = pop_size
        for row in range(dlg.alg_table.rowCount()):
            option = dlg.alg_table.item(row, 0).text()
            self.alg_options[option] = dlg.alg_table.cellWidget(row, 1).value()
        dlg.deleteLater()

    @Slot()
    def update_range(self) -> None:
        """Update range values to main canvas."""
        def t(row: int, col: int) -> Union[str, float]:
            item = self.parameter_list.item(row, col)
            if item is None:
                w: QDoubleSpinBox = self.parameter_list.cellWidget(row, col)
                return w.value()
            else:
                return item.text()
        self.update_ranges({
            cast(str, t(row, 0)): (
                cast(float, t(row, 2)),
                cast(float, t(row, 3)),
                cast(float, t(row, 4)),
            ) for row in range(self.parameter_list.rowCount())
            if t(row, 1) == 'placement'
        })

    @Slot(name='on_expr_copy_clicked')
    def __copy_expr(self) -> None:
        """Copy profile expression."""
        text = self.expression_string.text()
        if text:
            QApplication.clipboard().setText(text)

    @Slot(bool, name='on_wavelet_mode_option_clicked')
    def __set_norm(self, enabled: bool) -> None:
        """Set normalization."""
        self.shape_only_option.setChecked(enabled)
        self.shape_only_option.setEnabled(not enabled)
예제 #6
0
class CollectionsDialog(QDialog, Ui_Dialog):
    """Option dialog.

    Load the settings after closed.
    Any add, rename, delete operations will be apply immediately
    """
    collections: Dict[str, Any]
    params: Mapping[str, Any]

    def __init__(
        self,
        collections: Mapping[str, Any],
        get_collection: Callable[[], Mapping[str, Any]],
        project_no_save: Callable[[], None],
        show_ticks: int,
        monochrome: bool,
        parent: QWidget
    ):
        """We put the 'collections' (from iteration widget) reference here."""
        super(CollectionsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint
                            & ~Qt.WindowContextHelpButtonHint)
        self.collections = dict(collections)
        self.get_collection = get_collection
        self.project_no_save = project_no_save

        # Current profile name
        self.name = ""
        self.params = {}
        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        self.preview_canvas.set_show_ticks(show_ticks)
        self.preview_canvas.set_monochrome_mode(monochrome)
        self.common_list.addItems(all_collections())
        self.collections_list.addItems(self.collections)

        # Splitter
        self.main_splitter.setSizes([200, 200])
        self.sub_splitter.setSizes([100, 200])

        self.__has_collection()
        self.__can_open()

    @Slot(str, name='on_collections_list_currentTextChanged')
    def __can_open(self, _=None) -> None:
        """Set the button box to enable when data is already."""
        self.button_box.button(QDialogButtonBox.Open).setEnabled(
            self.collections_list.currentRow() > -1
        )

    def __has_collection(self) -> None:
        """Set the buttons to enable when user choose a data."""
        has_collection = bool(self.collections)
        for button in [
            self.rename_button,
            self.copy_button,
            self.delete_button
        ]:
            button.setEnabled(has_collection)

    @Slot(name='on_rename_button_clicked')
    def __rename(self) -> None:
        """Show up a string input to change the data name."""
        row = self.collections_list.currentRow()
        if not row > -1:
            return

        name, ok = QInputDialog.getText(
            self,
            "Profile name",
            "Please enter the profile name:"
        )
        if not ok:
            return

        if not name:
            QMessageBox.warning(
                self,
                "Profile name",
                "Can not use blank string to rename."
            )
            return

        item = self.collections_list.item(row)
        self.collections[name] = self.collections.pop(item.text())
        item.setText(name)
        self.project_no_save()

    @Slot(name='on_copy_button_clicked')
    def __copy(self) -> None:
        """Ask a name to copy a data."""
        row = self.collections_list.currentRow()
        if not row > -1:
            return

        name, ok = QInputDialog.getText(
            self,
            "Profile name",
            "Please enter a new profile name:"
        )
        if not ok:
            return

        if not name:
            QMessageBox.warning(
                self,
                "Profile name",
                "Can not use blank string to rename."
            )
            return

        name_old = self.collections_list.item(row).text()
        self.collections[name] = self.collections[name_old].copy()
        self.collections_list.addItem(name)
        self.project_no_save()

    @Slot(name='on_delete_button_clicked')
    def __delete(self) -> None:
        """Delete a data."""
        row = self.collections_list.currentRow()
        if not row > -1:
            return

        if QMessageBox.question(
            self,
            "Delete",
            "Do you want to delete this structure?"
        ) != QMessageBox.Yes:
            return

        item = self.collections_list.takeItem(row)
        self.collections.pop(item.text())
        self.preview_canvas.clear()
        self.__has_collection()
        self.project_no_save()

    @Slot(QListWidgetItem, name='on_common_list_itemClicked')
    def __choose_common(self, _=None) -> None:
        """Update preview canvas for common data."""
        item = self.common_list.currentItem()
        if not item:
            return

        self.name = item.text()
        self.params = collection_list(self.name)
        self.preview_canvas.from_profile(self.params)

    @Slot(QListWidgetItem, name='on_collections_list_itemClicked')
    def __choose_collections(self, _=None) -> None:
        """Update preview canvas for a project data."""
        item = self.collections_list.currentItem()
        if not item:
            return

        self.name = item.text()
        self.params = deepcopy(self.collections[self.name])
        self.preview_canvas.from_profile(self.params)

    @Slot(name='on_project_button_clicked')
    def __from_canvas(self) -> None:
        """Get a collection data from current mechanism."""
        try:
            collection = self.get_collection()
        except ValueError as error:
            QMessageBox.warning(self, "Mechanism not support.", str(error))
            return

        num = 0
        name = f"mechanism{num}"
        while name in self.collections:
            name = f"mechanism{num}"
            num += 1
        self.collections[name] = deepcopy(collection)
        self.collections_list.addItem(name)
        self.project_no_save()
        self.__has_collection()

    @Slot(name='on_common_load_clicked')
    @Slot(QListWidgetItem, name='on_common_list_itemDoubleClicked')
    def __load_common(self, _=None) -> None:
        """Load a common data and close."""
        self.__choose_common()
        self.accept()

    @Slot(name='on_button_box_accepted')
    @Slot(QListWidgetItem, name='on_collections_list_itemDoubleClicked')
    def __load_collections(self, _=None) -> None:
        """Load a project data and close."""
        self.__choose_collections()
        self.accept()