Exemple #1
0
class RosBagWidget(QtGui.QWidget):

    ##
    # @brief
    #
    # @return

    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.file_name = ''
        self.bag = None
        self.data = {}
        self.plot_data = {}
        self.gridLayout = None
        self.label1 = None
        self.loadBtn = None
        self.tree = None
        self.addBtn = None
        self.editBtn = None
        self.removeBtn = None
        self.plotBtn = None
        self.plotTree = None
        self.plotData = None
        self.plotwin = None
        self.plot_num = 1
        self.params = [{
            'type':
            'group',
            'name':
            u'plot params',
            'children': [{
                'type': 'str',
                'name': 'x axis name',
                'value': u'time(s)'
            }, {
                'type': 'str',
                'name': 'y axis name',
                'value': u'position(m)'
            }, {
                'type': 'str',
                'name': 'plot name',
                'value': u'plot name'
            }]
        }]
        self.line_mark_list = [
            '+-', '*-', 'o-', '^-', 's-', 'x-', '.-', 'p-', 'h-', 'd-'
        ]
        self.msg_types = []
        self.topics = {}
        self.topics_data = {}
        self.msg_types_blacklist = [
            'sensor_msgs/PointCloud', 'sensor_msgs/Image'
        ]
        self.setup_ui(self)

    ##
    # @brief
    #
    # @param Form
    #
    # @return
    def setup_ui(self, Form):
        self.setObjectName("Form")
        self.resize(217, 499)
        self.gridLayout = QtGui.QGridLayout(self)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setVerticalSpacing(0)
        self.gridLayout.setObjectName("gridLayout")
        self.label1 = QtGui.QLabel("")
        self.label1.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
        self.label1.setStyleSheet("border:1px solid black;")
        self.gridLayout.addWidget(self.label1, 0, 0)
        self.loadBtn = QtGui.QPushButton("...")
        self.loadBtn.setFixedWidth(20)
        self.gridLayout.addWidget(self.loadBtn, 0, 1)
        self.loadBtn.clicked.connect(self.load_file)
        self.tree = DataTreeWidget(data=self.data)
        self.gridLayout.addWidget(self.tree, 1, 0, 1, 2)
        self.addBtn = QtGui.QPushButton("")
        add_icon = QtGui.QIcon()
        add_icon.addPixmap(QtGui.QPixmap("resources/add.png"),
                           QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.addBtn.setIcon(add_icon)
        self.addBtn.clicked.connect(self.add_plot)
        self.gridLayout.addWidget(self.addBtn, 0, 2)
        self.editBtn = QtGui.QPushButton("")
        edit_icon = QtGui.QIcon()
        edit_icon.addPixmap(QtGui.QPixmap("resources/edit.png"),
                            QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.editBtn.setIcon(edit_icon)
        self.editBtn.clicked.connect(self.edit_plot)
        self.gridLayout.addWidget(self.editBtn, 0, 3)
        self.removeBtn = QtGui.QPushButton("")
        remove_icon = QtGui.QIcon()
        remove_icon.addPixmap(QtGui.QPixmap("resources/remove.png"),
                              QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.removeBtn.setIcon(remove_icon)
        self.removeBtn.clicked.connect(self.remove_plot)
        self.gridLayout.addWidget(self.removeBtn, 0, 4)
        self.plotBtn = QtGui.QPushButton("plot")
        # plot_icon = QtGui.QIcon()
        # plot_icon.addPixmap(QtGui.QPixmap("resources/plot.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        # self.plotBtn.setIcon(plot_icon)
        self.plotBtn.clicked.connect(self.plot)
        self.gridLayout.addWidget(self.plotBtn, 0, 5)
        self.plotTree = ParameterTree()
        self.plotData = Parameter.create(name='params',
                                         type='group',
                                         children=self.params)
        self.plotTree.setParameters(self.plotData, showTop=False)
        self.gridLayout.addWidget(self.plotTree, 1, 2, 1, 4)
        # self.tree2.setSizePolicity(self.tree1.width()/2, 499)
        # self.tree1.resize(setWidth(150)
        # self.tree2.setWidth(50)
        self.plotData.sigTreeStateChanged.connect(self.change)

    ##
    # @brief
    #
    # @param param
    # @param changes
    #
    # @return
    def change(self, param, changes):  # todo change plot params
        print("tree changes:(to do)")
        for param, change, data in changes:
            path = self.plotData.childPath(param)
            print(path)
            if path is not None:
                childName = '.'.join(path)
            else:
                childName = param.name()
            print('  parameter: %s' % childName)
            print('  change:    %s' % change)
            print('  data:      %s' % str(data))
            print('  ----------')

    ##
    # @brief
    #
    # @param file_name
    # @param start_dir
    #
    # @return
    def load_file(self, file_name=None, start_dir=None):
        """Load a rosbag (*.bag) file.
        """
        start = time.clock()
        if not file_name:
            if not start_dir:
                start_dir = '.'
            self.file_dialog = FileDialog(None, "Load rosbag ..", start_dir,
                                          "rosbag (*.bag)")
            # file_dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
            self.file_dialog.show()
            self.file_dialog.fileSelected.connect(self.load_file)
            return
        file_name = unicode(file_name)
        self.file_name = file_name
        self.label1.setText(str(file_name))
        self.file_info(file_name)
        end = time.clock()
        print("load file cost %s s" % (end - start))

    ##
    # @brief
    #
    # @param file_name
    #
    # @return
    def file_info(self, file_name=None):
        if not file_name:
            self.data = {}
        else:
            self.bag = rosbag.Bag(file_name)
            self.tree.setData(self.bag.get_type_and_topic_info()[1])
            # print(self.bag.get_type_and_topic_info()[1])
            bag_length = self.get_bag_length(
                self.bag.get_type_and_topic_info()[1]) * 10
            if len(file_name) * 8 > bag_length:
                bag_length = len(file_name) * 8
            self.label1.setFixedWidth(bag_length)
            # clear data
            self.msg_types = []
            self.topics = {}
            self.topics_data = {}
            # print(self.get_bag_length(self.bag.get_type_and_topic_info()[1]))
            for m in self.bag.get_type_and_topic_info()[0]:
                self.msg_types.append(m)
            for t in self.bag.get_type_and_topic_info()[1]:
                if self.bag.get_type_and_topic_info(
                )[1][t][0] not in self.msg_types_blacklist:
                    get_topic_params_cmd = "rostopic echo -b " + file_name + " -n 1 -p " + t
                    topic_parms = os.popen(get_topic_params_cmd).read()
                    topic_parms_list = self.topic_str2list(topic_parms)
                    self.topics[t] = topic_parms_list
                    get_topic_data_cmd = "rostopic echo -b " + self.file_name + " -p " + t
                    topic_data = os.popen(get_topic_data_cmd).read()
                    topic_data_list = self.topic_data_str2list(topic_data[:-1])
                    self.topics_data[t] = topic_data_list
                else:
                    print(t + "is not proper for plot")
                # print("topic_data_list", topic_data_list)
                # break

    @staticmethod
    ##
    # @brief
    #
    # @param data
    #
    # @return
    def get_bag_length(data):
        max_topic_len = 0
        max_topic_type_len = 0
        for topic in data:
            if len(topic) > max_topic_len:
                max_topic_len = len(topic)
            if len(data[topic][0]) > max_topic_type_len:
                max_topic_type_len = len(data[topic][0])
        # print(max_topic_len, max_topic_type_len)
        return max_topic_len + max_topic_type_len

    @staticmethod
    ##
    # @brief
    #
    # @param topic_str
    #
    # @return
    def topic_str2list(topic_str):
        # print(len(topic_str))
        # print(topic_str)
        topic_list = []
        if len(topic_str) > 0:
            # print(topic_str[0])
            out1 = re.split('\n', topic_str)
            params = re.split(',', out1[0])
            values = re.split(',', out1[1])
            # print(len(params), len(values))
            for i in range(len(params)):
                if values[i].strip('-').replace(".", '').isdigit():
                    topic_list.append(params[i])
            # print(topic_list)
            return topic_list

    @staticmethod
    ##
    # @brief
    #
    # @param topic_data_str
    #
    # @return
    def topic_data_str2list(topic_data_str):
        topic_data_list = []
        if len(topic_data_str) > 0:
            out1 = re.split('\n', topic_data_str)
            for o in out1:
                data_list = re.split(',', o)
                topic_data_list.append(data_list)
            return topic_data_list

    ##
    # @brief
    #
    # @return
    def add_plot(self):
        if len(self.file_name):
            #print("add plot")
            self.plotwin = PlotWin(self.topics, self.plot_num)
            self.plotwin.setWindowModality(QtCore.Qt.ApplicationModal)
            self.plotwin.setGeometry(500, 500, 600, 150)
            if self.plotwin.exec_():  # accepted
                #print("ok")
                self.plot_num = self.plot_num + 1
                if len(self.plotwin.plot_name.text()) > 0:
                    self.plotwin.param['name'] = unicode(
                        self.plotwin.plot_name.text()) + " " + unicode(
                            self.plotwin.topic_name.currentText())
                    self.plotwin.param['type'] = 'group'
                    x = {
                        'name': 'x',
                        'type': 'str',
                        'value': unicode(self.plotwin.field1.currentText())
                    }
                    y = {
                        'name': 'y',
                        'type': 'str',
                        'value': unicode(self.plotwin.field2.currentText())
                    }
                    color = self.plotwin.colorBtn.color(mode='byte')
                    color_str = ''
                    for i in color[:-1]:
                        c = str(hex(i)).replace("0x", '')
                        if len(c) < 2:
                            c = '0' + c
                        color_str = color_str + c
                    # print(color, color_str)
                    line_color = {
                        'name': 'line_color',
                        'type': 'color',
                        'value': color_str,
                        'tip': "This is a color button"
                    }
                    line_mark = {
                        'name': 'line_mark',
                        'type': 'list',
                        'values': self.line_mark_list,
                        'value': unicode(self.plotwin.line_mark.currentText())
                    }
                    self.plotwin.param['children'] = [
                        x, y, line_color, line_mark
                    ]
                    self.params.append(self.plotwin.param)
                    self.plotData = Parameter.create(
                        name='params',
                        type='group',
                        children=[self.plotwin.param])
                    self.plotTree.addParameters(self.plotData, showTop=False)
                else:
                    print("Please input the plot name first.")

            else:
                print("cancel")
        else:
            print("Please add bag file first!")

    ##
    # @brief
    #
    # @return
    def edit_plot(self):
        # print("edit plot", self.plotTree.selectedItems(), self.plotTree.currentItem())
        if hasattr(self.plotTree.currentItem(), "param"):
            print("edit plot " + re.split(
                '\'', str(getattr(self.plotTree.currentItem(), "param")))[1] +
                  "(to do)")
            # if hasattr(getattr(self.plotTree.currentItem(), "param"), "name"):
            # print(getattr(getattr(self.plotTree.currentItem(), "param"), "name"))
        # print("%x" % id(self.params[0]))

    ##
    # @brief
    #
    # @return
    def remove_plot(self):
        if hasattr(self.plotTree.currentItem(), "param"):
            print("remove plot " + re.split(
                '\'', str(getattr(self.plotTree.currentItem(), "param")))[1] +
                  "(to do)")

    ##
    # @brief
    #
    # @return
    def plot(self):
        print("plot")
        plots = []
        for p in self.params[1:]:
            topic_name = re.split(' ', p['name'])
            # print(topic_name[1])
            x_name = None
            y_name = None
            line_color = None
            line_mark = None
            for c in p['children']:
                if c['name'] == 'x':
                    x_name = c['value']
                elif c['name'] == 'y':
                    y_name = c['value']
                elif c['name'] == 'line_color':
                    line_color = c['value']
                elif c['name'] == 'line_mark':
                    line_mark = c['value']
            plots.append([
                topic_name[0], topic_name[1], x_name, y_name, line_color,
                line_mark
            ])
        # print(plots)

        # plt.figure(figsize=(12, 10))
        for p in plots:
            x, y = self.get_bag_data(p)
            plt.plot(x, y, p[5], color=("#" + p[4]), label=p[0])
        plt.xlabel(self.params[0]['children'][0]['value'])
        plt.ylabel(self.params[0]['children'][1]['value'])
        plt.title(self.params[0]['children'][2]['value'])
        # plt.ylim(-1.2, 1.2)
        plt.legend(loc='upper right')
        plt.grid(True)
        # plt.savefig(gflags.FLAGS.title + '.svg', format= 'svg')
        plt.show()

    ##
    # @brief
    #
    # @param plot_info
    #
    # @return
    def get_bag_data(self, plot_info):
        x = []
        y = []
        plot_data = self.topics_data[plot_info[1]]
        x_index = plot_data[0].index(plot_info[2])
        y_index = plot_data[0].index(plot_info[3])
        # print(plot_data, x_index, y_index)
        for pd in plot_data[1:]:
            # print('len(pd)', len(pd))
            # print('x_index', x_index)
            # print('y_index', y_index)
            x.append(pd[x_index])
            y.append(pd[y_index])
            # break
        return x, y
class FunctionWidget(fw.FeatureWidget):
    """
    Subclass of FeatureWidget that defines attributes to show parameters to a given function and run the function
    with the given parameters. These should be used with the corresponding FunctionManager to run Tomography pipeline
    workflows


    Attributes
    ----------
    func_name : str
        Function name
    subfunc_name : str
        Specific function name
    input_functions : dict
        dictionary with keys being parameters of this function to be overriden, and values being a FunctionWidget
        whose function will override said parameter
    param_dict : dict
        Dictionary with parameter names and values
    _function : function
        Function object corresponding to the function represented by widget
    package : str
        name of package to which function belongs
    params : pyqtgraph.Parameter
        Parameter instance with function parameter exposed in UI
    missing_args : list of str
        Names of missing arguments not contained in param_dict

    Signals
    -------
    sigTestRange(QtGui.QWidget, str, tuple, dict)
        Emitted when parameter range test is requested. Emits the sending widget, a string with a message to log, and
        a tuple with the range values for the parameter


    Parameters
    ----------
    name : str
        generic name of function
    subname : str
        specific name of function under the generic name category
    package : python package
        package
    input_functions : dict, optional
        dictionary with keys being parameters of this function to be overriden, and values being a FunctionWidget
        whose function will override said parameter
    checkable : bool, optional
        bool to set the function to be toggled on and of when running constructed workflows
    closeable : bool, optional
        bool to set if the function can be deleted from the pipeline editor
    parent : QWidget
        parent of this FunctionWidget
    """

    sigTestRange = QtCore.Signal(QtGui.QWidget, str, tuple)

    # TODO perhaps its better to not pass in the package object but only a string, package object can be retrived from reconpkgs.packages dict
    def __init__(self,
                 name,
                 subname,
                 package,
                 input_functions=None,
                 checkable=True,
                 closeable=True,
                 parent=None):
        self.name = name
        if name != subname:
            self.name += ' (' + subname + ')'
        super(FunctionWidget, self).__init__(self.name,
                                             checkable=checkable,
                                             closeable=closeable,
                                             parent=parent)

        self.func_name = name
        self.subfunc_name = subname
        self.input_functions = {}
        self.param_dict = {}
        self._function = getattr(package, config.names[self.subfunc_name][0])

        #perhaps unnecessary
        self.package = package.__name__

        # TODO have the children kwarg be passed to __init__
        self.params = Parameter.create(
            name=self.name,
            children=config.parameters[self.subfunc_name],
            type='group',
            readonly=False)

        self.form = ParameterTree(showHeader=False)
        self.form.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.form.customContextMenuRequested.connect(self.paramMenuRequested)
        self.form.setParameters(self.params, showTop=True)

        # Initialize parameter dictionary with keys and default values
        self.updateParamsDict()
        argspec = inspect.getargspec(self._function)
        default_argnum = len(argspec[3])
        self.param_dict.update({
            key: val
            for (key, val) in zip(argspec[0][-default_argnum:], argspec[3])
        })
        for key, val in self.param_dict.iteritems():
            if key in [p.name() for p in self.params.children()]:
                self.params.param(key).setValue(val)
                self.params.param(key).setDefault(val)

        # Create a list of argument names (this will most generally be the data passed to the function)
        self.missing_args = [
            i for i in argspec[0] if i not in self.param_dict.keys()
        ]

        self.parammenu = QtGui.QMenu()
        action = QtGui.QAction('Test Parameter Range', self)
        action.triggered.connect(self.testParamTriggered)
        self.parammenu.addAction(action)

        self.previewButton.customContextMenuRequested.connect(
            self.menuRequested)
        self.menu = QtGui.QMenu()

        if input_functions is not None:
            for param, ipf in input_functions.iteritems():
                self.addInputFunction(param, ipf)

        # wire up param changed signals
        for param in self.params.children():
            param.sigValueChanged.connect(self.paramChanged)

        # change on/off icons
        icon = QtGui.QIcon()
        if checkable:
            icon.addPixmap(QtGui.QPixmap("xicam/gui/icons_51.png"),
                           QtGui.QIcon.Normal, QtGui.QIcon.Off)
            icon.addPixmap(QtGui.QPixmap("xicam/gui/icons_45.png"),
                           QtGui.QIcon.Normal, QtGui.QIcon.On)
            self.previewButton.setCheckable(True)
        else:
            icon.addPixmap(QtGui.QPixmap("xicam/gui/icons_45.png"),
                           QtGui.QIcon.Normal, QtGui.QIcon.Off)
            self.previewButton.setCheckable(False)
            self.previewButton.setChecked(True)

        self.previewButton.setIcon(icon)
        self.previewButton.setFlat(True)
        self.previewButton.setChecked(True)
        self.previewButton.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)

        self.allowed_types = {
            'str': str,
            'int': int,
            'float': float,
            'bool': bool,
            'unicode': unicode
        }

        # set widgets to never hide their subfunctions
        self.expand()

    def collapse(self):
        """
        This catches all "collapse" requests and passes them, so that FunctionWidgets are not collapsable
        """
        pass

    @property
    def enabled(self):
        """
        Boolean showing if the function widget is enabled (eye open/closed)
        """
        if self.previewButton.isChecked(
        ) or not self.previewButton.isCheckable():
            return True
        return False

    @enabled.setter
    def enabled(self, val):
        """
        Set enabled value by toggling the previewButton only if the widget is checkable
        """
        if val and self.previewButton.isCheckable():
            self.previewButton.setChecked(True)
        else:
            self.previewButton.setChecked(False)

    @property
    def exposed_param_dict(self):
        """
        Parameter dictionary with only the parameters that are shown in GUI
        """
        param_dict = {
            key: val
            for (key, val) in self.updated_param_dict.iteritems()
            if key in [param.name() for param in self.params.children()]
        }
        return param_dict

    @property
    def partial(self):
        """
        Package up all parameters into a functools.partial
        """
        return partial(self._function, **self.updated_param_dict)

    @property
    def updated_param_dict(self):
        """
        Return the param dict of the FunctionWidget updated with proper types
        """

        if hasattr(self, 'defaults'):
            param_dict = {}
            for key, val in self.param_dict.iteritems():
                if type(val) is str and 'None' in val:
                    pass
                elif key in self.defaults.iterkeys():
                    arg_type = self.defaults[key]['type']
                    try:
                        param_dict[key] = arg_type(val)
                    except ValueError:
                        param_dict[key] = None
                else:
                    param_dict[key] = val
            return param_dict
        else:
            return self.param_dict

    @property
    def func_signature(self):
        """
        String for function signature. Hopefully this can eventually be used to save workflows as scripts :)
        """
        signature = str(self._function.__name__) + '('
        for arg in self.missing_args:
            signature += '{},'.format(arg)
        for param, value in self.param_dict.iteritems():
            signature += '{0}={1},'.format(param, value) if not isinstance(value, str) else \
                '{0}=\'{1}\','.format(param, value)
        return signature[:-1] + ')'

    def updateParamsDict(self):
        """
        Update the values of the parameter dictionary with the current values in UI
        """
        self.param_dict.update(
            {param.name(): param.value()
             for param in self.params.children()})
        for p, ipf in self.input_functions.iteritems():
            ipf.updateParamsDict()

    def addInputFunction(self, parameter, functionwidget):
        """
        Add an input function widget

        Parameters
        ----------
        parameter : str
            Parameter name that will be overriden by return value of the input function
        functionwidget : FunctionWidget
            FunctionWidget representing the input function

        """

        if parameter in self.input_functions:  # Check to see if parameter already has input function
            if functionwidget.subfunc_name == self.input_functions[
                    parameter].subfunc_name:
                raise AttributeError(
                    'Input function already exists'
                )  # skip if the input function already exists
            self.removeInputFunction(
                parameter)  # Remove it if it will be replaced
        self.input_functions[parameter] = functionwidget
        self.addSubFeature(functionwidget)
        functionwidget.sigDelete.connect(
            lambda: self.removeInputFunction(parameter))

    def removeInputFunction(self, parameter):
        """
        Remove the input function for the given parameter

        Parameters
        ----------
        parameter : str
            Parameter name that will be overriden by return value of the input function

        """
        function = self.input_functions.pop(parameter)
        self.removeSubFeature(function)

    def paramChanged(self, param):
        """
        Slot connected to a pg.Parameter.sigChanged signal
        """
        if hasattr(self, 'defaults'):
            try:
                arg_type = self.defaults[param.name()]['type']
                try:
                    self.allowed_types[arg_type](param.value())
                    self.param_dict.update({param.name(): param.value()})
                except ValueError:
                    if param.value() == "None":
                        self.param_dict.update({param.name(): param.value()})
                    else:
                        param.setValue(self.param_dict[param.name()])
            except KeyError:
                self.param_dict.update({param.name(): param.value()})
        else:
            self.param_dict.update({param.name(): param.value()})

    def allReadOnly(self, boolean):
        """
        Make all parameter read only
        """
        for param in self.params.children():
            param.setReadonly(boolean)

    def menuRequested(self):
        """
        Context menu for functionWidget. Default is not menu.
        """
        pass

    def paramMenuRequested(self, pos):
        """
        Menus when a parameter in the form is right clicked
        """
        if self.form.currentItem().parent():
            self.parammenu.exec_(self.form.mapToGlobal(pos))

    def testParamTriggered(self):
        """
        Slot when a parameter range is clicked. Will emit the parameter name and the chosen range
        """
        param = self.form.currentItem().param
        if param.type() == 'int' or param.type() == 'float':
            start, end, step = None, None, None
            if 'limits' in param.opts:
                start, end = param.opts['limits']
                step = (end - start) / 3 + 1
            elif param.value() is not None:
                start, end, step = param.value() / 2, 4 * (
                    param.value()) / 2, param.value() / 2
            test = TestRangeDialog(param.type(), (start, end, step))
        elif param.type() == 'list':
            test = TestListRangeDialog(param.opts['values'])
        else:
            return
        if test.exec_():
            self.sigTestRange.emit(self, param.name(), test.selectedRange())