Esempio n. 1
0
class Traces(QGraphicsView):
    """Main widget that contains the recordings to be plotted.

    Attributes
    ----------
    parent : instance of QMainWindow
        the main window.
    config : instance of ConfigTraces
        settings for this widget

    y_scrollbar_value : int
        position of the vertical scrollbar
    data : instance of ChanTime
        filtered and reref'ed data

    chan : list of str
        list of channels (labels and channel group)
    chan_pos : list of int
        y-position of each channel (based on value at 0)
    chan_scale : list of float
        scaling factor for each channel
    time_pos : list of QPointF
        we need to keep track of the position of time label during creation
    sel_chan : int
        index of self.chan of the first selected channel
    sel_xy : tuple of 2 floats
        x and y position of the first selected point

    scene : instance of QGraphicsScene
        the main scene.
    idx_label : list of instance of QGraphicsSimpleTextItem
        the channel labels on the y-axis
    idx_time : list of instance of QGraphicsSimpleTextItem
        the time labels on the x-axis
    idx_sel : instance of QGraphicsRectItem
        the rectangle showing the selection (both for selection and event)
    idx_info : instance of QGraphicsSimpleTextItem
        the rectangle showing the selection
    idx_markers : list of QGraphicsRectItem
        list of markers in the dataset
    idx_annot : list of QGraphicsRectItem
        list of user-made annotations
    """
    def __init__(self, parent):
        super().__init__()
        self.parent = parent
        self.config = ConfigTraces(self.parent.overview.update_position)

        self.y_scrollbar_value = 0
        self.data = None
        self.chan = []
        self.chan_pos = []  # used later to find out which channel we're using
        self.chan_scale = []
        self.time_pos = []
        self.sel_chan = None
        self.sel_xy = (None, None)

        self.scene = None
        self.idx_label = []
        self.idx_time = []
        self.idx_sel = None
        self.idx_info = None
        self.idx_markers = []
        self.idx_annot = []

        self.create_action()

    def create_action(self):
        """Create actions associated with this widget."""
        actions = {}

        act = QAction(QIcon(ICON['step_prev']), 'Previous Step', self)
        act.setShortcut(QKeySequence.MoveToPreviousChar)
        act.triggered.connect(self.step_prev)
        actions['step_prev'] = act

        act = QAction(QIcon(ICON['step_next']), 'Next Step', self)
        act.setShortcut(QKeySequence.MoveToNextChar)
        act.triggered.connect(self.step_next)
        actions['step_next'] = act

        act = QAction(QIcon(ICON['page_prev']), 'Previous Page', self)
        act.setShortcut(QKeySequence.MoveToPreviousPage)
        act.triggered.connect(self.page_prev)
        actions['page_prev'] = act

        act = QAction(QIcon(ICON['page_next']), 'Next Page', self)
        act.setShortcut(QKeySequence.MoveToNextPage)
        act.triggered.connect(self.page_next)
        actions['page_next'] = act

        act = QAction(QIcon(ICON['zoomprev']), 'Wider Time Window', self)
        act.setShortcut(QKeySequence.ZoomIn)
        act.triggered.connect(self.X_more)
        actions['X_more'] = act

        act = QAction(QIcon(ICON['zoomnext']), 'Narrower Time Window', self)
        act.setShortcut(QKeySequence.ZoomOut)
        act.triggered.connect(self.X_less)
        actions['X_less'] = act

        act = QAction(QIcon(ICON['zoomin']), 'Larger Amplitude', self)
        act.setShortcut(QKeySequence.MoveToPreviousLine)
        act.triggered.connect(self.Y_more)
        actions['Y_less'] = act

        act = QAction(QIcon(ICON['zoomout']), 'Smaller Amplitude', self)
        act.setShortcut(QKeySequence.MoveToNextLine)
        act.triggered.connect(self.Y_less)
        actions['Y_more'] = act

        act = QAction(QIcon(ICON['ydist_more']), 'Larger Y Distance', self)
        act.triggered.connect(self.Y_wider)
        actions['Y_wider'] = act

        act = QAction(QIcon(ICON['ydist_less']), 'Smaller Y Distance', self)
        act.triggered.connect(self.Y_tighter)
        actions['Y_tighter'] = act

        act = QAction(QIcon(ICON['chronometer']), '6 Hours Earlier', self)
        act.triggered.connect(partial(self.add_time, -6 * 60 * 60))
        actions['addtime_-6h'] = act

        act = QAction(QIcon(ICON['chronometer']), '1 Hour Earlier', self)
        act.triggered.connect(partial(self.add_time, -60 * 60))
        actions['addtime_-1h'] = act

        act = QAction(QIcon(ICON['chronometer']), '10 Minutes Earlier', self)
        act.triggered.connect(partial(self.add_time, -10 * 60))
        actions['addtime_-10min'] = act

        act = QAction(QIcon(ICON['chronometer']), '10 Minutes Later', self)
        act.triggered.connect(partial(self.add_time, 10 * 60))
        actions['addtime_10min'] = act

        act = QAction(QIcon(ICON['chronometer']), '1 Hour Later', self)
        act.triggered.connect(partial(self.add_time, 60 * 60))
        actions['addtime_1h'] = act

        act = QAction(QIcon(ICON['chronometer']), '6 Hours Later', self)
        act.triggered.connect(partial(self.add_time, 6 * 60 * 60))
        actions['addtime_6h'] = act

        self.action = actions

    def read_data(self):
        """Read the data to plot."""
        window_start = self.parent.value('window_start')
        window_end = window_start + self.parent.value('window_length')
        dataset = self.parent.info.dataset
        groups = self.parent.channels.groups

        chan_to_read = []
        for one_grp in groups:
            chan_to_read.extend(one_grp['chan_to_plot'] + one_grp['ref_chan'])

        if not chan_to_read:
            return
        data = dataset.read_data(chan=chan_to_read,
                                 begtime=window_start,
                                 endtime=window_end)

        max_s_freq = self.parent.value('max_s_freq')
        if data.s_freq > max_s_freq:
            q = int(data.s_freq / max_s_freq)
            lg.debug('Decimate (no low-pass filter) at ' + str(q))

            data.data[0] = data.data[0][:, slice(None, None, q)]
            data.axis['time'][0] = data.axis['time'][0][slice(None, None, q)]
            data.s_freq = int(data.s_freq / q)

        self.data = _create_data_to_plot(data, self.parent.channels.groups)

    def display(self):
        """Display the recordings."""
        if self.data is None:
            return

        if self.scene is not None:
            self.y_scrollbar_value = self.verticalScrollBar().value()
            self.scene.clear()

        self.create_chan_labels()
        self.create_time_labels()

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')

        time_height = max([x.boundingRect().height() for x in self.idx_time])
        label_width = window_length * self.parent.value('label_ratio')
        scene_height = (len(self.idx_label) * self.parent.value('y_distance') +
                        time_height)

        self.scene = QGraphicsScene(window_start - label_width,
                                    0,
                                    window_length + label_width,
                                    scene_height)
        self.setScene(self.scene)

        self.idx_markers = []
        self.idx_annot = []

        self.add_chan_labels()
        self.add_time_labels()
        self.add_traces()
        self.display_grid()
        self.display_markers()
        self.display_annotations()

        self.resizeEvent(None)
        self.verticalScrollBar().setValue(self.y_scrollbar_value)
        self.parent.info.display_view()
        self.parent.overview.display_current()

    def create_chan_labels(self):
        """Create the channel labels, but don't plot them yet.

        Notes
        -----
        It's necessary to have the width of the labels, so that we can adjust
        the main scene.
        """
        self.idx_label = []
        for one_grp in self.parent.channels.groups:
            for one_label in one_grp['chan_to_plot']:
                item = QGraphicsSimpleTextItem(one_label)
                item.setBrush(QBrush(QColor(one_grp['color'])))
                item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                self.idx_label.append(item)

    def create_time_labels(self):
        """Create the time labels, but don't plot them yet.

        Notes
        -----
        It's necessary to have the height of the time labels, so that we can
        adjust the main scene.

        Not very robust, because it uses seconds as integers.
        """
        min_time = int(floor(min(self.data.axis['time'][0])))
        max_time = int(ceil(max(self.data.axis['time'][0])))
        n_time_labels = self.parent.value('n_time_labels')

        self.idx_time = []
        self.time_pos = []
        for one_time in linspace(min_time, max_time, n_time_labels):
            x_label = (self.data.start_time +
                       timedelta(seconds=one_time)).strftime('%H:%M:%S')
            item = QGraphicsSimpleTextItem(x_label)
            item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            self.idx_time.append(item)
            self.time_pos.append(QPointF(one_time,
                                         len(self.idx_label) *
                                         self.parent.value('y_distance')))

    def add_chan_labels(self):
        """Add channel labels on the left."""
        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        label_width = window_length * self.parent.value('label_ratio')

        for row, one_label_item in enumerate(self.idx_label):
            self.scene.addItem(one_label_item)
            one_label_item.setPos(window_start - label_width,
                                  self.parent.value('y_distance') * row +
                                  self.parent.value('y_distance') / 2)

    def add_time_labels(self):
        """Add time labels at the bottom."""
        for text, pos in zip(self.idx_time, self.time_pos):
            self.scene.addItem(text)
            text.setPos(pos)

    def add_traces(self):
        """Add traces based on self.data."""
        y_distance = self.parent.value('y_distance')
        self.chan = []
        self.chan_pos = []
        self.chan_scale = []

        row = 0
        for one_grp in self.parent.channels.groups:
            for one_chan in one_grp['chan_to_plot']:

                # channel name
                chan_name = one_chan + ' (' + one_grp['name'] + ')'

                # trace
                dat = (self.data(trial=0, chan=chan_name) *
                       self.parent.value('y_scale'))
                dat *= -1  # flip data, upside down
                path = self.scene.addPath(Path(self.data.axis['time'][0],
                                               dat))
                path.setPen(QPen(QColor(one_grp['color']), LINE_WIDTH))

                # adjust position
                chan_pos = y_distance * row + y_distance / 2
                path.setPos(0, chan_pos)
                row += 1

                self.chan.append(chan_name)
                self.chan_scale.append(one_grp['scale'])
                self.chan_pos.append(chan_pos)

    def display_grid(self):
        """Display grid on x-axis and y-axis."""
        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length

        if self.parent.value('grid_x'):
            x_tick = self.parent.value('grid_xtick')
            x_ticks = arange(window_start, window_end + x_tick, x_tick)
            for x in x_ticks:
                x_pos = [x, x]
                y_pos = [0,
                         self.parent.value('y_distance') * len(self.idx_label)]
                path = self.scene.addPath(Path(x_pos, y_pos))
                path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
                                 Qt.DotLine))

        if self.parent.value('grid_y'):
            for one_label_item in self.idx_label:
                x_pos = [window_start, window_end]
                y_pos = [one_label_item.y(), one_label_item.y()]
                path = self.scene.addPath(Path(x_pos, y_pos))
                path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine))

    def display_markers(self):
        """Add markers on top of first plot."""
        for item in self.idx_markers:
            self.scene.removeItem(item)
        self.idx_markers = []

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length
        y_distance = self.parent.value('y_distance')

        markers = []
        if self.parent.info.markers is not None:
            if self.parent.value('marker_show'):
                markers = self.parent.info.markers

        for mrk in markers:
            if window_start <= mrk['end'] and window_end >= mrk['start']:

                mrk_start = max((mrk['start'], window_start))
                mrk_end = min((mrk['end'], window_end))
                color = QColor(self.parent.value('marker_color'))

                item = QGraphicsRectItem(mrk_start, 0,
                                         mrk_end - mrk_start,
                                         len(self.idx_label) * y_distance)
                item.setPen(color)
                item.setBrush(color)
                item.setZValue(-9)
                self.scene.addItem(item)

                item = TextItem_with_BG(color.darker(200))
                item.setText(mrk['name'])
                item.setPos(mrk['start'],
                            len(self.idx_label) *
                            self.parent.value('y_distance'))
                item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                item.setRotation(-90)
                self.scene.addItem(item)
                self.idx_markers.append(item)

    def display_annotations(self):
        """Mark all the bookmarks/events, on top of first plot."""
        for item in self.idx_annot:
            self.scene.removeItem(item)
        self.idx_annot = []

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length
        y_distance = self.parent.value('y_distance')
        raw_chan_name = list(map(take_raw_name, self.chan))

        bookmarks = []
        events = []

        if self.parent.notes.annot is not None:
            if self.parent.value('annot_show'):
                bookmarks = self.parent.notes.annot.get_bookmarks()
                events = self.parent.notes.get_selected_events((window_start,
                                                                window_end))
        annotations = bookmarks + events

        for annot in annotations:

            if window_start <= annot['end'] and window_end >= annot['start']:

                mrk_start = max((annot['start'], window_start))
                mrk_end = min((annot['end'], window_end))
                if annot in bookmarks:
                    color = QColor(self.parent.value('annot_bookmark_color'))
                if annot in events:
                    color = convert_name_to_color(annot['name'])

                if annot['chan'] == ['']:
                    h_annot = len(self.idx_label) * y_distance
                    y_annot = (0, )

                    item = TextItem_with_BG(color.darker(200))
                    item.setText(annot['name'])
                    item.setPos(annot['start'],
                                len(self.idx_label) * y_distance)
                    item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                    item.setRotation(-90)
                    self.scene.addItem(item)
                    self.idx_annot.append(item)
                    zvalue = -8

                else:
                    h_annot = y_distance
                    # find indices of channels with annotations
                    chan_idx_in_mrk = in1d(raw_chan_name, annot['chan'])
                    y_annot = asarray(self.chan_pos)[chan_idx_in_mrk]
                    y_annot -= y_distance / 2
                    zvalue = -7

                for y in y_annot:
                    item = QGraphicsRectItem(mrk_start, y,
                                             mrk_end - mrk_start, h_annot)
                    item.setPen(color)
                    item.setBrush(color)
                    item.setZValue(zvalue)
                    self.scene.addItem(item)
                    self.idx_annot.append(item)

    def step_prev(self):
        """Go to the previous step."""
        window_start = (self.parent.value('window_start') -
                        self.parent.value('window_length') /
                        self.parent.value('window_step'))
        self.parent.overview.update_position(window_start)

    def step_next(self):
        """Go to the next step."""
        window_start = (self.parent.value('window_start') +
                        self.parent.value('window_length') /
                        self.parent.value('window_step'))
        self.parent.overview.update_position(window_start)

    def page_prev(self):
        """Go to the previous page."""
        window_start = (self.parent.value('window_start') -
                        self.parent.value('window_length'))
        self.parent.overview.update_position(window_start)

    def page_next(self):
        """Go to the next page."""
        window_start = (self.parent.value('window_start') +
                        self.parent.value('window_length'))
        self.parent.overview.update_position(window_start)

    def add_time(self, extra_time):
        """Go to the predefined time forward."""
        window_start = self.parent.value('window_start') + extra_time
        self.parent.overview.update_position(window_start)

    def X_more(self):
        """Zoom in on the x-axis."""
        self.parent.value('window_length',
                          self.parent.value('window_length') * 2)
        self.parent.overview.update_position()

    def X_less(self):
        """Zoom out on the x-axis."""
        self.parent.value('window_length',
                          self.parent.value('window_length') / 2)
        self.parent.overview.update_position()

    def X_length(self, new_window_length):
        """Use presets for length of the window."""
        self.parent.value('window_length', new_window_length)
        self.parent.overview.update_position()

    def Y_more(self):
        """Increase the amplitude."""
        self.parent.value('y_scale', self.parent.value('y_scale') * 2)
        self.parent.traces.display()

    def Y_less(self):
        """Decrease the amplitude."""
        self.parent.value('y_scale', self.parent.value('y_scale') / 2)
        self.parent.traces.display()

    def Y_ampl(self, new_y_scale):
        """Make amplitude on Y axis using predefined values"""
        self.parent.value('y_scale', new_y_scale)
        self.parent.traces.display()

    def Y_wider(self):
        """Increase the distance of the lines."""
        self.parent.value('y_distance', self.parent.value('y_distance') * 1.4)
        self.parent.traces.display()

    def Y_tighter(self):
        """Decrease the distance of the lines."""
        self.parent.value('y_distance', self.parent.value('y_distance') / 1.4)
        self.parent.traces.display()

    def Y_dist(self, new_y_distance):
        """Use preset values for the distance between lines."""
        self.parent.value('y_distance', new_y_distance)
        self.parent.traces.display()

    def mousePressEvent(self, event):
        """Create a marker or start selection

        Parameters
        ----------
        event : instance of QtCore.QEvent
            it contains the position that was clicked.
        """
        if not self.scene:
            return

        xy_scene = self.mapToScene(event.pos())
        chan_idx = argmin(abs(asarray(self.chan_pos) - xy_scene.y()))
        self.sel_chan = chan_idx
        self.sel_xy = (xy_scene.x(), xy_scene.y())

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()

        if not (chk_marker or chk_event):
            channame = self.chan[self.sel_chan] + ' in selected window'
            self.parent.spectrum.show_channame(channame)

    def mouseMoveEvent(self, event):
        """When normal selection, update power spectrum with current selection.
        Otherwise, show the range of the new marker.
        """
        if not self.scene:
            return

        if self.idx_sel in self.scene.items():
            self.scene.removeItem(self.idx_sel)
            self.idx_sel = None

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()

        if chk_marker or chk_event:
            xy_scene = self.mapToScene(event.pos())
            y_distance = self.parent.value('y_distance')
            pos = QRectF(self.sel_xy[0],
                         0,
                         xy_scene.x() - self.sel_xy[0],
                         len(self.idx_label) * y_distance)
            item = QGraphicsRectItem(pos.normalized())
            item.setPen(NoPen)

            if chk_marker:
                color = QColor(self.parent.value('annot_bookmark_color'))

            elif chk_event:
                eventtype = self.parent.notes.idx_eventtype.currentText()
                color = convert_name_to_color(eventtype)

            item.setBrush(QBrush(color.lighter(115)))
            item.setZValue(-10)
            self.scene.addItem(item)
            self.idx_sel = item
            return

        xy_scene = self.mapToScene(event.pos())
        pos = QRectF(self.sel_xy[0], self.sel_xy[1],
                     xy_scene.x() - self.sel_xy[0],
                     xy_scene.y() - self.sel_xy[1])
        self.idx_sel = QGraphicsRectItem(pos.normalized())
        self.idx_sel.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH))
        self.scene.addItem(self.idx_sel)

        if self.idx_info in self.scene.items():
            self.scene.removeItem(self.idx_info)

        duration = '{0:0.2f}s'.format(abs(xy_scene.x() - self.sel_xy[0]))

        # get y-size, based on scaling too
        y = abs(xy_scene.y() - self.sel_xy[1])
        scale = self.parent.value('y_scale') * self.chan_scale[self.sel_chan]
        height = '{0:0.3f}uV'.format(y / scale)

        item = TextItem_with_BG()
        item.setText(duration + ' ' + height)
        item.setPos(self.sel_xy[0], self.sel_xy[1])
        self.scene.addItem(item)
        self.idx_info = item

        trial = 0
        time = self.parent.traces.data.axis['time'][trial]
        beg_win = min((self.sel_xy[0], xy_scene.x()))
        end_win = max((self.sel_xy[0], xy_scene.x()))
        time_of_interest = time[(time >= beg_win) & (time < end_win)]
        if len(time_of_interest) > MINIMUM_N_SAMPLES:
            data = self.parent.traces.data(trial=trial,
                                           chan=self.chan[self.sel_chan],
                                           time=time_of_interest)
            n_data = len(data)
            n_pad = (power(2, ceil(log2(n_data))) - n_data) / 2
            data = pad(data, (int(ceil(n_pad)), int(floor(n_pad))), 'constant')

            self.parent.spectrum.display(data)

    def mouseReleaseEvent(self, event):
        """Create a new event or marker, or show the previous power spectrum
        """
        if not self.scene:
            return

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()

        if chk_marker or chk_event:

            x_in_scene = self.mapToScene(event.pos()).x()

            # it can happen that selection is empty (f.e. double-click)
            if self.sel_xy[0] is not None:
                # max resolution = sampling frequency
                # in case there is no data
                s_freq = self.parent.info.dataset.header['s_freq']
                at_s_freq = lambda x: round(x * s_freq) / s_freq
                start = at_s_freq(self.sel_xy[0])
                end = at_s_freq(x_in_scene)

                if abs(end - start) < self.parent.value('min_marker_dur'):
                    end = start

                if start <= end:
                    time = (start, end)
                else:
                    time = (end, start)

                if chk_marker:
                    self.parent.notes.add_bookmark(time)

                elif chk_event:
                    eventtype = self.parent.notes.idx_eventtype.currentText()
                    self.parent.notes.add_event(eventtype, time)

        else:  # normal selection

            if self.idx_info in self.scene.items():
                self.scene.removeItem(self.idx_info)
            self.idx_info = None

            # restore spectrum
            self.parent.spectrum.update()
            self.parent.spectrum.display_window()

        # general garbage collection
        self.sel_chan = None
        self.sel_xy = (None, None)

        if self.idx_sel in self.scene.items():
            self.scene.removeItem(self.idx_sel)
            self.idx_sel = None

    def resizeEvent(self, event):
        """Resize scene so that it fits the whole widget.

        Parameters
        ----------
        event : instance of QtCore.QEvent
            not important

        Notes
        -----
        This function overwrites Qt function, therefore the non-standard
        name. Argument also depends on Qt.

        The function is used to change the scale of view, so that the scene
        fits the whole scene. There are two problems that I could not fix: 1)
        how to give the width of the label in absolute width, 2) how to strech
        scene just enough that it doesn't trigger a scrollbar. However, it's
        pretty good as it is now.
        """
        if self.scene is not None:
            ratio = self.width() / (self.scene.width() * 1.1)
            self.resetTransform()
            self.scale(ratio, 1)

    def reset(self):
        self.y_scrollbar_value = 0
        self.data = None
        self.chan = []
        self.chan_pos = []
        self.chan_scale = []
        self.sel_chan = None
        self.sel_xy = (None, None)

        if self.scene is not None:
            self.scene.clear()
        self.scene = None
        self.idx_sel = None
        self.idx_info = None
        self.idx_label = []
        self.idx_time = []
        self.time_pos = []
