Пример #1
0
class CollectionsDialog(QDialog, Ui_Dialog):

    """Option dialog.

    Load the settings after closed.
    Any add, rename, delete operations will be apply immediately
    """

    def __init__(
        self,
        collections: Dict[str, Any],
        get_collection: Callable[[], Dict[str, Any]],
        parent: QWidget
    ):
        """We put the 'collections' (from iteration widget) reference here."""
        super(CollectionsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(
            self.windowFlags() &
            ~Qt.WindowContextHelpButtonHint |
            Qt.WindowMaximizeButtonHint
        )

        self.collections = collections
        self.getCollection = get_collection

        # Current profile name.
        self.__name_loaded = ""

        def get_solutions_func() -> Tuple[str, ...]:
            """Return solutions to preview canvas."""
            try:
                return self.collections[self.__name_loaded]['Expression']
            except KeyError:
                if self.__name_loaded == "Four bar linkage mechanism":
                    return _mech_params_4_bar['Expression']
                elif self.__name_loaded == "Eight bar linkage mechanism":
                    return _mech_params_8_bar['Expression']
                elif self.__name_loaded == "Ball lifter linkage mechanism":
                    return _mech_params_ball_lifter['Expression']
                else:
                    return ()

        self.PreviewCanvas = PreviewCanvas(get_solutions_func, self)
        self.preview_layout.addWidget(self.PreviewCanvas)
        self.show_solutions.clicked.connect(self.PreviewCanvas.setShowSolutions)
        for name in self.collections:
            self.collections_list.addItem(name)

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

        # Signals
        self.common_list.currentTextChanged.connect(self.__choose_common)
        self.common_list.itemDoubleClicked.connect(self.__load_common)
        self.common_load.clicked.connect(self.__load_common)
        self.collections_list.currentTextChanged.connect(self.__choose_collections)
        self.collections_list.currentTextChanged.connect(self.__can_open)
        self.collections_list.itemDoubleClicked.connect(self.__load_collections)
        self.buttonBox.accepted.connect(self.__load_collections)
        self.__has_collection()
        self.__can_open()

    def __can_open(self):
        """Set the button box to enable when data is already."""
        self.buttonBox.button(QDialogButtonBox.Open).setEnabled(
            self.collections_list.currentRow() > -1
        )

    def __has_collection(self):
        """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)

    def name(self) -> str:
        return self.__name_loaded

    def params(self) -> Dict[str, Any]:
        return self.__mech_params

    @pyqtSlot(name='on_rename_button_clicked')
    def __rename(self):
        """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)

    @pyqtSlot(name='on_copy_button_clicked')
    def __copy(self):
        """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)

    @pyqtSlot(name='on_delete_button_clicked')
    def __delete(self):
        """Delete a data."""
        row = self.collections_list.currentRow()
        if not row > -1:
            return
        reply = QMessageBox.question(
            self,
            "Delete",
            "Do you want to delete this structure?"
        )
        if reply != QMessageBox.Yes:
            return
        item = self.collections_list.takeItem(row)
        del self.collections[item.text()]
        self.PreviewCanvas.clear()
        self.__has_collection()

    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def __choose_common(self, p0: Union[str, QListWidgetItem, None] = None):
        """Update preview canvas for common data."""
        item = self.common_list.currentItem()
        if not item:
            return
        self.__name_loaded = item.text()
        if self.__name_loaded == "Four bar linkage mechanism":
            self.__mech_params = deepcopy(_mech_params_4_bar)
        elif self.__name_loaded == "Eight bar linkage mechanism":
            self.__mech_params = deepcopy(_mech_params_8_bar)
        elif self.__name_loaded == "Ball lifter linkage mechanism":
            self.__mech_params = deepcopy(_mech_params_ball_lifter)
        self.PreviewCanvas.from_profile(self.__mech_params)

    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def __choose_collections(self, p0: Union[str, QListWidgetItem, None] = None):
        """Update preview canvas for a workbook data."""
        item = self.collections_list.currentItem()
        if not item:
            return
        self.__name_loaded = item.text()
        self.__mech_params = deepcopy(self.collections[self.__name_loaded])
        self.PreviewCanvas.from_profile(self.__mech_params)

    @pyqtSlot(name='on_workbook_button_clicked')
    def __from_canvas(self):
        """Get a collection data from current mechanism."""
        try:
            collection = self.getCollection()
        except ValueError as e:
            QMessageBox.warning(self, "Mechanism not support.", str(e))
        else:
            num = 0
            name = f"mechanism{num}"
            while name in self.collections:
                name = f"mechanism{num}"
                num += 1
            self.collections[name] = collection.copy()
            self.collections_list.addItem(name)

    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def __load_common(self, p0: Optional[QListWidgetItem] = None):
        """Load a common data and close."""
        self.__choose_common()
        self.accept()

    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def __load_collections(self, p0: Optional[QListWidgetItem] = None):
        """Load a workbook data and close."""
        self.__choose_collections()
        self.accept()
Пример #2
0
class DimensionalSynthesis(QWidget, Ui_Form):
    
    """Dimensional synthesis widget."""
    
    fixPointRange = pyqtSignal(dict)
    pathChanged = pyqtSignal(dict)
    mergeResult = pyqtSignal(int, tuple)
    
    def __init__(self, parent):
        super(DimensionalSynthesis, self).__init__(parent)
        self.setupUi(self)
        self.mechanismParams = {}
        self.path = {}
        #A pointer reference of 'collections'.
        self.collections = parent.CollectionTabPage.CollectionsTriangularIteration.collections
        #Data and functions.
        self.mechanism_data = []
        self.inputFrom = parent.inputFrom
        self.unsaveFunc = parent.workbookNoSave
        self.Settings = deepcopy(defaultSettings)
        self.algorithmParams_default()
        #Canvas
        def get_solutions_func():
            try:
                return replace_by_dict(self.mechanismParams)
            except KeyError:
                return tuple()
        self.PreviewCanvas = PreviewCanvas(get_solutions_func, self)
        self.preview_layout.addWidget(self.PreviewCanvas)
        self.show_solutions.clicked.connect(self.PreviewCanvas.setShowSolutions)
        #Splitter
        self.up_splitter.setSizes([80, 100])
        #Table widget column width.
        self.ground_joints.setColumnWidth(0, 50)
        self.ground_joints.setColumnWidth(1, 80)
        self.ground_joints.setColumnWidth(2, 70)
        self.ground_joints.setColumnWidth(3, 70)
        self.ground_joints.setColumnWidth(4, 80)
        #Default value of algorithm parameters.
        self.type0.clicked.connect(self.algorithmParams_default)
        self.type1.clicked.connect(self.algorithmParams_default)
        self.type2.clicked.connect(self.algorithmParams_default)
        #Signals
        self.Result_list.clicked.connect(self.hasResult)
        self.clear_button.clicked.connect(self.clear_settings)
        self.clear()
    
    def clear(self):
        """Clear all sub-widgets."""
        self.mechanism_data.clear()
        self.Result_list.clear()
        self.clear_settings()
        self.hasResult()
    
    def clear_settings(self):
        """Clear sub-widgets that contain the setting."""
        self.on_path_clear_clicked(ask=False)
        self.path.clear()
        self.mechanismParams.clear()
        self.PreviewCanvas.clear()
        self.Settings.clear()
        self.Settings = deepcopy(defaultSettings)
        self.profile_name.setText("No setting")
        self.type2.setChecked(True)
        self.ground_joints.setRowCount(0)
        self.target_points.clear()
        self.Expression.clear()
        self.Link_Expression.clear()
        self.updateRange()
        self.isGenerate()
    
    def loadResults(self,
        mechanism_data: List[Dict[str, Any]]
    ):
        """Append results of workbook database to memory."""
        for e in mechanism_data:
            self.mechanism_data.append(e)
            self.add_result(e)
    
    def currentPathChanged(self):
        """Call the canvas to update to current target path."""
        self.pathChanged.emit({
            name: tuple(path)
            for name, path in self.path.items()
        })
        self.isGenerate()
    
    def currentPath(self) -> List[Tuple[float, float]]:
        """Return the pointer of current target path."""
        item = self.target_points.currentItem()
        if item:
            return self.path[item.text()]
        else:
            return []
    
    @pyqtSlot(str)
    def on_target_points_currentTextChanged(self, text=None):
        """Switch to the current target path."""
        self.path_list.clear()
        for x, y in self.currentPath():
            self.path_list.addItem("({:.04f}, {:.04f})".format(x, y))
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_path_clear_clicked(self, ask: bool =True):
        """Clear the current target path."""
        if ask:
            reply = QMessageBox.question(self,
                "Clear path",
                "Are you sure to clear the current path?"
            )
            if reply != QMessageBox.Yes:
                return
        self.currentPath().clear()
        self.path_list.clear()
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_path_copy_clicked(self):
        """Copy the current path coordinates to clipboard."""
        QApplication.clipboard().setText('\n'.join(
            "{},{}".format(x, y)
            for x, y in self.currentPath()
        ))
    
    @pyqtSlot()
    def on_path_paste_clicked(self):
        """Paste path data from clipboard."""
        self.readPathFromCSV(charSplit(";|,|\n", QApplication.clipboard().text()))
    
    @pyqtSlot()
    def on_importCSV_clicked(self):
        """Paste path data from a text file."""
        fileName = self.inputFrom(
            "Path data",
            ["Text File (*.txt)", "CSV File (*.csv)"]
        )
        if not fileName:
            return
        data = []
        with open(fileName, newline='') as stream:
            reader = csv.reader(stream, delimiter=' ', quotechar='|')
            for row in reader:
                data += ' '.join(row).split(',')
        self.readPathFromCSV(data)
    
    def readPathFromCSV(self, data: List[str]):
        """Trun STR to FLOAT then add them to current target path."""
        try:
            data = [
                (round(float(data[i]), 4), round(float(data[i + 1]), 4))
                for i in range(0, len(data), 2)
            ]
        except:
            QMessageBox.warning(self, "File error",
                "Wrong format.\nIt should be look like this:" +
                "\n0.0,0.0[\\n]" * 3
            )
        else:
            for e in data:
                self.add_point(e[0], e[1])
    
    @pyqtSlot()
    def on_importXLSX_clicked(self):
        """Paste path data from a Excel file."""
        fileName = self.inputFrom(
            "Excel file",
            ["Microsoft Office Excel (*.xlsx *.xlsm *.xltx *.xltm)"]
        )
        if not fileName:
            return
        wb = openpyxl.load_workbook(fileName)
        ws = wb.get_sheet_by_name(wb.get_sheet_names()[0])
        data = []
        #Keep finding until there is no value.
        i = 1
        while True:
            x = ws.cell(row=i, column=1).value
            y = ws.cell(row=i, column=2).value
            if x == None or y == None:
                break
            try:
                data.append((round(float(x), 4), round(float(y), 4)))
            except:
                QMessageBox.warning(self,
                    "File error",
                    "Wrong format.\n" +
                    "The datasheet seems to including non-digital cell."
                )
                break
            i += 1
        for x, y in data:
            self.add_point(x, y)
    
    @pyqtSlot()
    def on_pathAdjust_clicked(self):
        """Show up path adjust dialog and
        get back the changes of current target path.
        """
        dlg = Path_adjust_show(self)
        dlg.show()
        if not dlg.exec_():
            return
        self.on_path_clear_clicked()
        for e in dlg.r_path:
            self.add_point(e[0], e[1])
        self.currentPathChanged()
    
    def add_point(self, x: float, y: float):
        """Add path data to list widget and
        current target path.
        """
        x = round(x, 4)
        y = round(y, 4)
        self.currentPath().append((x, y))
        self.path_list.addItem("({:.04f}, {:.04f})".format(x, y))
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_close_path_clicked(self):
        """Add a the last point same as first point."""
        currentPath = self.currentPath()
        if (self.path_list.count() > 1) and (currentPath[0] != currentPath[-1]):
            self.add_point(*currentPath[0])
    
    @pyqtSlot()
    def on_point_up_clicked(self):
        """Target point move up."""
        row = self.path_list.currentRow()
        if not ((row > 0) and (self.path_list.count() > 1)):
            return
        path = self.currentPath()
        path.insert(row - 1, (path[row][0], path[row][1]))
        del path[row + 1]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row - 1, "({}, {})".format(x, y))
        self.path_list.takeItem(row + 1)
        self.path_list.setCurrentRow(row - 1)
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_point_down_clicked(self):
        """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.currentPath()
        path.insert(row + 2, (path[row][0], path[row][1]))
        del path[row]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row+2, "({}, {})".format(x, y))
        self.path_list.takeItem(row)
        self.path_list.setCurrentRow(row+1)
        self.currentPathChanged()
    
    @pyqtSlot()
    def on_point_delete_clicked(self):
        """Delete a target point."""
        row = self.path_list.currentRow()
        if not row > -1:
            return
        del self.currentPath()[row]
        self.path_list.takeItem(row)
        self.currentPathChanged()
    
    def isGenerate(self):
        """Set button enable if all the data are already."""
        self.pointNum.setText(
            "<html><head/><body><p><span style=\"font-size:12pt; color:#00aa00;\">" +
            str(self.path_list.count()) +
            "</span></p></body></html>"
        )
        n = bool(self.mechanismParams) and (self.path_list.count() > 1)
        self.pathAdjust.setEnabled(n)
        self.generate_button.setEnabled(n)
    
    @pyqtSlot()
    def on_generate_button_clicked(self):
        """Start synthesis."""
        #Check if the number of target points are same.
        leng = -1
        for path in self.path.values():
            if leng<0:
                leng = len(path)
            if len(path)!=leng:
                QMessageBox.warning(self, "Target Error",
                    "The length of target paths should be the same."
                )
                return
        #Get the algorithm type.
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        elif self.type2.isChecked():
            type_num = AlgorithmType.DE
        #Deep copy it so the pointer will not the same.
        mechanismParams = deepcopy(self.mechanismParams)
        mechanismParams['Target'] = deepcopy(self.path)
        for key in ('Driver', 'Follower'):
            for name in mechanismParams[key]:
                row = name_in_table(self.ground_joints, name)
                mechanismParams[key][name] = (
                    self.ground_joints.cellWidget(row, 2).value(),
                    self.ground_joints.cellWidget(row, 3).value(),
                    self.ground_joints.cellWidget(row, 4).value()
                )
        for name in ['IMax', 'IMin', 'LMax', 'LMin', 'FMax', 'FMin', 'AMax', 'AMin']:
            mechanismParams[name] = self.Settings[name]
        setting = {'report': self.Settings['report']}
        if 'maxGen' in self.Settings:
            setting['maxGen'] = self.Settings['maxGen']
        elif 'minFit' in self.Settings:
            setting['minFit'] = self.Settings['minFit']
        elif 'maxTime' in self.Settings:
            setting['maxTime'] = self.Settings['maxTime']
        setting.update(self.Settings['algorithmPrams'])
        #Start progress dialog.
        dlg = Progress_show(
            type_num,
            mechanismParams,
            setting,
            self
        )
        dlg.show()
        if not dlg.exec_():
            return
        for m in dlg.mechanisms:
            self.mechanism_data.append(m)
            self.add_result(m)
        self.setTime(dlg.time_spand)
        self.unsaveFunc()
        QMessageBox.information(self,
            "Dimensional Synthesis",
            "Your tasks is all completed.",
            QMessageBox.Ok
        )
        print("Finished.")
    
    def setTime(self, time: float):
        """Set the time label."""
        self.timeShow.setText(
            "<html><head/><body><p><span style=\"font-size:16pt\">" +
            "{}[min] {:.02f}[s]".format(int(time // 60), time % 60) +
            "</span></p></body></html>"
        )
    
    def add_result(self, result: Dict[str, Any]):
        """Add result items, except add to the list."""
        item = QListWidgetItem(result['Algorithm'])
        interrupt = result['interrupted']
        if interrupt=='False':
            item.setIcon(QIcon(QPixmap(":/icons/task-completed.png")))
        elif interrupt=='N/A':
            item.setIcon(QIcon(QPixmap(":/icons/question-mark.png")))
        else:
            item.setIcon(QIcon(QPixmap(":/icons/interrupted.png")))
        text = "{} ({})".format(
            result['Algorithm'],
            "No interrupt." if interrupt=='False' else "Interrupt at {}".format(interrupt)
        )
        if interrupt == 'N/A':
            text += "\n※Completeness is not clear."
        item.setToolTip(text)
        self.Result_list.addItem(item)
    
    @pyqtSlot()
    def on_deleteButton_clicked(self):
        """Delete a result."""
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        reply = QMessageBox.question(self,
            "Delete",
            "Delete this result from list?"
        )
        if reply != QMessageBox.Yes:
            return
        del self.mechanism_data[row]
        self.Result_list.takeItem(row)
        self.unsaveFunc()
        self.hasResult()
    
    @pyqtSlot()
    def hasResult(self):
        """Set enable if there has any result."""
        for button in [
            self.mergeButton,
            self.deleteButton,
            self.Result_load_settings,
            self.Result_chart,
            self.Result_clipboard
        ]:
            button.setEnabled(self.Result_list.currentRow()>-1)
    
    @pyqtSlot(QModelIndex)
    def on_Result_list_doubleClicked(self, index):
        """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)
        dlg.show()
    
    @pyqtSlot()
    def on_mergeButton_clicked(self):
        """Merge mechanism into main canvas."""
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        reply = QMessageBox.question(self,
            "Merge",
            "Merge this result to your canvas?"
        )
        if reply == QMessageBox.Yes:
            self.mergeResult.emit(row, self.get_path(row))
    
    def get_path(self, row: int):
        """Using result data to generate paths of mechanism."""
        Result = self.mechanism_data[row]
        expr_angles, expr_links, expr_points = triangle_class(Result['Expression'])
        if len(expr_angles)>1:
            return tuple()
        '''
        expr_angles: ('a0', ...)
        expr_links: ('L0', 'L1', 'L2', ...)
        expr_points: ('A', 'B', 'C', 'D', 'E', ...)
        '''
        Paths = tuple([] for i in range(len(expr_points)))
        for a in range(360 + 1):
            data_dict = {e: Result[e] for e in expr_links}
            data_dict.update({e: Result[e] for e in Result['Driver']})
            data_dict.update({e: Result[e] for e in Result['Follower']})
            data_dict.update({expr_angles[0]: radians(a)})
            expr_parser(Result['Expression'], data_dict)
            for i, e in enumerate(expr_points):
                x, y = data_dict[e]
                if x!=nan:
                    Paths[i].append((x, y))
        return tuple(
            tuple(path) if len(set(path))>1 else ()
            for path in Paths
        )
    
    @pyqtSlot()
    def on_Result_chart_clicked(self):
        """Show up the chart dialog."""
        dlg = ChartDialog("Convergence Value", self.mechanism_data, self)
        dlg.show()
    
    @pyqtSlot()
    def on_Result_clipboard_clicked(self):
        """Copy pretty print result as text."""
        QApplication.clipboard().setText(
            pprint.pformat(self.mechanism_data[self.Result_list.currentRow()])
        )
    
    @pyqtSlot()
    def on_save_button_clicked(self):
        """Save as new profile to collection widget."""
        if not self.mechanismParams:
            return
        name, ok = QInputDialog.getText(self,
            "Profile name",
            "Please enter the profile name:"
        )
        if not ok:
            return
        i = 0
        while (name not in self.collections) and (not name):
            name = "Structure_{}".format(i)
        mechanismParams = deepcopy(self.mechanismParams)
        for key in [
            'Driver',
            'Follower',
            'Target'
        ]:
            for name in mechanismParams[key]:
                mechanismParams[key][name] = None
        self.collections[name] = mechanismParams
        self.unsaveFunc()
    
    @pyqtSlot()
    def on_load_profile_clicked(self):
        """Load profile from collections dialog."""
        dlg = CollectionsDialog(self)
        dlg.show()
        if not dlg.exec_():
            return
        self.clear_settings()
        self.mechanismParams = dlg.mechanismParams
        self.profile_name.setText(dlg.name_loaded)
        self.Expression.setText(self.mechanismParams['Expression'])
        self.Link_Expression.setText(self.mechanismParams['Link_Expression'])
        self.set_profile()
        self.isGenerate()
    
    def set_profile(self):
        """Set profile to sub-widgets."""
        params = self.mechanismParams
        self.path.clear()
        self.target_points.clear()
        for name in sorted(params['Target']):
            self.target_points.addItem(name)
            path = params['Target'][name]
            if path:
                self.path[name] = path.copy()
            else:
                self.path[name] = []
        if self.target_points.count():
            self.target_points.setCurrentRow(0)
        gj = {}
        for key in ('Driver', 'Follower'):
            gj.update(params[key])
        self.ground_joints.setRowCount(0)
        self.ground_joints.setRowCount(len(gj))
        def spinbox(v, prefix=False):
            s = QDoubleSpinBox(self)
            s.setMinimum(-1000000.0)
            s.setMaximum(1000000.0)
            s.setSingleStep(10.0)
            s.setValue(v)
            if prefix:
                s.setPrefix("±")
            return s
        nd = {k: int(v.replace('P', '')) for k, v in params['name_dict'].items()}
        for row, name in enumerate(sorted(gj)):
            coord = gj[name]
            self.ground_joints.setItem(row, 0, QTableWidgetItem(name))
            self.ground_joints.setItem(row, 1,
                QTableWidgetItem('Driver' if name in params['Driver'] else 'Follower')
            )
            self.ground_joints.setCellWidget(row, 2,
                spinbox(coord[0] if coord else params['pos'][nd[name]][0])
            )
            self.ground_joints.setCellWidget(row, 3,
                spinbox(coord[1] if coord else params['pos'][nd[name]][1])
            )
            self.ground_joints.setCellWidget(row, 4,
                spinbox(coord[2] if coord else 50., True)
            )
        for row in range(self.ground_joints.rowCount()):
            for column in range(2, 5):
                self.ground_joints.cellWidget(row, column).valueChanged.connect(self.updateRange)
        self.updateRange()
        self.PreviewCanvas.from_profile(self.mechanismParams)
    
    @pyqtSlot()
    def on_Result_load_settings_clicked(self):
        """Load settings from a result."""
        self.hasResult()
        row = self.Result_list.currentRow()
        if not row>-1:
            return
        self.clear_settings()
        Result = self.mechanism_data[row]
        if Result['Algorithm'] == str(AlgorithmType.RGA):
            self.type0.setChecked(True)
        elif Result['Algorithm'] == str(AlgorithmType.Firefly):
            self.type1.setChecked(True)
        elif Result['Algorithm'] == str(AlgorithmType.DE):
            self.type2.setChecked(True)
        self.profile_name.setText("External setting")
        #External setting.
        self.Expression.setText(Result['Expression'])
        self.Link_Expression.setText(Result['Link_Expression'])
        #Copy to mechanism params.
        self.mechanismParams.clear()
        for key in [
            'Driver',
            'Follower',
            'Target'
        ]:
            self.mechanismParams[key] = Result[key].copy()
        for key in [
            'Link_Expression',
            'Expression',
            'constraint',
            'Graph',
            'name_dict',
            'pos',
            'cus',
            'same'
        ]:
            self.mechanismParams[key] = Result[key]
        self.set_profile()
        self.setTime(Result['time'])
        settings = Result['settings']
        self.Settings = {
            'report': settings['report'],
            'IMax': Result['IMax'], 'IMin': Result['IMin'],
            'LMax': Result['LMax'], 'LMin': Result['LMin'],
            'FMax': Result['FMax'], 'FMin': Result['FMin'],
            'AMax': Result['AMax'], 'AMin': Result['AMin']
        }
        if 'maxGen' in settings:
            self.Settings['maxGen'] = settings['maxGen']
        elif 'minFit' in settings:
            self.Settings['minFit'] = settings['minFit']
        elif 'maxTime' in settings:
            self.Settings['maxTime'] = settings['maxTime']
        algorithmPrams = settings.copy()
        del algorithmPrams['report']
        self.Settings['algorithmPrams'] = algorithmPrams
    
    def algorithmParams_default(self):
        """Set the algorithm settings to default."""
        if self.type0.isChecked():
            self.Settings['algorithmPrams'] = GeneticPrams.copy()
        elif self.type1.isChecked():
            self.Settings['algorithmPrams'] = FireflyPrams.copy()
        elif self.type2.isChecked():
            self.Settings['algorithmPrams'] = DifferentialPrams.copy()
    
    @pyqtSlot()
    def on_advanceButton_clicked(self):
        """Get the settings from advance dialog."""
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        elif self.type2.isChecked():
            type_num = AlgorithmType.DE
        dlg = Options_show(type_num, self.Settings)
        dlg.show()
        if not dlg.exec_():
            return
        tablePL = lambda row: dlg.PLTable.cellWidget(row, 1).value()
        self.Settings = {
            'report': dlg.report.value(),
            'IMax': tablePL(0), 'IMin': tablePL(1),
            'LMax': tablePL(2), 'LMin': tablePL(3),
            'FMax': tablePL(4), 'FMin': tablePL(5),
            'AMax': tablePL(6), 'AMin': tablePL(7)
        }
        if dlg.maxGen_option.isChecked():
            self.Settings['maxGen'] = dlg.maxGen.value()
        elif dlg.minFit_option.isChecked():
            self.Settings['minFit'] = dlg.minFit.value()
        elif dlg.maxTime_option.isChecked():
            #Three spinbox value translate to second.
            self.Settings['maxTime'] = (
                dlg.maxTime_h.value() * 3600 +
                dlg.maxTime_m.value() * 60 +
                dlg.maxTime_s.value()
            )
        tableAP = lambda row: dlg.APTable.cellWidget(row, 1).value()
        popSize = dlg.popSize.value()
        if type_num == AlgorithmType.RGA:
            self.Settings['algorithmPrams'] = {
                'nPop': popSize,
                'pCross': tableAP(0),
                'pMute': tableAP(1),
                'pWin': tableAP(2),
                'bDelta': tableAP(3)
            }
        elif type_num == AlgorithmType.Firefly:
            self.Settings['algorithmPrams'] = {
                'n': popSize,
                'alpha': tableAP(0),
                'betaMin': tableAP(1),
                'gamma': tableAP(2),
                'beta0': tableAP(3)
            }
        elif type_num == AlgorithmType.DE:
            self.Settings['algorithmPrams'] = {
                'NP': popSize,
                'strategy': tableAP(0),
                'F': tableAP(1),
                'CR': tableAP(2)
            }
    
    @pyqtSlot(float)
    def updateRange(self, p0=None):
        """Update range values to main canvas."""
        def t(x, y):
            item = self.ground_joints.item(x, y)
            if item:
                return item.text()
            else:
                return self.ground_joints.cellWidget(x, y).value()
        self.fixPointRange.emit({
            t(row, 0): (t(row, 2), t(row, 3), t(row, 4))
            for row in range(self.ground_joints.rowCount())
        })
    
    @pyqtSlot()
    def on_Expression_copy_clicked(self):
        """Copy profile expression."""
        text = self.Expression.text()
        if text:
            QApplication.clipboard().setText(text)
    
    @pyqtSlot()
    def on_Link_Expression_copy_clicked(self):
        """Copy profile linkage expression."""
        text = self.Link_Expression.text()
        if text:
            QApplication.clipboard().setText(text)
Пример #3
0
class CollectionsDialog(QDialog, Ui_Dialog):
    """Option dialog.
    
    Load the settings after closed.
    """
    def __init__(self, parent):
        super(CollectionsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags()
                            & ~Qt.WindowContextHelpButtonHint
                            | Qt.WindowMaximizeButtonHint)
        self.collections = parent.collections
        self.name_loaded = ""

        def get_solutions_func() -> Tuple[str]:
            """Return solutions to preview canvas."""
            try:
                return self.collections[self.name_loaded]['Expression']
            except KeyError:
                if self.name_loaded == "Four bar linkage mechanism":
                    return mech_params_4Bar['Expression']
                elif self.name_loaded == "Eight bar linkage mechanism":
                    return mech_params_8Bar['Expression']
                elif self.name_loaded == "Ball lifter linkage mechanism":
                    return mech_params_BallLifter['Expression']
                else:
                    return tuple()

        self.PreviewCanvas = PreviewCanvas(get_solutions_func, self)
        self.preview_layout.addWidget(self.PreviewCanvas)
        self.show_solutions.clicked.connect(
            self.PreviewCanvas.setShowSolutions)
        for name in self.collections:
            self.collections_list.addItem(name)
        #Splitter
        self.main_splitter.setSizes([200, 200])
        #Signals
        self.common_list.currentTextChanged.connect(self.__chooseCommon)
        self.common_list.itemClicked.connect(self.__chooseCommon)
        self.common_load.clicked.connect(self.__loadCommon)
        self.common_list.itemDoubleClicked.connect(self.__loadCommon)
        self.collections_list.currentTextChanged.connect(
            self.__chooseCollections)
        self.collections_list.itemClicked.connect(self.__chooseCollections)
        self.buttonBox.accepted.connect(self.__loadCollections)
        self.collections_list.itemDoubleClicked.connect(self.__loadCollections)
        self.collections_list.currentRowChanged.connect(self.__canOpen)
        self.__hasCollection()
        self.__canOpen()

    def __canOpen(self):
        """Set the button box to enable when data is already."""
        self.buttonBox.button(QDialogButtonBox.Open).setEnabled(
            self.collections_list.currentRow() > -1)

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

    @pyqtSlot()
    def on_rename_button_clicked(self):
        """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)

    @pyqtSlot()
    def on_copy_button_clicked(self):
        """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)

    @pyqtSlot()
    def on_delete_button_clicked(self):
        """Delete a data."""
        row = self.collections_list.currentRow()
        if not row > -1:
            return
        reply = QMessageBox.question(self, "Delete",
                                     "Do you want to delete this structure?")
        if reply != QMessageBox.Yes:
            return
        item = self.collections_list.takeItem(row)
        del self.collections[item.text()]
        self.PreviewCanvas.clear()
        self.__hasCollection()

    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def __chooseCommon(self, p0=None):
        """Update preview canvas for common data."""
        text = self.common_list.currentItem().text()
        if not text:
            return
        self.name_loaded = text
        if text == "Four bar linkage mechanism":
            self.mech_params = deepcopy(mech_params_4Bar)
        elif text == "Eight bar linkage mechanism":
            self.mech_params = deepcopy(mech_params_8Bar)
        elif self.name_loaded == "Ball lifter linkage mechanism":
            self.mech_params = deepcopy(mech_params_BallLifter)
        self.PreviewCanvas.from_profile(self.mech_params)

    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def __chooseCollections(self, p0=None):
        """Update preview canvas for a workbook data."""
        text = self.collections_list.currentItem().text()
        if not text:
            return
        self.name_loaded = text
        self.mech_params = self.collections[self.name_loaded]
        self.PreviewCanvas.from_profile(self.mech_params)

    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def __loadCommon(self, p0=None):
        """Load a common data and close."""
        self.__chooseCommon(self.common_list.currentItem().text())
        self.accept()

    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def __loadCollections(self, p0=None):
        """Load a workbook data and close."""
        self.__chooseCollections(self.collections_list.currentItem().text())
        self.accept()
Пример #4
0
class CollectionsDialog(QDialog, Ui_Dialog):
    def __init__(self, parent):
        super(CollectionsDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        self.collections = parent.collections
        self.name_loaded = ""
        #Canvas
        def get_solutions_func():
            try:
                return replace_by_dict(self.collections[self.name_loaded])
            except KeyError:
                if self.name_loaded=="Four bar linkage mechanism":
                    return replace_by_dict(mechanismParams_4Bar)
                elif self.name_loaded=="Eight bar linkage mechanism":
                    return replace_by_dict(mechanismParams_8Bar)
                elif self.name_loaded=="Ball lifter linkage mechanism":
                    return replace_by_dict(mechanismParams_BallLifter)
                else:
                    return tuple()
        self.PreviewCanvas = PreviewCanvas(get_solutions_func, self)
        self.preview_layout.addWidget(self.PreviewCanvas)
        self.show_solutions.clicked.connect(self.PreviewCanvas.setShowSolutions)
        for name in self.collections:
            self.collections_list.addItem(name)
        #Splitter
        self.main_splitter.setSizes([200, 200])
        #Signals
        self.common_list.currentTextChanged.connect(self.choose_common)
        self.common_list.itemClicked.connect(self.choose_common)
        self.common_load.clicked.connect(self.load_common)
        self.common_list.itemDoubleClicked.connect(self.load_common)
        self.collections_list.currentTextChanged.connect(self.choose_collections)
        self.collections_list.itemClicked.connect(self.choose_collections)
        self.buttonBox.accepted.connect(self.load_collections)
        self.collections_list.itemDoubleClicked.connect(self.load_collections)
        self.collections_list.currentRowChanged.connect(self.canOpen)
        self.hasCollection()
        self.canOpen()
    
    def canOpen(self):
        self.buttonBox.button(QDialogButtonBox.Open).setEnabled(self.collections_list.currentRow()>-1)
    
    def hasCollection(self):
        hasCollection = bool(self.collections)
        for button in [self.rename_button, self.copy_button, self.delete_button]:
            button.setEnabled(hasCollection)
    
    @pyqtSlot()
    def on_rename_button_clicked(self):
        row = self.collections_list.currentRow()
        if row>-1:
            name, ok = QInputDialog.getText(self, "Profile name", "Please enter the profile name:")
            if ok:
                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)
    
    @pyqtSlot()
    def on_copy_button_clicked(self):
        row = self.collections_list.currentRow()
        if row>-1:
            name, ok = QInputDialog.getText(self, "Profile name", "Please enter a new profile name:")
            if ok:
                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)
    
    @pyqtSlot()
    def on_delete_button_clicked(self):
        row = self.collections_list.currentRow()
        if row>-1:
            reply = QMessageBox.question(self, "Delete", "Do you want to delete this structure?",
                (QMessageBox.Yes | QMessageBox.No), QMessageBox.Yes)
            if reply==QMessageBox.Yes:
                item = self.collections_list.takeItem(row)
                del self.collections[item.text()]
                self.PreviewCanvas.clear()
                self.hasCollection()
    
    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def choose_common(self, p0=None):
        text = self.common_list.currentItem().text()
        if text:
            self.name_loaded = text
            if text=="Four bar linkage mechanism":
                self.mechanismParams = deepcopy(mechanismParams_4Bar)
            elif text=="Eight bar linkage mechanism":
                self.mechanismParams = deepcopy(mechanismParams_8Bar)
            elif self.name_loaded=="Ball lifter linkage mechanism":
                self.mechanismParams = deepcopy(mechanismParams_BallLifter)
            self.PreviewCanvas.from_profile(self.mechanismParams)
    
    @pyqtSlot(str)
    @pyqtSlot(QListWidgetItem)
    def choose_collections(self, p0=None):
        text = self.collections_list.currentItem().text()
        if text:
            self.name_loaded = text
            self.mechanismParams = self.collections[self.name_loaded]
            self.PreviewCanvas.from_profile(self.mechanismParams)
    
    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def load_common(self, p0=None):
        self.choose_common(self.common_list.currentItem().text())
        self.accept()
    
    @pyqtSlot()
    @pyqtSlot(QListWidgetItem)
    def load_collections(self, p0=None):
        self.choose_collections(self.collections_list.currentItem().text())
        self.accept()
    
    @pyqtSlot(bool)
    def on_switch_name_clicked(self, checked):
        if checked:
            self.PreviewCanvas.setNameDict({})
        else:
            self.PreviewCanvas.setNameDict(self.mechanismParams['name_dict'])
Пример #5
0
class CollectionsDialog(QDialog, Ui_Dialog):

    """Option dialog.

    Load the settings after closed.
    Any add, rename, delete operations will be apply immediately
    """

    def __init__(
        self,
        collections: Dict[str, Any],
        get_collection: Callable[[], Dict[str, Any]],
        unsave_func: Callable[[], None],
        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.WindowContextHelpButtonHint |
            Qt.WindowMaximizeButtonHint
        )

        self.collections = collections
        self.get_collection = get_collection
        self.unsave_func = unsave_func

        # Current profile name
        self.name = ""
        self.params: Dict[str, Any] = {}

        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)
        self.preview_canvas.set_monochrome_mode(monochrome)
        self.common_list.addItems(collection_list)
        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):
        """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):
        """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):
        """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.unsave_func()

    @Slot(name='on_copy_button_clicked')
    def __copy(self):
        """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.unsave_func()

    @Slot(name='on_delete_button_clicked')
    def __delete(self):
        """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.unsave_func()

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

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

    @Slot(QListWidgetItem, name='on_collections_list_itemClicked')
    def __choose_collections(self, _=None):
        """Update preview canvas for a workbook 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_workbook_button_clicked')
    def __from_canvas(self):
        """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] = collection.copy()
        self.collections_list.addItem(name)
        self.unsave_func()
        self.__has_collection()

    @Slot(name='on_common_load_clicked')
    @Slot(QListWidgetItem, name='on_common_list_itemDoubleClicked')
    def __load_common(self, _=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):
        """Load a workbook data and close."""
        self.__choose_collections()
        self.accept()
