Пример #1
0
class InputsWidget(QWidget, Ui_Form):
    """There has following functions:

    + Function of mechanism variables settings.
    + Path recording.
    """
    __paths: Dict[str, _Paths]
    __slider_paths: Dict[str, _SliderPaths]

    about_to_resolve = Signal()

    def __init__(self, parent: MainWindowBase):
        super(InputsWidget, self).__init__(parent)
        self.setupUi(self)
        # parent's function pointer
        self.free_move_button = parent.free_move_button
        self.entities_point = parent.entities_point
        self.entities_link = parent.entities_link
        self.vpoints = parent.vpoint_list
        self.vlinks = parent.vlink_list
        self.main_canvas = parent.main_canvas
        self.solve = parent.solve
        self.reload_canvas = parent.reload_canvas
        self.output_to = parent.output_to
        self.conflict = parent.conflict
        self.dof = parent.dof
        self.right_input = parent.right_input
        self.command_stack = parent.cmd_stack
        self.set_coords_as_current = parent.set_coords_as_current
        self.get_back_position = parent.get_back_position
        # Angle panel
        self.dial = QRotatableView(self)
        self.dial.setStatusTip("Input widget of rotatable joint.")
        self.dial.setEnabled(False)
        self.dial.value_changed.connect(self.__update_var)
        self.dial_spinbox.valueChanged.connect(self.__set_var)
        self.inputs_dial_layout.insertWidget(0, self.dial)
        # Play button
        self.variable_stop.clicked.connect(self.variable_value_reset)
        # Timer for play button
        self.inputs_play_shaft = QTimer()
        self.inputs_play_shaft.setInterval(10)
        self.inputs_play_shaft.timeout.connect(self.__change_index)
        # Change the point coordinates with current position
        self.update_pos.clicked.connect(self.set_coords_as_current)
        # Record list
        self.record_list.blockSignals(True)
        self.record_list.addItem(_AUTO_PATH)
        self.record_list.setCurrentRow(0)
        self.record_list.blockSignals(False)
        self.__paths = {_AUTO_PATH: self.main_canvas.path_preview}
        self.__slider_paths = {_AUTO_PATH: self.main_canvas.slider_path_preview}

        def slot(widget: QCheckBox) -> Callable[[int], None]:
            @Slot(int)
            def func(ind: int) -> None:
                widget.setEnabled(ind >= 0
                                  and self.vpoints[ind].type != VJoint.R)

            return func

        # Slot option
        self.plot_joint.currentIndexChanged.connect(slot(self.plot_joint_slot))
        self.wrt_joint.currentIndexChanged.connect(slot(self.wrt_joint_slot))

    def clear(self) -> None:
        """Clear function to reset widget status."""
        self.__paths = {_AUTO_PATH: self.__paths[_AUTO_PATH]}
        for _ in range(self.record_list.count() - 1):
            self.record_list.takeItem(1)
        self.variable_list.clear()

    def __set_angle_mode(self) -> None:
        """Change to angle input."""
        self.dial.set_minimum(0)
        self.dial.set_maximum(360)
        self.dial_spinbox.setMinimum(0)
        self.dial_spinbox.setMaximum(360)

    def __set_unit_mode(self) -> None:
        """Change to unit input."""
        self.dial.set_minimum(-500)
        self.dial.set_maximum(500)
        self.dial_spinbox.setMinimum(-500)
        self.dial_spinbox.setMaximum(500)

    def paths(self) -> Mapping[str, _Paths]:
        """Return current path data."""
        return _no_auto_path(self.__paths)

    def slider_paths(self) -> Mapping[str, _SliderPaths]:
        """Return current path data."""
        return _no_auto_path(self.__slider_paths)

    @Slot(tuple)
    def set_selection(self, selections: Sequence[int]) -> None:
        """Set one selection from canvas."""
        self.joint_list.setCurrentRow(selections[0])

    @Slot()
    def clear_selection(self) -> None:
        """Clear the points selection."""
        self.driver_list.clear()
        self.joint_list.setCurrentRow(-1)

    @Slot(int, name='on_joint_list_currentRowChanged')
    def __update_relate_points(self, _=None) -> None:
        """Change the point row from input widget."""
        self.driver_list.clear()
        item: Optional[QListWidgetItem] = self.joint_list.currentItem()
        if item is None:
            return
        p0 = _variable_int(item.text())
        base_point = self.vpoints[p0]
        type_int = base_point.type
        if type_int == VJoint.R:
            for i, vpoint in enumerate(self.vpoints):
                if i == p0:
                    continue
                if base_point.same_link(vpoint):
                    if base_point.grounded() and vpoint.grounded():
                        continue
                    self.driver_list.addItem(f"[{vpoint.type_str}] Point{i}")
        elif type_int in {VJoint.P, VJoint.RP}:
            self.driver_list.addItem(f"[{base_point.type_str}] Point{p0}")

    @Slot(int, name='on_driver_list_currentRowChanged')
    def __set_add_var_enabled(self, _=None) -> None:
        """Set enable of 'add variable' button."""
        driver = self.driver_list.currentIndex()
        self.variable_add.setEnabled(driver != -1)

    @Slot(name='on_variable_add_clicked')
    def __add_inputs_variable(
        self,
        p0: Optional[int] = None,
        p1: Optional[int] = None
    ) -> None:
        """Add variable with '->' sign."""
        if p0 is None:
            item: Optional[QListWidgetItem] = self.joint_list.currentItem()
            if item is None:
                return
            p0 = _variable_int(item.text())
        if p1 is None:
            item = self.driver_list.currentItem()
            if item is None:
                return
            p1 = _variable_int(item.text())
        # Check DOF
        if self.dof() <= self.input_count():
            QMessageBox.warning(
                self,
                "Wrong DOF",
                "The number of variable must no more than degrees of freedom."
            )
            return
        # Check same link
        if not self.vpoints[p0].same_link(self.vpoints[p1]):
            QMessageBox.warning(
                self,
                "Wrong pair",
                "The base point and driver point should at the same link."
            )
            return
        # Check repeated pairs
        for p0_, p1_, _ in self.input_pairs():
            if {p0, p1} == {p0_, p1_} and self.vpoints[p0].type == VJoint.R:
                QMessageBox.warning(
                    self,
                    "Wrong pair",
                    "There already have a same pair."
                )
                return
        if p0 == p1:
            # One joint by offset
            value = self.vpoints[p0].true_offset()
        else:
            # Two joints by angle
            value = self.vpoints[p0].slope_angle(self.vpoints[p1])
        self.command_stack.push(AddInput('->'.join((
            f'Point{p0}',
            f"Point{p1}",
            f"{value:.02f}",
        )), self.variable_list))

    def add_inputs_variables(self, variables: _Vars) -> None:
        """Add from database."""
        for p0, p1 in variables:
            self.__add_inputs_variable(p0, p1)

    @Slot(QListWidgetItem, name='on_variable_list_itemClicked')
    def __dial_ok(self, _=None) -> None:
        """Set the angle of base link and drive link."""
        if self.inputs_play_shaft.isActive():
            return
        row = self.variable_list.currentRow()
        enabled = row > -1
        is_rotatable = (
            enabled
            and not self.free_move_button.isChecked()
            and self.right_input()
        )
        self.dial.setEnabled(is_rotatable)
        self.dial_spinbox.setEnabled(is_rotatable)
        self.oldVar = self.dial.value()
        self.variable_play.setEnabled(is_rotatable)
        self.variable_speed.setEnabled(is_rotatable)
        item: Optional[QListWidgetItem] = self.variable_list.currentItem()
        if item is None:
            return
        expr = item.text().split('->')
        p0 = int(expr[0].replace('Point', ''))
        p1 = int(expr[1].replace('Point', ''))
        value = float(expr[2])
        if p0 == p1:
            self.__set_unit_mode()
        else:
            self.__set_angle_mode()
        self.dial.set_value(value if enabled else 0)

    def variable_excluding(self, row: Optional[int] = None) -> None:
        """Remove variable if the point was been deleted. Default: all."""
        one_row: bool = row is not None
        for i, (b, d, _) in enumerate(self.input_pairs()):
            # If this is not origin point any more
            if one_row and row != b:
                continue
            self.command_stack.push(DeleteInput(i, self.variable_list))

    @Slot(name='on_variable_remove_clicked')
    def remove_var(self, row: int = -1) -> None:
        """Remove and reset angle."""
        if row == -1:
            row = self.variable_list.currentRow()
        if not row > -1:
            return
        self.variable_stop.click()
        self.command_stack.push(DeleteInput(row, self.variable_list))
        self.get_back_position()
        self.solve()

    def interval(self) -> float:
        """Return interval value."""
        return self.record_interval.value()

    def input_count(self) -> int:
        """Use to show input variable count."""
        return self.variable_list.count()

    def input_pairs(self) -> Iterator[Tuple[int, int, float]]:
        """Back as point number code."""
        for row in range(self.variable_list.count()):
            var = self.variable_list.item(row).text().split('->')
            p0 = int(var[0].replace('Point', ''))
            p1 = int(var[1].replace('Point', ''))
            angle = float(var[2])
            yield p0, p1, angle

    def variable_reload(self) -> None:
        """Auto check the points and type."""
        self.joint_list.clear()
        self.plot_joint.clear()
        self.wrt_joint.clear()
        for i in range(self.entities_point.rowCount()):
            type_text = self.entities_point.item(i, 2).text()
            for w in [self.joint_list, self.plot_joint, self.wrt_joint]:
                w.addItem(f"[{type_text}] Point{i}")
        self.variable_value_reset()

    @Slot(float)
    def __set_var(self, value: float) -> None:
        self.dial.set_value(value)

    @Slot(float)
    def __update_var(self, value: float) -> None:
        """Update the value when rotating QDial."""
        item = self.variable_list.currentItem()
        self.dial_spinbox.blockSignals(True)
        self.dial_spinbox.setValue(value)
        self.dial_spinbox.blockSignals(False)
        if item:
            item_text = item.text().split('->')
            item_text[-1] = f"{value:.02f}"
            item.setText('->'.join(item_text))
            self.about_to_resolve.emit()
        if (
            self.record_start.isChecked()
            and abs(self.oldVar - value) > self.record_interval.value()
        ):
            self.main_canvas.record_path()
            self.oldVar = value

    def variable_value_reset(self) -> None:
        """Reset the value of QDial."""
        if self.inputs_play_shaft.isActive():
            self.variable_play.setChecked(False)
            self.inputs_play_shaft.stop()
        self.get_back_position()
        for i, (p0, p1, _) in enumerate(self.input_pairs()):
            self.variable_list.item(i).setText('->'.join([
                f'Point{p0}',
                f'Point{p1}',
                f"{self.vpoints[p0].slope_angle(self.vpoints[p1]):.02f}",
            ]))
        self.__dial_ok()
        self.solve()

    @Slot(bool, name='on_variable_play_toggled')
    def __play(self, toggled: bool) -> None:
        """Triggered when play button was changed."""
        self.dial.setEnabled(not toggled)
        self.dial_spinbox.setEnabled(not toggled)
        if toggled:
            self.inputs_play_shaft.start()
        else:
            self.inputs_play_shaft.stop()

    @Slot()
    def __change_index(self) -> None:
        """QTimer change index."""
        index = self.dial.value()
        speed = self.variable_speed.value()
        extreme_rebound = (
            self.conflict.isVisible()
            and self.extremeRebound.isChecked()
        )
        if extreme_rebound:
            speed = -speed
            self.variable_speed.setValue(speed)
        index += speed * 0.06 * (3 if extreme_rebound else 1)
        self.dial.set_value(index)

    @Slot(bool, name='on_record_start_toggled')
    def __start_record(self, toggled: bool) -> None:
        """Save to file path data."""
        if toggled:
            self.main_canvas.record_start(int(
                self.dial_spinbox.maximum() / self.record_interval.value()
            ))
            return
        path, path_slider = self.main_canvas.get_record_path()
        name, ok = QInputDialog.getText(
            self,
            "Recording completed!",
            "Please input name tag:"
        )
        i = 0
        name = name or f"Record_{i}"
        while name in self.__paths:
            name = f"Record_{i}"
            i += 1
        QMessageBox.information(self, "Record",
                                "The name tag is being used or empty.")
        self.add_path(name, path, path_slider)

    def add_path(self, name: str, path: _Paths, slider: _SliderPaths) -> None:
        """Add path function."""
        self.command_stack.push(AddPath(
            self.record_list,
            name,
            self.__paths,
            self.__slider_paths,
            path,
            slider
        ))
        self.record_list.setCurrentRow(self.record_list.count() - 1)

    def load_paths(self, paths: Mapping[str, _Paths],
                   slider_paths: Mapping[str, _SliderPaths]) -> None:
        """Add multiple paths."""
        for name, path in paths.items():
            self.add_path(name, path, slider_paths.get(name, {}))

    @Slot(name='on_record_remove_clicked')
    def __remove_path(self) -> None:
        """Remove path data."""
        row = self.record_list.currentRow()
        if not row > 0:
            return
        self.command_stack.push(DeletePath(
            row,
            self.record_list,
            self.__paths,
            self.__slider_paths
        ))
        self.record_list.setCurrentRow(self.record_list.count() - 1)
        self.reload_canvas()

    @Slot(QListWidgetItem, name='on_record_list_itemDoubleClicked')
    def __path_dlg(self, item: QListWidgetItem) -> None:
        """View path data."""
        name = item.text().split(":", maxsplit=1)[0]
        try:
            paths = self.__paths[name]
        except KeyError:
            return
        points_text = ", ".join(f"Point{i}" for i in range(len(paths)))
        if QMessageBox.question(
            self,
            "Path data",
            f"This path data including {points_text}.",
            (QMessageBox.Save | QMessageBox.Close),
            QMessageBox.Close
        ) != QMessageBox.Save:
            return
        file_name = self.output_to(
            "path data",
            ["Comma-Separated Values (*.csv)", "Text file (*.txt)"]
        )
        if not file_name:
            return
        with open(file_name, 'w+', encoding='utf-8', newline='') as stream:
            w = writer(stream)
            for path in paths:
                for point in path:
                    w.writerow(point)
                w.writerow(())
        logger.info(f"Output path data: {file_name}")

    def __current_path_name(self) -> str:
        """Return the current path name."""
        return self.record_list.currentItem().text().split(':', maxsplit=1)[0]

    @Slot(name='on_copy_path_clicked')
    def __copy_path(self):
        """Copy path from record list."""
        name = self.__current_path_name()
        num = 0
        name_copy = f"{name}_{num}"
        while name_copy in self.__paths:
            name_copy = f"{name}_{num}"
            num += 1
        self.add_path(name_copy, copy(self.__paths[name]), {})

    @Slot(name='on_cp_data_button_clicked')
    def __copy_path_data(self) -> None:
        """Copy current path data to clipboard."""
        data = self.__paths[self.__current_path_name()]
        if not data:
            return
        QApplication.clipboard().setText('\n'.join(
            f"[{x}, {y}]," for x, y in data[self.plot_joint.currentIndex()]
        ))

    @Slot(name='on_show_button_clicked')
    def __show_path(self) -> None:
        """Show specified path."""
        self.main_canvas.set_path_show(self.plot_joint.currentIndex())

    @Slot(name='on_show_all_button_clicked')
    def __show_all_path(self) -> None:
        """Show all paths."""
        self.record_show.setChecked(True)
        self.main_canvas.set_path_show(-1)

    @Slot(bool, name='on_record_show_toggled')
    def __set_path_show(self, toggled: bool) -> None:
        """Show all paths or hide."""
        self.main_canvas.set_path_show(-1 if toggled else -2)

    @Slot(int, name='on_record_list_currentRowChanged')
    def __set_path(self, _=None) -> None:
        """Reload the canvas when switch the path."""
        if not self.record_show.isChecked():
            self.record_show.setChecked(True)
        self.reload_canvas()

    def current_path(self) -> Tuple[_Paths, _SliderPaths]:
        """Return current path data to main canvas.

        + No path.
        + Show path data.
        + Auto preview.
        """
        row = self.record_list.currentRow()
        if row in {0, -1}:
            return (), {}
        name = self.record_list.item(row).text().split(':')[0]
        return self.__paths.get(name, ()), self.__slider_paths.get(name, {})

    @Slot(name='on_variable_up_clicked')
    @Slot(name='on_variable_down_clicked')
    def __set_variable_priority(self) -> None:
        row = self.variable_list.currentRow()
        if not row > -1:
            return
        item = self.variable_list.currentItem()
        self.variable_list.insertItem(
            row + (-1 if self.sender() == self.variable_up else 1),
            self.variable_list.takeItem(row)
        )
        self.variable_list.setCurrentItem(item)

    @Slot(name='on_animate_button_clicked')
    def __animate(self) -> None:
        """Make a motion animation."""
        name = self.__current_path_name()
        data = self.__paths.get(name, [])
        if not data:
            return
        dlg = AnimateDialog(self.vpoints, self.vlinks, data,
                            self.__slider_paths.get(name, {}),
                            self.main_canvas.monochrome, self)
        dlg.show()
        dlg.exec_()
        dlg.deleteLater()

    @Slot(name='on_plot_button_clicked')
    def __plot(self) -> None:
        """Plot the data. Show the X and Y axes as two line."""
        joint = self.plot_joint.currentIndex()
        name = self.__current_path_name()
        data = self.__paths.get(name, [])
        slider_data = self.__slider_paths.get(name, {})
        if not data:
            return
        if self.plot_joint_slot.isChecked():
            pos = array(slider_data.get(joint, []))
        else:
            pos = array(data[joint])
        if self.wrt_label.isChecked():
            joint_wrt = self.wrt_joint.currentIndex()
            if self.wrt_joint_slot.isChecked():
                pos[:] -= array(slider_data.get(joint_wrt, []))
            else:
                pos[:] -= array(data[joint_wrt])
        plot = {}
        row = 0
        for button, value in [
            (self.plot_pos, lambda: pos),
            (self.plot_vel, vel := lambda: derivative(pos)),
            (self.plot_acc, acc := lambda: derivative(vel())),
            (self.plot_jerk, lambda: derivative(acc())),
            (self.plot_curvature, cur := lambda: curvature(data[joint])),
            (self.plot_signature, lambda: path_signature(cur())),
            (self.plot_norm, lambda: norm_path(pos)),
            (self.plot_norm_pca, lambda: norm_pca(pos)),
            (self.plot_fourier, lambda: _fourier(pos)),
        ]:  # type: QCheckBox, Callable[[], ndarray]
            if button.isChecked():
                row += 1
                plot[button.text()] = value()
        if row < 1:
            QMessageBox.warning(self, "No target", "No any plotting target.")
            return
        polar = self.p_coord_sys.isChecked()
        col = 1
        if polar:
            row, col = col, row
        dlg = DataChartDialog(self, "Analysis", row, col, polar)
        dlg.setWindowIcon(QIcon(QPixmap("icons:formula.png")))
        ax = dlg.ax()
        for p, (title, xy) in enumerate(plot.items()):
            ax_i = ax[p]
            ax_i.set_title(title)
            if title == "Path Signature":
                ax_i.plot(xy[:, 0], xy[:, 1])
                ax_i.set_ylabel(r"$\kappa$")
                ax_i.set_xlabel(r"$\int|\kappa|dt$")
            elif xy.ndim == 2:
                x = xy[:, 0]
                y = xy[:, 1]
                if self.c_coord_sys.isChecked():
                    ax_i.plot(x, label='x')
                    ax_i.plot(y, label='y')
                    ax_i.legend()
                else:
                    r = hypot(x, y)
                    theta = arctan2(y, x)
                    ax_i.plot(theta, r, linewidth=5)
            else:
                ax_i.plot(xy)
        dlg.set_margin(0.2)
        dlg.show()
        dlg.exec_()
        dlg.deleteLater()