Esempio n. 2
0
class Spectrum(QWidget):
    """Plot the power spectrum for a specified channel.

    Attributes
    ----------
    parent : instance of QMainWindow
        the main window.
    x_limit : tuple or list
        2 values specifying the limit on x-axis
    y_limit : tuple or list
        2 values specifying the limit on y-axis
    log : bool
        log-transform the data or not
    idx_chan : instance of QComboBox
        the element with the list of channel names.
    idx_x_min : instance of QLineEdit
        value with min x value
    idx_x_max : instance of QLineEdit
        value with max x value
    idx_y_min : instance of QLineEdit
        value with min y value
    idx_y_max : instance of QLineEdit
        value with max y value
    idx_log : instance of QCheckBox
        widget that defines if log should be used or not
    idx_fig : instance of QGraphicsView
        the view with the power spectrum
    scene : instance of QGraphicsScene
        the scene with GraphicsItems

    Notes
    -----
    If data contains NaN, it doesn't create any spectrum (feature or bug?).
    """
    def __init__(self, parent):
        super().__init__()
        self.parent = parent

        self.config = ConfigSpectrum(self.display_window)

        self.selected_chan = None
        self.idx_chan = None
        self.idx_fig = None
        self.scene = None

        self.create()

    def create(self):
        """Create empty scene for power spectrum."""
        self.idx_chan = QComboBox()
        self.idx_chan.activated.connect(self.display_window)

        self.idx_fig = QGraphicsView(self)
        self.idx_fig.scale(1, -1)

        layout = QVBoxLayout()
        layout.addWidget(self.idx_chan)
        layout.addWidget(self.idx_fig)
        self.setLayout(layout)

        self.resizeEvent(None)

    def show_channame(self, chan_name):
        self.selected_chan = self.idx_chan.currentIndex()

        self.idx_chan.clear()
        self.idx_chan.addItem(chan_name)
        self.idx_chan.setCurrentIndex(0)

    def update(self):
        """Add channel names to the combobox."""
        self.idx_chan.clear()
        for chan_name in self.parent.traces.chan:
            self.idx_chan.addItem(chan_name)

        if self.selected_chan is not None:
            self.idx_chan.setCurrentIndex(self.selected_chan)
            self.selected_chan = None

    def display_window(self):
        """Read the channel name from QComboBox and plot its spectrum.

        This function is necessary it reads the data and it sends it to
        self.display. When the user selects a smaller chunk of data from the
        visible traces, then we don't need to call this function.
        """
        if self.idx_chan.count() == 0:
            self.update()

        chan_name = self.idx_chan.currentText()
        lg.debug('Power spectrum for channel ' + chan_name)

        if chan_name:
            trial = 0
            data = self.parent.traces.data(trial=trial, chan=chan_name)
            self.display(data)
        else:
            self.scene.clear()

    def display(self, data):
        """Make graphicsitem for spectrum figure.

        Parameters
        ----------
        data : ndarray
            1D vector containing the data only

        This function can be called by self.display_window (which reads the
        data for the selected channel) or by the mouse-events functions in
        traces (which read chunks of data from the user-made selection).
        """
        value = self.config.value
        self.scene = QGraphicsScene(value['x_min'], value['y_min'],
                                    value['x_max'] - value['x_min'],
                                    value['y_max'] - value['y_min'])
        self.idx_fig.setScene(self.scene)

        self.add_grid()
        self.resizeEvent(None)

        s_freq = self.parent.traces.data.s_freq
        f, Pxx = welch(data, fs=s_freq, nperseg=int(min(
            (s_freq, len(data)))))  # force int

        freq_limit = (value['x_min'] <= f) & (f <= value['x_max'])

        if self.config.value['log']:
            Pxx_to_plot = log(Pxx[freq_limit])
        else:
            Pxx_to_plot = Pxx[freq_limit]

        self.scene.addPath(Path(f[freq_limit], Pxx_to_plot),
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))

    def add_grid(self):
        """Add axis and ticks to figure.

        Notes
        -----
        I know that visvis and pyqtgraphs can do this in much simpler way, but
        those packages create too large a padding around the figure and this is
        pretty fast.

        """
        value = self.config.value

        # X-AXIS
        # x-bottom
        self.scene.addLine(value['x_min'], value['y_min'],
                           value['x_min'], value['y_max'],
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # at y = 0, dashed
        self.scene.addLine(value['x_min'], 0, value['x_max'], 0,
                           QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DashLine))
        # ticks on y-axis
        y_high = int(floor(value['y_max']))
        y_low = int(ceil(value['y_min']))
        x_length = (value['x_max'] - value['x_min']) / value['x_tick']
        for y in range(y_low, y_high):
            self.scene.addLine(value['x_min'], y, value['x_min'] + x_length, y,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # Y-AXIS
        # left axis
        self.scene.addLine(value['x_min'], value['y_min'],
                           value['x_max'], value['y_min'],
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # larger ticks on x-axis every 10 Hz
        x_high = int(floor(value['x_max']))
        x_low = int(ceil(value['x_min']))
        y_length = (value['y_max'] - value['y_min']) / value['y_tick']
        for x in range(x_low, x_high, 10):
            self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # smaller ticks on x-axis every 10 Hz
        y_length = (value['y_max'] - value['y_min']) / value['y_tick'] / 2
        for x in range(x_low, x_high, 5):
            self.scene.addLine(x, value['y_min'], x, value['y_min'] + y_length,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))

    def resizeEvent(self, event):
        """Fit the whole scene in view.

        Parameters
        ----------
        event : instance of Qt.Event
            not important

        """
        value = self.config.value
        self.idx_fig.fitInView(value['x_min'], value['y_min'],
                               value['x_max'] - value['x_min'],
                               value['y_max'] - value['y_min'])

    def reset(self):
        """Reset widget as new"""
        self.idx_chan.clear()
        if self.scene is not None:
            self.scene.clear()
        self.scene = None
Esempio n. 3
0
class Traces(QGraphicsView):
    """Main widget that contains the recordings to be plotted.

    Attributes
    ----------
    parent : instance of QMainWindow
        the main window.
    config : instance of ConfigTraces
        settings for this widget

    y_scrollbar_value : int
        position of the vertical scrollbar
    data : instance of ChanTime
        filtered and reref'ed data

    chan : list of str
        list of channels (labels and channel group)
    chan_pos : list of int
        y-position of each channel (based on value at 0)
    chan_scale : list of float
        scaling factor for each channel
    time_pos : list of QPointF
        we need to keep track of the position of time label during creation
    sel_chan : int
        index of self.chan of the first selected channel
    sel_xy : tuple of 2 floats
        x and y position of the first selected point

    scene : instance of QGraphicsScene
        the main scene.
    idx_label : list of instance of QGraphicsSimpleTextItem
        the channel labels on the y-axis
    idx_time : list of instance of QGraphicsSimpleTextItem
        the time labels on the x-axis
    idx_sel : instance of QGraphicsRectItem
        the rectangle showing the selection (both for selection and event)
    idx_info : instance of QGraphicsSimpleTextItem
        the rectangle showing the selection
    idx_markers : list of QGraphicsRectItem
        list of markers in the dataset
    idx_annot : list of QGraphicsRectItem
        list of user-made annotations
    """
    def __init__(self, parent):
        super().__init__()
        self.parent = parent
        self.config = ConfigTraces(self.parent.overview.update_position)

        self.y_scrollbar_value = 0
        self.data = None
        self.chan = []
        self.chan_pos = []  # used later to find out which channel we're using
        self.chan_scale = []
        self.time_pos = []
        self.sel_chan = None
        self.sel_xy = (None, None)

        self.scene = None
        self.idx_label = []
        self.idx_time = []
        self.idx_sel = None
        self.idx_info = None
        self.idx_markers = []
        self.idx_annot = []
        self.idx_annot_labels = []
        self.cross_chan_mrk = True
        self.highlight = None
        self.event_sel = None
        self.current_event = None
        self.current_event_row = None
        self.current_etype = None
        self.deselect = None
        self.ready = True

        self.create_action()

    def create_action(self):
        """Create actions associated with this widget."""
        actions = {}

        act = QAction(QIcon(ICON['step_prev']), 'Previous Step', self)
        act.setShortcut('[')
        act.triggered.connect(self.step_prev)
        actions['step_prev'] = act

        act = QAction(QIcon(ICON['step_next']), 'Next Step', self)
        act.setShortcut(']')
        act.triggered.connect(self.step_next)
        actions['step_next'] = act

        act = QAction(QIcon(ICON['page_prev']), 'Previous Page', self)
        act.setShortcut(QKeySequence.MoveToPreviousChar)
        act.triggered.connect(self.page_prev)
        actions['page_prev'] = act

        act = QAction(QIcon(ICON['page_next']), 'Next Page', self)
        act.setShortcut(QKeySequence.MoveToNextChar)
        act.triggered.connect(self.page_next)
        actions['page_next'] = act

        act = QAction('Go to Epoch', self)
        act.setShortcut(QKeySequence.FindNext)
        act.triggered.connect(self.go_to_epoch)
        actions['go_to_epoch'] = act

        act = QAction('Line Up with Epoch', self)
        act.setShortcut('F4')
        act.triggered.connect(self.line_up_with_epoch)
        actions['line_up_with_epoch'] = act

        act = QAction(QIcon(ICON['zoomprev']), 'Wider Time Window', self)
        act.setShortcut(QKeySequence.ZoomIn)
        act.triggered.connect(self.X_more)
        actions['X_more'] = act

        act = QAction(QIcon(ICON['zoomnext']), 'Narrower Time Window', self)
        act.setShortcut(QKeySequence.ZoomOut)
        act.triggered.connect(self.X_less)
        actions['X_less'] = act

        act = QAction(QIcon(ICON['zoomin']), 'Larger Scaling', self)
        act.setShortcut(QKeySequence.MoveToPreviousLine)
        act.triggered.connect(self.Y_more)
        actions['Y_less'] = act

        act = QAction(QIcon(ICON['zoomout']), 'Smaller Scaling', self)
        act.setShortcut(QKeySequence.MoveToNextLine)
        act.triggered.connect(self.Y_less)
        actions['Y_more'] = act

        act = QAction(QIcon(ICON['ydist_more']), 'Larger Y Distance', self)
        act.triggered.connect(self.Y_wider)
        actions['Y_wider'] = act

        act = QAction(QIcon(ICON['ydist_less']), 'Smaller Y Distance', self)
        act.triggered.connect(self.Y_tighter)
        actions['Y_tighter'] = act

        act = QAction(QIcon(ICON['chronometer']), '6 Hours Earlier', self)
        act.triggered.connect(partial(self.add_time, -6 * 60 * 60))
        actions['addtime_-6h'] = act

        act = QAction(QIcon(ICON['chronometer']), '1 Hour Earlier', self)
        act.triggered.connect(partial(self.add_time, -60 * 60))
        actions['addtime_-1h'] = act

        act = QAction(QIcon(ICON['chronometer']), '10 Minutes Earlier', self)
        act.triggered.connect(partial(self.add_time, -10 * 60))
        actions['addtime_-10min'] = act

        act = QAction(QIcon(ICON['chronometer']), '10 Minutes Later', self)
        act.triggered.connect(partial(self.add_time, 10 * 60))
        actions['addtime_10min'] = act

        act = QAction(QIcon(ICON['chronometer']), '1 Hour Later', self)
        act.triggered.connect(partial(self.add_time, 60 * 60))
        actions['addtime_1h'] = act

        act = QAction(QIcon(ICON['chronometer']), '6 Hours Later', self)
        act.triggered.connect(partial(self.add_time, 6 * 60 * 60))
        actions['addtime_6h'] = act

        act = QAction('Go to Next Event', self)
        act.setShortcut('s')
        act.triggered.connect(self.next_event)
        actions['next_event'] = act

        act = QAction('Delete Event and Go to Next', self)
        act.setShortcut('d')
        act.triggered.connect(partial(self.next_event, True))
        actions['del_and_next_event'] = act

        act = QAction('Next Event of Same Type', self)
        act.setCheckable(True)
        act.setChecked(True)
        actions['next_of_same_type'] = act

        act = QAction('Change Event Type', self)
        act.setShortcut('e')
        act.triggered.connect(self.change_event_type)
        actions['change_event_type'] = act

        act = QAction('Centre Window Around Event', self)
        act.setCheckable(True)
        act.setChecked(True)
        actions['centre_event'] = act

        act = QAction('Full-length Markers', self)
        act.setCheckable(True)
        act.setChecked(True)
        act.triggered.connect(self.display_annotations)
        actions['cross_chan_mrk'] = act

        # Misc
        act = QAction('Export to svg...', self)
        act.triggered.connect(partial(export_graphics, MAIN=self.parent))
        actions['export_svg'] = act

        self.action = actions

    def read_data(self):
        """Read the data to plot."""
        window_start = self.parent.value('window_start')
        window_end = window_start + self.parent.value('window_length')
        dataset = self.parent.info.dataset
        groups = self.parent.channels.groups

        chan_to_read = []
        for one_grp in groups:
            chan_to_read.extend(one_grp['chan_to_plot'] + one_grp['ref_chan'])

        if not chan_to_read:
            return

        lg.debug(
            f'Reading data from dataset: begtime={window_start:10.3f}, endtime={window_end:10.3f}, {len(chan_to_read)} channels'
        )
        data = dataset.read_data(chan=chan_to_read,
                                 begtime=window_start,
                                 endtime=window_end)

        max_s_freq = self.parent.value('max_s_freq')
        if data.s_freq > max_s_freq:
            q = int(data.s_freq / max_s_freq)
            lg.debug('Decimate (no low-pass filter) at ' + str(q))

            data.data[0] = data.data[0][:, slice(None, None, q)]
            data.axis['time'][0] = data.axis['time'][0][slice(None, None, q)]
            data.s_freq = int(data.s_freq / q)

        self.data = _create_data_to_plot(data, self.parent.channels.groups)

    def display(self):
        """Display the recordings."""
        if self.data is None:
            return

        if self.scene is not None:
            self.y_scrollbar_value = self.verticalScrollBar().value()
            self.scene.clear()

        self.create_chan_labels()
        self.create_time_labels()

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')

        time_height = max([x.boundingRect().height() for x in self.idx_time])
        label_width = window_length * self.parent.value('label_ratio')
        scene_height = (len(self.idx_label) * self.parent.value('y_distance') +
                        time_height)

        self.scene = QGraphicsScene(window_start - label_width, 0,
                                    window_length + label_width, scene_height)
        self.setScene(self.scene)

        self.idx_markers = []
        self.idx_annot = []
        self.idx_annot_labels = []

        self.add_chan_labels()
        self.add_time_labels()
        self.add_traces()
        self.display_grid()
        self.display_markers()
        self.display_annotations()

        self.resizeEvent(None)
        self.verticalScrollBar().setValue(self.y_scrollbar_value)
        self.parent.info.display_view()
        self.parent.overview.display_current()

    def create_chan_labels(self):
        """Create the channel labels, but don't plot them yet.

        Notes
        -----
        It's necessary to have the width of the labels, so that we can adjust
        the main scene.
        """
        self.idx_label = []
        for one_grp in self.parent.channels.groups:
            for one_label in one_grp['chan_to_plot']:
                item = QGraphicsSimpleTextItem(one_label)
                item.setBrush(QBrush(QColor(one_grp['color'])))
                item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                self.idx_label.append(item)

    def create_time_labels(self):
        """Create the time labels, but don't plot them yet.

        Notes
        -----
        It's necessary to have the height of the time labels, so that we can
        adjust the main scene.

        Not very robust, because it uses seconds as integers.
        """
        min_time = int(floor(min(self.data.axis['time'][0])))
        max_time = int(ceil(max(self.data.axis['time'][0])))
        n_time_labels = self.parent.value('n_time_labels')

        self.idx_time = []
        self.time_pos = []
        for one_time in linspace(min_time, max_time, n_time_labels):
            x_label = (self.data.start_time +
                       timedelta(seconds=one_time)).strftime('%H:%M:%S')
            item = QGraphicsSimpleTextItem(x_label)
            item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            self.idx_time.append(item)
            self.time_pos.append(
                QPointF(one_time,
                        len(self.idx_label) * self.parent.value('y_distance')))

    def add_chan_labels(self):
        """Add channel labels on the left."""
        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        label_width = window_length * self.parent.value('label_ratio')

        for row, one_label_item in enumerate(self.idx_label):
            self.scene.addItem(one_label_item)
            one_label_item.setPos(
                window_start - label_width,
                self.parent.value('y_distance') * row +
                self.parent.value('y_distance') / 2)

    def add_time_labels(self):
        """Add time labels at the bottom."""
        for text, pos in zip(self.idx_time, self.time_pos):
            self.scene.addItem(text)
            text.setPos(pos)

    def add_traces(self):
        """Add traces based on self.data."""
        y_distance = self.parent.value('y_distance')
        self.chan = []
        self.chan_pos = []
        self.chan_scale = []

        row = 0
        for one_grp in self.parent.channels.groups:
            for one_chan in one_grp['chan_to_plot']:

                # channel name
                chan_name = one_chan + ' (' + one_grp['name'] + ')'

                # trace
                dat = (self.data(trial=0, chan=chan_name) *
                       self.parent.value('y_scale'))
                dat *= -1  # flip data, upside down (because y grows downward)
                path = self.scene.addPath(Path(self.data.axis['time'][0], dat))
                path.setPen(QPen(QColor(one_grp['color']), LINE_WIDTH))

                # adjust position
                chan_pos = y_distance * row + y_distance / 2
                path.setPos(0, chan_pos)
                row += 1

                self.chan.append(chan_name)
                self.chan_scale.append(one_grp['scale'])
                self.chan_pos.append(chan_pos)

    def display_grid(self):
        """Display grid on x-axis and y-axis."""
        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length

        if self.parent.value('grid_x'):
            x_tick = self.parent.value('grid_xtick')
            x_ticks = arange(window_start, window_end + x_tick, x_tick)
            for x in x_ticks:
                x_pos = [x, x]
                y_pos = [
                    0,
                    self.parent.value('y_distance') * len(self.idx_label)
                ]
                path = self.scene.addPath(Path(x_pos, y_pos))
                path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine))

        if self.parent.value('grid_y'):
            y_tick = (self.parent.value('grid_ytick') *
                      self.parent.value('y_scale'))
            for one_label_item in self.idx_label:
                x_pos = [window_start, window_end]
                y = one_label_item.y()

                y_pos_0 = [y, y]
                path_0 = self.scene.addPath(Path(x_pos, y_pos_0))
                path_0.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine))

                y_up = one_label_item.y() + y_tick
                y_pos_up = [y_up, y_up]
                path_up = self.scene.addPath(Path(x_pos, y_pos_up))
                path_up.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
                                    Qt.DotLine))

                y_down = one_label_item.y() - y_tick
                y_pos_down = [y_down, y_down]
                path_down = self.scene.addPath(Path(x_pos, y_pos_down))
                path_down.setPen(
                    QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DotLine))

    def display_markers(self):
        """Add markers on top of first plot."""
        for item in self.idx_markers:
            self.scene.removeItem(item)
        self.idx_markers = []

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length
        y_distance = self.parent.value('y_distance')

        markers = []
        if self.parent.info.markers is not None:
            if self.parent.value('marker_show'):
                markers = self.parent.info.markers

        for mrk in markers:
            if window_start <= mrk['end'] and window_end >= mrk['start']:

                mrk_start = max((mrk['start'], window_start))
                mrk_end = min((mrk['end'], window_end))
                color = QColor(self.parent.value('marker_color'))
                h_annot = len(self.idx_label) * y_distance

                mrk_dur = amax((mrk_end - mrk_start,
                                self.parent.value('min_marker_display_dur')))
                item = RectMarker(mrk_start,
                                  0,
                                  mrk_dur,
                                  h_annot,
                                  zvalue=-9,
                                  color=color)
                self.scene.addItem(item)

                item = TextItem_with_BG(color.darker(200))
                item.setText(str(mrk['name']))
                item.setPos(
                    mrk['start'],
                    len(self.idx_label) * self.parent.value('y_distance'))
                item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                item.setRotation(-90)
                self.scene.addItem(item)
                self.idx_markers.append(item)

    def display_annotations(self):
        """Mark all the bookmarks/events, on top of first plot."""
        for item in self.idx_annot:
            self.scene.removeItem(item)
        self.idx_annot = []
        for item in self.idx_annot_labels:
            self.scene.removeItem(item)
        self.idx_annot_labels = []
        self.highlight = None

        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        window_end = window_start + window_length
        y_distance = self.parent.value('y_distance')

        bookmarks = []
        events = []

        if self.parent.notes.annot is not None:
            if self.parent.value('annot_show'):
                bookmarks = self.parent.notes.annot.get_bookmarks()
                events = self.parent.notes.get_selected_events(
                    (window_start, window_end))
        annotations = bookmarks + events

        for annot in annotations:

            if window_start <= annot['end'] and window_end >= annot['start']:

                mrk_start = max((annot['start'], window_start))
                mrk_end = min((annot['end'], window_end))
                if annot in bookmarks:
                    color = QColor(self.parent.value('annot_bookmark_color'))
                if annot in events:
                    color = convert_name_to_color(annot['name'])

                if logical_or(annot['chan'] == [''],
                              self.action['cross_chan_mrk'].isChecked()):
                    h_annot = len(self.idx_label) * y_distance

                    item = TextItem_with_BG(color.darker(200))
                    item.setText(annot['name'])
                    item.setPos(annot['start'],
                                len(self.idx_label) * y_distance)
                    item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
                    item.setRotation(-90)
                    self.scene.addItem(item)
                    self.idx_annot_labels.append(item)
                    mrk_dur = amax(
                        (mrk_end - mrk_start,
                         self.parent.value('min_marker_display_dur')))

                    item = RectMarker(mrk_start,
                                      0,
                                      mrk_dur,
                                      h_annot,
                                      zvalue=-8,
                                      color=color.lighter(120))

                    self.scene.addItem(item)
                    self.idx_annot.append(item)

                if annot['chan'] != ['']:
                    # find indices of channels with annotations
                    chan_idx_in_mrk = in1d(self.chan, annot['chan'])
                    y_annot = asarray(self.chan_pos)[chan_idx_in_mrk]
                    y_annot -= y_distance / 2
                    mrk_dur = amax(
                        (mrk_end - mrk_start,
                         self.parent.value('min_marker_display_dur')))

                    for y in y_annot:
                        item = RectMarker(mrk_start,
                                          y,
                                          mrk_dur,
                                          y_distance,
                                          zvalue=-7,
                                          color=color)
                        self.scene.addItem(item)
                        self.idx_annot.append(item)

    def step_prev(self):
        """Go to the previous step."""
        window_start = around(
            self.parent.value('window_start') -
            self.parent.value('window_length') /
            self.parent.value('window_step'), 2)
        if window_start < 0:
            return
        self.parent.overview.update_position(window_start)

    def step_next(self):
        """Go to the next step."""
        window_start = around(
            self.parent.value('window_start') +
            self.parent.value('window_length') /
            self.parent.value('window_step'), 2)

        self.parent.overview.update_position(window_start)

    def page_prev(self):
        """Go to the previous page."""
        window_start = (self.parent.value('window_start') -
                        self.parent.value('window_length'))
        if window_start < 0:
            return
        self.parent.overview.update_position(window_start)

    def page_next(self):
        """Go to the next page."""
        window_start = (self.parent.value('window_start') +
                        self.parent.value('window_length'))
        self.parent.overview.update_position(window_start)

    def go_to_epoch(self, checked=False, test_text_str=None):
        """Go to any window"""
        if test_text_str is not None:
            time_str = test_text_str
            ok = True
        else:
            time_str, ok = QInputDialog.getText(
                self, 'Go To Epoch', 'Enter start time of the '
                'epoch,\nin seconds ("1560") '
                'or\nas absolute time '
                '("22:30")')

        if not ok:
            return

        try:
            rec_start_time = self.parent.info.dataset.header['start_time']
            window_start = _convert_timestr_to_seconds(time_str,
                                                       rec_start_time)
        except ValueError as err:
            error_dialog = QErrorMessage()
            error_dialog.setWindowTitle('Error moving to epoch')
            error_dialog.showMessage(str(err))
            if test_text_str is None:
                error_dialog.exec()
            self.parent.statusBar().showMessage(str(err))
            return

        self.parent.overview.update_position(window_start)

    def line_up_with_epoch(self):
        """Go to the start of the present epoch."""
        if self.parent.notes.annot is None:  # TODO: remove if buttons are disabled
            error_dialog = QErrorMessage()
            error_dialog.setWindowTitle('Error moving to epoch')
            error_dialog.showMessage('No score file loaded')
            error_dialog.exec()
            return

        new_window_start = self.parent.notes.annot.get_epoch_start(
            self.parent.value('window_start'))

        self.parent.overview.update_position(new_window_start)

    def add_time(self, extra_time):
        """Go to the predefined time forward."""
        window_start = self.parent.value('window_start') + extra_time
        self.parent.overview.update_position(window_start)

    def X_more(self):
        """Zoom out on the x-axis."""
        new_length = self.parent.value('window_length') * 2
        self.parent.value('window_length', new_length)
        new_start = self.parent.value('window_start') - new_length / 4
        self.parent.value('window_start', new_start)

        self.parent.overview.update_position()

    def X_less(self):
        """Zoom in on the x-axis."""
        new_length = self.parent.value('window_length') / 2
        self.parent.value('window_length', new_length)
        new_start = self.parent.value('window_start') + new_length / 2
        self.parent.value('window_start', new_start)

        self.parent.overview.update_position()

    def X_length(self, new_window_length):
        """Use presets for length of the window."""
        self.parent.value('window_length', new_window_length)
        self.parent.overview.update_position()

    def Y_more(self):
        """Increase the scaling."""
        self.parent.value('y_scale', self.parent.value('y_scale') * 2)
        self.parent.traces.display()

    def Y_less(self):
        """Decrease the scaling."""
        self.parent.value('y_scale', self.parent.value('y_scale') / 2)
        self.parent.traces.display()

    def Y_ampl(self, new_y_scale):
        """Make scaling on Y axis using predefined values"""
        self.parent.value('y_scale', new_y_scale)
        self.parent.traces.display()

    def Y_wider(self):
        """Increase the distance of the lines."""
        self.parent.value('y_distance', self.parent.value('y_distance') * 1.4)
        self.parent.traces.display()

    def Y_tighter(self):
        """Decrease the distance of the lines."""
        self.parent.value('y_distance', self.parent.value('y_distance') / 1.4)
        self.parent.traces.display()

    def Y_dist(self, new_y_distance):
        """Use preset values for the distance between lines."""
        self.parent.value('y_distance', new_y_distance)
        self.parent.traces.display()

    def mousePressEvent(self, event):
        """Create a marker or start selection

        Parameters
        ----------
        event : instance of QtCore.QEvent
            it contains the position that was clicked.
        """
        if not self.scene:
            return

        if self.event_sel or self.current_event:
            self.parent.notes.idx_eventtype.setCurrentText(self.current_etype)
            self.current_etype = None
            self.current_event = None
            self.deselect = True
            self.event_sel = None
            self.current_event_row = None
            self.scene.removeItem(self.highlight)
            self.highlight = None
            self.parent.statusBar().showMessage('')
            return

        self.ready = False
        self.event_sel = None

        xy_scene = self.mapToScene(event.pos())
        chan_idx = argmin(abs(asarray(self.chan_pos) - xy_scene.y()))
        self.sel_chan = chan_idx
        self.sel_xy = (xy_scene.x(), xy_scene.y())

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()

        if not (chk_marker or chk_event):
            channame = self.chan[self.sel_chan] + ' in selected window'
            self.parent.spectrum.show_channame(channame)

        # Make annotations clickable
        else:
            for annot in self.idx_annot:
                if annot.contains(xy_scene):
                    self.highlight_event(annot)
                    if chk_event:
                        row = self.parent.notes.find_row(
                            annot.marker.x(),
                            annot.marker.x() + annot.marker.width())
                        self.parent.notes.idx_annot_list.setCurrentCell(row, 0)
                    break

        self.ready = True

    def mouseMoveEvent(self, event):
        """When normal selection, update power spectrum with current selection.
        Otherwise, show the range of the new marker.
        """
        if not self.scene:
            return

        if self.event_sel or self.deselect:
            return

        if self.sel_xy[0] is None or self.sel_xy[1] is None:
            return

        if self.idx_sel in self.scene.items():
            self.scene.removeItem(self.idx_sel)
            self.idx_sel = None

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()

        if chk_marker or chk_event:
            xy_scene = self.mapToScene(event.pos())
            y_distance = self.parent.value('y_distance')
            pos = QRectF(self.sel_xy[0], 0,
                         xy_scene.x() - self.sel_xy[0],
                         len(self.idx_label) * y_distance)
            item = QGraphicsRectItem(pos.normalized())
            item.setPen(NoPen)

            if chk_marker:
                color = QColor(self.parent.value('annot_bookmark_color'))

            elif chk_event:
                eventtype = self.parent.notes.idx_eventtype.currentText()
                color = convert_name_to_color(eventtype)

            item.setBrush(QBrush(color.lighter(115)))
            item.setZValue(-10)
            self.scene.addItem(item)
            self.idx_sel = item
            return

        xy_scene = self.mapToScene(event.pos())
        pos = QRectF(self.sel_xy[0], self.sel_xy[1],
                     xy_scene.x() - self.sel_xy[0],
                     xy_scene.y() - self.sel_xy[1])
        self.idx_sel = QGraphicsRectItem(pos.normalized())
        self.idx_sel.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH))
        self.scene.addItem(self.idx_sel)

        if self.idx_info in self.scene.items():
            self.scene.removeItem(self.idx_info)

        duration = '{0:0.3f}s'.format(abs(xy_scene.x() - self.sel_xy[0]))

        # get y-size, based on scaling too
        y = abs(xy_scene.y() - self.sel_xy[1])
        scale = self.parent.value('y_scale') * self.chan_scale[self.sel_chan]
        height = '{0:0.3f}uV'.format(y / scale)

        item = TextItem_with_BG()
        item.setText(duration + ' ' + height)
        item.setPos(self.sel_xy[0], self.sel_xy[1])
        self.scene.addItem(item)
        self.idx_info = item

        trial = 0
        time = self.parent.traces.data.axis['time'][trial]
        beg_win = min((self.sel_xy[0], xy_scene.x()))
        end_win = max((self.sel_xy[0], xy_scene.x()))
        time_of_interest = time[(time >= beg_win) & (time < end_win)]
        if len(time_of_interest) > MINIMUM_N_SAMPLES:
            data = self.parent.traces.data(trial=trial,
                                           chan=self.chan[self.sel_chan],
                                           time=time_of_interest)
            n_data = len(data)
            n_pad = (power(2, ceil(log2(n_data))) - n_data) / 2
            data = pad(data, (int(ceil(n_pad)), int(floor(n_pad))), 'constant')

            self.parent.spectrum.display(data)

    def mouseReleaseEvent(self, event):
        """Create a new event or marker, or show the previous power spectrum
        """
        if not self.scene:
            return

        if self.event_sel:
            return

        if self.deselect:
            self.deselect = False
            return

        if not self.ready:
            return

        chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
        chk_event = self.parent.notes.action['new_event'].isChecked()
        y_distance = self.parent.value('y_distance')

        if chk_marker or chk_event:

            x_in_scene = self.mapToScene(event.pos()).x()
            y_in_scene = self.mapToScene(event.pos()).y()

            # it can happen that selection is empty (f.e. double-click)
            if self.sel_xy[0] is not None:
                # max resolution = sampling frequency
                # in case there is no data
                s_freq = self.parent.info.dataset.header['s_freq']
                at_s_freq = lambda x: round(x * s_freq) / s_freq
                start = at_s_freq(self.sel_xy[0])
                end = at_s_freq(x_in_scene)

                if abs(end - start) < self.parent.value('min_marker_dur'):
                    end = start

                if start <= end:
                    time = (start, end)
                else:
                    time = (end, start)

                if chk_marker:
                    self.parent.notes.add_bookmark(time)

                elif chk_event and start != end:
                    eventtype = self.parent.notes.idx_eventtype.currentText()

                    # if dragged across > 1.5 chan, event is marked on all chan
                    if abs(y_in_scene - self.sel_xy[1]) > 1.5 * y_distance:
                        chan = ''
                    else:
                        chan_idx = int(floor(self.sel_xy[1] / y_distance))
                        chan = self.chan[chan_idx]

                    self.parent.notes.add_event(eventtype, time, chan)

        else:  # normal selection

            if self.idx_info in self.scene.items():
                self.scene.removeItem(self.idx_info)
            self.idx_info = None

            # restore spectrum
            self.parent.spectrum.update()
            self.parent.spectrum.display_window()

        # general garbage collection
        self.sel_chan = None
        self.sel_xy = (None, None)

        if self.idx_sel in self.scene.items():
            self.scene.removeItem(self.idx_sel)
            self.idx_sel = None

    def keyPressEvent(self, event):
        chk_event = self.parent.notes.action['new_event'].isChecked()
        chk_book = self.parent.notes.action['new_bookmark'].isChecked()
        if not ((chk_event or chk_book) and self.event_sel):
            return

        annot = self.event_sel
        highlight = self.highlight
        annot_start = annot.marker.x()
        annot_end = annot_start + annot.marker.width()

        if type(event) == QKeyEvent and (event.key() == Qt.Key_Delete
                                         or event.key() == Qt.Key_Backspace):
            if chk_event:
                self.parent.notes.remove_event(time=(annot_start, annot_end))
            elif chk_book:
                self.parent.notes.remove_bookmark(time=(annot_start,
                                                        annot_end))
            self.scene.removeItem(highlight)
            msg = 'Deleted event from {} to {}'.format(annot_start, annot_end)
            self.parent.statusBar().showMessage(msg)
            self.event_sel = None
            self.highlight = None
            self.parent.notes.idx_eventtype.setCurrentText(self.current_etype)
            self.current_etype = None
            self.current_event = None
            self.display_annotations

    def highlight_event(self, annot):
        """Highlight an annotation on the trace.

        Parameters
        ----------
        annot : intance of wonambi.widgets.utils.RectMarker
            existing annotation
        """
        beg = annot.marker.x()
        end = beg + annot.marker.width()
        window_start = self.parent.value('window_start')
        window_length = self.parent.value('window_length')
        events = self.parent.notes.get_selected_events(
            (window_start, window_start + window_length))
        ev = [x for x in events if (x['start'] == annot.marker.x() or \
                                    x['end'] == annot.marker.y())]

        if ev:
            annot_name = ev[0]['name']

            msg = "Event of type '{}' from {} to {}".format(
                annot_name, beg, end)
            self.current_etype = self.parent.notes.idx_eventtype.currentText()
            self.parent.notes.idx_eventtype.setCurrentText(annot_name)
            self.current_event = ev[0]
        else:
            msg = "Marker from {} to {}".format(beg, end)
        self.parent.statusBar().showMessage(msg)

        highlight = self.highlight = RectMarker(annot.marker.x(),
                                                annot.marker.y(),
                                                annot.marker.width(),
                                                annot.marker.height(),
                                                zvalue=-5,
                                                color=QColor(255, 255, 51))
        self.scene.addItem(highlight)
        self.event_sel = annot

    def next_event(self, delete=False):
        """Go to next event."""
        if delete:
            msg = "Delete this event? This cannot be undone."
            msgbox = QMessageBox(QMessageBox.Question, 'Delete event', msg)
            msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msgbox.setDefaultButton(QMessageBox.Yes)
            response = msgbox.exec_()
            if response == QMessageBox.No:
                return

        event_sel = self.event_sel
        if event_sel is None:
            return

        notes = self.parent.notes

        if not self.current_event_row:
            row = notes.find_row(
                event_sel.marker.x(),
                event_sel.marker.x() + event_sel.marker.width())
        else:
            row = self.current_event_row

        same_type = self.action['next_of_same_type'].isChecked()
        if same_type:
            target = notes.idx_annot_list.item(row, 2).text()

        if delete:
            notes.delete_row()
            msg = 'Deleted event from {} to {}.'.format(
                event_sel.marker.x(),
                event_sel.marker.x() + event_sel.marker.width())
            self.parent.statusBar().showMessage(msg)
            row -= 1

        if row + 1 == notes.idx_annot_list.rowCount():
            return

        if not same_type:
            next_row = row + 1
        else:
            next_row = None
            types = notes.idx_annot_list.property('name')[row + 1:]

            for i, ty in enumerate(types):
                if ty == target:
                    next_row = row + 1 + i
                    break

            if next_row is None:
                return

        self.current_event_row = next_row
        notes.go_to_marker(next_row, 0, 'annot')
        notes.idx_annot_list.setCurrentCell(next_row, 0)

    def change_event_type(self):
        """Action: change highlighted event's type by cycling through event
        type list."""
        if self.current_event is None:
            return

        hl_params = self.highlight.params
        self.scene.removeItem(self.highlight)

        ev = self.current_event
        new_name = self.parent.notes.change_event_type(name=ev['name'],
                                                       time=(ev['start'],
                                                             ev['end']),
                                                       chan=ev['chan'])
        msg = "Event from {} to {} changed type from '{}' to '{}'".format(
            ev['start'], ev['end'], ev['name'], new_name)
        ev['name'] = new_name

        self.current_event = ev
        self.current_etype = new_name
        #self.event_sel = True
        self.parent.notes.idx_eventtype.setCurrentText(new_name)
        self.parent.statusBar().showMessage(msg)
        self.display_annotations()
        self.highlight = RectMarker(*hl_params)
        self.scene.addItem(self.highlight)

    def resizeEvent(self, event):
        """Resize scene so that it fits the whole widget.

        Parameters
        ----------
        event : instance of QtCore.QEvent
            not important

        Notes
        -----
        This function overwrites Qt function, therefore the non-standard
        name. Argument also depends on Qt.

        The function is used to change the scale of view, so that the scene
        fits the whole scene. There are two problems that I could not fix: 1)
        how to give the width of the label in absolute width, 2) how to strech
        scene just enough that it doesn't trigger a scrollbar. However, it's
        pretty good as it is now.
        """
        if self.scene is not None:
            ratio = self.width() / (self.scene.width() * 1.1)
            self.resetTransform()
            self.scale(ratio, 1)

    def reset(self):
        self.y_scrollbar_value = 0
        self.data = None
        self.chan = []
        self.chan_pos = []
        self.chan_scale = []
        self.sel_chan = None
        self.sel_xy = (None, None)

        if self.scene is not None:
            self.scene.clear()
        self.scene = None
        self.idx_sel = None
        self.idx_info = None
        self.idx_label = []
        self.idx_time = []
        self.time_pos = []
