#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A GUI application to view the raw activPAL accelerometer data."""
# Created on 21 Jun 2017
#
# @author: R-Broadley
import sys
import os
import numpy as np
import matplotlib.dates as mdates
from PyQt5 import QtCore
from PyQt5.QtWidgets import (QFileDialog, QWidget, QPushButton, QLabel, QSlider,
QAction, QHBoxLayout)
from scipy.signal import argrelmax
from .base import BaseQuestionDialog, SpacerWidget
from .raw_viewer import UIFilePlot
from .raw_viewer import MainWindow as BasePlotWindow
# This is a subclass of raw_viewer.MainWindow (imported as BasePlotWindow)
[docs]class MainWindow(BasePlotWindow):
"""A raw_viewer.MainWindow subclass which adds the ability to mark points."""
[docs] def __init__(self, parent=None, request_confidence=True):
"""
Create an instance of a MainWindow.
Parameters
----------
parent : object
The parent object.
request_confidence : bool
Sets whether a dialog will appear, when the save button is pressed,
which requests the confidence the correct point has been marked.
"""
super(MainWindow, self).__init__(parent)
self.request_confidence = request_confidence
def _setup_toolbar(self):
"""Add Save button, plot controls and Mark button to toolbar."""
saveAct = QAction('Save', self)
saveAct.setShortcut('Ctrl+S')
saveAct.triggered.connect(self.save_button_action)
self.toolbar.addAction(saveAct)
self.toolbar.addWidget(SpacerWidget())
self._add_plot_controls()
self.toolbar.addWidget(SpacerWidget())
self.markbtn = QPushButton('Mark')
self.markbtn.setCheckable(True)
self.markbtn.clicked[bool].connect(self.toggle_plot_marking)
self.toolbar.addWidget(self.markbtn)
def toggle_plot_marking(self):
"""Activate / Deactivate the plot marking feature."""
if self.markbtn.isChecked():
self.FilePlot.mark_active = True
self.statusBar().showMessage('Marker Active')
else:
self.FilePlot.mark_active = False
self.statusBar().showMessage('Ready')
def _setup_window(self):
"""Set up window and plot canvas."""
self.FilePlot = UIFileMarkingPlot(parent=self)
self.setWindowTitle("Raw activPAL Data Marker")
self.setCentralWidget(self.FilePlot)
# @QtCore.pyqtSlot()
def save_button_action(self):
"""Run pre-save actions then save."""
# TODO check point has been marked
if self.request_confidence:
confidence_dialog = ConfidenceDialog(parent=self)
confidence_dialog.exec_()
# import ipdb; ipdb.set_trace()
if confidence_dialog.result() == 1:
self.confidence = confidence_dialog.get_confidence()
else:
return
self.save()
another_point_dialog = AnotherPointDialog(parent=self)
another_point_dialog.exec_()
if another_point_dialog.result() == 1:
self.clear_markers()
else:
another_file_dialog = AnotherFileDialog(parent=self)
another_file_dialog.exec_()
if another_file_dialog.result() == 1:
self.load_new()
else:
self.close()
# @QtCore.pyqtSlot()
def clear_markers(self):
"""Clear all placed markers."""
self.FilePlot.clear_markers()
# @QtCore.pyqtSlot()
def load_new(self):
"""Fetch new file, load the data and plot."""
self.clear_markers()
self.select_file()
self.run_main()
# @QtCore.pyqtSlot()
def save(self):
"""Save marked points sample number, datetime, (and confidence) to csv file."""
file_dir = os.path.expanduser('~')
save_file_path, _ = QFileDialog.getSaveFileName(
self, "Select Save File",
'/'.join([file_dir, 'activpal-markers.csv']),
"mark record (*.csv)",
options=QFileDialog.DontConfirmOverwrite)
if not os.path.isfile(save_file_path):
with open(save_file_path, 'w') as f:
f.write('File,Sample,DateTime,Confidence\n')
with open(save_file_path, 'a') as f:
try:
f.write(self.file_path)
except AttributeError:
f.write('Unknown File')
f.write(',')
try:
f.write(str(self.FilePlot.marked_sample))
except AttributeError:
f.write('Could not get sample number')
f.write(',')
try:
f.write(str(self.FilePlot.marked_datetime))
except AttributeError:
f.write('Could not get datetime')
f.write(',')
try:
f.write(str(self.confidence))
except AttributeError:
f.write('NaN')
f.write('\n')
[docs]class UIFileMarkingPlot(UIFilePlot):
"""
A QWidget which displays, and allow marking of, activPAL raw data.
Displays x, y, z and rss.
"""
[docs] def __init__(self, parent=None, file_path=None, center=None, width=None):
"""
Create a UIFileMarkingPlot widget.
Parameters
----------
parent : object
The parent object.
file_path : str
The path of the file to plot.
center : float
The point the plot should be horizontally centered around.
width : float | int
The width of the plot (range of the x axis) in days.
"""
super(UIFileMarkingPlot, self).__init__(parent, file_path)
# Interaction
self.canvas.mpl_connect('button_press_event', self._on_click)
def _on_click(self, event):
"""."""
try:
self.mark_active
except AttributeError:
return
if event.inaxes is None or not self.mark_active:
return
xdata = event.xdata
xdata_datetime = mdates.num2date(xdata).replace(tzinfo=None)
if event.button == 1:
datetime_index = self.data.signals.index
sn = (np.abs(datetime_index - xdata_datetime)).argmin()
self.marked_sample = self.get_nearest_peak(
self.data.rss.values, sn)
self.marked_datetime = datetime_index[self.marked_sample]
try:
self.marker = self.update_marker(
self.marker, self.marked_datetime)
except AttributeError:
self.marker = self.create_marker(self.marked_datetime)
self.canvas.draw()
def create_marker(self, xdata, linecolor='k'):
"""
Create a marker line on the plot.
Parameters
----------
xdata :
The point (x axis) where the marker should be.
linecolor : str
The code for the desired line color.
"""
marker = []
lineproperties = {'color': linecolor, 'alpha': 1, 'linestyle': ':'}
for i in range(3):
ydata = self.axes[i].get_ylim()
marker.append(self.axes[i].plot(
[xdata, xdata], ydata, **lineproperties)[0])
self._set_xticks()
return marker
def update_marker(self, marker, xdata):
"""
Update the location of the given marker.
Parameters
----------
marker :
The marker which should be updated.
xdata :
The point (x axis) where the marker should be.
Returns
-------
marker:
The marker (set of lines) which has been updated.
"""
if not marker == []:
for m in marker:
m.set_xdata([xdata, xdata])
return marker
else:
raise AttributeError # Marker does not exist
def clear_markers(self):
"""Clear all stored markers."""
if hasattr(self, 'marker'):
for i in range(len(self.marker)):
self.marker.pop(0).remove()
self.marked_datetime = None
self.marked_sample = None
self.canvas.draw()
def get_nearest_peak(self, input_array, sample_number):
"""
Find the peak in the given array nearest the given samle number.
Parameters
----------
input_array : numpy.ndarray
The array which should be searched for the nearest peak.
sample_number : int
The index of the point from which the nearest peak should be found.
Returns
-------
sample_number:
The sample number of the nearest peak.
"""
zone_width = 1200
zone_start = sample_number - zone_width
if zone_start < 0:
zone_start = 0
zone_end = sample_number + zone_width
if zone_end > len(input_array):
zone_end = len(input_array)
test_zone = input_array[zone_start: zone_end]
peaks = argrelmax(test_zone, order=3)[0] + zone_start
if len(peaks) is not 0 and np.ptp(test_zone) > 0.25:
diff = np.abs(peaks - sample_number)
min_diff_loc = np.argmin(diff)
return peaks[min_diff_loc]
else:
return sample_number
[docs]class ConfidenceDialog(BaseQuestionDialog):
"""
A BaseQuestionDialog to request a confidence rating from the user.
See Also
--------
uos_activpal.gui.base.BaseDialog : A QDialog subclass which includes
additional setup, mainly styling.
uos_activpal.gui.base.BaseMessageDialog : A BaseDialog subclass desiged
for displaying messages.
uos_activpal.gui.base.BaseQuestionDialog : A BaseDialog subclass desiged
for asking binary (yes | no) questions.
"""
[docs] def __init__(self, parent=None):
"""
Create an instance of a ConfidenceDialog.
Parameters
----------
parent : object
The parent object.
"""
super(ConfidenceDialog, self).__init__(parent)
self.setWindowTitle("Set Confidence")
self.buttonleft.setText('Return')
self.buttonright.setText('Submit')
self.set_question('How confident are you that the point\n'
'which you marked is correct?')
self.confidence_slider = ConfidenceSlider()
self.main_space_addWidget(self.confidence_slider)
def get_confidence(self):
"""Get the current confidence rating."""
return self.confidence_slider.get_slider_value()
[docs]class AnotherPointDialog(BaseQuestionDialog):
"""
A BaseQuestionDialog to ask if the user wants to mark another point in this file.
See Also
--------
uos_activpal.gui.base.BaseDialog : A QDialog subclass which includes
additional setup, mainly styling.
uos_activpal.gui.base.BaseMessageDialog : A BaseDialog subclass desiged
for displaying messages.
uos_activpal.gui.base.BaseQuestionDialog : A BaseDialog subclass desiged
for asking binary (yes | no) questions.
"""
[docs] def __init__(self, parent=None):
"""
Create an instance of a AnotherPointDialog.
Parameters
----------
parent : object
The parent object.
"""
super(AnotherPointDialog, self).__init__(parent)
self.setWindowTitle("Mark Another Point?")
self.set_question('Do you want to mark another point in this file?')
[docs]class AnotherFileDialog(BaseQuestionDialog):
"""
A BaseQuestionDialog to ask if the user wants to mark another point in another file.
See Also
--------
uos_activpal.gui.base.BaseDialog : A QDialog subclass which includes
additional setup, mainly styling.
uos_activpal.gui.base.BaseMessageDialog : A BaseDialog subclass desiged
for displaying messages.
uos_activpal.gui.base.BaseQuestionDialog : A BaseDialog subclass desiged
for asking binary (yes | no) questions.
"""
[docs] def __init__(self, parent=None):
"""
Create an instance of a AnotherPointDialog.
Parameters
----------
parent : object
The parent object.
"""
super(AnotherFileDialog, self).__init__(parent)
self.setWindowTitle("Mark Another File?")
self.set_question('Do you want to mark a point in another file?')
[docs]class ConfidenceSlider(QWidget):
"""A Qwidget which contains a 1 to 10 slider and the label 'Confidence:'."""
[docs] def __init__(self, parent=None):
"""Create a ConfidenceSlider widget."""
super(ConfidenceSlider, self).__init__(parent)
# Slider Widget
self.slider = QSlider(QtCore.Qt.Horizontal)
self.slider.setMinimum(1)
self.slider.setMaximum(10)
self.slider.setValue(1)
self.slider.setTickInterval(1)
self.slider.setTickPosition(QSlider.TicksAbove)
self.slider.setMaximumWidth(250)
# Slider Value Label
self.confidencelabel = QLabel('1 / 10')
self.confidencelabel.setAlignment(QtCore.Qt.AlignRight)
self.confidencelabel.setMinimumWidth(20)
self._slider_move()
self.slider.valueChanged.connect(self._slider_move)
# Lazy redefine slider mouse press event
self.slider.mousePressEvent = self._slider_click
# Layout
self.setMaximumWidth(400)
self.setMaximumHeight(200)
self_layout = QHBoxLayout()
self_layout.setContentsMargins(50, 25, 50, 25)
self_layout.addWidget(QLabel('Confidence:'))
self_layout.addWidget(self.confidencelabel)
self_layout.addWidget(QLabel('/ 10'))
self_layout.addWidget(self.slider)
self.setLayout(self_layout)
def _slider_move(self):
"""Define the action when the slider is moved (Updates the value label)."""
self.confidence = self.slider.value()
self.confidencelabel.setText(str(self.confidence))
def get_slider_value(self):
"""Return the current value of the slider."""
return self.slider.value()
def _slider_click(self, event):
maximum = self.slider.maximum()
width = self.slider.width()
point = event.pos().x()
new_val = np.round((point / width) * 10)
self.slider.setValue(new_val)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())