Пример #2
0
 def __fitting_preview(self) -> None:
     """Curve fitting preview."""
     dlg = DataChartDialog(self, "Preview")
     ax = dlg.ax()[0]
     ax.plot(self.path[:, 0], self.path[:, 1], 'ro')
     path = self.__gen_fitting()
     ax.plot(path[:, 0], path[:, 1], 'b--')
     dlg.set_margin(0.2)
     dlg.show()
     dlg.exec()
     dlg.deleteLater()
Пример #3
0
 def __plot(self) -> None:
     """Plot the data. Show the X and Y axises as two line."""
     joint = self.plot_joint.currentIndex()
     name = self.__current_path_name()
     data = self.__paths.get(name, [])
     slider_data = self.__slider_paths.get(name, {})
     if not data:
         return
     if self.plot_joint_slot.isChecked():
         pos = array(slider_data.get(joint, []))
     else:
         pos = array(data[joint])
     if self.wrt_label.isChecked():
         joint_wrt = self.wrt_joint.currentIndex()
         if self.wrt_joint_slot.isChecked():
             pos[:] -= array(slider_data.get(joint_wrt, []))
         else:
             pos[:] -= array(data[joint_wrt])
     vel = derivative(pos)
     acc = derivative(vel)
     cur = curvature(data[joint])
     plot = {}
     plot_count = 0
     if self.plot_pos.isChecked():
         plot_count += 1
         plot["Position"] = pos
     if self.plot_vel.isChecked():
         plot_count += 1
         plot["Velocity"] = vel
     if self.plot_acc.isChecked():
         plot_count += 1
         plot["Acceleration"] = acc
     if self.plot_jerk.isChecked():
         plot_count += 1
         plot["Jerk"] = derivative(acc)
     if self.plot_curvature.isChecked():
         plot_count += 1
         plot["Curvature"] = cur
     if self.plot_signature.isChecked():
         plot_count += 1
         plot["Path Signature"] = path_signature(cur)
     if plot_count < 1:
         QMessageBox.warning(self, "No target", "No any plotting target.")
         return
     polar = self.p_coord_sys.isChecked()
     row = plot_count
     col = 1
     if polar:
         row, col = col, row
     dlg = DataChartDialog(self, "Analysis", row, col, polar)
     dlg.setWindowIcon(QIcon(QPixmap(":/icons/formula.png")))
     ax = dlg.ax()
     for p, (title, xy) in enumerate(plot.items()):
         ax_i = ax[p]
         ax_i.set_title(title)
         if title == "Path Signature":
             ax_i.plot(xy[:, 0], xy[:, 1])
             ax_i.set_ylabel(r"$\kappa$")
             ax_i.set_xlabel(r"$\int|\kappa|dt$")
         elif xy.ndim == 2:
             x = xy[:, 0]
             y = xy[:, 1]
             if self.c_coord_sys.isChecked():
                 ax_i.plot(x, label='x')
                 ax_i.plot(y, label='y')
                 ax_i.legend()
             else:
                 r = hypot(x, y)
                 theta = arctan2(y, x)
                 ax_i.plot(theta, r, linewidth=5)
         else:
             ax_i.plot(xy)
     dlg.set_margin(0.2)
     dlg.show()
     dlg.exec_()
     dlg.deleteLater()