Esempio n. 4
0
class Screenshot(QGraphicsView):
    """ Main Class """

    screen_shot_grabed = pyqtSignal(QImage)
    widget_closed = pyqtSignal()

    def __init__(self, flags=constant.DEFAULT, parent=None):
        """
        flags: binary flags. see the flags in the constant.py
        """
        super().__init__(parent)

        # Init
        self.penColorNow = QColor(PENCOLOR)
        self.penSizeNow = PENSIZE
        self.fontNow = QFont('Sans')
        self.clipboard = QApplication.clipboard()

        self.drawListResult = [
        ]  # draw list that sure to be drew, [action, coord]
        self.drawListProcess = None  # the process to the result
        self.selectedArea = QRect(
        )  # a QRect instance which stands for the selected area
        self.selectedAreaRaw = QRect()
        self.mousePosition = MousePosition.OUTSIDE_AREA  # mouse position
        self.screenPixel = None
        self.textRect = None

        self.mousePressed = False
        self.action = ACTION_SELECT
        self.mousePoint = self.cursor().pos()

        self.startX, self.startY = 0, 0  # the point where you start
        self.endX, self.endY = 0, 0  # the point where you end
        self.pointPath = QPainterPath(
        )  # the point mouse passes, used by draw free line
        self.itemsToRemove = [
        ]  # the items that should not draw on screenshot picture
        self.textPosition = None

        # result
        self.target_img = None

        # Init window
        self.getscreenshot()
        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)

        self.setMouseTracking(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setContentsMargins(0, 0, 0, 0)
        self.setStyleSheet("QGraphicsView { border-style: none; }")

        self.tooBar = MyToolBar(flags, self)
        self.tooBar.trigger.connect(self.changeAction)

        self.penSetBar = None
        if flags & constant.RECT or flags & constant.ELLIPSE or flags & constant.LINE or flags & constant.FREEPEN \
                or flags & constant.ARROW or flags & constant.TEXT:
            self.penSetBar = PenSetWidget(self)
            self.penSetBar.penSizeTrigger.connect(self.changePenSize)
            self.penSetBar.penColorTrigger.connect(self.changePenColor)
            self.penSetBar.fontChangeTrigger.connect(self.changeFont)

        self.textInput = TextInput(self)
        self.textInput.inputChanged.connect(self.textChange)
        self.textInput.cancelPressed.connect(self.cancelInput)
        self.textInput.okPressed.connect(self.okInput)

        self.graphicsScene = QGraphicsScene(0, 0, self.screenPixel.width(),
                                            self.screenPixel.height())

        self.show()
        self.setScene(self.graphicsScene)
        self.windowHandle().setScreen(QGuiApplication.screenAt(QCursor.pos()))
        self.scale = self.get_scale()
        # self.setFixedSize(self.screenPixel.width(), self.screenPixel.height())
        self.setGeometry(QGuiApplication.screenAt(QCursor.pos()).geometry())
        self.showFullScreen()
        self.redraw()

        QShortcut(QKeySequence('ctrl+s'),
                  self).activated.connect(self.saveScreenshot)
        QShortcut(QKeySequence('esc'), self).activated.connect(self.close)

    @staticmethod
    def take_screenshot(flags):
        loop = QEventLoop()
        screen_shot = Screenshot(flags)
        screen_shot.show()
        screen_shot.widget_closed.connect(loop.quit)

        loop.exec()
        img = screen_shot.target_img
        return img

    def getscreenshot(self):
        screen = QGuiApplication.screenAt(QCursor.pos())
        self.screenPixel = screen.grabWindow(0)

    def mousePressEvent(self, event):
        """
        :type event: QMouseEvent
        :param event:
        :return:
        """
        if event.button() != Qt.LeftButton:
            return

        if self.action is None:
            self.action = ACTION_SELECT

        self.startX, self.startY = event.x(), event.y()

        if self.action == ACTION_SELECT:
            if self.mousePosition == MousePosition.OUTSIDE_AREA:
                self.mousePressed = True
                self.selectedArea = QRect()
                self.selectedArea.setTopLeft(QPoint(event.x(), event.y()))
                self.selectedArea.setBottomRight(QPoint(event.x(), event.y()))
                self.redraw()
            elif self.mousePosition == MousePosition.INSIDE_AREA:
                self.mousePressed = True
            else:
                pass
        elif self.action == ACTION_MOVE_SELECTED:
            if self.mousePosition == MousePosition.OUTSIDE_AREA:
                self.action = ACTION_SELECT
                self.selectedArea = QRect()
                self.selectedArea.setTopLeft(QPoint(event.x(), event.y()))
                self.selectedArea.setBottomRight(QPoint(event.x(), event.y()))
                self.redraw()
            self.mousePressed = True
        elif self.action in DRAW_ACTION:
            self.mousePressed = True
            if self.action == ACTION_FREEPEN:
                self.pointPath = QPainterPath()
                self.pointPath.moveTo(QPoint(event.x(), event.y()))
            elif self.action == ACTION_TEXT:
                if self.textPosition is None:
                    self.textPosition = QPoint(event.x(), event.y())
                    self.textRect = None
                    self.redraw()

    def mouseMoveEvent(self, event: QMouseEvent):
        """
        :type event: QMouseEvent
        :param event:
        :return:
        """
        self.mousePoint = QPoint(event.globalPos().x(), event.globalPos().y())

        if self.action is None:
            self.action = ACTION_SELECT

        if not self.mousePressed:
            point = QPoint(event.x(), event.y())
            self.detectMousePosition(point)
            self.setCursorStyle()
            self.redraw()
        else:
            self.endX, self.endY = event.x(), event.y()

            # if self.mousePosition != OUTSIDE_AREA:
            #    self.action = ACTION_MOVE_SELECTED

            if self.action == ACTION_SELECT:
                self.selectedArea.setBottomRight(QPoint(event.x(), event.y()))
                self.redraw()
            elif self.action == ACTION_MOVE_SELECTED:
                self.selectedArea = QRect(self.selectedAreaRaw)

                if self.mousePosition == MousePosition.INSIDE_AREA:
                    moveToX = event.x() - self.startX + self.selectedArea.left(
                    )
                    moveToY = event.y() - self.startY + self.selectedArea.top()
                    if 0 <= moveToX <= self.screenPixel.width(
                    ) - 1 - self.selectedArea.width():
                        self.selectedArea.moveLeft(moveToX)
                    if 0 <= moveToY <= self.screenPixel.height(
                    ) - 1 - self.selectedArea.height():
                        self.selectedArea.moveTop(moveToY)
                    self.selectedArea = self.selectedArea.normalized()
                    self.selectedAreaRaw = QRect(self.selectedArea)
                    self.startX, self.startY = event.x(), event.y()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_LEFT_SIDE:
                    moveToX = event.x() - self.startX + self.selectedArea.left(
                    )
                    if moveToX <= self.selectedArea.right():
                        self.selectedArea.setLeft(moveToX)
                        self.selectedArea = self.selectedArea.normalized()
                        self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_RIGHT_SIDE:
                    moveToX = event.x(
                    ) - self.startX + self.selectedArea.right()
                    self.selectedArea.setRight(moveToX)
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_UP_SIDE:
                    moveToY = event.y() - self.startY + self.selectedArea.top()
                    self.selectedArea.setTop(moveToY)
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_DOWN_SIDE:
                    moveToY = event.y(
                    ) - self.startY + self.selectedArea.bottom()
                    self.selectedArea.setBottom(moveToY)
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_TOP_LEFT_CORNER:
                    moveToX = event.x() - self.startX + self.selectedArea.left(
                    )
                    moveToY = event.y() - self.startY + self.selectedArea.top()
                    self.selectedArea.setTopLeft(QPoint(moveToX, moveToY))
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_BOTTOM_RIGHT_CORNER:
                    moveToX = event.x(
                    ) - self.startX + self.selectedArea.right()
                    moveToY = event.y(
                    ) - self.startY + self.selectedArea.bottom()
                    self.selectedArea.setBottomRight(QPoint(moveToX, moveToY))
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_TOP_RIGHT_CORNER:
                    moveToX = event.x(
                    ) - self.startX + self.selectedArea.right()
                    moveToY = event.y() - self.startY + self.selectedArea.top()
                    self.selectedArea.setTopRight(QPoint(moveToX, moveToY))
                    self.selectedArea = self.selectedArea.normalized()
                    self.redraw()
                elif self.mousePosition == MousePosition.ON_THE_BOTTOM_LEFT_CORNER:
                    moveToX = event.x() - self.startX + self.selectedArea.left(
                    )
                    moveToY = event.y(
                    ) - self.startY + self.selectedArea.bottom()
                    self.selectedArea.setBottomLeft(QPoint(moveToX, moveToY))
                    self.redraw()
                else:
                    pass
            elif self.action == ACTION_RECT:
                self.drawRect(self.startX, self.startY, event.x(), event.y(),
                              False)
                self.redraw()
                pass
            elif self.action == ACTION_ELLIPSE:
                self.drawEllipse(self.startX, self.startY, event.x(),
                                 event.y(), False)
                self.redraw()
            elif self.action == ACTION_ARROW:
                self.drawArrow(self.startX, self.startY, event.x(), event.y(),
                               False)
                self.redraw()
            elif self.action == ACTION_LINE:
                self.drawLine(self.startX, self.startY, event.x(), event.y(),
                              False)
                self.redraw()
            elif self.action == ACTION_FREEPEN:
                y1, y2 = event.x(), event.y()
                rect = self.selectedArea.normalized()
                if y1 <= rect.left():
                    y1 = rect.left()
                elif y1 >= rect.right():
                    y1 = rect.right()

                if y2 <= rect.top():
                    y2 = rect.top()
                elif y2 >= rect.bottom():
                    y2 = rect.bottom()

                self.pointPath.lineTo(y1, y2)
                self.drawFreeLine(self.pointPath, False)
                self.redraw()

    def mouseReleaseEvent(self, event):
        """
        :type event: QMouseEvent
        :param event:
        :return:
        """
        if event.button() != Qt.LeftButton:
            return

        if self.mousePressed:
            self.mousePressed = False
            self.endX, self.endY = event.x(), event.y()

            if self.action == ACTION_SELECT:
                self.selectedArea.setBottomRight(QPoint(event.x(), event.y()))
                self.selectedAreaRaw = QRect(self.selectedArea)
                self.action = ACTION_MOVE_SELECTED
                self.redraw()
            elif self.action == ACTION_MOVE_SELECTED:
                self.selectedAreaRaw = QRect(self.selectedArea)
                self.redraw()
                # self.action = None
            elif self.action == ACTION_RECT:
                self.drawRect(self.startX, self.startY, event.x(), event.y(),
                              True)
                self.redraw()
            elif self.action == ACTION_ELLIPSE:
                self.drawEllipse(self.startX, self.startY, event.x(),
                                 event.y(), True)
                self.redraw()
            elif self.action == ACTION_ARROW:
                self.drawArrow(self.startX, self.startY, event.x(), event.y(),
                               True)
                self.redraw()
            elif self.action == ACTION_LINE:
                self.drawLine(self.startX, self.startY, event.x(), event.y(),
                              True)
                self.redraw()
            elif self.action == ACTION_FREEPEN:
                self.drawFreeLine(self.pointPath, True)
                self.redraw()

    def detectMousePosition(self, point):
        """
        :type point: QPoint
        :param point: the mouse position you want to check
        :return:
        """
        if self.selectedArea == QRect():
            self.mousePosition = MousePosition.OUTSIDE_AREA
            return

        if self.selectedArea.left() - ERRORRANGE <= point.x(
        ) <= self.selectedArea.left() and (self.selectedArea.top() - ERRORRANGE
                                           <= point.y() <=
                                           self.selectedArea.top()):
            self.mousePosition = MousePosition.ON_THE_TOP_LEFT_CORNER
        elif self.selectedArea.right() <= point.x() <= self.selectedArea.right(
        ) + ERRORRANGE and (self.selectedArea.top() - ERRORRANGE <= point.y()
                            <= self.selectedArea.top()):
            self.mousePosition = MousePosition.ON_THE_TOP_RIGHT_CORNER
        elif self.selectedArea.left() - ERRORRANGE <= point.x(
        ) <= self.selectedArea.left() and (
                self.selectedArea.bottom() <= point.y() <=
                self.selectedArea.bottom() + ERRORRANGE):
            self.mousePosition = MousePosition.ON_THE_BOTTOM_LEFT_CORNER
        elif self.selectedArea.right() <= point.x() <= self.selectedArea.right(
        ) + ERRORRANGE and (self.selectedArea.bottom() <= point.y() <=
                            self.selectedArea.bottom() + ERRORRANGE):
            self.mousePosition = MousePosition.ON_THE_BOTTOM_RIGHT_CORNER
        elif -ERRORRANGE <= point.x() - self.selectedArea.left() <= 0 and (
                self.selectedArea.topLeft().y() < point.y() <
                self.selectedArea.bottomLeft().y()):
            self.mousePosition = MousePosition.ON_THE_LEFT_SIDE
        elif 0 <= point.x() - self.selectedArea.right() <= ERRORRANGE and (
                self.selectedArea.topRight().y() < point.y() <
                self.selectedArea.bottomRight().y()):
            self.mousePosition = MousePosition.ON_THE_RIGHT_SIDE
        elif -ERRORRANGE <= point.y() - self.selectedArea.top() <= 0 and (
                self.selectedArea.topLeft().x() < point.x() <
                self.selectedArea.topRight().x()):
            self.mousePosition = MousePosition.ON_THE_UP_SIDE
        elif 0 <= point.y() - self.selectedArea.bottom() <= ERRORRANGE and (
                self.selectedArea.bottomLeft().x() < point.x() <
                self.selectedArea.bottomRight().x()):
            self.mousePosition = MousePosition.ON_THE_DOWN_SIDE
        elif not self.selectedArea.contains(point):
            self.mousePosition = MousePosition.OUTSIDE_AREA
        else:
            self.mousePosition = MousePosition.INSIDE_AREA

    def setCursorStyle(self):
        if self.action in DRAW_ACTION:
            self.setCursor(Qt.CrossCursor)
            return

        if self.mousePosition == MousePosition.ON_THE_LEFT_SIDE or \
                        self.mousePosition == MousePosition.ON_THE_RIGHT_SIDE:

            self.setCursor(Qt.SizeHorCursor)
        elif self.mousePosition == MousePosition.ON_THE_UP_SIDE or \
                        self.mousePosition == MousePosition.ON_THE_DOWN_SIDE:

            self.setCursor(Qt.SizeVerCursor)
        elif self.mousePosition == MousePosition.ON_THE_TOP_LEFT_CORNER or \
                        self.mousePosition == MousePosition.ON_THE_BOTTOM_RIGHT_CORNER:

            self.setCursor(Qt.SizeFDiagCursor)
        elif self.mousePosition == MousePosition.ON_THE_TOP_RIGHT_CORNER or \
                        self.mousePosition == MousePosition.ON_THE_BOTTOM_LEFT_CORNER:

            self.setCursor(Qt.SizeBDiagCursor)
        elif self.mousePosition == MousePosition.OUTSIDE_AREA:
            self.setCursor(Qt.ArrowCursor)
        elif self.mousePosition == MousePosition.INSIDE_AREA:
            self.setCursor(Qt.OpenHandCursor)
        else:
            self.setCursor(Qt.ArrowCursor)
            pass

    def drawMagnifier(self):
        # First, calculate the magnifier position due to the mouse position
        watchAreaWidth = 16
        watchAreaHeight = 16
        watchAreaPixmap = QPixmap()

        cursor_pos = self.mousePoint

        watchArea = QRect(
            QPoint(cursor_pos.x() - watchAreaWidth / 2,
                   cursor_pos.y() - watchAreaHeight / 2),
            QPoint(cursor_pos.x() + watchAreaWidth / 2,
                   cursor_pos.y() + watchAreaHeight / 2))
        if watchArea.left() < 0:
            watchArea.moveLeft(0)
            watchArea.moveRight(watchAreaWidth)
        if self.mousePoint.x() + watchAreaWidth / 2 >= self.screenPixel.width(
        ):
            watchArea.moveRight(self.screenPixel.width() - 1)
            watchArea.moveLeft(watchArea.right() - watchAreaWidth)
        if self.mousePoint.y() - watchAreaHeight / 2 < 0:
            watchArea.moveTop(0)
            watchArea.moveBottom(watchAreaHeight)
        if self.mousePoint.y(
        ) + watchAreaHeight / 2 >= self.screenPixel.height():
            watchArea.moveBottom(self.screenPixel.height() - 1)
            watchArea.moveTop(watchArea.bottom() - watchAreaHeight)

        # tricks to solve the hidpi impact on QCursor.pos()
        watchArea.setTopLeft(
            QPoint(watchArea.topLeft().x() * self.scale,
                   watchArea.topLeft().y() * self.scale))
        watchArea.setBottomRight(
            QPoint(watchArea.bottomRight().x() * self.scale,
                   watchArea.bottomRight().y() * self.scale))
        watchAreaPixmap = self.screenPixel.copy(watchArea)

        # second, calculate the magnifier area
        magnifierAreaWidth = watchAreaWidth * 10
        magnifierAreaHeight = watchAreaHeight * 10
        fontAreaHeight = 40

        cursorSize = 24
        magnifierArea = QRectF(
            QPoint(QCursor.pos().x() + cursorSize,
                   QCursor.pos().y() + cursorSize),
            QPoint(QCursor.pos().x() + cursorSize + magnifierAreaWidth,
                   QCursor.pos().y() + cursorSize + magnifierAreaHeight))
        if magnifierArea.right() >= self.screenPixel.width():
            magnifierArea.moveLeft(QCursor.pos().x() - magnifierAreaWidth -
                                   cursorSize / 2)
        if magnifierArea.bottom() + fontAreaHeight >= self.screenPixel.height(
        ):
            magnifierArea.moveTop(QCursor.pos().y() - magnifierAreaHeight -
                                  cursorSize / 2 - fontAreaHeight)

        # third, draw the watch area to magnifier area
        watchAreaScaled = watchAreaPixmap.scaled(
            QSize(magnifierAreaWidth * self.scale,
                  magnifierAreaHeight * self.scale))
        magnifierPixmap = self.graphicsScene.addPixmap(watchAreaScaled)
        magnifierPixmap.setOffset(magnifierArea.topLeft())

        # then draw lines and text
        self.graphicsScene.addRect(QRectF(magnifierArea),
                                   QPen(QColor(255, 255, 255), 2))
        self.graphicsScene.addLine(
            QLineF(QPointF(magnifierArea.center().x(), magnifierArea.top()),
                   QPointF(magnifierArea.center().x(),
                           magnifierArea.bottom())),
            QPen(QColor(0, 255, 255), 2))
        self.graphicsScene.addLine(
            QLineF(QPointF(magnifierArea.left(),
                           magnifierArea.center().y()),
                   QPointF(magnifierArea.right(),
                           magnifierArea.center().y())),
            QPen(QColor(0, 255, 255), 2))

        # get the rgb of mouse point
        pointRgb = QColor(self.screenPixel.toImage().pixel(self.mousePoint))

        # draw information
        self.graphicsScene.addRect(
            QRectF(
                magnifierArea.bottomLeft(),
                magnifierArea.bottomRight() + QPoint(0, fontAreaHeight + 30)),
            Qt.black, QBrush(Qt.black))
        rgbInfo = self.graphicsScene.addSimpleText(
            ' Rgb: ({0}, {1}, {2})'.format(pointRgb.red(), pointRgb.green(),
                                           pointRgb.blue()))
        rgbInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 5))
        rgbInfo.setPen(QPen(QColor(255, 255, 255), 2))

        rect = self.selectedArea.normalized()
        sizeInfo = self.graphicsScene.addSimpleText(' Size: {0} x {1}'.format(
            rect.width() * self.scale,
            rect.height() * self.scale))
        sizeInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 15) +
                        QPoint(0, fontAreaHeight / 2))
        sizeInfo.setPen(QPen(QColor(255, 255, 255), 2))

    def get_scale(self):
        return self.devicePixelRatio()

    def saveScreenshot(self,
                       clipboard=False,
                       fileName='screenshot.png',
                       picType='png'):
        fullWindow = QRect(0, 0, self.width() - 1, self.height() - 1)
        selected = QRect(self.selectedArea)
        if selected.left() < 0:
            selected.setLeft(0)
        if selected.right() >= self.width():
            selected.setRight(self.width() - 1)
        if selected.top() < 0:
            selected.setTop(0)
        if selected.bottom() >= self.height():
            selected.setBottom(self.height() - 1)

        source = (fullWindow & selected)
        source.setTopLeft(
            QPoint(source.topLeft().x() * self.scale,
                   source.topLeft().y() * self.scale))
        source.setBottomRight(
            QPoint(source.bottomRight().x() * self.scale,
                   source.bottomRight().y() * self.scale))
        image = self.screenPixel.copy(source)
        image.setDevicePixelRatio(1)

        if clipboard:
            QGuiApplication.clipboard().setImage(QImage(image),
                                                 QClipboard.Clipboard)
        else:
            image.save(fileName, picType, 10)
        self.target_img = image
        self.screen_shot_grabed.emit(QImage(image))

    def redraw(self):
        self.graphicsScene.clear()

        # draw screenshot
        self.graphicsScene.addPixmap(self.screenPixel)

        # prepare for drawing selected area
        rect = QRectF(self.selectedArea)
        rect = rect.normalized()

        topLeftPoint = rect.topLeft()
        topRightPoint = rect.topRight()
        bottomLeftPoint = rect.bottomLeft()
        bottomRightPoint = rect.bottomRight()
        topMiddlePoint = (topLeftPoint + topRightPoint) / 2
        leftMiddlePoint = (topLeftPoint + bottomLeftPoint) / 2
        bottomMiddlePoint = (bottomLeftPoint + bottomRightPoint) / 2
        rightMiddlePoint = (topRightPoint + bottomRightPoint) / 2

        # draw the picture mask
        mask = QColor(0, 0, 0, 155)

        if self.selectedArea == QRect():
            self.graphicsScene.addRect(0, 0, self.screenPixel.width(),
                                       self.screenPixel.height(),
                                       QPen(Qt.NoPen), mask)
        else:
            self.graphicsScene.addRect(0, 0, self.screenPixel.width(),
                                       topRightPoint.y(), QPen(Qt.NoPen), mask)
            self.graphicsScene.addRect(0, topLeftPoint.y(), topLeftPoint.x(),
                                       rect.height(), QPen(Qt.NoPen), mask)
            self.graphicsScene.addRect(
                topRightPoint.x(), topRightPoint.y(),
                self.screenPixel.width() - topRightPoint.x(), rect.height(),
                QPen(Qt.NoPen), mask)
            self.graphicsScene.addRect(
                0, bottomLeftPoint.y(), self.screenPixel.width(),
                self.screenPixel.height() - bottomLeftPoint.y(),
                QPen(Qt.NoPen), mask)

        # draw the toolBar
        if self.action != ACTION_SELECT:
            spacing = 5
            # show the toolbar first, then move it to the correct position
            # because the width of it may be wrong if this is the first time it shows
            self.tooBar.show()

            dest = QPointF(rect.bottomRight() -
                           QPointF(self.tooBar.width(), 0) -
                           QPointF(spacing, -spacing))
            if dest.x() < spacing:
                dest.setX(spacing)
            pen_set_bar_height = self.penSetBar.height(
            ) if self.penSetBar is not None else 0
            if dest.y() + self.tooBar.height(
            ) + pen_set_bar_height >= self.height():
                if rect.top() - self.tooBar.height(
                ) - pen_set_bar_height < spacing:
                    dest.setY(rect.top() + spacing)
                else:
                    dest.setY(rect.top() - self.tooBar.height() -
                              pen_set_bar_height - spacing)

            self.tooBar.move(dest.toPoint())

            if self.penSetBar is not None:
                self.penSetBar.show()
                self.penSetBar.move(dest.toPoint() +
                                    QPoint(0,
                                           self.tooBar.height() + spacing))

                if self.action == ACTION_TEXT:
                    self.penSetBar.showFontWidget()
                else:
                    self.penSetBar.showPenWidget()
        else:
            self.tooBar.hide()

            if self.penSetBar is not None:
                self.penSetBar.hide()

        # draw the list
        for step in self.drawListResult:
            self.drawOneStep(step)

        if self.drawListProcess is not None:
            self.drawOneStep(self.drawListProcess)
            if self.action != ACTION_TEXT:
                self.drawListProcess = None

        if self.selectedArea != QRect():
            self.itemsToRemove = []

            # draw the selected rectangle
            pen = QPen(QColor(0, 255, 255), 2)
            self.itemsToRemove.append(self.graphicsScene.addRect(rect, pen))

            # draw the drag point
            radius = QPoint(3, 3)
            brush = QBrush(QColor(0, 255, 255))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(topLeftPoint - radius, topLeftPoint + radius), pen,
                    brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(topMiddlePoint - radius, topMiddlePoint + radius),
                    pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(topRightPoint - radius, topRightPoint + radius),
                    pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(leftMiddlePoint - radius, leftMiddlePoint + radius),
                    pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(rightMiddlePoint - radius,
                           rightMiddlePoint + radius), pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(bottomLeftPoint - radius, bottomLeftPoint + radius),
                    pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(bottomMiddlePoint - radius,
                           bottomMiddlePoint + radius), pen, brush))
            self.itemsToRemove.append(
                self.graphicsScene.addEllipse(
                    QRectF(bottomRightPoint - radius,
                           bottomRightPoint + radius), pen, brush))

        # draw the textedit
        if self.textPosition is not None:
            textSpacing = 50
            position = QPoint()
            if self.textPosition.x() + self.textInput.width(
            ) >= self.screenPixel.width():
                position.setX(self.textPosition.x() - self.textInput.width())
            else:
                position.setX(self.textPosition.x())

            if self.textRect is not None:
                if self.textPosition.y() + self.textInput.height(
                ) + self.textRect.height() >= self.screenPixel.height():
                    position.setY(self.textPosition.y() -
                                  self.textInput.height() -
                                  self.textRect.height())
                else:
                    position.setY(self.textPosition.y() +
                                  self.textRect.height())
            else:
                if self.textPosition.y() + self.textInput.height(
                ) >= self.screenPixel.height():
                    position.setY(self.textPosition.y() -
                                  self.textInput.height())
                else:
                    position.setY(self.textPosition.y())

            self.textInput.move(position)
            self.textInput.show()
            # self.textInput.getFocus()

        # draw the magnifier
        if self.action == ACTION_SELECT:
            self.drawMagnifier()
            if self.mousePressed:
                self.drawSizeInfo()

        if self.action == ACTION_MOVE_SELECTED:
            self.drawSizeInfo()

    # deal with every step in drawList
    def drawOneStep(self, step):
        """
        :type step: tuple
        """
        if step[0] == ACTION_RECT:
            self.graphicsScene.addRect(
                QRectF(QPointF(step[1], step[2]), QPointF(step[3], step[4])),
                step[5])
        elif step[0] == ACTION_ELLIPSE:
            self.graphicsScene.addEllipse(
                QRectF(QPointF(step[1], step[2]), QPointF(step[3], step[4])),
                step[5])
        elif step[0] == ACTION_ARROW:
            arrow = QPolygonF()

            linex = float(step[1] - step[3])
            liney = float(step[2] - step[4])
            line = sqrt(pow(linex, 2) + pow(liney, 2))

            # in case to divided by 0
            if line == 0:
                return

            sinAngel = liney / line
            cosAngel = linex / line

            # sideLength is the length of bottom side of the body of an arrow
            # arrowSize is the size of the head of an arrow, left and right
            # sides' size is arrowSize, and the bottom side's size is arrowSize / 2
            sideLength = step[5].width()
            arrowSize = 8
            bottomSize = arrowSize / 2

            tmpPoint = QPointF(step[3] + arrowSize * sideLength * cosAngel,
                               step[4] + arrowSize * sideLength * sinAngel)

            point1 = QPointF(step[1] + sideLength * sinAngel,
                             step[2] - sideLength * cosAngel)
            point2 = QPointF(step[1] - sideLength * sinAngel,
                             step[2] + sideLength * cosAngel)
            point3 = QPointF(tmpPoint.x() - sideLength * sinAngel,
                             tmpPoint.y() + sideLength * cosAngel)
            point4 = QPointF(tmpPoint.x() - bottomSize * sideLength * sinAngel,
                             tmpPoint.y() + bottomSize * sideLength * cosAngel)
            point5 = QPointF(step[3], step[4])
            point6 = QPointF(tmpPoint.x() + bottomSize * sideLength * sinAngel,
                             tmpPoint.y() - bottomSize * sideLength * cosAngel)
            point7 = QPointF(tmpPoint.x() + sideLength * sinAngel,
                             tmpPoint.y() - sideLength * cosAngel)

            arrow.append(point1)
            arrow.append(point2)
            arrow.append(point3)
            arrow.append(point4)
            arrow.append(point5)
            arrow.append(point6)
            arrow.append(point7)
            arrow.append(point1)

            self.graphicsScene.addPolygon(arrow, step[5], step[6])
        elif step[0] == ACTION_LINE:
            self.graphicsScene.addLine(
                QLineF(QPointF(step[1], step[2]), QPointF(step[3], step[4])),
                step[5])
        elif step[0] == ACTION_FREEPEN:
            self.graphicsScene.addPath(step[1], step[2])
        elif step[0] == ACTION_TEXT:
            textAdd = self.graphicsScene.addSimpleText(step[1], step[2])
            textAdd.setPos(step[3])
            textAdd.setBrush(QBrush(step[4]))
            self.textRect = textAdd.boundingRect()

    # draw the size information on the top left corner
    def drawSizeInfo(self):
        sizeInfoAreaWidth = 200
        sizeInfoAreaHeight = 30
        spacing = 5
        rect = self.selectedArea.normalized()
        sizeInfoArea = QRect(rect.left(),
                             rect.top() - spacing - sizeInfoAreaHeight,
                             sizeInfoAreaWidth, sizeInfoAreaHeight)

        if sizeInfoArea.top() < 0:
            sizeInfoArea.moveTopLeft(rect.topLeft() + QPoint(spacing, spacing))
        if sizeInfoArea.right() >= self.screenPixel.width():
            sizeInfoArea.moveTopLeft(rect.topLeft() -
                                     QPoint(spacing, spacing) -
                                     QPoint(sizeInfoAreaWidth, 0))
        if sizeInfoArea.left() < spacing:
            sizeInfoArea.moveLeft(spacing)
        if sizeInfoArea.top() < spacing:
            sizeInfoArea.moveTop(spacing)

        self.itemsToRemove.append(
            self.graphicsScene.addRect(QRectF(sizeInfoArea), Qt.white,
                                       QBrush(Qt.black)))

        sizeInfo = self.graphicsScene.addSimpleText('  {0} x {1}'.format(
            rect.width() * self.scale,
            rect.height() * self.scale))
        sizeInfo.setPos(sizeInfoArea.topLeft() + QPoint(0, 2))
        sizeInfo.setPen(QPen(QColor(255, 255, 255), 2))
        self.itemsToRemove.append(sizeInfo)

    def drawRect(self, x1, x2, y1, y2, result):
        rect = self.selectedArea.normalized()
        tmpRect = QRect(QPoint(x1, x2), QPoint(y1, y2)).normalized()
        resultRect = rect & tmpRect
        tmp = [
            ACTION_RECT,
            resultRect.topLeft().x(),
            resultRect.topLeft().y(),
            resultRect.bottomRight().x(),
            resultRect.bottomRight().y(),
            QPen(QColor(self.penColorNow), int(self.penSizeNow))
        ]
        if result:
            self.drawListResult.append(tmp)
        else:
            self.drawListProcess = tmp

    def drawEllipse(self, x1, x2, y1, y2, result):
        rect = self.selectedArea.normalized()
        tmpRect = QRect(QPoint(x1, x2), QPoint(y1, y2)).normalized()
        resultRect = rect & tmpRect
        tmp = [
            ACTION_ELLIPSE,
            resultRect.topLeft().x(),
            resultRect.topLeft().y(),
            resultRect.bottomRight().x(),
            resultRect.bottomRight().y(),
            QPen(QColor(self.penColorNow), int(self.penSizeNow))
        ]
        if result:
            self.drawListResult.append(tmp)
        else:
            self.drawListProcess = tmp

    def drawArrow(self, x1, x2, y1, y2, result):
        rect = self.selectedArea.normalized()
        if y1 <= rect.left():
            y1 = rect.left()
        elif y1 >= rect.right():
            y1 = rect.right()

        if y2 <= rect.top():
            y2 = rect.top()
        elif y2 >= rect.bottom():
            y2 = rect.bottom()

        tmp = [
            ACTION_ARROW, x1, x2, y1, y2,
            QPen(QColor(self.penColorNow), int(self.penSizeNow)),
            QBrush(QColor(self.penColorNow))
        ]
        if result:
            self.drawListResult.append(tmp)
        else:
            self.drawListProcess = tmp

    def drawLine(self, x1, x2, y1, y2, result):
        rect = self.selectedArea.normalized()
        if y1 <= rect.left():
            y1 = rect.left()
        elif y1 >= rect.right():
            y1 = rect.right()

        if y2 <= rect.top():
            y2 = rect.top()
        elif y2 >= rect.bottom():
            y2 = rect.bottom()

        tmp = [
            ACTION_LINE, x1, x2, y1, y2,
            QPen(QColor(self.penColorNow), int(self.penSizeNow))
        ]
        if result:
            self.drawListResult.append(tmp)
        else:
            self.drawListProcess = tmp

    def drawFreeLine(self, pointPath, result):
        tmp = [
            ACTION_FREEPEN,
            QPainterPath(pointPath),
            QPen(QColor(self.penColorNow), int(self.penSizeNow))
        ]
        if result:
            self.drawListResult.append(tmp)
        else:
            self.drawListProcess = tmp

    def textChange(self):
        if self.textPosition is None:
            return
        self.text = self.textInput.getText()
        self.drawListProcess = [
            ACTION_TEXT,
            str(self.text),
            QFont(self.fontNow),
            QPoint(self.textPosition),
            QColor(self.penColorNow)
        ]
        self.redraw()

    def undoOperation(self):
        if len(self.drawListResult) == 0:
            self.action = ACTION_SELECT
            self.selectedArea = QRect()
            self.selectedAreaRaw = QRect()
            self.tooBar.hide()
            if self.penSetBar is not None:
                self.penSetBar.hide()
        else:
            self.drawListResult.pop()
        self.redraw()

    def saveOperation(self):
        filename = QFileDialog.getSaveFileName(self, 'Save file',
                                               './screenshot.png',
                                               '*.png;;*.jpg')
        if len(filename[0]) == 0:
            return
        else:
            self.saveScreenshot(False, filename[0], filename[1][2:])
            self.close()

    def close(self):
        self.widget_closed.emit()
        super().close()
        self.tooBar.close()
        if self.penSetBar is not None:
            self.penSetBar.close()

    def saveToClipboard(self):
        QApplication.clipboard().setText('Test in save function')

        self.saveScreenshot(True)
        self.close()

    # slots
    def changeAction(self, nextAction):
        QApplication.clipboard().setText('Test in changeAction function')

        if nextAction == ACTION_UNDO:
            self.undoOperation()
        elif nextAction == ACTION_SAVE:
            self.saveOperation()
        elif nextAction == ACTION_CANCEL:
            self.close()
        elif nextAction == ACTION_SURE:
            self.saveToClipboard()

        else:
            self.action = nextAction

        self.setFocus()

    def changePenSize(self, nextPenSize):
        self.penSizeNow = nextPenSize

    def changePenColor(self, nextPenColor):
        self.penColorNow = nextPenColor

    def cancelInput(self):
        self.drawListProcess = None
        self.textPosition = None
        self.textRect = None
        self.textInput.hide()
        self.textInput.clearText()
        self.redraw()

    def okInput(self):
        self.text = self.textInput.getText()
        self.drawListResult.append([
            ACTION_TEXT,
            str(self.text),
            QFont(self.fontNow),
            QPoint(self.textPosition),
            QColor(self.penColorNow)
        ])
        self.textPosition = None
        self.textRect = None
        self.textInput.hide()
        self.textInput.clearText()
        self.redraw()

    def changeFont(self, font):
        self.fontNow = font
