"""Progress widget for the straditization
The ProgressWidget defined here is a ListWidget to show the current state
of the stradititization.
**Disclaimer**
Copyright (C) 2018-2019 Philipp S. Sommer
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>."""
import os.path as osp
import datetime as dt
from PyQt5 import QtWidgets, QtGui, QtCore
import numpy as np
import pandas as pd
from psyplot_gui.common import get_icon as get_psy_icon
from straditize.widgets import StraditizerControlBase, get_doc_file, doc_files
from collections import OrderedDict
from straditize.widgets.data import get_reader_name, int_list2str
ALL_TASKS = 'all'
DONE = 'done'
NOTYET = 'not yet ready'
TASK_TODO = 'todo'
icons = {DONE: get_psy_icon('valid.png'),
NOTYET: get_psy_icon('warning.png'),
TASK_TODO: get_psy_icon('invalid.png'),
}
[docs]class ProgressTask(QtWidgets.QListWidgetItem):
"""The base class for an item that should be shown in the ProgressWidget"""
@property
def is_finished(self):
"""A property that is True, when the task is finished"""
return True
@property
def try_finished(self):
"""Same as :attr:`is_finished` but catches every exception"""
try:
return self.is_finished
except Exception:
pass
@property
def done_by_user(self):
"""boolean that is True, when the task is marked as done by the user"""
stradi = self.straditizer
return stradi is not None and self.name in stradi._done_tasks
@done_by_user.setter
def done_by_user(self, value):
stradi = self.straditizer
if stradi is not None:
if value:
stradi._done_tasks.add(self.name)
else:
stradi._done_tasks.difference_update({self.name})
#: Boolean that is True if the task is ready to be solved
is_ready = True
@property
def done(self):
"""True if :attr:`is_finished` or :attr:`done_by_user`"""
return self.done_by_user or (
self.is_ready and all(t.done for t in self.dependencies_tasks) and
self.try_finished)
#: The name of the task
name = ''
#: The summary of the task that is shown in the progress widget
summary = ''
#: The tooltip of the item
task_tooltip = ''
#: The tooltip when the straditizer is done. If None, the
#: :attr:`task_tooltip` is used
done_tooltip = None
#: List of :attr:`name` attributes that this task is depending on
dependencies = []
#: rst file that should be displayed on double click. The filename shuould
#: be without .rst ending.
rst_file = None
@property
def dependencies_tasks(self):
list_widget = self.listWidget()
dependencies = self.dependencies
return [item for item in map(list_widget.item,
range(list_widget.count()))
if item.name in dependencies]
@property
def progress_widget(self):
"""The progress widget that shows this task"""
return self.listWidget().parent()
@property
def straditizer(self):
"""The straditizer of the GUI"""
return self.progress_widget.straditizer
@property
def data_reader(self):
"""The data reader of the straditizer"""
return self.straditizer.data_reader
@property
def colnames_reader(self):
"""The column names reader of the straditizer"""
return self.straditizer.colnames_reader
@property
def state(self):
"""The state of the task"""
if self.done:
return DONE
elif not self.is_ready or not all(
task.done for task in self.dependencies_tasks):
return NOTYET
else:
return TASK_TODO
[docs] def refresh(self):
pw = self.progress_widget
state = self.state
icon = icons[state]
visible = pw.visible_tasks in ['all', state]
self.setIcon(QtGui.QIcon(icon))
self.setHidden(not visible)
if self.done:
try:
tt = self.done_tooltip
except Exception:
tt = ''
if tt is None:
tt = self.task_tooltip
else:
tt = self.task_tooltip
self.setToolTip(tt)
def __init__(self, parent):
"""
Parameters
----------
parent: QListWidget
The list that holds this item
"""
super().__init__(self.summary, parent)
[docs]class InitStraditizerTask(ProgressTask):
"""The task to initialize a straditizer"""
name = 'init_stradi'
summary = 'Load an image or project'
task_tooltip = 'Load a straditizer image or project to get started'
rst_file = 'load_image'
@property
def done_tooltip(self):
try:
image = self.straditizer.get_attr('image_file')
except KeyError:
return "Loaded diagram"
else:
return "Loaded " + (osp.basename(image) if image else "diagram")
@property
def is_finished(self):
return self.straditizer is not None
[docs]class DataLimitsTask(ProgressTask):
"""The task to check the data limits"""
name = 'datalim'
summary = 'Limits of the diagram'
task_tooltip = ('Specify the corners for the data part of the diagram by '
'clicking the <i>Select data part</i> button')
rst_file = 'select_data_part'
dependencies = ['init_stradi']
@property
def done_tooltip(self):
stradi = self.straditizer
return "Data limits are set to x={} and y={}".format(
tuple(stradi.data_xlim), tuple(stradi.data_ylim))
@property
def is_finished(self):
return self.straditizer.data_xlim is not None
[docs]class DataReaderTask(ProgressTask):
"""A task for initializing the reader"""
name = 'init_reader'
summary = 'Initialize the diagram reader'
task_tooltip = ('Choose the appropriate reader type and click the '
'<i>Convert image</i> button')
rst_file = 'select_reader'
dependencies = ['datalim']
@property
def is_finished(self):
return self.straditizer.data_reader is not None
@property
def done_tooltip(self):
reader = self.straditizer.data_reader
return "Initialized <i>%s</i> reader" % (get_reader_name(reader) or '')
[docs]class ColumnsTask(ProgressTask):
"""A task to separate columns"""
name = 'columns'
summary = 'Separate the columns'
task_tooltip = ('Separate the columns (subdiagrams) of the stratigraphic '
'diagram')
rst_file = 'select_column_starts'
dependencies = ['init_reader']
@property
def is_finished(self):
return self.data_reader._column_starts is not None
@property
def done_tooltip(self):
ncols = len(self.data_reader._column_starts)
return 'Marked %i columns' % ncols
[docs]class RemoveArtifactsTask(ProgressTask):
"""Task to clean the binary image"""
name = 'remove_artifacts'
summary = 'Clean the diagram image'
task_tooltip = ('Remove all artifacts in the diagram part that do not '
'represent data. Mark this task as done when you are '
'finished.')
rst_file = 'removing_features'
dependencies = ['columns']
@property
def is_finished(self):
return (self.data_reader._full_df is not None and
len(self.data_reader._full_df))
@property
def done_tooltip(self):
if self.try_finished:
return 'The data has been digitized'
[docs]class RemoveLinesTask(RemoveArtifactsTask):
"""Task to remove y-axes, x-axes, horizontal and vertical lines"""
name = 'remove_lines'
summary = 'Remove y-, x-axes and other lines'
task_tooltip = ('In order to clean the diagram part, remove all vertical '
'and horizontal lines in the reader image. Mark this task '
'as done when you are finished.')
rst_file = 'remove_lines'
dependencies = ['columns']
@property
def is_finished(self):
return (len(self.data_reader.vline_locs) and
len(self.data_reader.hline_locs)) or super().is_finished
@property
def done_tooltip(self):
if (len(self.data_reader.vline_locs) and
len(self.data_reader.hline_locs)):
return '%i vertical and %i horizontal lines have been removed' % (
len(self.data_reader.vline_locs),
len(self.data_reader.hline_locs))
else:
return super().done_tooltip
[docs]class OccurencesTask(ProgressTask):
"""Task to handle occurences"""
name = 'occurences'
summary = 'Select occurence markers'
task_tooltip = ('Pollen diagrams often have markers for low taxon '
'percentages to show their occurence. Mark this task as '
'done if your diagram does not have them.')
rst_file = 'occurences'
dependencies = ['columns']
@property
def is_finished(self):
return bool(self.data_reader.occurences)
@property
def done_tooltip(self):
return "Marked %i occurences" % len(self.data_reader.occurences)
[docs]class SelectExaggerationsTask(ProgressTask):
"""Task to handle exaggerations"""
name = 'exag'
summary = 'Select exagerations'
task_tooltip = ('Pollen diagrams often display an exaggerated value of '
'of the taxon percentage. You can select these '
'exaggerations using the <i>Exaggerations</i> menu. '
'Mark this task as done if your diagram does not have '
'them.')
rst_file = 'exaggerations'
dependencies = ['columns']
@property
def is_finished(self):
return self.data_reader.exaggerated_reader is not None
@property
def done_tooltip(self):
return "Created exaggerations reader"
[docs]class DigitizeTask(ProgressTask):
"""Task to digitize the data"""
name = 'digitize'
summary = 'Digitize the diagram'
task_tooltip = 'Click the `Digitize` button to digitize the diagram'
rst_file = 'digitize'
dependencies = ['remove_artifacts', 'remove_lines']
@property
def is_finished(self):
return (self.data_reader._full_df is not None and
len(self.data_reader._full_df))
@property
def done_tooltip(self):
if self.try_finished:
return 'The data has been digitized'
[docs]class SamplesTask(ProgressTask):
"""Task to edit and find samples"""
name = 'samples'
summary = 'Find and edit the samples'
rst_file = 'samples'
task_tooltip = 'Find and edit the samples using the `Edit samples` menu'
dependencies = ['digitize']
@property
def is_finished(self):
locs = self.data_reader._sample_locs
return locs is not None and len(locs)
@property
def done_tooltip(self):
return 'Found %i samples' % len(self.data_reader._sample_locs)
[docs]class YTranslationTask(ProgressTask):
"""Task to specify the y-axis conversion from pixel to data units"""
name = 'yaxis_trans'
summary = "Transform the y-axis"
rst_file = 'yaxis_translation'
task_tooltip = "Transform the y-axis from pixel to data units"
dependencies = ['init_reader']
@property
def is_finished(self):
return self.straditizer._yaxis_px_orig is not None
@property
def done_tooltip(self):
px2data = self.straditizer.px2data_y
return "Transforming 1 pixel to %1.4g data units" % (
np.diff(px2data(np.array([0, 1])))[0])
[docs]class XTranslationTask(ProgressTask):
"""Task to specify the x-axes conversion from pixel to data units"""
name = 'xaxes_trans'
summary = 'Transform the x-axes'
rst_file = 'xaxis_translation'
dependencies = ['columns']
@property
def task_tooltip(self):
try:
finished = self.is_finished
except AttributeError:
return "Transform the x-axes from pixel to data units"
else:
if finished:
return self.done_tooltip
elif self.data_reader._column_starts is None:
return "Transform the x-axes from pixel to data units"
else:
reader = self.data_reader
columns = [r.columns for r in reader.iter_all_readers
if r._xaxis_px_orig is None and
not r.is_exaggerated]
return ("Transform the x-axes from pixel to data units for "
"columns<ul>%s</ul>" % (
''.join('<li>%s</li>' % int_list2str(cols)
for cols in columns)))
@property
def is_finished(self):
return (self.data_reader._column_starts is not None and
all(reader._xaxis_px_orig is not None
for reader in self.data_reader.iter_all_readers))
@property
def done_tooltip(self):
reader = self.data_reader
readers = filter(lambda r: not r.is_exaggerated,
reader.iter_all_readers)
ret = "Transforming 1 pixel to<ul>"
for reader in readers:
s = reader.column_starts[0]
px2data = reader.px2data_x
ret += "<li>%1.4g data units for columns %s</li>" % (
np.diff(px2data(np.array([s, s+1])))[0],
int_list2str(reader.columns))
return ret + "</ul>"
[docs]class ColumnNamesTask(ProgressTask):
"""Task to specify the column names"""
name = 'column_names'
summary = 'Specify the column names'
task_tooltip = ('Click the <i>Edit column names</i> button to insert the '
'names for each column/variable in the diagram')
rst_file = 'column_names'
dependencies = ['columns']
@property
def is_finished(self):
return self.colnames_reader.column_names != list(map(
str, range(len(self.data_reader.all_column_starts))))
@property
def done_tooltip(self):
return "The column names are " + ', '.join(
self.colnames_reader.column_names)
[docs]class ExportTask(ProgressTask):
"""Task to export the final results"""
name = 'export'
summary = 'Export the final data'
task_tooltip = 'Export the final data'
rst_file = 'export'
dependencies = ['samples']
@property
def is_finished(self):
return self.straditizer.get_attr('exported') is not None
@property
def done_tooltip(self):
return "Exported to {}".format(self.straditizer.get_attr('exported'))
[docs]class SaveProjectTask(ProgressTask):
"""Task to remember saving the project"""
name = 'save_project'
summary = 'Save the straditizer'
dependencies = ['init_stradi']
rst_file = 'save_and_load'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.refresh_timer = QtCore.QTimer(self.progress_widget)
self.refresh_timer.timeout.connect(self.refresh)
self.refresh_timer.start(60000)
self.tooltip_timer = QtCore.QTimer(self.progress_widget)
self.tooltip_timer.timeout.connect(self.refresh_tooltip)
self.tooltip_timer.start(5000)
@property
def is_finished(self):
"""True if the project was saved less than 5 minutes ago"""
try:
loaded = self.straditizer.get_attr('loaded')
except (KeyError, AttributeError):
pass
else:
if (dt.datetime.now() - pd.to_datetime(loaded)).seconds < 300:
return True
saved = self.straditizer.get_attr('saved')
td = dt.datetime.now() - pd.to_datetime(saved)
return td.seconds < 300
@property
def task_tooltip(self):
try:
saved = self.straditizer.get_attr('saved')
except KeyError:
return "The Project has never been saved!"
except AttributeError:
return "No project is open to save"
else:
try:
loaded = self.straditizer.get_attr('loaded')
except (KeyError, AttributeError):
pass
else:
if saved < loaded:
return 'The project has not been saved in this session'
td = dt.datetime.now() - pd.to_datetime(saved)
return 'Last saved %i minutes and %i seconds ago' % (
(td.seconds // 60) % 60, td.seconds % 60)