Пример #4
0
 def __cc_plot(self):
     """Plot cross correlation."""
     p = int(self.plot_joint.currentText().replace('P', ''))
     target = self.canvas2.get_target()
     ans = self.canvas2.get_path()
     dlg = DataChartDialog(self, "Cross Correlation", 3)
     ax = dlg.ax()
     c1 = curvature(ans[p])
     c2 = curvature(target[p])
     ps1 = path_signature(c1)
     ps2 = path_signature(c2)
     cc = cross_correlation(ps1, ps2, 0.1)
     ps2[:, 0] += cc.argmax() * 0.1
     ax[0].set_title(f"Cross Correlation of Point{p}")
     ax[0].plot(cc)
     ax[1].set_title("Curvature")
     ax[1].plot(c1, label=f"Point{p}")
     ax[1].plot(c2, label=f"Target Path")
     ax[1].legend()
     ax[2].set_title("Path Signature")
     ax[2].plot(ps1[:, 0], ps1[:, 1], label=f"Point{p}")
     ax[2].plot(ps2[:, 0], ps2[:, 1], label=f"Target Path")
     ax[2].legend()
     dlg.set_margin(0.2)
     dlg.show()
     dlg.exec_()
     dlg.deleteLater()
Пример #5
0
 def __cc_plot(self):
     """Plot cross correlation."""
     p = int(self.plot_joint.currentText().replace('P', ''))
     target = self.canvas2.get_target()
     c1 = curvature(self.canvas2.get_path()[p])
     c2 = curvature(target[p])
     p1 = path_signature(c1)
     p2 = path_signature(c2, 100 - 100 / (len(target[p]) + 1))
     cc = cross_correlation(p1, p2, _U)
     cc_argmax = cc.argmax()
     p2[:, 0] += cc_argmax * _U
     m_p1 = p1.copy()
     m_p1[:, 0] += p1.max()
     p1 = concatenate((p1, m_p1), axis=0)
     del m_p1
     dlg = DataChartDialog(self, "Cross Correlation", 3)
     ax = dlg.ax()
     ax[0].set_title("Curvature")
     ax[0].plot(c1, label=f"Point{p}")
     ax[0].plot(c2, label=f"Target Path")
     ax[0].legend()
     ax[1].set_title("Path Signature")
     ax[1].plot(p1[:, 0], p1[:, 1], label=f"Point{p}")
     ax[1].plot(p2[:, 0], p2[:, 1], label=f"Target Path")
     ax[1].plot(cc_argmax * _U, p2[0, 1], 'ro', label=f"Shift Origin")
     ax[1].legend()
     ax[2].set_title(f"Cross Correlation of Point{p}")
     ax[2].plot(linspace(0, len(cc) * _U, len(cc)), cc)
     ax[2].plot(cc_argmax * _U, cc[cc_argmax], 'ro')
     dlg.set_margin(0.2)
     dlg.show()
     dlg.exec_()
     dlg.deleteLater()