Esempio n. 5
0
class imwin(QGraphicsView):  #Subclass QLabel for interaction w/ QPixmap
    def __init__(self, parent=None):
        super(imwin, self).__init__(parent)
        self.scene = QGraphicsScene()
        self.view = QGraphicsView(self.scene)

        self.pixmap = None
        self._lastpos = None
        self._thispos = None
        self.delta = QtCore.QPointF(0, 0)
        self.nm = None
        self.measuring_length = False
        self.measuring_widths = False
        self.measuring_area = False
        self.measuring_angle = False
        self._zoom = 1
        self.newPos = None
        self.oldPos = None
        self.factor = 1.0
        self.numwidths = None
        self.widthNames = []  #initialize as empty list
        self.d = {}  #dictionary for line items
        #self.k = 0 #initialize counter so lines turn yellow
        self.L = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0)))
        self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0)))
        self.scene.realline = None
        self.scene.testline = None
        self.setMouseTracking(True)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setRenderHints(QtGui.QPainter.Antialiasing
                            | QtGui.QPainter.SmoothPixmapTransform)
        self.setRenderHint(QtGui.QPainter.Antialiasing, True)
        self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, True)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setInteractive(False)

    def keyPressEvent(self, event):  #shift modifier for panning
        if event.key() == QtCore.Qt.Key_Shift:
            pos = QtGui.QCursor.pos()
            self.oldPos = self.mapToScene(self.mapFromGlobal(pos))

    def mouseMoveEvent(self, event):
        data = self.mapToScene(event.pos())
        rules = [
            self.measuring_length, self.measuring_angle, self.measuring_area
        ]

        modifiers = QApplication.keyboardModifiers()
        if modifiers == QtCore.Qt.ShiftModifier and self.oldPos:
            QApplication.setOverrideCursor(QtCore.Qt.PointingHandCursor)
            self.newPos = data
            delta = self.newPos - self.oldPos
            self.translate(delta.x(), delta.y())
        elif (any(rules) or self.measuring_widths):
            QApplication.setOverrideCursor(
                QtCore.Qt.CrossCursor)  #change cursor
        else:
            QApplication.setOverrideCursor(
                QtCore.Qt.ArrowCursor)  #change cursor

        #dragging line
        if self._thispos and any(rules):
            if self.measuring_length:
                self.parent().statusbar.showMessage(
                    'Click to place next point... double click to finish')
            if self.measuring_area:
                self.parent().statusbar.showMessage(
                    'Click to place next point... close polygon to finish')
            if self.measuring_angle:
                self.parent().statusbar.showMessage(
                    'Click point to define vector')

            end = QtCore.QPointF(data)  #self.mapToScene(event.pos()))
            start = self._thispos

            if self.measuring_angle and self._lastpos:
                start = self._thispos

            if self.scene.testline:  #remove old line
                self.scene.removeItem(self.scene.testline)
                self.scene.testline = False

            if self.measuring_area and self.line_count > 2:
                intersect, xi, yi, k = self.A.checkIntersect(
                    data.x(), data.y())
                if self.scene.area_ellipseItem:  #remove existing intersect
                    self.scene.removeItem(self.scene.area_ellipseItem)
                    self.scene.area_ellipseItem = False
                if self.scene.polyItem:
                    self.scene.removeItem(self.scene.polyItem)
                    self.scene.polyItem = False
                if intersect:
                    #indicate intersect point
                    p = QtCore.QPointF(xi, yi)
                    self.scene.area_ellipseItem = QGraphicsEllipseItem(
                        0, 0, 10, 10)
                    self.scene.area_ellipseItem.setPos(p.x() - 10 / 2,
                                                       p.y() - 10 / 2)
                    self.scene.area_ellipseItem.setBrush(
                        QtGui.QBrush(QtCore.Qt.blue,
                                     style=QtCore.Qt.SolidPattern))
                    self.scene.area_ellipseItem.setFlag(
                        QGraphicsItem.ItemIgnoresTransformations, False
                    )  #size stays small, but doesnt translate if set to false
                    self.scene.addItem(self.scene.area_ellipseItem)
                    #shade polygon region
                    points = [
                        QtCore.QPointF(x, y)
                        for x, y in zip(self.A.x[k:], self.A.y[k:])
                    ]
                    points.append(QtCore.QPointF(xi, yi))
                    self.scene.polyItem = QGraphicsPolygonItem(
                        QtGui.QPolygonF(points))
                    self.scene.polyItem.setBrush(
                        QtGui.QBrush(QtGui.QColor(255, 255, 255, 127)))
                    self.scene.addItem(self.scene.polyItem)

            self.scene.testline = QGraphicsLineItem(QtCore.QLineF(start, end))
            self.scene.addItem(self.scene.testline)

    def mouseDoubleClickEvent(self, event):
        def qpt2pt(x, y):
            Q = self.mapFromScene(self.mapToScene(x, y))
            return Q.x(), Q.y()

        #only delete lines if bezier fit
        if self.measuring_length and self.parent().bezier.isChecked() and (len(
                np.vstack((self.L.x, self.L.y)).T) > 2):
            self.parent().statusbar.showMessage('Length measurement complete.')
            #Remove most recent items drawn (exact lines)
            nl = self.line_count
            for k, i in enumerate(self.scene.items()):
                if k < nl:
                    self.scene.removeItem(i)  #set item to false?

        if self._lastpos and self.measuring_length:
            # catmull roms spline instead?
            # https://codeplea.com/introduction-to-splines

            if (self.parent().bezier.isChecked()) and (len(
                    np.vstack((self.L.x, self.L.y)).T) > 2):
                nt = 2000  #max(1000, self.numwidths * 50)  #num of interpolating points

                # https://gist.github.com/Alquimista/1274149
                # https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html
                def bernstein(i, n, t):
                    return comb(n, i) * t**(n - i) * (1 - t)**i

                def bezier_rational(points, nt):

                    n = len(points)
                    xp = np.array([p[0] for p in points])
                    yp = np.array([p[1] for p in points])
                    t = np.linspace(0.0, 1.0, nt)

                    #Bezier curve
                    B = np.array([bernstein(i, n - 1, t) for i in range(0, n)])
                    xb = np.dot(xp, B)[::-1]
                    yb = np.dot(yp, B)[::-1]

                    #Analytic gradient for bezier curve
                    Qx = n * np.diff(xp)
                    Qy = n * np.diff(yp)
                    Bq = np.array(
                        [bernstein(i, n - 2, t) for i in range(0, n - 1)])
                    dxb = np.dot(Qx, Bq)[::-1]
                    dyb = np.dot(Qy, Bq)[::-1]

                    m = np.vstack((dxb, dyb))
                    m *= (1 / np.linalg.norm(m, axis=0))
                    return xb, yb, m

                points = np.vstack((self.L.x, self.L.y)).T
                self.xs, self.ys, self.m = bezier_rational(points, nt)

                pts = np.array(list(map(qpt2pt, self.xs, self.ys)))
                x, y = pts[:, 0], pts[:, 1]

                #integrate for length
                self.l = np.cumsum(np.hypot(np.gradient(x), np.gradient(y)))
                #self.measurements[-1] = self.l[-1]
                #self.lengths[-1] = self.l[-1]

                #draw cubic line to interpolated points
                for i in range(1, nt - 1):
                    start = self.mapFromScene(
                        self.mapToScene(self.xs[i - 1],
                                        self.ys[i - 1]))  #+ self.pos()
                    mid = self.mapFromScene(
                        self.mapToScene(self.xs[i], self.ys[i]))  #+ self.pos()
                    end = self.mapFromScene(
                        self.mapToScene(self.xs[i + 1],
                                        self.ys[i + 1]))  # + self.pos()
                    path = QtGui.QPainterPath(start)
                    path.cubicTo(start, mid, end)
                    self.scene.addPath(path)

            if (not self.parent().bezier.isChecked()) or (len(
                    np.vstack((self.L.x, self.L.y)).T) <= 2):
                pts = np.array(list(map(qpt2pt, self.L.x, self.L.y)))
                x, y = pts[:, 0], pts[:, 1]

                self.l = np.cumsum(np.hypot(np.diff(x),
                                            np.diff(y)))  #integrate for length
                #self.measurements[-1] = self.l[-1]

            self.lengths[-1] = self.l[-1]
            self.lengths.extend([np.nan])
            self.widths.append([])
            self.widthNames.append([])

        #self.measurements.extend([np.nan])
        QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)  #change cursor
        if self.parent().bezier.isChecked():
            self.parent().widthsButton.setEnabled(True)

        self.parent().lengthButton.setChecked(False)
        self.parent().angleButton.setChecked(False)
        self.measuring_length = False
        self.measuring_angle = False
        self._thispos = False

    def polyClose(self):  #make into hot key not button
        if self.measuring_area:
            if self.line_count > 2:  #cant make polygon w/ two lines
                self.measuring_area = False
                A = self.A.calcArea()
                self.areaValues = np.append(self.areaValues,
                                            A)  #add area values
                #draw permanent polygon
                points = [
                    QtCore.QPointF(x, y) for x, y in zip(self.A.x, self.A.y)
                ]
                self.scene.polyItem2 = QGraphicsPolygonItem(
                    QtGui.QPolygonF(points))
                self.scene.polyItem2.setBrush(
                    QtGui.QBrush(QtGui.QColor(255, 255, 255, 127)))
                if self.scene.polyItem:
                    self.scene.removeItem(
                        self.scene.polyItem)  #remove mouseover polygon
                    self.scene.polyItem = False  #remove mouseover polygon
                self.scene.removeItem(self.scene.testline)
                self.scene.testline = False
                self.scene.addItem(self.scene.polyItem2)  #shade in polygon
                self.parent().statusbar.showMessage(
                    'Polygon area measurement completed')
                self.parent().areaButton.setChecked(False)
                self.parent().bezier.setEnabled(
                    True)  #make bezier fit available again
            else:
                print("cannot draw polygon with fewer than three vertices")

    def measure_widths(self):
        def qpt2pt(x, y):
            Q = self.mapFromScene(self.mapToScene(x, y))
            return Q.x(), Q.y()

        self.measuring_widths = True
        self.parent().widthsButton.setChecked(True)
        self.k = 0
        self.W = posData(np.empty(shape=(0, 0)),
                         np.empty(shape=(0, 0)))  #preallocate custom widths
        #number of possible measurements per segment (length + #widths)
        #self.measurements = np.empty((0, self.iw.nm + 1), int) * np.nan
        self.numwidths = int(self.parent().subWin.numwidths.text())
        #self.measurements[-1] = np.append( self.l[-1], np.zeros(self.numwidths-1)*np.nan ) #preallocate measurements
        self.widths[-1] = np.empty(self.numwidths - 1,
                                   dtype='float')  #preallocate measurements
        self.widthNames[-1] = [
            '{0:2.2f}% Width'.format(100 * f / self.numwidths)
            for f in np.arange(1, self.numwidths)
        ]

        self.nspines = 2 * (self.numwidths - 1)
        self.parent().statusbar.showMessage(
            'Click point along spines to make width measurements perpindicular to the length segment'
        )

        #get pts for width drawing
        bins = np.linspace(0, self.l[-1], self.numwidths + 1)
        inds = np.digitize(self.l, bins)
        __, self.inddec = np.unique(inds, return_index=True)

        pts = np.array(list(map(qpt2pt, self.xs, self.ys)))
        x, y = pts[:, 0], pts[:, 1]

        self.xp, self.yp = x[self.inddec], y[self.inddec]
        self.slopes = self.m[:, self.inddec]

        #Identify width spine points
        self.xsw = x[inds]
        self.ysw = y[inds]

        #Draw Widths
        for k, (x, y) in enumerate(zip(self.xp[1:-1], self.yp[1:-1])):

            x1, y1 = x, y
            L = self.pixmap_fit.width()
            H = self.pixmap_fit.height()
            v = self.slopes[:, k + 1]
            vx = v[1]
            vy = -v[0]
            t0 = np.hypot(L, H)
            t2 = 0

            #intersect: rectangle
            for offset in ([0, 0], [L, H]):
                for ev in ([1, 0], [0, 1]):
                    A = np.matrix([[vx, ev[0]], [vy, ev[1]]])
                    b = np.array([offset[0] - x1, offset[1] - y1])
                    T = np.linalg.solve(A, b)[0]
                    t0 = min(T, t0,
                             key=abs)  #find nearest intersection to bounds

            #Find 2nd furthest intersection within bounds
            bounds = np.array([(L - x1) / vx, (H - y1) / vy, -x1 / vx,
                               -y1 / vy])
            t2 = max(-t0, np.sign(-t0) * np.partition(bounds, -2)[-2], key=abs)

            x0 = x1 + t0 * vx
            y0 = y1 + t0 * vy
            x2 = x1 + t2 * vx
            y2 = y1 + t2 * vy

            for l, (x, y) in enumerate(zip([x0, x2], [y0, y2])):
                start = QtCore.QPointF(x1, y1)
                end = QtCore.QPointF(x, y)
                self.scene.interpLine = QGraphicsLineItem(
                    QtCore.QLineF(start, end))
                self.d["{}".format(2 * k + l)] = self.scene.interpLine
                self.scene.addItem(self.scene.interpLine)
                if k == 0 and l == 0:
                    self.scene.interpLine.setPen(
                        QtGui.QPen(QtGui.QColor('yellow')))

    def mousePressEvent(self, event):
        #http://pyqt.sourceforge.net/Docs/PyQt4/qgraphicsscenemouseevent.html
        #https://stackoverflow.com/questions/21197658/how-to-get-pixel-on-qgraphicspixmapitem-on-a-qgraphicsview-from-a-mouse-click
        data = self.mapToScene(event.pos())

        #draw piecewise lines for non-width measurements
        rules = [
            self.measuring_length, self.measuring_angle, self.measuring_area
        ]

        if self.scene.testline and self._thispos and any(rules):
            start = self._thispos
            end = QtCore.QPointF(data)

            if self._lastpos and self.measuring_angle:

                a = self._lastpos - self._thispos
                b = data - self._thispos
                a = np.array([a.x(), a.y()])
                b = np.array([b.x(), b.y()])
                self.measuring_angle = False
                t = np.arccos(
                    np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
                t *= 180 / np.pi  #convert to degrees
                self.T.update(t)
                self.angleValues = np.append(self.angleValues, t)
                self.parent().statusbar.showMessage(
                    'Angle measurement complete')
                self.parent().angleButton.setChecked(False)
                self.parent().bezier.setEnabled(True)

            self.scene.realline = QGraphicsLineItem(QtCore.QLineF(start, end))
            self.scene.addItem(self.scene.realline)

        #Collect piecewise line start/end points
        self._lastpos = self._thispos  # save old position value
        self._thispos = QtCore.QPointF(data)  # update current position

        if self.measuring_length:
            self.L.update(data.x(), data.y())  # update total length
            self.line_count += 1

        elif self.measuring_area:
            self.line_count += 1
            intersect = False
            if self.line_count > 2:  #cant make polygon w/ two lines
                intersect, xi, yi, k = self.A.checkIntersect(
                    data.x(), data.y())
                self.parent().areaButton.setEnabled(True)
            if intersect:
                self.measuring_area = False
                self.A.update(xi, yi)  #update with intersect point
                self.A.x, self.A.y = self.A.x[k:], self.A.y[
                    k:]  #only use points after intersection
                A = self.A.calcArea()
                self.areaValues = np.append(self.areaValues,
                                            A)  #add area values
                #draw permanent polygon
                points = [
                    QtCore.QPointF(x, y) for x, y in zip(self.A.x, self.A.y)
                ]
                self.scene.polyItem2 = QGraphicsPolygonItem(
                    QtGui.QPolygonF(points))
                self.scene.polyItem2.setBrush(
                    QtGui.QBrush(QtGui.QColor(255, 255, 255, 127)))
                self.scene.removeItem(
                    self.scene.polyItem)  #remove mouseover polygon
                self.scene.polyItem = False  #remove mouseover polygon
                self.scene.addItem(self.scene.polyItem2)  #shade in polygon
                self.parent().statusbar.showMessage(
                    'Polygon area measurement completed')
                self.parent().areaButton.setChecked(False)
                self.parent().bezier.setEnabled(
                    True)  #make bezier fit available again
                QApplication.setOverrideCursor(
                    QtCore.Qt.ArrowCursor)  #change cursor
            else:
                self.A.update(data.x(), data.y())  #update with click point

        #https://stackoverflow.com/questions/30898846/qgraphicsview-items-not-being-placed-where-they-should-be
        if self.measuring_widths:  #measure widths, snap to spines

            k = int(self.k / 2) + 1  #same origin for spine on either side
            x0, y0 = self.xp[k], self.yp[k]
            x1, y1 = data.x(), data.y()

            #perpindicular slopes
            vx = self.slopes[:, k][1]
            vy = -self.slopes[:, k][0]

            A = np.matrix([[vx, -vy], [vy, vx]])
            b = np.array([x1 - x0, y1 - y0])
            t = np.linalg.solve(A, b)

            xi = x0 + t[0] * vx
            yi = y0 + t[0] * vy

            self.W.update(xi, yi)
            p = QtCore.QPointF(xi, yi)

            s = 10  #dot size
            self.scene.ellipseItem = QGraphicsEllipseItem(0, 0, s, s)
            self.scene.ellipseItem.setPos(p.x() - s / 2, p.y() - s / 2)
            self.scene.ellipseItem.setBrush(
                QtGui.QBrush(QtCore.Qt.red, style=QtCore.Qt.SolidPattern))
            self.scene.ellipseItem.setFlag(
                QGraphicsItem.ItemIgnoresTransformations,
                False)  #size stays small, but doesnt translate if false
            self.scene.addItem(self.scene.ellipseItem)
            self.k += 1

            if self.k < self.nspines:
                self.d[str(self.k)].setPen(QtGui.QPen(
                    QtGui.QColor('yellow')))  #Highlight next spine
            if self.k == self.nspines:
                self.parent().statusbar.showMessage(
                    'Width measurements complete')
                self.measuring_widths = False
                self.parent().widthsButton.setEnabled(False)
                self.parent().widthsButton.setChecked(False)
                self.parent().bezier.setEnabled(True)
                width = np.sqrt(
                    (self.W.x[1::2] - self.W.x[0::2])**2 +
                    (self.W.y[1::2] - self.W.y[0::2])**2)  #calculate widths
                #self.measurements[-1,1:] = width #update most recent row w/ length measurement
                #self.measurements[-1][1:] = width
                self.widths[-1] = width

    #MouseWheel Zoom
    def wheelEvent(self, event):
        #https://stackoverflow.com/questions/35508711/how-to-enable-pan-and-zoom-in-a-qgraphicsview
        #transform coordinates correctly
        #https://stackoverflow.com/questions/20942586/controlling-the-pan-to-anchor-a-point-when-zooming-into-an-image
        #https://stackoverflow.com/questions/41226194/pyqt4-pixel-information-on-rotated-image
        zoomInFactor = 1.05
        zoomOutFactor = 1 / zoomInFactor

        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        self.setResizeAnchor(QGraphicsView.NoAnchor)
        oldPos = self.mapToScene(event.pos())

        #Zoom #https://quick-geek.github.io/answers/885796/index.html
        #y component for mouse with two wheels
        if event.angleDelta().y() > 0:
            zoomFactor = zoomInFactor
        else:
            zoomFactor = zoomOutFactor
        self.scale(zoomFactor, zoomFactor)

        newPos = self.mapToScene(event.pos())  #Get the new position
        delta = newPos - oldPos
        self.translate(delta.x(), delta.y())  #Move scene to old position
Esempio n. 6
0
class NodesEditor(QDockWidget):
    nodeCreator = {}  ##dict()

    def __init__(self, text, parent: QWidget = None, flags=Qt.WindowFlags()):
        super(NodesEditor, self).__init__(text, parent, flags)

        self.nodesViewArea = QGraphicsView(self)
        self.nodesScene = QGraphicsScene(self)
        self.setWidget(self.nodesViewArea)
        self.nodesViewArea.setAcceptDrops(True)
        self.nodesViewArea.setDragMode(QGraphicsView.RubberBandDrag)

        self.nodesViewArea.setScene(self.nodesScene)
        self.nodesScene.installEventFilter(self)

        self.nodes = []
        self.connectFrom = None
        self.connectTo = None
        self.connectItem = None
        self.idconnect = -1

    def addNode(self, node: Node):
        self.nodes.append(node)
        self.InitNodeItems(node)
        return node

    def DeleteNode(self, node: Node):
        #if node is None:
        #    print("null delete")
        #    return
        self.DeleteConnections(node)
        idn = -1
        for i in range(0, len(self.nodes)):
            if node == self.nodes[i]:
                idn = i
        self.nodesScene.removeItem(node.item)
        node.item = 0
        if idn >= 0:
            del self.nodes[idn]

    def DeleteConnections(self, node: Node):
        for i in range(0, len(self.nodes)):
            inputs = self.nodes[i].inputs
            for j in range(0, len(inputs)):
                if inputs[j].node == node or self.nodes[i] == node:
                    if inputs[j].connector is not None:
                        self.nodesScene.removeItem(inputs[j].connector)
                    inputs[j].connector = None
                    inputs[j].node = None
                    inputs[j].out = 0

    def DeleteConnection(self, item: QGraphicsItem):
        for i in range(0, len(self.nodes)):
            inputs = self.nodes[i].inputs
            for j in range(0, len(inputs)):
                if inputs[j].connector == item:
                    if inputs[j].connector is not None:
                        self.nodesScene.removeItem(inputs[j].connector)
                    inputs[j].connector = None
                    inputs[j].node = None
                    inputs[j].out = 0

    def GetNodeItem(self, item: QGraphicsItem):
        node = None
        if item is not None:
            if item.parentItem() is not None:
                item = item.parentItem()
            for i in range(0, len(self.nodes)):
                if self.nodes[i].item == item:
                    node = self.nodes[i]
        return node

    def GetNode(self, pos: QPointF):
        item = self.nodesScene.itemAt(pos, self.nodesViewArea.transform())
        node = None
        if item is not None:
            if item.parentItem() is not None:
                item = item.parentItem()
            for i in range(0, len(self.nodes)):
                if self.nodes[i].item == item:
                    node = self.nodes[i]
        return node

    def eventFilter(self, obj, event):
        if event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Delete:
            lists = self.nodesScene.selectedItems()
            for i in range(0, len(lists)):
                if lists[i].type() == 2:  #QGraphicsPathItem.Type:
                    self.DeleteConnection(lists[i])
                if lists[i].type() == 3:  #QGraphicsRectItem.Type
                    self.DeleteNode(self.GetNodeItem(lists[i]))
            self.clearConnection()
            self.updateConnectors()

        if event.type() == QEvent.GraphicsSceneMousePress and (
                self.connectTo or self.connectFrom):
            if (event.buttons() & Qt.LeftButton) != 0:
                p = event.scenePos()
                node = self.GetNode(p)
                if node is not None:
                    if self.connectTo is not None:
                        ido = node.GetOutputId(p)
                        if ido >= 0:
                            self.connectTo.inputs[self.idconnect].node = node
                            self.connectTo.inputs[self.idconnect].out = ido
                    if self.connectFrom is not None:
                        idi = node.GetInputId(p)
                        if idi >= 0:
                            node.inputs[idi].node = self.connectFrom
                            node.inputs[idi].out = self.idconnect
                self.clearConnection()
                self.updateConnectors()

        if event.type() == QEvent.GraphicsSceneContextMenu:
            p = event.scenePos()
            node = self.GetNode(p)
            self.clearConnection()
            if node is not None:
                idi = node.GetInputId(p)
                ido = node.GetOutputId(p)
                if idi >= 0:
                    self.connectTo = node
                    self.idconnect = idi
                elif ido >= 0:
                    self.connectFrom = node
                    self.idconnect = ido
                if idi >= 0 or ido >= 0:
                    return True

        if event.type() == QEvent.GraphicsSceneDragEnter or event.type(
        ) == QEvent.GraphicsSceneDragMove or event.type(
        ) == QEvent.GraphicsSceneDrop:
            event.acceptProposedAction()
            if event.type() == QEvent.GraphicsSceneDrop:
                bytearray = event.mimeData().data(
                    event.mimeData().formats()[0])
                data_items = self.decode_data(bytearray)
                text = data_items[0][Qt.DisplayRole].value()
                if text in NodesEditor.nodeCreator:
                    node = NodesEditor.nodeCreator[text]()
                    self.addNode(node).setPos(event.scenePos())
            return True

        if event.type() == QEvent.GraphicsSceneMouseMove:
            if (event.buttons() & Qt.LeftButton) != 0:
                self.updateConnectors()
            if self.connectTo is not None or self.connectFrom is not None:
                p1 = QPointF(0, 0)
                p2 = QPointF(0, 0)
                if self.connectFrom is not None:
                    p1 = event.scenePos()
                    p2 = self.connectFrom.GetOutputPoint(self.idconnect)
                if self.connectTo is not None:
                    p1 = self.connectTo.GetInputPoint(self.idconnect)
                    p2 = event.scenePos()
                self.connectItem = self.updateConnector(
                    self.connectItem, p1, p2, False)

        return QDockWidget.eventFilter(self, obj, event)

    def decode_data(self, bytearray):
        data = []
        item = {}

        ds = QDataStream(bytearray)
        while not ds.atEnd():

            row = ds.readInt32()
            column = ds.readInt32()

            map_items = ds.readInt32()
            for i in range(map_items):

                key = ds.readInt32()

                value = QVariant()
                ds >> value
                item[Qt.ItemDataRole(key)] = value

            data.append(item)

        return data

    def InitNodeItems(self, node: Node):
        wr = node.windowRect
        winRect = self.nodesScene.addRect(wr, QPen(),
                                          QBrush(QColor(0xFFFFFFFF)))
        winRect.setFlag(QGraphicsItem.ItemIsMovable)
        winRect.setFlag(QGraphicsItem.ItemIsSelectable)
        titleRect = self.nodesScene.addRect(wr.x(), wr.y(), wr.width(), 20)
        titleRect.setParentItem(winRect)
        title = self.nodesScene.addText(node.GetTitle())
        title.setParentItem(titleRect)

        node.item = winRect
        node.ItemsInit(self.nodesScene)
        winRect.setRect(node.windowRect)

    def updateConnectors(self):
        for i in range(0, len(self.nodes)):
            c = self.nodes[i].inputs
            if len(c) <= 0:
                continue
            for j in range(0, len(c)):
                if c[j].node is not None:
                    p1 = self.nodes[i].GetInputPoint(j)
                    p2 = c[j].node.GetOutputPoint(c[j].out)
                    c[j].connector = self.updateConnector(
                        c[j].connector, p1, p2)
                elif (c[j].connector != None):
                    self.nodesScene.removeItem(c[j].connector)
                    c[j].connector = None

    def updateConnector(self,
                        item: QGraphicsPathItem,
                        p1: QPointF,
                        p2: QPointF,
                        select: bool = True):
        path = QPainterPath(p1)
        path.quadTo(p1 + QPointF(-15, 0), (p1 + p2) * 0.5)
        path.quadTo(p2 + QPointF(15, 0), p2)
        if item is None:
            item = self.nodesScene.addPath(path)
        else:
            item.setPath(path)
        item.setZValue(-1)
        if select:
            item.setFlag(QGraphicsItem.ItemIsSelectable)
        return item

    def clearConnection(self):
        self.connectFrom = None
        self.connectTo = None
        if self.connectItem is not None:
            self.nodesScene.removeItem(self.connectItem)
            self.connectItem = None

    def indexOfNode(self, node):
        for i in range(0, len(self.nodes)):
            if self.nodes[i] == node:
                return i
        return -1

    def SeveToFile(self, path):
        #print(path)
        f = QFile(path)
        if f.open(QIODevice.WriteOnly):
            stream = QTextStream(f)
            stream.setCodec("UTF-8")
            doc = QDomDocument()
            xmlInstruct = doc.createProcessingInstruction(
                "xml", "version=\"1\" encoding=\"UTF-8\"")
            doc.appendChild(xmlInstruct)
            mainEl = doc.createElement("Nodes")
            doc.appendChild(mainEl)

            for i in range(0, len(self.nodes)):
                objectEl = doc.createElement("Node")
                mainEl.appendChild(objectEl)

                idoEl = doc.createElement("Id")
                objectEl.appendChild(idoEl)
                idoEltext = doc.createTextNode(str(i))
                idoEl.appendChild(idoEltext)

                typeEl = doc.createElement("Type")
                objectEl.appendChild(typeEl)
                nameEltext = doc.createTextNode(type(self.nodes[i]).__name__)
                typeEl.appendChild(nameEltext)

                posEl = doc.createElement("Pos")
                objectEl.appendChild(posEl)
                pos = self.nodes[i].item.scenePos()
                posEltext = doc.createTextNode(
                    str(pos.x()) + "," + str(pos.y()))
                posEl.appendChild(posEltext)

                liksEl = doc.createElement("Links")
                objectEl.appendChild(liksEl)
                inputs = self.nodes[i].inputs
                for j in range(0, len(inputs)):
                    if inputs[j].node is not None:
                        linkEl = doc.createElement("Link")
                        liksEl.appendChild(linkEl)

                        idEl = doc.createElement("Id")
                        linkEl.appendChild(idEl)
                        idEltext = doc.createTextNode(str(j))
                        idEl.appendChild(idEltext)

                        nodeEl = doc.createElement("Node")
                        linkEl.appendChild(nodeEl)
                        idnEltext = doc.createTextNode(
                            str(self.indexOfNode(inputs[j].node)))
                        nodeEl.appendChild(idnEltext)

                        outEl = doc.createElement("Out")
                        linkEl.appendChild(outEl)
                        outEltext = doc.createTextNode(str(inputs[j].out))
                        outEl.appendChild(outEltext)

            doc.save(stream, 4)
            f.close()
            return True
        return False

    def LoadFromFile(self, path):
        #print(path)
        self.nodes = []
        self.nodesScene.clear()
        file = QFile(path)
        if not file.open(QIODevice.ReadOnly):
            return False

        doc = QDomDocument()
        if not doc.setContent(file):
            file.close()
            return False
        file.close()

        docElem = doc.documentElement()
        n = docElem.firstChild()
        while not n.isNull():
            en = n.toElement()
            #print(en.tagName())
            if en.tagName() == "Node":
                np = n.firstChild()
                typeN = ""
                pos = QPointF(0, 0)
                links = None
                while not np.isNull():
                    enp = np.toElement()
                    if enp.tagName() == "Type":
                        typeN = enp.text()
                    if enp.tagName() == "Pos":
                        post = enp.text().split(",")
                        pos = QPointF(float(post[0]), float(post[1]))
                    if enp.tagName() == "Links":
                        links = np
                    np = np.nextSibling()
                if typeN in NodesEditor.nodeCreator:
                    node = NodesEditor.nodeCreator[typeN]()
                    self.addNode(node).setPos(pos)
                    if links is not None:
                        nl = links.firstChild()
                        while not nl.isNull():
                            npl = nl.firstChild()
                            idl = -1
                            idn = -1
                            out = -1
                            while not npl.isNull():
                                enpl = npl.toElement()
                                if enpl.tagName() == "Id":
                                    idl = int(enp.text())
                                if enpl.tagName() == "Node":
                                    idn = int(enp.text())
                                if enpl.tagName() == "Out":
                                    out = int(enp.text())
                                npl = npl.nextSibling()
                            if idl >= 0:
                                node.inputs[idl].node = self.nodes[idn]
                                node.inputs[idl].out = out
                            nl = nl.nextSibling()
            n = n.nextSibling()
        self.updateConnectors()
        return True
Esempio n. 7
0
class TableViewWidget(QGraphicsView):
    g_table_view = None
    g_detect_size = 200
    g_detect_text = "position"
    #g_detect_text = "quality"
    #g_detect_text = "none"
    g_rplidar_remanence = False
    g_rplidar_plot_life_ms = 1000


    def __init__(self, parent = None, ihm_type='pc'):
        super(TableViewWidget, self).__init__(parent)
        if ihm_type=='pc':
            #self.setFixedSize(900,600)
            self.setFixedSize(960,660)
        elif ihm_type=='pc-mini':
            #self.setFixedSize(600,400)
            self.setFixedSize(640,440)
        else:
            #self.setFixedSize(225,150)
            self.setFixedSize(240,165)
        #self.setSceneRect(QRectF(0,-1500,2000,3000))
        self.setSceneRect(QRectF(-100,-1600,2200,3200))
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        
        self._robots = {}
        self._waypoints = []

        redium = QColor.fromCmykF(0,1,1,0.1)
        greenium = QColor.fromCmykF(0.7,0,0.9,0)
        blueium = QColor.fromCmykF(0.9,0.4,0,0)
        goldenium = QColor('white')
        yellow = QColor.fromCmykF(0,0.25,1,0)
        purple = QColor.fromCmykF(0.5,0.9,0,0.05)
        background = QColor(40,40,40)
        darker = QColor(20,20,20)

#        big_robot_poly = QPolygonF([
#            QPointF(-135,-151),
#            QPointF(60,-151),
#            QPointF(170,-91),
#            QPointF(170,-45),
#            QPointF(111,-40),
#            QPointF(111,40),
#            QPointF(170,45),
#            QPointF(170,91),
#            QPointF(60,151),
#            QPointF(-135,151)
#            ])

        little_robot_poly = QPolygonF([
            QPointF(  50,   0),
            QPointF( 100,  85),
            QPointF(  65, 115),
            QPointF( -65, 115),
            QPointF(-100,  85),
            QPointF(-100, -85),
            QPointF( -65,-115),
            QPointF(  65,-115),
            QPointF( 100, -85)
            ])

        #self._scene = QGraphicsScene(QRectF(0,-1500,2000,3000))
        self._scene = QGraphicsScene(QRectF(-100,-1600,2200,3200))

#        self._big_robot = self._scene.addPolygon(big_robot_poly, QPen(), QBrush(QColor('red')))
#        self._big_robot.setZValue(1)
        #self._robots['little'] = Robot(self._scene)
        self._little_robot = self._scene.addPolygon(little_robot_poly, QPen(), QBrush(QColor('red')))
        self._little_robot.setZValue(1)
        #self._friend_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('green')))
        self._friend_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white')))
        self._friend_robot.setZValue(1)
        self._friend_robot.setPos(-1 * 1000, -1 * 1000)
        if os.name == 'nt':
            self._friend_robot_text = self._scene.addText("0123456", QFont("Calibri",80));
        else:
            self._friend_robot_text = self._scene.addText("0123456", QFont("System",40));
        self._friend_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40)
        self._friend_robot_text.setRotation(-90)
        self._friend_robot_text.setTransform(QTransform(1.0, 0.0, 0.0,  0.0, -1.0, 0.0,   0.0, 0.0, 1.0))
        self._friend_robot_text.setZValue(1)
        #self._adv1_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('white')))
        self._adv1_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white')))
        self._adv1_robot.setZValue(1)
        self._adv1_robot.setPos(-1 * 1000, -1 * 1000)
        if os.name == 'nt':
            self._adv1_robot_text = self._scene.addText("0", QFont("Calibri",80));
        else:
            self._adv1_robot_text = self._scene.addText("0", QFont("System",40));
        self._adv1_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40)
        self._adv1_robot_text.setRotation(-90)
        self._adv1_robot_text.setTransform(QTransform(1.0, 0.0, 0.0,  0.0, -1.0, 0.0,   0.0, 0.0, 1.0))
        self._adv1_robot_text.setZValue(1)
        #self._adv2_robot = self._scene.addEllipse(-100, -100, 200, 200, QPen(QBrush(QColor('black')),4), QBrush(QColor('blue')))
        self._adv2_robot = self._scene.addEllipse(-100, -100, TableViewWidget.g_detect_size, TableViewWidget.g_detect_size, QPen(QBrush(QColor('black')),4), QBrush(QColor('white')))
        self._adv2_robot.setZValue(1)
        self._adv2_robot.setPos(-1 * 1000, -1 * 1000)
        if os.name == 'nt':
            self._adv2_robot_text = self._scene.addText("0", QFont("Calibri",80));
        else:
            self._adv2_robot_text = self._scene.addText("0", QFont("System",40));
        self._adv2_robot_text.setPos(-1 * 1000 - 60, -1 * 1000 - 40)
        self._adv2_robot_text.setRotation(-90)
        self._adv2_robot_text.setTransform(QTransform(1.0, 0.0, 0.0,  0.0, -1.0, 0.0,   0.0, 0.0, 1.0))
        self._adv2_robot_text.setZValue(1)
        self.setScene(self._scene)

        self.rotate(90)
        if ihm_type=='pc':
            self.scale(0.3, -0.3)
        elif ihm_type=='pc-mini':
            self.scale(0.2, -0.2)
        else:
            self.scale(0.075, -0.075)

        #self._scene.addRect(QRectF(0,-1500,2000,3000),QPen(), QBrush(background))

        f=open("widgets/table_2020_600x400.png","rb")
        my_buff=f.read()
        test_img_pixmap2 = QPixmap()
        test_img_pixmap2.loadFromData(my_buff)
        #self.setPixmap(test_img_pixmap2)
        self._bg_img = QGraphicsPixmapItem(test_img_pixmap2)
        self._bg_img.setTransform(QTransform(1.0, 0.0, 0.0,  0.0, -1.0, 0.0,   0.0, 0.0, 0.2))
        self._bg_img.setRotation(-90)
        self._bg_img.setPos(0, -1500)
        self._scene.addItem(self._bg_img);

        # Scenario 2020

        #Port principal "bleu"
        self._scene.addRect(QRectF(500,-1120,570,20),QPen(), QBrush(blueium))
        self._scene.addRect(QRectF(500,-1500,30,400),QPen(), QBrush(greenium))
        self._scene.addRect(QRectF(1070,-1500,30,400),QPen(), QBrush(redium))

        #Port secondaire "bleu"
        self._scene.addRect(QRectF(1700,150,20,300),QPen(), QBrush(blueium))
        self._scene.addRect(QRectF(1700,150,300,100),QPen(), QBrush(greenium))
        self._scene.addRect(QRectF(1700,350,300,100),QPen(), QBrush(redium))

        #Bouees cote "bleu"
        self._scene.addEllipse(QRectF(1200-35,-1200-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(1080-35,-1050-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(510-35,-1050-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(400-35,-1200-35,70,70),QPen(), QBrush(redium))

        self._scene.addEllipse(QRectF(100-35,-830-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(400-35,-550-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(800-35,-400-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(1200-35,-230-35,70,70),QPen(), QBrush(greenium))

        self._scene.addEllipse(QRectF(1650-35,-435-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(1650-35,-165-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(1955-35,-495-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(1955-35,-105-35,70,70),QPen(), QBrush(greenium))

        #Port principal "jaune"
        self._scene.addRect(QRectF(500,1100,570,20),QPen(), QBrush(yellow))
        self._scene.addRect(QRectF(500,1100,30,400),QPen(), QBrush(redium))
        self._scene.addRect(QRectF(1070,1100,30,400),QPen(), QBrush(greenium))

        #Port secondaire "jaune"
        self._scene.addRect(QRectF(1700,-450,20,300),QPen(), QBrush(yellow))
        self._scene.addRect(QRectF(1700,-450,300,100),QPen(), QBrush(greenium))
        self._scene.addRect(QRectF(1700,-250,300,100),QPen(), QBrush(redium))

        #Bouees cote "jaune"
        self._scene.addEllipse(QRectF(1200-35,1200-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(1080-35,1050-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(510-35,1050-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(400-35,1200-35,70,70),QPen(), QBrush(greenium))

        self._scene.addEllipse(QRectF(100-35,830-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(400-35,550-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(800-35,400-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(1200-35,230-35,70,70),QPen(), QBrush(redium))

        self._scene.addEllipse(QRectF(1650-35,435-35,70,70),QPen(), QBrush(redium))
        self._scene.addEllipse(QRectF(1650-35,165-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(1955-35,495-35,70,70),QPen(), QBrush(greenium))
        self._scene.addEllipse(QRectF(1955-35,105-35,70,70),QPen(), QBrush(redium))

        #dbg_plt_sz = 3
        #self._scene.addEllipse(1000 - dbg_plt_sz, 0 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white')))

        self._points = []
        #self.setSceneRect(QRectF(0,-150,200,300))

        self._traj_segm_l = []

        self._debug_edit_mode = False
        self._debug_edit_point_l = []

#        self._big_robot_x = 0
#        self._big_robot_y = 0
        self._little_robot_x = 0
        self._little_robot_y = 0

        self.last_plot_ts = 0
        self.plot_graph_l = []
        
        self._plot_items = []

        TableViewWidget.g_table_view = self
        
    def set_strategy(self, strategy):
        greenium = QColor.fromCmykF(0.7,0,0.9,0)
        #greenium.setAlphaF(0.2)
        for id_, pos in strategy['strategy']['map']['waypoints'].items():
            wp = self._scene.addEllipse(QRectF(pos[0]-10,pos[1]-10,20,20),QPen(), QBrush(greenium))
            self._waypoints.append(wp)
        for id_, pose in strategy['strategy']['map']['poses'].items():
            p = strategy['strategy']['map']['waypoints'][pose[0]]
            path = QPainterPath()
            cos_ = math.cos(pose[1] * math.pi / 180)
            sin_ = math.sin(pose[1] * math.pi / 180)
            l = 40
            w = 20
            path.moveTo(p[0] + l * cos_, p[1] + l * sin_)
            path.lineTo(p[0] -l * cos_ + w * sin_, p[1] - l * sin_ - w * cos_)
            path.lineTo(p[0] -l * cos_ - w * sin_, p[1] - l * sin_ + w * cos_)
            path.closeSubpath()
            itm = self._scene.addPath(path, QPen(), QBrush(greenium))
            
        for id_, area in strategy['strategy']['map']['areas'].items():
            path = QPainterPath()
            v = area['vertices'][0]
            path.moveTo(v[0], v[1])
            for v in area['vertices'][1:]: 
                path.lineTo(v[0], v[1])
            path.closeSubpath()
            itm = self._scene.addPath(path, QPen(), QBrush(greenium))
            self._waypoints.append(wp)


    def add_points(self, points):
        for p in points:
            pt = self._scene.addEllipse(p[0]-10, p[1]-10, 20, 20,  QPen(), QBrush(QColor('grey')))
            pt.setZValue(1)
            self._points.append((pt, p))

    def sizeHint(self):
        return QSize(600,400)

    def set_client(self, client):
        self._client = client
        self._client.propulsion_telemetry.connect(self.update_telemetry)
        self._client.rplidar_plot.connect(self.update_plots)
        self._client.rplidar_robot_detection.connect(self.update_other_robots)
        
    def update_telemetry(self, telemetry):
#        self._big_robot.setPos(telemetry.x * 1000, telemetry.y * 1000)
#        self._big_robot.setRotation(telemetry.yaw * 180 / math.pi)
#        self._big_robot_x = telemetry.x * 1000
#        self._big_robot_y = telemetry.y * 1000
        self._little_robot.setPos(telemetry.pose.position.x * 1000, telemetry.pose.position.y * 1000)
        self._little_robot.setRotation(telemetry.pose.yaw * 180 / math.pi)
        self._little_robot_x = telemetry.pose.position.x * 1000
        self._little_robot_y = telemetry.pose.position.y * 1000

    def update_plots(self, my_plot):
        dbg_plt_sz = 1
        for i in self._plot_items:
            self._scene.removeItem(i)
        self._plot_items = []
            
        #self.last_plot_ts = my_plot.timestamp
        for pt in my_plot.points:
            itm = self._scene.addEllipse(pt.x * 1000 - dbg_plt_sz, pt.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('red')),4), QBrush(QColor('red')))
            self._plot_items.append(itm)
        return
        my_plot_ellipse = self._scene.addEllipse(my_plot.x * 1000 - dbg_plt_sz, my_plot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('red')),4), QBrush(QColor('red')))
        self.plot_graph_l.append((my_plot,my_plot_ellipse))
        for rec in self.plot_graph_l:
            if (self.last_plot_ts-rec[0].timestamp>TableViewWidget.g_rplidar_plot_life_ms):
                rec_ellipse = rec[1]
                self._scene.removeItem(rec_ellipse)
                self.plot_graph_l.remove(rec)

    def update_other_robots(self, other_robot):
        dbg_plt_sz = 3
        if (other_robot.id == 0):
            self._friend_robot.setPos(other_robot.x * 1000, other_robot.y * 1000)
            if (TableViewWidget.g_detect_text == "quality"):
                self._friend_robot_text.setPlainText("%d"%other_robot.samples)
            elif (TableViewWidget.g_detect_text == "position"):
                self._friend_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000))
            else:
                self._friend_robot_text.setPlainText("")
            self._friend_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40)
            if TableViewWidget.g_rplidar_remanence:
                self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white')))
        elif (other_robot.id == 1):
            self._adv1_robot.setPos(other_robot.x * 1000, other_robot.y * 1000)
            if (TableViewWidget.g_detect_text == "quality"):
                self._adv1_robot_text.setPlainText("%d"%other_robot.samples)
            elif (TableViewWidget.g_detect_text == "position"):
                self._adv1_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000))
            else:
                self._adv1_robot_text.setPlainText("")
            self._adv1_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40)
            if TableViewWidget.g_rplidar_remanence:
                self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white')))
        elif (other_robot.id == 2):
            self._adv2_robot.setPos(other_robot.x * 1000, other_robot.y * 1000)
            if (TableViewWidget.g_detect_text == "quality"):
                self._adv2_robot_text.setPlainText("%d"%other_robot.samples)
            elif (TableViewWidget.g_detect_text == "position"):
                self._adv2_robot_text.setPlainText("%d,%d"%(other_robot.x*1000,other_robot.y*1000))
            else:
                self._adv2_robot_text.setPlainText("")
            self._adv2_robot_text.setPos(other_robot.x * 1000 - 60, other_robot.y * 1000 - 40)
            if TableViewWidget.g_rplidar_remanence:
                self._scene.addEllipse(other_robot.x * 1000 - dbg_plt_sz, other_robot.y * 1000 - dbg_plt_sz, 2*dbg_plt_sz, 2*dbg_plt_sz, QPen(QBrush(QColor('white')),4), QBrush(QColor('white')))

    def debug_set_start(self, _new_x, _new_y):
        self.debug_start_x = _new_x
        self.debug_start_y = _new_y
        self.debug_cur_x = _new_x
        self.debug_cur_y = _new_y
        if self._debug_edit_mode:
            self._debug_edit_point_l.append((_new_x,_new_y))

    def debug_line_to(self, _new_x, _new_y):
        my_segm = self._scene.addLine(self.debug_cur_x, self.debug_cur_y, _new_x, _new_y, QPen(QColor(255,255,255)));
        self._traj_segm_l.append(my_segm)
        self.debug_cur_x = _new_x
        self.debug_cur_y = _new_y

    def debug_clear_lines(self):
        for l in self._traj_segm_l:
            self._scene.removeItem(l)
        self._traj_segm_l = []

    def debug_start_edit(self, _new_x, _new_y):
        self.debug_clear_lines()
        self._debug_edit_mode = True
        self.debug_start_x = _new_x
        self.debug_start_y = _new_y
        self.debug_cur_x = _new_x
        self.debug_cur_y = _new_y
        self._debug_edit_point_l = [(_new_x,_new_y)]

    def debug_start_edit_rel(self):
        self.debug_clear_lines()
        self._debug_edit_mode = True
        self.debug_start_x = self._little_robot_x
        self.debug_start_y = self._little_robot_y
        self.debug_cur_x = self._little_robot_x
        self.debug_cur_y = self._little_robot_y
        self._debug_edit_point_l = [(self._little_robot_x,self._little_robot_y)]

    def debug_stop_edit(self):
        self._debug_edit_mode = False
        return self._debug_edit_point_l

    def mousePressEvent(self, event):
        print ("pix:<{},{}>".format(event.x(),event.y()))
        #realY = 3000.0*(event.x()-450.0)/900.0
        #realX = 2000.0*(event.y())/600.0
        realY = 3200.0*(event.x()-480.0)/960.0
        realX = 2200.0*(event.y()-30.0)/660.0
        print ("real:<{},{}>".format(realX,realY))
        if self._debug_edit_mode:
            self._debug_edit_point_l.append((realX,realY))
            self.debug_line_to(realX, realY)
Esempio n. 8
0
class Spectrum(QWidget):
    """Plot the power spectrum for a specified channel.

    Attributes
    ----------
    parent : instance of QMainWindow
        the main window.
    x_limit : tuple or list
        2 values specifying the limit on x-axis
    y_limit : tuple or list
        2 values specifying the limit on y-axis
    log : bool
        log-transform the data or not
    idx_chan : instance of QComboBox
        the element with the list of channel names.
    idx_x_min : instance of QLineEdit
        value with min x value
    idx_x_max : instance of QLineEdit
        value with max x value
    idx_y_min : instance of QLineEdit
        value with min y value
    idx_y_max : instance of QLineEdit
        value with max y value
    idx_log : instance of QCheckBox
        widget that defines if log should be used or not
    idx_fig : instance of QGraphicsView
        the view with the power spectrum
    scene : instance of QGraphicsScene
        the scene with GraphicsItems

    Notes
    -----
    If data contains NaN, it doesn't create any spectrum (feature or bug?).
    """
    def __init__(self, parent):
        super().__init__()
        self.parent = parent

        self.config = ConfigSpectrum(self.display_window)

        self.selected_chan = None
        self.idx_chan = None
        self.idx_fig = None
        self.scene = None

        self.create()

    def create(self):
        """Create empty scene for power spectrum."""
        self.idx_chan = QComboBox()
        self.idx_chan.activated.connect(self.display_window)

        self.idx_fig = QGraphicsView(self)
        self.idx_fig.scale(1, -1)

        layout = QVBoxLayout()
        layout.addWidget(self.idx_chan)
        layout.addWidget(self.idx_fig)
        self.setLayout(layout)

        self.resizeEvent(None)

    def show_channame(self, chan_name):
        self.selected_chan = self.idx_chan.currentIndex()

        self.idx_chan.clear()
        self.idx_chan.addItem(chan_name)
        self.idx_chan.setCurrentIndex(0)

    def update(self):
        """Add channel names to the combobox."""
        self.idx_chan.clear()
        for chan_name in self.parent.traces.chan:
            self.idx_chan.addItem(chan_name)

        if self.selected_chan is not None:
            self.idx_chan.setCurrentIndex(self.selected_chan)
            self.selected_chan = None

    def display_window(self):
        """Read the channel name from QComboBox and plot its spectrum.

        This function is necessary it reads the data and it sends it to
        self.display. When the user selects a smaller chunk of data from the
        visible traces, then we don't need to call this function.
        """
        if self.idx_chan.count() == 0:
            self.update()

        chan_name = self.idx_chan.currentText()
        lg.info('Power spectrum for channel ' + chan_name)

        if chan_name:
            trial = 0
            data = self.parent.traces.data(trial=trial, chan=chan_name)
            self.display(data)
        else:
            self.scene.clear()

    def display(self, data):
        """Make graphicsitem for spectrum figure.

        Parameters
        ----------
        data : ndarray
            1D vector containing the data only

        This function can be called by self.display_window (which reads the
        data for the selected channel) or by the mouse-events functions in
        traces (which read chunks of data from the user-made selection).
        """
        value = self.config.value
        self.scene = QGraphicsScene(value['x_min'], value['y_min'],
                                    value['x_max'] - value['x_min'],
                                    value['y_max'] - value['y_min'])
        self.idx_fig.setScene(self.scene)

        self.add_grid()
        self.resizeEvent(None)

        s_freq = self.parent.traces.data.s_freq
        f, Pxx = welch(data, fs=s_freq,
                       nperseg=int(min((s_freq, len(data)))))  # force int

        freq_limit = (value['x_min'] <= f) & (f <= value['x_max'])

        if self.config.value['log']:
            Pxx_to_plot = log(Pxx[freq_limit])
        else:
            Pxx_to_plot = Pxx[freq_limit]

        self.scene.addPath(Path(f[freq_limit], Pxx_to_plot),
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))

    def add_grid(self):
        """Add axis and ticks to figure.

        Notes
        -----
        I know that visvis and pyqtgraphs can do this in much simpler way, but
        those packages create too large a padding around the figure and this is
        pretty fast.

        """
        value = self.config.value

        # X-AXIS
        # x-bottom
        self.scene.addLine(value['x_min'], value['y_min'],
                           value['x_min'], value['y_max'],
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # at y = 0, dashed
        self.scene.addLine(value['x_min'], 0,
                           value['x_max'], 0,
                           QPen(QColor(LINE_COLOR), LINE_WIDTH, Qt.DashLine))
        # ticks on y-axis
        y_high = int(floor(value['y_max']))
        y_low = int(ceil(value['y_min']))
        x_length = (value['x_max'] - value['x_min']) / value['x_tick']
        for y in range(y_low, y_high):
            self.scene.addLine(value['x_min'], y,
                               value['x_min'] + x_length, y,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # Y-AXIS
        # left axis
        self.scene.addLine(value['x_min'], value['y_min'],
                           value['x_max'], value['y_min'],
                           QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # larger ticks on x-axis every 10 Hz
        x_high = int(floor(value['x_max']))
        x_low = int(ceil(value['x_min']))
        y_length = (value['y_max'] - value['y_min']) / value['y_tick']
        for x in range(x_low, x_high, 10):
            self.scene.addLine(x, value['y_min'],
                               x, value['y_min'] + y_length,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))
        # smaller ticks on x-axis every 10 Hz
        y_length = (value['y_max'] - value['y_min']) / value['y_tick'] / 2
        for x in range(x_low, x_high, 5):
            self.scene.addLine(x, value['y_min'],
                               x, value['y_min'] + y_length,
                               QPen(QColor(LINE_COLOR), LINE_WIDTH))

    def resizeEvent(self, event):
        """Fit the whole scene in view.

        Parameters
        ----------
        event : instance of Qt.Event
            not important

        """
        value = self.config.value
        self.idx_fig.fitInView(value['x_min'],
                               value['y_min'],
                               value['x_max'] - value['x_min'],
                               value['y_max'] - value['y_min'])

    def reset(self):
        """Reset widget as new"""
        self.idx_chan.clear()
        if self.scene is not None:
            self.scene.clear()
        self.scene = None
Esempio n. 9
0
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.ui = ui_main_window.Ui_MainWindow()
        self.ui.setupUi(self)

        self.cities = cities

        self.autoplay = False
        self._zoom = 1
        self.genetic = None
        self.paths = []
        self.last = None

        self.scene = QGraphicsScene()
        self.ui.graphicsView.setScene(self.scene)

        self.ui.popSize.setValue(20)
        self.ui.algorithm.addItem('ant', AntColony)
        self.ui.algorithm.addItem('genetic', Genetic)
        self.ui.algorithm.addItem('solver', Test)

        self.ui.playBtn.clicked.connect(self.on_play_click)
        self.ui.nextBtn.clicked.connect(self.next)
        self.ui.restartBtn.clicked.connect(self.restart)
        self.ui.showCur.stateChanged.connect(self.redraw)

        self.timer = QTimer()
        self.timer.timeout.connect(self.gen_next)
        self.timer.start(1)

        for city in self.cities:
            self.scene.addEllipse(city.x, city.y, 5, 5, QPen(),
                                  QBrush(QColor(255, 0, 0)))
            self.scene.addSimpleText(city.name).setPos(city.x + 5, city.y + 5)

        self.start()

    def restart(self):
        self.start()
        self.autoplay = True

    def start(self):
        try:
            seed = int(self.ui.seed.text())
        except ValueError as e:
            seed = -1

        if seed <= 0:
            seed = random.randint(0, 1000)

        #seed = 100
        print(seed)
        random.seed(seed)
        np.random.seed(seed)

        self.clear_paths()
        self.ui.distance.setText("")
        clazz = self.ui.algorithm.currentData()
        self.genetic = clazz(self.cities, int(self.ui.popSize.text())).run({})

    def on_play_click(self, play):
        self.autoplay = play

    def gen_next(self):
        if self.autoplay:
            self.next()

    @QtCore.pyqtSlot()
    def next(self):
        try:
            trajectories = next(self.genetic)
        except StopIteration:
            self.ui.playBtn.setChecked(False)
            self.autoplay = False
            return

        self.ui.distance.setText(f"{trajectories[0].distance:.2f} units")

        self.last = trajectories
        self.draw_trajectories(trajectories)

    def redraw(self):
        self.draw_trajectories(self.last)

    def draw_trajectories(self, trajectories):
        self.clear_paths()

        pens = [QPen(QColor(255, 0, 0)), QPen(QColor(0, 0, 0))]

        for i, trajectory in enumerate(trajectories):
            if i != 0 and not self.ui.showCur.isChecked():
                break

            path = QPainterPath()
            path.moveTo(self.cities[trajectory.path[0]].x,
                        self.cities[trajectory.path[0]].y)
            for hop in trajectory.path:
                city = self.cities[hop]
                path.lineTo(city.x, city.y)
            path.lineTo(self.cities[trajectory.path[0]].x,
                        self.cities[trajectory.path[0]].y)

            self.paths.append(self.scene.addPath(path, pen=pens[i]))

    def clear_paths(self):
        for path in self.paths:
            self.scene.removeItem(path)
        self.paths = []
Esempio n. 10
0
    class imwin(QGraphicsView):  #Subclass QLabel for interaction w/ QPixmap
        def __init__(self, parent=None):
            super(imwin, self).__init__(parent)
            QApplication.setOverrideCursor(
                QtCore.Qt.CrossCursor)  #change cursor
            self.scene = QGraphicsScene()
            self.view = QGraphicsView(self.scene)

            #self.bezier_fit = True
            self.pixmap = None
            self._lastpos = None
            self._thispos = None
            self.delta = QtCore.QPointF(0, 0)
            self.nm = None
            self.measuring_custom = False
            self.measuring_length = False
            self.measuring_widths = False
            self.measuring_angle = False
            self._zoom = 1
            self.newPos = None
            self.oldPos = None
            self.factor = 1.0
            self.numwidths = None
            self.widthnames = []
            #self.lengths = []
            #self.widths = []
            self.d = {}  #dictionary for line items
            #self.k = 0 #initialize counter so lines turn yellow
            self.A = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0)))
            self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0)))
            self.scene.realline = None
            self.scene.testline = None
            self.setMouseTracking(True)
            self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            self.setRenderHints(QtGui.QPainter.Antialiasing
                                | QtGui.QPainter.SmoothPixmapTransform)
            self.setRenderHint(QtGui.QPainter.Antialiasing, True)
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
            self.setInteractive(False)
            #self.setFrameShape(QtGui.QFrame.NoFrame)

        def keyPressEvent(self, event):  #shift modifier for panning
            if event.key() == QtCore.Qt.Key_Shift:
                pos = QtGui.QCursor.pos()
                self.oldPos = self.mapToScene(self.mapFromGlobal(pos))

        def mouseMoveEvent(self, event):

            #Shift to pan
            modifiers = QApplication.keyboardModifiers()
            if modifiers == QtCore.Qt.ShiftModifier and self.oldPos:
                self.newPos = self.mapToScene(event.pos())
                delta = self.newPos - self.oldPos
                self.translate(delta.x(), delta.y())

            #dragging line
            elif self._thispos and (
                    self.measuring_length
                    or self.measuring_angle):  #only on mouse press
                if self.measuring_length:
                    self.parent().statusBar.showMessage(
                        'Click to place next point... double click to finish')
                if self.measuring_angle:
                    self.parent().statusBar.showMessage(
                        'Click point to define vector')

                end = QtCore.QPointF(self.mapToScene(event.pos()))
                start = self._thispos

                if self.measuring_angle and self._lastpos:
                    start = self._lastpos

                if self.scene.testline:  #remove old line
                    self.scene.removeItem(self.scene.testline)

                self.scene.testline = QGraphicsLineItem(
                    QtCore.QLineF(start, end))
                self.scene.addItem(self.scene.testline)

        def mouseDoubleClickEvent(self, event):
            def qpt2pt(x, y):
                Q = self.mapFromScene(self.mapToScene(x, y))
                return Q.x(), Q.y()

            #only delete lines if bezier fit
            if self.measuring_length and self.parent().bezier.isChecked():
                self.parent().statusBar.showMessage(
                    'Length measurement complete.')
                #Remove most recent items drawn (exact lines)
                nl = self.line_count
                for k, i in enumerate(self.scene.items()):
                    if k < nl:
                        self.scene.removeItem(i)

            if self._lastpos and not (self.measuring_widths
                                      or self.measuring_angle):

                #catmull roms spline instead?
                #https://codeplea.com/introduction-to-splines
                n = max(1000,
                        self.numwidths * 50)  #num of interpolating points

                if self.parent().bezier.isChecked():
                    #https://gist.github.com/Alquimista/1274149

                    def bernstein_poly(i, n, t):
                        return comb(n, i) * (t**(n - i)) * (1 - t)**i

                    points = np.vstack((self.A.x, self.A.y)).T

                    def bezier_curve(points, nTimes=n):

                        nPoints = len(points)
                        xPoints = np.array([p[0] for p in points])
                        yPoints = np.array([p[1] for p in points])
                        t = np.linspace(0.0, 1.0, nTimes)
                        polynomial_array = np.array([
                            bernstein_poly(i, nPoints - 1, t)
                            for i in range(0, nPoints)
                        ])

                        xvals = np.dot(xPoints, polynomial_array)[::-1]
                        yvals = np.dot(yPoints, polynomial_array)[::-1]
                        slopes = np.gradient(yvals) / np.gradient(
                            xvals)  #change with analytic gradient
                        return xvals, yvals, slopes

                    self.xs, self.ys, slopes = bezier_curve(points, nTimes=n)

                    pts = np.array(list(map(qpt2pt, self.xs, self.ys)))
                    x, y = pts[:, 0], pts[:, 1]

                    self.l = np.cumsum(np.hypot(
                        np.gradient(x), np.gradient(y)))  #integrate for length
                    add = np.concatenate(
                        (self.l[-1], np.empty(self.nm) * np.nan), axis=None
                    )  #add length and row of nans for possible widths
                    add = np.expand_dims(add, axis=0)
                    self.measurements = np.append(self.measurements,
                                                  add,
                                                  axis=0)

                    #get pts for width drawing
                    bins = np.linspace(0, self.l[-1], self.numwidths + 1)
                    inds = np.digitize(self.l, bins)
                    __, self.inddec = np.unique(inds, return_index=True)

                    self.xp, self.yp = x[self.inddec], y[self.inddec]
                    self.m = slopes[self.inddec]

                    #Identify width spine points
                    self.xsw = x[inds]
                    self.ysw = y[inds]

                    for i in range(1, n - 1):
                        start = self.mapFromScene(
                            self.mapToScene(self.xs[i - 1],
                                            self.ys[i - 1]))  #+ self.pos()
                        mid = self.mapFromScene(
                            self.mapToScene(self.xs[i],
                                            self.ys[i]))  #+ self.pos()
                        end = self.mapFromScene(
                            self.mapToScene(self.xs[i + 1],
                                            self.ys[i + 1]))  # + self.pos()
                        path = QtGui.QPainterPath(start)
                        path.cubicTo(start, mid, end)
                        self.scene.addPath(path)

                if not self.parent().bezier.isChecked():

                    pts = np.array(list(map(qpt2pt, self.A.x, self.A.y)))
                    x, y = pts[:, 0], pts[:, 1]

                    self.l = np.cumsum(np.hypot(
                        np.diff(x), np.diff(y)))  #integrate for length
                    add = np.concatenate(
                        (self.l[-1], np.empty(self.nm) * np.nan), axis=None
                    )  #add length and row of nans for possible widths
                    add = np.expand_dims(add, axis=0)
                    self.measurements = np.append(self.measurements,
                                                  add,
                                                  axis=0)

            self.measuring_angle = False
            self.measuring_length = False
            self._thispos = False

        def measure_widths(self):
            self.measuring_widths = True
            self.k = 0
            self.W = posData(np.empty(shape=(0, 0)),
                             np.empty(shape=(0,
                                             0)))  #preallocate custom widths
            self.nspines = 2 * (self.numwidths - 1)
            self.parent().statusBar.showMessage(
                'Click point along spines to make width measurements perpindicular to the length segment'
            )
            #Draw widths
            for k, m in enumerate(self.m[1:-1]):  #only middle widths
                x1, y1 = self.xp[k + 1], self.yp[k + 1]

                x2 = self.pixmap_fit.width()  # - x1
                y2 = -(1 / m) * (x2 - x1) + y1

                y0 = self.pixmap_fit.height()  # - y1
                x0 = -m * (y0 - y1) + x1

                #use larger distance
                if np.hypot((x1 - x0), (y1 - y0)) > np.hypot((x1 - x2),
                                                             (y1 - y2)):
                    x2 = x1 + (x1 - x0)
                    y2 = y1 + (y1 - y0)
                else:
                    x0 = x1 + (x1 - x2)
                    y0 = y1 + (y1 - y2)

                # Limit spines to size of image...I am sure there is a cleaner way to do this
                if y2 > self.pixmap_fit.height():
                    y2 = self.pixmap_fit.height()
                    x2 = -m * (y2 - y1) + x1
                elif y2 < 0:
                    y2 = 0
                    x2 = -m * (y2 - y1) + x1

                if x0 > self.pixmap_fit.width():
                    x0 = self.pixmap_fit.width()
                    y0 = -(1 / m) * (x0 - x1) + y1
                elif x0 < 0:
                    x0 = 0
                    y0 = -(1 / m) * (x0 - x1) + y1

                # Limit spines to size of image...I am sure there is a cleaner way to do this
                if y0 > self.pixmap_fit.height():
                    y0 = self.pixmap_fit.height()
                    x0 = -m * (y0 - y1) + x1
                elif y0 < 0:
                    y0 = 0
                    x0 = -m * (y0 - y1) + x1

                if x2 > self.pixmap_fit.width():
                    x2 = self.pixmap_fit.width()
                    y2 = -(1 / m) * (x2 - x1) + y1
                elif x2 < 0:
                    x2 = 0
                    y2 = -(1 / m) * (x2 - x1) + y1

                for l, (x, y) in enumerate(zip([x0, x2], [y0, y2])):
                    start = QtCore.QPointF(x1, y1)
                    end = QtCore.QPointF(x, y)
                    self.scene.interpLine = QGraphicsLineItem(
                        QtCore.QLineF(start, end))
                    self.d["{}".format(2 * k + l)] = self.scene.interpLine
                    self.scene.addItem(self.scene.interpLine)

                    if k == 0 and l == 0:
                        self.scene.interpLine.setPen(
                            QtGui.QPen(QtGui.QColor('yellow')))

        def mousePressEvent(self, event):
            #http://pyqt.sourceforge.net/Docs/PyQt4/qgraphicsscenemouseevent.html

            data = self.mapToScene(event.pos())
            #https://stackoverflow.com/questions/21197658/how-to-get-pixel-on-qgraphicspixmapitem-on-a-qgraphicsview-from-a-mouse-click

            #draw piecewise lines
            if self.scene.testline and (
                    self.measuring_length
                    or self.measuring_angle) and self._thispos:
                start = self._thispos
                end = QtCore.QPointF(data)

                if self._lastpos and self.measuring_angle:

                    start = self._lastpos
                    self.measuring_angle = False
                    a = np.array([data.x() - start.x(), data.y() - start.y()])
                    b = np.array([
                        self._thispos.x() - start.x(),
                        self._thispos.y() - start.y()
                    ])
                    t = np.arccos(
                        np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
                    t *= 180 / np.pi  #convert to degrees
                    self.T.update(t)
                    self.parent().statusBar.showMessage(
                        'Angle measurement complete')

                self.scene.realline = QGraphicsLineItem(
                    QtCore.QLineF(start, end))
                #self.scene.realline.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
                self.scene.addItem(self.scene.realline)

            #Collect piecewise line start/end points
            if self.measuring_angle:
                self._lastpos = self._thispos  # save old position value
                self._thispos = QtCore.QPointF(data)  # update current position

            if self.measuring_length:
                self._lastpos = self._thispos  # save old position value
                self._thispos = QtCore.QPointF(data)  # update current position
                self.A.update(data.x(), data.y())  # update total length
                self.line_count += 1

            #https://stackoverflow.com/questions/30898846/qgraphicsview-items-not-being-placed-where-they-should-be
            if self.measuring_widths:  #measure widths, snap to spines

                #y2 = m*(x2-x0)+y0
                #y2 = mp*(x2-x1)+y1
                #solves this system of equations in Ax=b form
                k = int(self.k / 2) + 1  #same origin for spine on either side
                x0, y0 = self.xp[k], self.yp[k]
                x1, y1 = data.x(), data.y()
                m = self.m[k]  #tangent to whale length curve
                mp = -1 / self.m[k]  #perpindicular to whale length curve

                A = np.matrix([[mp, -1], [m, -1]])
                b = np.array([-y0 + mp * x0, -y1 + m * x1])
                x = np.linalg.solve(A, b)
                p = QtCore.QPointF(x[0], x[1])
                self.W.update(data.x(), data.y())

                s = 10  #dot size
                self.scene.ellipseItem = QGraphicsEllipseItem(0, 0, s, s)
                self.scene.ellipseItem.setPos(p.x() - s / 2, p.y() - s / 2)
                self.scene.ellipseItem.setBrush(
                    QtGui.QBrush(QtCore.Qt.red, style=QtCore.Qt.SolidPattern))
                self.scene.ellipseItem.setFlag(
                    QGraphicsItem.ItemIgnoresTransformations,
                    False)  #size stays small, but doesnt translate if false
                self.scene.addItem(self.scene.ellipseItem)
                self.k += 1

                if self.k < self.nspines:
                    self.d[str(self.k)].setPen(
                        QtGui.QPen(
                            QtGui.QColor('yellow')))  #Highlight next spine
                if self.k == self.nspines:
                    self.parent().statusBar.showMessage(
                        'Width measurements complete')
                    self.measuring_widths = False
                    width = np.sqrt((self.W.x[1::2] - self.W.x[0::2])**2 +
                                    (self.W.y[1::2] - self.W.y[0::2])**
                                    2)  #calculate widths
                    self.measurements[
                        -1,
                        1:] = width  #update most recent row w/ length measurement

        #MouseWheel Zoom
        def wheelEvent(self, event):
            #https://stackoverflow.com/questions/35508711/how-to-enable-pan-and-zoom-in-a-qgraphicsview
            #transform coordinates correctly
            #https://stackoverflow.com/questions/20942586/controlling-the-pan-to-anchor-a-point-when-zooming-into-an-image
            #https://stackoverflow.com/questions/41226194/pyqt4-pixel-information-on-rotated-image
            zoomInFactor = 1.05
            zoomOutFactor = 1 / zoomInFactor

            self.setTransformationAnchor(QGraphicsView.NoAnchor)
            self.setResizeAnchor(QGraphicsView.NoAnchor)
            oldPos = self.mapToScene(event.pos())

            #Zoom
            #y component for mouse with two wheels
            #https://quick-geek.github.io/answers/885796/index.html
            if event.angleDelta().y() > 0:
                zoomFactor = zoomInFactor
            else:
                zoomFactor = zoomOutFactor
            self.scale(zoomFactor, zoomFactor)

            newPos = self.mapToScene(event.pos())  #Get the new position
            delta = newPos - oldPos
            self.translate(delta.x(), delta.y())  #Move scene to old position