Пример #6
0
class DimensionalSynthesis(QWidget, Ui_Form):
    """Dimensional synthesis widget.

    User can run the dimensional synthesis here.
    """
    def __init__(self, parent: MainWindowBase):
        """Reference names:

        + Iteration collections.
        + Result data.
        + Main window function references.
        """
        super(DimensionalSynthesis, self).__init__(parent)
        self.setupUi(self)

        self.mech_params: Dict[str, Any] = {}
        self.path: Dict[int, List[_Coord]] = {}

        # Some reference of 'collections'
        self.collections = parent.collection_tab_page.configure_widget.collections
        self.get_collection = parent.get_configure
        self.input_from = parent.input_from
        self.workbook_no_save = parent.workbook_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

        # Data and functions
        self.mechanism_data: List[Dict[str, Any]] = []
        self.alg_options: Dict[str, Union[int, float]] = {}
        self.alg_options.update(defaultSettings)
        self.alg_options.update(DifferentialPrams)
        self.__set_algorithm_default()

        self.preview_canvas = PreviewCanvas(self)
        self.preview_layout.addWidget(self.preview_canvas)

        # Splitter
        self.main_splitter.setStretchFactor(0, 100)
        self.main_splitter.setStretchFactor(1, 10)
        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.clear()

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

    def __clear_settings(self):
        """Clear sub-widgets that contain the setting."""
        self.__clear_path(ask=False)
        self.path.clear()
        self.mech_params.clear()
        self.preview_canvas.clear()
        self.alg_options.clear()
        self.alg_options.update(defaultSettings)
        self.alg_options.update(DifferentialPrams)
        self.profile_name.clear()
        self.type2.setChecked(True)
        self.parameter_list.setRowCount(0)
        self.target_points.clear()
        self.expression_string.clear()
        self.update_range()
        self.__able_to_generate()

    @Slot(name='on_clear_button_clicked')
    def __user_clear(self):
        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]]):
        """Append results of workbook database to memory."""
        for e in mechanism_data:
            self.mechanism_data.append(e)
            self.__add_result(e)

    def __current_path_changed(self):
        """Call the canvas to update to current target path."""
        self.set_solving_path(
            {f"P{name}": tuple(path)
             for name, path 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, _: str):
        """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):
        """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):
        """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):
        """Paste path data from clipboard."""
        self.__read_path_from_csv(
            char_split(r"[;,\n]",
                       QApplication.clipboard().text()))

    @Slot(name='on_import_csv_button_clicked')
    def __import_csv(self):
        """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
        data = []
        with open(file_name, 'r', encoding='utf-8', newline='') as f:
            for row in csv.reader(f, delimiter=' ', quotechar='|'):
                data += " ".join(row).split(',')
        self.__read_path_from_csv(data)

    def __read_path_from_csv(self, raw_data: List[str]):
        """Turn string to float then add them to current target path."""
        try:
            data = [(round(float(raw_data[i]),
                           4), round(float(raw_data[i + 1]), 4))
                    for i in range(0, len(raw_data), 2)]
        except (IndexError, ValueError):
            QMessageBox.warning(
                self, "File error",
                "Wrong format.\nIt should be look like this:" +
                ("\n0.0,0.0[\\n]" * 3))
        else:
            for x, y in data:
                self.add_point(x, y)

    @Slot(name='on_import_xlsx_button_clicked')
    def __import_xlsx(self):
        """Paste path data from a Excel file."""
        file_name = self.input_from(
            "Excel file",
            ["Microsoft Office Excel (*.xlsx *.xlsm *.xltx *.xltm)"])
        if not file_name:
            return
        wb = load_workbook(file_name)
        ws = wb.get_sheet_by_name(wb.get_sheet_names()[0])
        data = []
        # Keep finding until there is no value.
        i = 1
        while True:
            x = ws.cell(row=i, column=1).value
            y = ws.cell(row=i, column=2).value
            if None in {x, y}:
                break
            try:
                data.append((round(float(x), 4), round(float(y), 4)))
            except (IndexError, AttributeError):
                QMessageBox.warning(
                    self, "File error", "Wrong format.\n"
                    "The data sheet seems to including non-digital cell.")
                break
            i += 1
        for x, y in data:
            self.add_point(x, y)

    @Slot(name='on_path_adjust_button_clicked')
    def __adjust_path(self):
        """Show up path adjust dialog and
        get back the changes of current target path.
        """
        dlg = PathAdjustDialog(self)
        dlg.show()
        if not dlg.exec():
            dlg.deleteLater()
            return

        self.__clear_path(ask=False)
        for e in dlg.r_path:
            self.add_point(e[0], e[1])

        dlg.deleteLater()
        self.__current_path_changed()

    def add_point(self, x: float, y: float):
        """Add path data to list widget and current target path."""
        x = round(x, 4)
        y = round(y, 4)
        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()

    @Slot(float, float)
    def set_point(self, x: float, y: float):
        """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) < 5:
                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):
        """Add a the last point same as first point."""
        current_path = self.current_path()
        if (self.path_list.count() > 1) and (current_path[0] !=
                                             current_path[-1]):
            self.add_point(*current_path[0])

    @Slot(name='on_point_up_clicked')
    def __move_up_point(self):
        """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]))
        del path[row + 1]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row - 1, f"({x}, {y})")
        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):
        """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]))
        del path[row]
        x, y = self.path_list.currentItem().text()[1:-1].split(", ")
        self.path_list.insertItem(row + 2, f"({x}, {y})")
        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):
        """Delete a target point."""
        row = self.path_list.currentRow()
        if not row > -1:
            return
        del self.current_path()[row]
        self.path_list.takeItem(row)
        self.__current_path_changed()

    def __able_to_generate(self):
        """Set button enable if all the data are already."""
        self.pointNum.setText(
            "<p><span style=\"font-size:12pt;"
            f"color:#00aa00;\">{self.path_list.count()}</span></p>")
        n = bool(self.mech_params and (self.path_list.count() > 1)
                 and self.expression_string.text())
        self.path_adjust_button.setEnabled(n)
        self.synthesis_button.setEnabled(n)

    @Slot(name='on_synthesis_button_clicked')
    def __synthesis(self):
        """Start synthesis."""
        # Check if the number of 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.
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        else:
            type_num = AlgorithmType.DE
        # Deep copy it so the pointer will not the same.
        mech_params = deepcopy(self.mech_params)
        mech_params['Expression'] = parse_vpoints(
            mech_params.pop('Expression', []))
        mech_params['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_params['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(type_num, mech_params, self.alg_options, self)
        dlg.show()
        if not dlg.exec():
            dlg.deleteLater()
            return

        mechanisms = dlg.mechanisms
        mechanisms_plot: List[Dict[str, Any]] = []
        for data in 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.workbook_no_save()
        dlg.deleteLater()

        dlg = ChartDialog("Convergence Data", mechanisms_plot, self)
        dlg.show()
        dlg.exec()
        dlg.deleteLater()

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

    def __add_result(self, result: Dict[str, Any]):
        """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):
        """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.workbook_no_save()
        self.__has_result()

    @Slot(QModelIndex, name='on_result_list_clicked')
    def __has_result(self, *_):
        """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, _: QModelIndex):
        """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)
        dlg.show()
        dlg.exec()
        dlg.deleteLater()

    @Slot(name='on_merge_button_clicked')
    def __merge_result(self):
        """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[int, int]] = 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 = vpoints_configure(vpoints, input_list)
        b, d = input_list[0]
        base_angle = vpoints[b].slope_angle(vpoints[d])

        path = [[] 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]) == tuple:
                        path[i].append(coord[1])
                    else:
                        path[i].append(coord)
        return path

    @Slot(name='on_result_clipboard_clicked')
    def __copy_result_text(self):
        """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):
        """Save as new profile to collection widget."""
        if not self.mech_params:
            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_params = deepcopy(self.mech_params)
        for key in ('Placement', 'Target'):
            for mp in mech_params[key]:
                mech_params[key][mp] = None

        self.collections[name] = mech_params
        self.workbook_no_save()

    @Slot(name='on_load_profile_clicked')
    def __load_profile(self):
        """Load profile from collections dialog."""
        dlg = CollectionsDialog(self.collections, self.get_collection,
                                self.workbook_no_save,
                                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]):
        """Set profile to sub-widgets."""
        self.__clear_settings()
        self.mech_params = deepcopy(params)
        expression: str = self.mech_params['Expression']
        self.expression_string.setText(expression)
        target: Dict[int, List[_Coord]] = self.mech_params['Target']
        for name in sorted(target):
            self.target_points.addItem(f"P{name}")
            path = target[name]
            if path:
                self.path[name] = path.copy()
            else:
                self.path[name] = []
        if self.target_points.count():
            self.target_points.setCurrentRow(0)

        # Parameter of link length and input angle.
        link_list = set()
        for vlink in parse_vlinks(expression):
            if len(vlink.points) < 2:
                continue
            a = vlink.points[0]
            b = vlink.points[1]
            link_list.add(f"P{a}<->P{b}")
            for c in vlink.points[2:]:
                for d in (a, b):
                    link_list.add(f"P{c}<->P{d}")
        link_count = len(link_list)

        angle_list = set()
        input_list: List[Tuple[int, int]] = self.mech_params['input']
        for b, d in input_list:
            angle_list.add(f"P{b}->P{d}")
        angle_count = len(angle_list)

        self.parameter_list.setRowCount(0)
        placement: Dict[int, Tuple[float, float,
                                   float]] = self.mech_params['Placement']
        self.parameter_list.setRowCount(
            len(placement) + link_count + angle_count)

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

        # Position.
        pos_list = parse_pos(expression)
        same: Dict[int, int] = self.mech_params['same']
        for node, ref in sorted(same.items()):
            pos_list.insert(node, pos_list[ref])
        pos: Dict[int, _Coord] = dict(enumerate(pos_list))

        row = 0
        for name in sorted(placement):
            coord = placement[name]
            self.parameter_list.setItem(row, 0, QTableWidgetItem(f"P{name}"))
            self.parameter_list.setItem(row, 1, QTableWidgetItem('Placement'))
            x, y = pos[name]
            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 25., prefix=True),
            ]):
                s.valueChanged.connect(self.update_range)
                self.parameter_list.setCellWidget(row, i + 2, s)
            row += 1

        # Default value of upper and lower.
        for name in ('upper', 'lower'):
            if name not in self.mech_params:
                self.mech_params[name] = [0.] * (link_count + angle_count)

        upper_list: List[float] = self.mech_params['upper']
        lower_list: List[float] = self.mech_params['lower']

        def set_by_center(
                index: int,
                get_range: Callable[[], float]) -> Callable[[float], None]:
            """Return a slot function use to set limit value by center."""
            @Slot(float)
            def func(value: float):
                range_value = get_range()
                upper_list[index] = value + range_value
                lower_list[index] = value - range_value

            return func

        def set_by_range(
                index: int,
                get_value: Callable[[], float]) -> Callable[[float], None]:
            """Return a slot function use to set limit value by range."""
            @Slot(float)
            def func(value: float):
                center = get_value()
                upper_list[index] = center + value
                lower_list[index] = center - value

            return func

        for i, name in enumerate(sorted(link_list) + sorted(angle_list)):
            name_item = QTableWidgetItem(name)
            name_item.setToolTip(name)
            self.parameter_list.setItem(row, 0, name_item)
            if name in link_list:
                type_name = "Link"
            else:
                type_name = "Input"
            self.parameter_list.setItem(row, 1, QTableWidgetItem(type_name))
            # Set values (it will be same if not in the 'mech_params').
            upper = upper_list[i]
            if upper == 0.:
                upper = 100. if name in link_list else 360.
            lower = lower_list[i]
            if lower == 0. and name in link_list:
                lower = 0.
            upper_list[i] = upper
            lower_list[i] = lower
            # Spin box.
            error_range = (upper - lower) / 2
            default_value = error_range + lower
            if name in link_list:
                s1 = spinbox(default_value)
            else:
                s1 = spinbox(default_value, maximum=360.)
            self.parameter_list.setCellWidget(row, 2, s1)
            s2 = spinbox(error_range, prefix=True)
            self.parameter_list.setCellWidget(row, 4, s2)
            # Signal connections.
            s1.valueChanged.connect(set_by_center(i, s2.value))
            s2.valueChanged.connect(set_by_range(i, s1.value))
            row += 1

        self.preview_canvas.from_profile(self.mech_params)
        self.update_range()

        self.profile_name.setText(profile_name)

        # Default value of algorithm option.
        if 'settings' in self.mech_params:
            self.alg_options.update(self.mech_params['settings'])
        else:
            self.__set_algorithm_default()

        self.__able_to_generate()

    @Slot(name='on_result_load_settings_clicked')
    def __load_result_settings(self):
        """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]
        if result['Algorithm'] == str(AlgorithmType.RGA):
            self.type0.setChecked(True)
        elif result['Algorithm'] == str(AlgorithmType.Firefly):
            self.type1.setChecked(True)
        elif result['Algorithm'] == str(AlgorithmType.DE):
            self.type2.setChecked(True)
        # Copy to mechanism params.
        self.__set_profile("External setting", result)
        self.__set_time(result['time'])
        # Load settings.
        self.alg_options.clear()
        self.alg_options.update(result['settings'])

    @Slot(name='on_type0_clicked')
    @Slot(name='on_type1_clicked')
    @Slot(name='on_type2_clicked')
    def __set_algorithm_default(self):
        """Set the algorithm settings to default."""
        self.alg_options.clear()
        self.alg_options.update(defaultSettings)
        if self.type0.isChecked():
            self.alg_options.update(GeneticPrams)
        elif self.type1.isChecked():
            self.alg_options.update(FireflyPrams)
        elif self.type2.isChecked():
            self.alg_options.update(DifferentialPrams)

    @Slot(name='on_advance_button_clicked')
    def __show_advance(self):
        """Get the settings from advance dialog."""
        if self.type0.isChecked():
            type_num = AlgorithmType.RGA
        elif self.type1.isChecked():
            type_num = AlgorithmType.Firefly
        else:
            type_num = AlgorithmType.DE
        dlg = AlgorithmOptionDialog(type_num, 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")

        def from_table(row: int) -> Union[int, float]:
            """Get algorithm data from table."""
            return dlg.alg_table.cellWidget(row, 1).value()

        pop_size = dlg.pop_size.value()
        if type_num == AlgorithmType.RGA:
            self.alg_options['nPop'] = pop_size
            for i, tag in enumerate(('pCross', 'pMute', 'pWin', 'bDelta')):
                self.alg_options[tag] = from_table(i)
        elif type_num == AlgorithmType.Firefly:
            self.alg_options['n'] = pop_size
            for i, tag in enumerate(('alpha', 'beta_min', 'gamma', 'beta0')):
                self.alg_options[tag] = from_table(i)
        elif type_num == AlgorithmType.DE:
            self.alg_options['NP'] = pop_size
            for i, tag in enumerate(('strategy', 'F', 'CR')):
                self.alg_options[tag] = from_table(i)

        dlg.deleteLater()

    @Slot()
    def update_range(self):
        """Update range values to main canvas."""
        def t(x: int, y: int) -> Union[str, float]:
            item = self.parameter_list.item(x, y)
            if item is None:
                return self.parameter_list.cellWidget(x, y).value()
            else:
                return item.text()

        self.update_ranges({
            t(row, 0): (t(row, 2), t(row, 3), 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):
        """Copy profile expression."""
        text = self.expression_string.text()
        if text:
            QApplication.clipboard().setText(text)