# -*- coding: utf-8 -*-
"""The main control widget for handling the data
**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/>.
"""
from __future__ import division
import warnings
import weakref
import os.path as osp
import six
from functools import partial
import re
import numpy as np
import pickle
import pandas as pd
import xarray as xr
from straditize.widgets import StraditizerControlBase, get_icon
from psyplot_gui.compat.qtcompat import (
QPushButton, QLineEdit, QComboBox, QLabel, QDoubleValidator,
Qt, QHBoxLayout, QVBoxLayout, QWidget, QTreeWidgetItem,
with_qt5, QIcon, QIntValidator, QTreeWidget, QToolBar, QGridLayout,
QCheckBox, QInputDialog, QFileDialog, QMessageBox)
from straditize.common import docstrings
from psyplot.utils import unique_everseen
from itertools import chain
if with_qt5:
from PyQt5.QtWidgets import QGroupBox, QSpinBox
else:
from PyQt4.QtGui import QGroupBox, QSpinBox
if six.PY2:
from itertools import (
izip_longest as zip_longest, ifilterfalse as filterfalse)
else:
from itertools import zip_longest, filterfalse
reader_types = ['area', 'bars', 'rounded bars', 'stacked area',
'line']
[docs]def int_list2str(numbers):
"""Create a short string representation of an integer list
Parameters
----------
numbers: list of ints
Integer list
Returns
-------
str
The string representation
Examples
--------
``[1, 2, 3]`` becomes ``'1-3'``,
``[1, 2, 3, 5, 7, 8, 9]`` becomes ``'1-3, 5, 7-9'``"""
numbers = sorted(map(int, numbers))
s = str(numbers[0])
for last, curr, nex in zip_longest(numbers[:-1], numbers[1:], numbers[2:]):
if curr - last <= 1 and nex and nex - curr <= 1:
continue
elif curr - last <= 1:
s += '-%s' % curr
else:
s += ', %s' % curr
return s
[docs]def get_reader_name(reader):
"""Get the reader key in the :attr:`straditize.binary.readers` dictionary
Parameters
----------
reader: straditize.binary.DataReader
The reader for which to get the key in the
:attr:`straditize.binary.readers` dictionary
Returns
-------
str
The key in the :attr:`straditize.binary.readers` dictionary whose value
corresponds to the class of the given `reader`"""
from straditize.binary import readers
# import stacked_area_reader to make sure StackedReader is registered
import straditize.widgets.stacked_area_reader
for key, cls in readers.items():
if reader.__class__ is cls:
return key
[docs]class DigitizingControl(StraditizerControlBase):
"""An interface to :attr:`straditize.straditizer.Straditizer.data_reader`
This widgets contains the functionalities to interface with the data
readers for the stratigraphic diagram"""
@property
def reader(self):
"""The :attr:`straditize.straditizer.Straditizer.data_reader`"""
return self.straditizer.data_reader
@property
def tree(self):
"""The :attr:`straditize.widgets.StraditizerWidgets.tree`"""
return self.straditizer_widgets.tree
# --- Reader intialization ------------------------------------------------
#: Button for selecting the data box, see the
#: :meth:`straditize.straditizer.Straditizer.marks_for_data_selection`
#: method.
btn_select_data = None
#: Combobox for selecting the reader type
cb_reader_type = None
#: Button for initializing the reader
btn_init_reader = None
# --- Columns -------------------------------------------------------------
#: A QLineEdit to set the threshold for the column starts detection
txt_column_thresh = None
#: Button for selecting and modifying column starts, see the
#: :meth:`straditize.straditizer.Straditizer.marks_for_column_starts`
#: method.
btn_column_starts = None
#: Button for selecting and modifying column ends
btn_column_ends = None
#: Button to reset the column starts and ends
btn_reset_columns = None
# --- Child readers -------------------------------------------------------
#: Button to add a new column-specific child reader
btn_new_child_reader = None
# --- Exaggerations -------------------------------------------------------
#: A QLineEdit for the exageration factor
txt_exag_factor = None
#: A QComboBox to select the reader type for exaggerations
cb_exag_reader_type = None
#: Button to add an exaggerations reader
btn_new_exaggeration = None
#: Button to select the exaggerations
btn_select_exaggerations = None
#: Button to digitize the exaggerations
btn_digitize_exag = None
# --- Feature removement --------------------------------------------------
# ---- Disconnected parts -------------------------------------------------
#: Button for removing disconnected parts in the plot. See the
#: :meth:`straditize.binary.DataReader.show_disconnected_parts` method
btn_show_disconnected_parts = None
#: A QLineEdit to set the `fromlast` keyword for the
#: :meth:`straditize.binary.DataReader.show_disconnected_parts` method
txt_fromlast = None
#: A QCheckBox to enable and disable the `fromlast` keyword in the
#: :meth:`straditize.binary.DataReader.show_disconnected_parts` method
cb_fromlast = None
#: A QLineEdit to set the `from0` keyword for the
#: :meth:`straditize.binary.DataReader.show_disconnected_parts` method
txt_from0 = None
#: A QCheckBox to enable and disable the `from0` keyword in the
#: :meth:`straditize.binary.DataReader.show_disconnected_parts` method
cb_from0 = None
# ---- Cross column features ----------------------------------------------
#: A button to
#: :meth:`~straditize.binary.DataReader.show_cross_column_features`
btn_show_cross_column = None
#: A QLineEdit to select the minimum pixels (`min_px`) for a cross column
#: feature
txt_cross_column_px = None
# ---- Small features -----------------------------------------------------
#: A button to :meth:`~straditize.binary.DataReader.show_small_parts`
btn_show_small_parts = None
#: A QLineEdit to set the size for small parts
txt_max_small_size = None
#: A button to highlight small selections using the
#: :meth:`straditize.label_selection.LabelSelection.highlight_small_selections`
#: method
btn_highlight_small_selection = None
#: A QLineEdit to set the maximal size for
#: :attr:`highlighting small features <btn_highlight_small_selection>`
txt_min_highlight = None
# ---- Parts at column ends -----------------------------------------------
#: A button to show the parts that touch the column end
btn_show_parts_at_column_ends = None
# ---- Line detection -----------------------------------------------------
#: LineEditor for specifying the fraction of vertical and horizontal lines
txt_line_fraction = None
#: A QSpinBox to select the minimum linewidth
sp_min_lw = None
#: A QSpinBox to select the maximum linewidth
sp_max_lw = None
#: QCheckBox to enable and disable the maximum linewidth as a criterion
cb_max_lw = None
#: Button for removing vertical lines
btn_remove_vlines = None
#: button for removing horizontal lines
btn_remove_hlines = None
#: Button for removing y-axes
btn_remove_yaxes = None
#: button for removing x-axes
btn_remove_xaxes = None
# --- Digitizing ----------------------------------------------------------
#: Button for digitizing the diagram
btn_digitize = None
#: Line edit for setting the tolerance for bars
txt_tolerance = None
# --- Samples -------------------------------------------------------------
#: A button to find the samples with the
#: :meth:`straditize.binary.DataReader.find_samples` method
btn_find_samples = None
#: A QLinEdit to specify the minimum length of a potential sample to be
#: included in the sample finding algorithm (see :attr:`btn_find_samples`)
txt_min_len = None
#: A QLinEdit to specify the maximum length of a potential sample to be
#: included in the sample finding algorithm (see :attr:`btn_find_samples`)
txt_max_len = None
#: A QSpinBox to set the minimum distance between to samples in the
#: sample finding algorithm
sp_pixel_tol = None
#: A button to load samples from a file
btn_load_samples = None
#: A button to edit the samples (see the
#: :meth:`straditize.straditizer.Straditizer.marks_for_samples` and
#: :meth:`straditize.straditizer.Straditizer.marks_for_samples_sep`)
btn_edit_samples = None
#: A button to reset the samples
btn_reset_samples = None
#: A QCheckBox to edit the samples in a separate figure and not inside the
#: original diagram
cb_edit_separate = None
#: A QLineEdit to specify the number or rows in a plot for editing the
#: samples in a separate figure (see :attr:`btn_edit_samples` and
#: :attr:`cb_edit_separate`)
txt_edit_rows = None
# --- Occurences ----------------------------------------------------------
#: A button to select occurences in the data part (see the
#: :meth:`enable_occurences_selection` method)
btn_select_occurences = None
#: A button to edit the occurences with the
#: :meth:`straditize.straditizer.Straditizer.marks_for_occurences` method
btn_edit_occurences = None
#: A QLineEdit to set the value for occurences in the final data
txt_occurences_value = None
#: A QCheckBox to remove the occurences in the plot after selection
cb_remove_occurences = None
#: A :class:`BarSplitter` to split too long bars
tree_bar_split = None
#: A QComboBox to select whether to fill the :attr:`tree_bar_split` with
#: too long, overlapping, or all bars
cb_split_source = None
_change_reader = True
digitize_item = None
@property
def selection_toolbar(self):
return self.straditizer_widgets.selection_toolbar
@docstrings.dedent
def __init__(self, straditizer_widgets, item):
"""
Parameters
----------
%(StraditizerControlBase.init_straditizercontrol.parameters)s
"""
# ---------------------------------------------------------------------
# --------------------------- Buttons ---------------------------------
# ---------------------------------------------------------------------
self.btn_select_data = QPushButton('Select data part')
self.btn_select_data.setToolTip(
'Create marks for selecting the data in the image')
self.cb_reader_type = cb = QComboBox()
cb.setEditable(False)
cb.addItems(reader_types)
cb.setCurrentIndex(0)
self.btn_init_reader = QPushButton('Convert image')
self.btn_init_reader.setToolTip(
'Convert the image to a binary image that can be read')
self.txt_column_thresh = QLineEdit()
self.txt_column_thresh.setValidator(QDoubleValidator(0, 100, 10))
self.txt_column_thresh.setToolTip(
'The fraction between 0 and 100 that has to be covered for a '
'valid column start. Set this to a low value, e.g. 1, if you have '
'columns with only a little bit of data.')
self.txt_column_thresh.setText('10')
self.btn_column_starts = QPushButton('Column starts')
self.btn_column_starts.setToolTip(
'Modify the column starts in the diagram')
self.btn_column_ends = QPushButton('ends')
self.btn_column_ends.setToolTip(
'Modify the column ends in the diagram')
self.btn_reset_columns = QPushButton('Reset')
self.btn_reset_columns.setToolTip(
'Reset the column starts')
self.btn_align_vertical = QPushButton('Align columns')
self.btn_align_vertical.setToolTip('Align the columns vertically')
# column specific readers
self.cb_readers = QComboBox()
self.cb_readers.setEditable(False)
self.btn_new_child_reader = QPushButton('+')
self.txt_exag_factor = QLineEdit()
self.txt_exag_factor.setValidator(QDoubleValidator(1, 100, 4))
self.txt_exag_factor.setText('10')
self.txt_exag_factor.setPlaceholderText('1-100')
self.txt_exag_factor.setToolTip(
'The factor that the data is exaggerated.')
self.cb_exag_reader_type = cb = QComboBox()
cb.setEditable(False)
cb.addItems(reader_types)
cb.setCurrentIndex(0)
self.btn_new_exaggeration = QPushButton('+')
self.btn_select_exaggerations = QPushButton('Select exaggerations')
self.btn_select_exaggerations.setToolTip(
'Select the features that represent the exaggerations')
self.txt_exag_percentage = QLineEdit()
self.txt_exag_percentage.setText('5')
self.txt_exag_percentage.setValidator(QDoubleValidator(0, 100, 4))
self.txt_exag_percentage.setPlaceholderText('0-100%')
self.txt_exag_percentage.setToolTip(
'The percentage of the column width under which the exaggerated '
'digitized result shall be used.')
self.txt_exag_absolute = QLineEdit()
self.txt_exag_absolute.setText('8')
self.txt_exag_absolute.setValidator(QIntValidator(0, 100000))
self.txt_exag_absolute.setPlaceholderText('1,2,3,... px')
self.txt_exag_absolute.setToolTip(
'The absolute pixel value under which the exaggerated '
'digitized result shall be used.')
self.btn_digitize_exag = QPushButton('Digitize exaggerations')
self.btn_digitize_exag.setToolTip('Digitize the exaggerations')
self.btn_show_disconnected_parts = QPushButton('Disconnected features')
self.btn_show_disconnected_parts.setToolTip(
'Highlight the disconnected features in the binary image, i.e. '
'features that are not associated with the data and might be '
'picture artifacts')
self.txt_fromlast = QLineEdit()
self.txt_fromlast.setValidator(QIntValidator())
self.txt_fromlast.setText('5')
self.txt_fromlast.setToolTip(
'Number of pixels after which a feature is regarded as '
'disconnected from the previous feature.')
self.cb_fromlast = QCheckBox('from previous feature by')
self.cb_fromlast.setChecked(True)
self.txt_from0 = QLineEdit()
self.txt_from0.setValidator(QIntValidator(0, 100000))
self.txt_from0.setText('10')
self.txt_from0.setToolTip(
'Number of pixels after which a feature is regarded as '
'disconnected from the column start.')
self.cb_from0 = QCheckBox('from column start by')
self.cb_from0.setChecked(True)
self.btn_show_cross_column = QPushButton('Cross column features')
self.btn_show_cross_column.setToolTip(
'Highlight features that cross multiple columns.')
self.txt_cross_column_px = QLineEdit()
self.txt_cross_column_px.setValidator(QIntValidator(0, 100000))
self.txt_cross_column_px.setText('50')
self.txt_cross_column_px.setToolTip(
'The number of pixels that must be in any of the columns')
self.btn_show_small_parts = QPushButton(
'Small features')
self.btn_show_small_parts.setToolTip(
'Remove features smaller than 6 pixels')
self.txt_max_small_size = QLineEdit()
self.txt_max_small_size.setValidator(QIntValidator())
self.txt_max_small_size.setText('6')
self.btn_highlight_small_selection = QPushButton(
'Highlight selection smaller than')
self.btn_highlight_small_selection.setCheckable(True)
self.txt_min_highlight = QLineEdit()
self.txt_min_highlight.setValidator(QIntValidator())
self.txt_min_highlight.setText('20')
self.btn_show_parts_at_column_ends = QPushButton(
'Features at column ends')
self.btn_show_parts_at_column_ends.setToolTip(
'Highlight the features in the binary image, that touch the end '
'of the corresponding column')
self.txt_line_fraction = QLineEdit()
self.txt_line_fraction.setValidator(QDoubleValidator(0.0, 100.0, 5))
self.txt_line_fraction.setText('30.0')
self.txt_line_fraction.setToolTip(
'The percentage that shall be used to identify a straight line')
self.sp_min_lw = QSpinBox()
self.sp_min_lw.setValue(1)
self.sp_min_lw.setMaximum(10000)
self.sp_min_lw.setToolTip(
'Set the minimal width for selected vertical or horizontal lines')
self.cb_max_lw = QCheckBox('Maximum line width')
self.sp_max_lw = QSpinBox()
self.sp_max_lw.setMaximum(10000)
self.sp_max_lw.setValue(20)
self.sp_max_lw.setToolTip(
'Set the maximal width for selected vertical or horizontal lines')
self.sp_max_lw.setEnabled(False)
self.btn_remove_vlines = QPushButton('vertical lines')
self.btn_remove_vlines.setToolTip(
'Remove vertical lines, i.e. y-axes')
self.btn_remove_yaxes = QPushButton('y-axes')
self.btn_remove_yaxes.setToolTip(
'Recognize and remove vertical axes in each of the plots')
self.btn_remove_hlines = QPushButton('horizontal lines')
self.btn_remove_hlines.setToolTip(
'Remove horizonal lines, i.e. lines parallel to the x-axis')
self.btn_remove_xaxes = QPushButton('x-axes')
self.btn_remove_xaxes.setToolTip(
'Recognize and remove x-axes at bottom and top of the data image')
self.btn_digitize = QPushButton('Digitize')
self.btn_digitize.setToolTip('Digitize the binary file')
self.btn_find_samples = QPushButton('Find samples')
self.btn_find_samples.setToolTip(
'Estimate positions of the samples in the diagram')
self.btn_load_samples = QPushButton('Load samples')
self.btn_load_samples.setToolTip(
'Load the sample locations from a CSV file')
self.btn_edit_samples = QPushButton('Edit samples')
self.btn_edit_samples.setToolTip(
'Modify and edit the samples')
self.btn_reset_samples = QPushButton('Reset')
self.btn_reset_samples.setToolTip('Reset the samples')
self.cb_edit_separate = QCheckBox('In separate figure')
self.cb_edit_separate.setToolTip(
'Edit the samples in a separate figure where you have one '
'plot for each column')
self.txt_edit_rows = QLineEdit()
self.txt_edit_rows.setValidator(QIntValidator(1, 1000))
self.txt_edit_rows.setToolTip(
'The number of plot rows in the editing figure?')
self.txt_edit_rows.setText('3')
self.txt_edit_rows.setEnabled(False)
self.txt_min_len = QLineEdit()
self.txt_min_len.setToolTip(
'Minimum length of a potential sample to be included')
self.txt_max_len = QLineEdit()
self.txt_max_len.setText('8')
self.txt_max_len.setToolTip(
'Maximum length of a potential sample to be included')
self.sp_pixel_tol = QSpinBox()
self.sp_pixel_tol.setMaximum(10000)
self.sp_pixel_tol.setValue(5)
self.sp_pixel_tol.setToolTip(
'Minimum distance between two samples in pixels')
self.btn_select_occurences = QPushButton('Select occurences')
self.btn_select_occurences.setToolTip(
'Select where a measurement was reported but without an '
'associated value')
self.btn_edit_occurences = QPushButton('Edit occurences')
self.btn_select_occurences.setToolTip(
'Edit the locations of occurences')
self.txt_occurences_value = QLineEdit()
self.txt_occurences_value.setText('-9999')
self.txt_occurences_value.setToolTip(
'Enter the value that shall be used for an occurence in the '
'final output data')
self.txt_occurences_value.setValidator(QDoubleValidator())
self.cb_remove_occurences = QCheckBox('Remove on apply')
self.cb_remove_occurences.setChecked(False)
self.cb_remove_occurences.setToolTip(
'Remove the selected features in the plot when clicking the '
'<i>apply</i> button.')
self.cb_remove_occurences.setVisible(False)
self.tree_bar_split = BarSplitter(straditizer_widgets)
self.cb_split_source = QComboBox()
self.cb_split_source.addItems(
['too long bars', 'overlapping bars', 'all bars'])
self.widgets2disable = [
self.btn_column_starts, self.btn_column_ends,
self.cb_readers, self.btn_new_child_reader,
self.txt_exag_factor, self.cb_exag_reader_type,
self.txt_exag_percentage, self.txt_exag_absolute,
self.btn_digitize_exag,
self.btn_new_exaggeration, self.btn_select_exaggerations,
self.btn_select_data, self.btn_remove_hlines,
self.btn_reset_columns, self.btn_reset_samples,
self.btn_remove_xaxes, self.btn_remove_yaxes,
self.btn_remove_vlines, self.txt_line_fraction,
self.sp_max_lw, self.sp_min_lw,
self.btn_show_disconnected_parts, self.txt_fromlast,
self.btn_show_cross_column, self.txt_cross_column_px,
self.btn_show_parts_at_column_ends,
self.btn_show_small_parts, self.txt_max_small_size,
self.btn_align_vertical,
self.btn_edit_samples, self.btn_find_samples,
self.btn_load_samples,
self.btn_select_occurences, self.btn_edit_occurences,
self.txt_occurences_value, self.cb_split_source
]
self.init_reader_kws = {}
self.init_straditizercontrol(straditizer_widgets, item)
# ---------------------------------------------------------------------
# --------------------------- Connections -----------------------------
# ---------------------------------------------------------------------
self.btn_select_data.clicked.connect(lambda: self.select_data_part())
self.btn_column_starts.clicked.connect(self.select_column_starts)
self.btn_column_ends.clicked.connect(self.modify_column_ends)
self.btn_reset_columns.clicked.connect(self.reset_column_starts)
self.cb_reader_type.currentTextChanged.connect(
self.toggle_txt_tolerance)
self.btn_init_reader.clicked.connect(self.init_reader)
self.btn_digitize.clicked.connect(self.digitize)
self.btn_digitize_exag.clicked.connect(self.digitize_exaggerations)
self.btn_digitize.clicked.connect(self.straditizer_widgets.refresh)
self.btn_remove_yaxes.clicked.connect(self.remove_yaxes)
self.btn_remove_xaxes.clicked.connect(self.remove_xaxes)
self.btn_remove_hlines.clicked.connect(self.remove_hlines)
self.btn_remove_vlines.clicked.connect(self.remove_vlines)
self.btn_show_disconnected_parts.clicked.connect(
self.show_disconnected_parts)
self.btn_show_parts_at_column_ends.clicked.connect(
self.show_parts_at_column_ends)
self.btn_align_vertical.clicked.connect(self.align_vertical)
self.btn_find_samples.clicked.connect(self.find_samples)
self.btn_load_samples.clicked.connect(self.load_samples)
self.btn_edit_samples.clicked.connect(self.edit_samples)
self.btn_reset_samples.clicked.connect(self.reset_samples)
self.btn_show_small_parts.clicked.connect(self.show_small_parts)
self.txt_max_small_size.textChanged.connect(
self._update_btn_show_small_parts)
self.txt_min_highlight.textChanged.connect(
self._update_btn_highlight_small_selection)
self.btn_highlight_small_selection.toggled.connect(
self.toggle_btn_highlight_small_selection)
self.cb_from0.stateChanged.connect(self.toggle_txt_from0)
self.cb_fromlast.stateChanged.connect(self.toggle_txt_fromlast)
self.btn_show_cross_column.clicked.connect(
self.show_cross_column_features)
self.btn_new_child_reader.clicked.connect(
self.enable_col_selection_for_new_reader)
self.cb_readers.currentTextChanged.connect(self.change_reader)
self.btn_new_exaggeration.clicked.connect(self.init_exaggerated_reader)
self.btn_select_exaggerations.clicked.connect(
self.select_exaggerated_features)
self.cb_edit_separate.stateChanged.connect(self.toggle_txt_edit_rows)
self.cb_max_lw.stateChanged.connect(self.toggle_sp_max_lw)
self.btn_select_occurences.clicked.connect(
self.enable_occurences_selection)
self.btn_edit_occurences.clicked.connect(self.edit_occurences)
self.txt_occurences_value.textChanged.connect(
self.set_occurences_value)
self.cb_split_source.currentIndexChanged.connect(
self.toggle_bar_split_source)
# disable warning if bars cannot be separated
warnings.filterwarnings('ignore', 'Could not separate bars',
UserWarning)
[docs] def refresh(self):
super(DigitizingControl, self).refresh()
self.tree_bar_split.refresh()
self.bar_split_child.setHidden(not self.tree_bar_split.filled)
if self.tree_bar_split.filled:
nsplit = sum(map(len,
self.straditizer.data_reader._splitted.values()))
self.cb_split_source.setItemText(0, '%i too long bar%s' % (
nsplit, 's' if nsplit > 1 else '')
)
noverlap = sum(map(len,
self.tree_bar_split.get_overlapping_bars()))
self.cb_split_source.setItemText(1, '%i overlapping bar%s' % (
noverlap, 's' if noverlap > 1 else ''))
else:
self.cb_split_source.setItemText(0, 'too long bars')
self.cb_split_source.setItemText(1, 'overlapping bars')
self.maybe_show_btn_reset_columns()
self.maybe_show_btn_reset_samples()
for w in [self.btn_init_reader, self.btn_digitize]:
w.setEnabled(self.should_be_enabled(w))
self.enable_or_disable_btn_highlight_small_selection()
if (self.straditizer is not None and
self.straditizer.data_reader is not None):
self.cb_reader_type.setCurrentText(get_reader_name(
self.straditizer.data_reader))
reader = self.straditizer.data_reader.exaggerated_reader
self.cb_exag_reader_type.setCurrentText(get_reader_name(
reader or self.straditizer.data_reader))
if reader is not None:
self.txt_exag_factor.setText(str(reader.is_exaggerated))
self.txt_occurences_value.setText(
str(self.straditizer.data_reader.occurences_value))
self.fill_cb_readers()
[docs] def toggle_sp_max_lw(self, state):
"""Toggle :attr:`sp_max_lw` based on :attr:`cb_max_lw`
"""
self.sp_max_lw.setEnabled(state == Qt.Checked)
[docs] def toggle_txt_fromlast(self, state):
"""Toggle :attr:`txt_fromlast` based on :attr:`cb_fromlast`
"""
self.txt_fromlast.setEnabled(state == Qt.Checked)
[docs] def toggle_txt_edit_rows(self, state):
"""Toggle :attr:`txt_edit_rows` based on :attr:`cb_edit_separate`"""
self.txt_edit_rows.setEnabled(state == Qt.Checked)
[docs] def toggle_txt_from0(self, state):
"""Toggle :attr:`txt_from0` based on :attr:`cb_from0`"""
self.txt_from0.setEnabled(state == Qt.Checked)
[docs] def enable_or_disable_btn_highlight_small_selection(self):
"""Enable the :attr:`btn_highlight_small_selection` during a selection
"""
enable = self.should_be_enabled(self.btn_highlight_small_selection)
self.btn_highlight_small_selection.setEnabled(enable)
self.btn_highlight_small_selection.setChecked(
enable and bool(self.selection_toolbar.data_obj._ellipses))
[docs] def maybe_show_btn_reset_columns(self):
"""Show the :attr:`btn_reset_columns` if the column starts are set"""
show = self.should_be_enabled(self.btn_reset_columns)
self.btn_reset_columns.setVisible(show)
self.btn_column_ends.setVisible(show)
[docs] def maybe_show_btn_reset_samples(self):
"""Show the :attr:`btn_reset_samples` if the samples are set"""
self.btn_reset_samples.setVisible(
self.should_be_enabled(self.btn_reset_samples))
[docs] def update_tolerance(self, s):
"""Set the readers :attr:`~straditizer.binary.BarDataReader.tolerance`
Parameters
----------
s: str or int
The tolerance for the
:attr:`straditizer.binary.BarDataReader.tolerance` attribute
"""
if (self.straditizer is not None and
self.straditizer.data_reader is not None and
hasattr(self.straditizer.data_reader, 'tolerance')):
self.straditizer.data_reader.tolerance = int(s or 0)
[docs] def toggle_txt_tolerance(self, s):
"""Set the visibility of the :attr:`txt_tolerance` based on the reader
Parameters
----------
s: str
The reader name. If there is *bars* in `s`, then the
:attr:`txt_tolerance` is displayed"""
enable = 'bars' in s
try:
self.txt_tolerance
except RuntimeError:
self.txt_tolerance = None
if enable:
if self.txt_tolerance is None:
self.tolerance_child = QTreeWidgetItem(0)
self.lbl_tolerance = QLabel('Tolerance:')
self.txt_tolerance = QLineEdit()
validator = QIntValidator()
validator.setBottom(0)
self.txt_tolerance.setValidator(validator)
self.txt_tolerance.setEnabled(False)
self.txt_tolerance.textChanged.connect(self.update_tolerance)
hbox = QHBoxLayout()
hbox.addWidget(self.lbl_tolerance)
hbox.addWidget(self.txt_tolerance)
self.digitize_item.addChild(self.tolerance_child)
w = QWidget()
w.setLayout(hbox)
self.tree.setItemWidget(self.tolerance_child, 0, w)
if self.txt_tolerance is not None:
self.txt_tolerance.setEnabled(enable)
self._set_txt_tolerance_tooltip()
# use the value of the reader by default
if (self.straditizer is not None and
self.straditizer.data_reader is not None):
self.txt_tolerance.setText(str(getattr(
self.straditizer.data_reader, 'tolerance', '')))
# otherwise use a default value of 10 for roundedtt bars
elif enable and int(self.txt_tolerance.text() or 2) == 2:
self.txt_tolerance.setText('10')
# and 2 for rectangular bars
elif (enable and s == 'bars' and
int(self.txt_tolerance.text() or 10) == 10):
self.txt_tolerance.setText('2')
# and nothing else
elif self.txt_tolerance is not None:
self.digitize_item.takeChild(self.digitize_item.indexOfChild(
self.tolerance_child))
del self.tolerance_child, self.txt_tolerance
def _set_txt_tolerance_tooltip(self):
enable = self.txt_tolerance and self.txt_tolerance.isEnabled()
if enable:
self.txt_tolerance.setToolTip(
'Enter the difference in height to distinguish to adjacent '
'bars')
else:
self.txt_tolerance.setToolTip('Not implemented.')
[docs] def should_be_enabled(self, w):
if self.straditizer is None:
return False
elif w in [self.btn_select_data]:
return True
elif w is self.btn_init_reader:
if (self.straditizer.data_xlim is None or
self.straditizer.data_ylim is None):
return False
elif (self.straditizer.data_xlim is None or
self.straditizer.data_ylim is None or
self.straditizer.data_reader is None):
return False
elif (w is self.btn_highlight_small_selection and
not self.selection_toolbar._selecting):
return False
# widgets depending on that the columns have been set already
elif (self.straditizer.data_reader._column_starts is None and
w in [self.btn_reset_columns, self.btn_align_vertical,
self.btn_column_ends,
self.cb_readers, self.btn_new_child_reader,
self.cb_exag_reader_type, self.btn_new_exaggeration,
self.txt_exag_factor, self.txt_exag_absolute,
self.txt_exag_percentage, self.btn_digitize_exag,
self.btn_show_disconnected_parts, self.txt_fromlast,
self.cb_fromlast, self.txt_from0, self.cb_from0,
self.btn_show_cross_column, self.txt_cross_column_px,
self.btn_remove_yaxes, self.btn_select_occurences,
self.btn_edit_occurences,
self.btn_show_parts_at_column_ends, self.btn_digitize]):
return False
elif (self.straditizer.data_reader.exaggerated_reader is None and
w in [self.txt_exag_percentage, self.txt_exag_absolute,
self.btn_digitize_exag, self.btn_select_exaggerations]):
return False
elif (w in [self.txt_exag_percentage, self.txt_exag_absolute,
self.btn_digitize_exag] and
not self.straditizer.data_reader.exaggerated_reader.binary.any()
):
return False
elif (self.straditizer.data_reader.exaggerated_reader is not None and
w in [self.cb_exag_reader_type, self.btn_new_exaggeration,
self.txt_exag_factor]):
return False
elif (w is self.btn_reset_samples and
self.straditizer.data_reader._sample_locs is None):
return False
elif (w in [self.btn_find_samples, self.btn_edit_samples,
self.btn_load_samples, self.btn_digitize_exag] and
self.straditizer.data_reader.full_df is None):
return False
elif (w is self.btn_show_small_parts and
not self.txt_max_small_size.text()):
return False
elif w is self.sp_max_lw and not self.cb_max_lw.isChecked():
return False
return True
[docs] def reset_column_starts(self):
"""Reset the column starts
Reset the column starts by calling the
:meth:`straditize.binary.DataReader.reset_column_starts` method"""
if self._ask_for_column_modification():
self.straditizer.data_reader.reset_column_starts()
self.maybe_show_btn_reset_columns()
self.refresh()
[docs] def reset_samples(self):
"""Reset the samples
Reset the samples by calling the
:meth:`straditize.binary.DataReader.reset_samples` method"""
self.straditizer.data_reader.reset_samples()
self.maybe_show_btn_reset_samples()
self.refresh()
[docs] def set_occurences_value(self, value):
"""Set the :attr:`~straditize.binary.DataReader.occurences_value`
Set the :attr:`straditize.binary.DataReader.occurence_value` of the
data_reader with the given value
Parameters
----------
value: float
The value to use for occurences"""
try:
value = float(value)
except (ValueError, TypeError):
return
self.straditizer.data_reader.occurences_value = value
[docs] def setup_children(self, item):
"""Set up the child items for a topLevelItem in the control tree"""
self.add_info_button(item, 'straditize_steps.rst')
# 0: start parts before creating the reader
vbox_start = QVBoxLayout()
hbox_start = QHBoxLayout()
hbox_start.addWidget(self.btn_select_data)
hbox_start.addWidget(self.cb_reader_type)
vbox_start.addLayout(hbox_start)
w = QWidget()
w.setLayout(vbox_start)
child = QTreeWidgetItem(0)
child.setText(0, 'Reader initialization')
item.addChild(child)
child2 = QTreeWidgetItem(0)
child.addChild(child2)
self.tree.setItemWidget(child2, 0, w)
self.tree.expandItem(child)
self.add_info_button(child2, 'select_data_part.rst',
connections=[self.btn_select_data])
# init reader
child4 = QTreeWidgetItem(0)
child.addChild(child4)
self.tree.setItemWidget(child4, 0, self.btn_init_reader)
self.add_info_button(child4, 'select_reader.rst',
connections=[self.btn_init_reader])
child = QTreeWidgetItem(0)
item.addChild(child)
w = QGroupBox('Column separations')
vbox_cols = QVBoxLayout()
hbox_thresh = QHBoxLayout()
hbox_thresh.addWidget(QLabel('Threshold:'))
hbox_thresh.addWidget(self.txt_column_thresh)
hbox_thresh.addWidget(QLabel('%'))
vbox_cols.addLayout(hbox_thresh)
hbox_cols = QHBoxLayout()
hbox_cols.addWidget(self.btn_column_starts)
hbox_cols.addWidget(self.btn_column_ends)
hbox_cols.addWidget(self.btn_reset_columns)
vbox_cols.addLayout(hbox_cols)
vbox_cols.addWidget(self.btn_align_vertical)
w.setLayout(vbox_cols)
self.tree.setItemWidget(child, 0, w)
self.button = self.straditizer_widgets.add_info_button(
child, 'select_column_starts.rst',
connections=[self.btn_column_starts])
# 1: column specific readers
self.current_reader_item = child = QTreeWidgetItem(0)
child.setText(0, 'Current reader')
item.addChild(child)
child2 = QTreeWidgetItem(0)
child.addChild(child2)
self.add_info_button(child, 'child_readers.rst',
connections=[self.btn_new_child_reader])
w = QWidget()
hbox = QHBoxLayout()
self.cb_readers.setSizeAdjustPolicy(QComboBox.AdjustToContents)
hbox.addWidget(self.cb_readers)
hbox.addStretch(0)
hbox.addWidget(self.btn_new_child_reader)
w.setLayout(hbox)
self.tree.setItemWidget(child2, 0, w)
# 2: Exaggerations readers
child = QTreeWidgetItem(0)
child.setText(0, 'Exaggerations')
item.addChild(child)
self.add_info_button(child, 'exaggerations.rst',
connections=[self.btn_new_exaggeration])
child2 = QTreeWidgetItem(0)
child.addChild(child2)
w = QWidget()
vbox = QVBoxLayout()
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Exaggeration factor:'))
hbox.addWidget(self.txt_exag_factor)
vbox.addLayout(hbox)
hbox = QHBoxLayout()
hbox.addWidget(self.cb_exag_reader_type)
hbox.addStretch(0)
hbox.addWidget(self.btn_new_exaggeration)
vbox.addLayout(hbox)
vbox.addWidget(self.btn_select_exaggerations)
digitizer_layout = QGridLayout()
digitizer_layout.addWidget(QLabel('Percentage:'), 0, 0)
digitizer_layout.addWidget(self.txt_exag_percentage, 0, 1)
digitizer_layout.addWidget(QLabel('%'), 0, 2)
digitizer_layout.addWidget(QLabel('Absolute:'), 1, 0)
digitizer_layout.addWidget(self.txt_exag_absolute, 1, 1)
digitizer_layout.addWidget(QLabel('px'), 1, 2)
digitizer_layout.addWidget(self.btn_digitize_exag, 2, 0, 1, 3)
vbox.addLayout(digitizer_layout)
w.setLayout(vbox)
self.tree.setItemWidget(child2, 0, w)
# 3: occurences
self.occurences_child = child = QTreeWidgetItem(0)
child.setText(0, 'Occurences')
item.addChild(child)
child2 = QTreeWidgetItem(0)
child.addChild(child2)
w = QWidget()
hbox = QHBoxLayout()
hbox.addWidget(self.btn_select_occurences)
hbox.addWidget(self.cb_remove_occurences)
w.setLayout(hbox)
self.tree.setItemWidget(child2, 0, w)
child2 = QTreeWidgetItem(0)
child.addChild(child2)
w = QWidget()
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Occurence value:'))
hbox.addWidget(self.txt_occurences_value)
w.setLayout(hbox)
self.tree.setItemWidget(child2, 0, w)
child2 = QTreeWidgetItem(0)
child.addChild(child2)
self.tree.setItemWidget(child2, 0, self.btn_edit_occurences)
self.add_info_button(child, 'occurences.rst',
connections=[self.btn_select_occurences,
self.btn_edit_occurences])
# 4: parts to remove features from the binary image
self.remove_child = child = QTreeWidgetItem(0)
child.setText(0, 'Remove features')
item.addChild(child)
self.add_info_button(child, 'removing_features.rst')
# disconnected parts
dc_child = QTreeWidgetItem(0)
child.addChild(dc_child)
self.tree.setItemWidget(dc_child, 0, self.btn_show_disconnected_parts)
self.add_info_button(dc_child, 'remove_disconnected_parts.rst',
connections=[self.btn_show_disconnected_parts])
dc_child2 = QTreeWidgetItem(0)
grid = QGridLayout()
grid.addWidget(self.cb_fromlast, 0, 0)
grid.addWidget(self.txt_fromlast, 0, 1)
grid.addWidget(QLabel('px'), 0, 2)
grid.addWidget(self.cb_from0, 1, 0)
grid.addWidget(self.txt_from0, 1, 1)
grid.addWidget(QLabel('px'), 1, 2)
w = QWidget()
w.setLayout(grid)
dc_child.addChild(dc_child2)
self.tree.setItemWidget(dc_child2, 0, w)
# parts at column ends
end_child = QTreeWidgetItem(0)
child.addChild(end_child)
self.tree.setItemWidget(end_child, 0,
self.btn_show_parts_at_column_ends)
self.add_info_button(end_child, 'remove_col_ends.rst',
connections=[self.btn_show_parts_at_column_ends])
# cross column features
cross_child = QTreeWidgetItem(0)
child.addChild(cross_child)
self.tree.setItemWidget(cross_child, 0, self.btn_show_cross_column)
self.add_info_button(cross_child, 'remove_cross_column.rst',
connections=[self.btn_show_cross_column])
cross_child2 = QTreeWidgetItem(0)
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Number of pixels:'))
hbox.addWidget(self.txt_cross_column_px)
hbox.addWidget(QLabel('px'))
w = QWidget()
w.setLayout(hbox)
cross_child.addChild(cross_child2)
self.tree.setItemWidget(cross_child2, 0, w)
# small parts
small_child = QTreeWidgetItem(0)
child.addChild(small_child)
self.tree.setItemWidget(small_child, 0, self.btn_show_small_parts)
self.add_info_button(small_child, 'remove_small_parts.rst',
connections=[self.btn_show_small_parts])
small_child2 = QTreeWidgetItem(0)
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Smaller than'))
hbox.addWidget(self.txt_max_small_size)
hbox.addWidget(QLabel('px'))
w = QWidget()
w.setLayout(hbox)
small_child.addChild(small_child2)
self.tree.setItemWidget(small_child2, 0, w)
# lines
self.remove_line_child = line_child = QTreeWidgetItem(0)
grid = QGridLayout()
grid.addWidget(self.btn_remove_xaxes, 0, 0)
grid.addWidget(self.btn_remove_yaxes, 0, 1)
grid.addWidget(self.btn_remove_hlines, 1, 0)
grid.addWidget(self.btn_remove_vlines, 1, 1)
w = QWidget()
w.setLayout(grid)
child.addChild(line_child)
self.tree.setItemWidget(line_child, 0, w)
self.add_info_button(line_child, 'remove_lines.rst',
connections=[self.btn_remove_vlines,
self.btn_remove_hlines,
self.btn_remove_yaxes,
self.btn_remove_xaxes])
w = QWidget()
line_child2 = QTreeWidgetItem(0)
layout = QGridLayout()
layout.addWidget(QLabel('Minimum fraction:'), 0, 0)
layout.addWidget(self.txt_line_fraction, 0, 1)
layout.addWidget(QLabel('%'), 0, 2)
layout.addWidget(QLabel('Minimum line width'), 1, 0)
layout.addWidget(self.sp_min_lw, 1, 1)
layout.addWidget(QLabel('px'), 1, 2)
layout.addWidget(self.cb_max_lw, 2, 0)
layout.addWidget(self.sp_max_lw, 2, 1)
layout.addWidget(QLabel('px'), 2, 2)
w.setLayout(layout)
line_child.addChild(line_child2)
self.tree.setItemWidget(line_child2, 0, w)
# highlight selection
highlight_child = QTreeWidgetItem(0)
child.addChild(highlight_child)
self.tree.setItemWidget(highlight_child, 0,
self.btn_highlight_small_selection)
highlight_child2 = QTreeWidgetItem(0)
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Smaller than'))
hbox.addWidget(self.txt_min_highlight)
hbox.addWidget(QLabel('px'))
w = QWidget()
w.setLayout(hbox)
highlight_child.addChild(highlight_child2)
self.tree.setItemWidget(highlight_child2, 0, w)
# 5: digitize button
self.digitize_item = child = QTreeWidgetItem(0)
item.addChild(child)
self.tree.setItemWidget(child, 0, self.btn_digitize)
self.add_info_button(child, 'digitize.rst',
connections=[self.btn_digitize])
# 6: bar splitter
self.bar_split_child = QTreeWidgetItem(0)
self.bar_split_child.setText(0, 'Split bars manually')
child2 = QTreeWidgetItem(0)
self.bar_split_child.addChild(child2)
self.tree.setItemWidget(child2, 0, self.cb_split_source)
child2 = QTreeWidgetItem(0)
self.bar_split_child.addChild(child2)
self.tree.setItemWidget(child2, 0, self.tree_bar_split)
item.addChild(self.bar_split_child)
self.bar_split_child.setHidden(not self.tree_bar_split.filled)
# 7: edit samples button
self.edit_samples_child = child = QTreeWidgetItem(0)
child.setText(0, 'Samples')
item.addChild(child)
self.add_info_button(child, 'samples.rst')
find_child = QTreeWidgetItem(0)
child.addChild(find_child)
self.tree.setItemWidget(find_child, 0, self.btn_find_samples)
self.add_info_button(find_child, 'find_samples.rst',
connections=[self.btn_find_samples])
find_child2 = QTreeWidgetItem(0)
samples_box = QGridLayout()
samples_box.addWidget(QLabel('Minimum length'), 0, 0)
samples_box.addWidget(QLabel('Maximum length'), 0, 1)
samples_box.addWidget(self.txt_min_len, 1, 0)
samples_box.addWidget(self.txt_max_len, 1, 1)
samples_box.addWidget(QLabel('Minimum distance'),
2, 0)
samples_box.addWidget(self.sp_pixel_tol, 2, 1)
samples_box.addWidget(QLabel('px'), 2, 2)
w = QWidget()
w.setLayout(samples_box)
find_child.addChild(find_child2)
self.tree.setItemWidget(find_child2, 0, w)
load_child = QTreeWidgetItem(0)
child.addChild(load_child)
self.tree.setItemWidget(load_child, 0, self.btn_load_samples)
self.add_info_button(load_child, 'load_samples.rst',
connections=[self.btn_load_samples])
edit_child = QTreeWidgetItem(0)
child.addChild(edit_child)
self.add_info_button(edit_child, 'edit_samples.rst',
connections=[self.btn_edit_samples])
hbox_cols = QHBoxLayout()
hbox_cols.addWidget(self.btn_edit_samples)
hbox_cols.addWidget(self.btn_reset_samples)
w = QWidget()
w.setLayout(hbox_cols)
self.tree.setItemWidget(edit_child, 0, w)
edit_child2 = QTreeWidgetItem(0)
samples_box = QGridLayout()
samples_box.addWidget(self.cb_edit_separate, 0, 0, 1, 2)
samples_box.addWidget(QLabel('Number of rows:'), 1, 0)
samples_box.addWidget(self.txt_edit_rows, 1, 1)
w = QWidget()
w.setLayout(samples_box)
edit_child.addChild(edit_child2)
self.tree.setItemWidget(edit_child2, 0, w)
[docs] def toggle_bar_split_source(self, i):
"""Fill the :attr:`tree_bar_split` based on the :attr:`cb_split_source`
Parameters
----------
i: int
The :meth:`BarSplitter.fill_table` is called with either
``'too-long'`` (if `i` is 0), ``'overlaps'`` (if `i` is 1) or
``'all'``"""
if i == 0:
self.tree_bar_split.fill_table('too-long')
elif i == 1:
self.tree_bar_split.fill_table('overlaps')
else:
self.tree_bar_split.fill_table('all')
[docs] def init_reader(self):
"""Initialize the reader
Initialize the data reader with the
:meth:`straditize.straditizer.Straditizer.init_reader` method"""
# make sure, the StackedReader is registered
import straditize.widgets.stacked_area_reader
kws = self.init_reader_kws.copy()
reader_type = self.cb_reader_type.currentText()
kws['reader_type'] = reader_type
if self.straditizer.data_reader is not None:
for reader in self.straditizer.data_reader.iter_all_readers:
reader.remove_plots()
self.straditizer.init_reader(**kws)
self.straditizer.show_full_image()
self.straditizer.draw_figure()
self.straditizer_widgets.refresh()
[docs] def init_exaggerated_reader(self):
"""Initialize the reader for exaggeration features"""
from straditize.binary import readers
reader_type = self.cb_exag_reader_type.currentText()
if reader_type == get_reader_name(self.straditizer.data_reader):
loader = None
else:
loader = readers[reader_type]
factor = float(self.txt_exag_factor.text())
self.straditizer.data_reader.create_exaggerations_reader(
factor, loader)
self.straditizer_widgets.refresh()
[docs] def select_exaggerated_features(self):
"""Enable the selection of exaggerated features"""
tb = self.selection_toolbar
self.apply_button.clicked.connect(self.finish_exaggerated_features)
reader = self.straditizer.data_reader
if reader.is_exaggerated:
reader = reader.non_exaggerated_reader
tb.data_obj = reader
tb.start_selection(
tb.labels, rgba=reader.image_array(), remove_on_apply=False)
tb.add_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.apply_button.setText('Select')
[docs] def finish_exaggerated_features(self):
"""Save the exaggerations in the exaggerations reader
This method finalizes the operation initialized by the
:meth:`select_exaggerated_features` by calling the
:meth:`straditize.binary.DataReader.mark_as_exaggerations` method"""
mask = self.selection_toolbar.data_obj.selected_part
self.straditizer.data_reader.mark_as_exaggerations(mask)
[docs] def fill_cb_readers(self):
"""Fill the :attr:`cb_readers` combo based on the current reader"""
self._change_reader = False
self.cb_readers.clear()
if self.straditizer is None or self.straditizer.data_reader is None:
return
reader = self.straditizer.data_reader
if reader.columns:
for child in chain([reader], reader.children):
name = get_reader_name(child) or child.__class__.__name__
exag = 'exag. ' if child.is_exaggerated else ''
self.cb_readers.addItem(
exag + name + ': Columns ' +
int_list2str(child.columns or []))
self._change_reader = True
[docs] def new_reader_for_selection(self, cls=None):
"""Create a new child reader for the selected columns
This method finishes the process started by
:meth:`enable_col_selection_for_new_reader`
Parameters
----------
cls: type
The subclass of the :class:`straditize.binary.DataReader` class
to use for the new reader. If None, a QInputDialog is opened and
we ask for a reader"""
reader = self.straditizer.data_reader
cols = sorted(reader._selected_cols)
if not cols:
raise ValueError("No columns selected!")
if not cls:
from straditize.binary import readers
current = get_reader_name(reader)
items = list(readers)
try: # put the current type at the front
items.insert(0, items.pop(items.index(current)))
except ValueError:
pass
name, ok = QInputDialog.getItem(
self.straditizer_widgets, 'Reader type',
'Select the reader type for the selected columns',
items)
if not ok:
return
cls = readers[name]
reader.new_child_for_cols(cols, cls)
self.straditizer_widgets.refresh()
[docs] def change_reader(self, txt):
"""Change the current parent reader
This changes the :attr:`straditize.straditizer.Straditizer.data_reader`
using the :meth:`straditize.binary.DataReader.set_as_parent` method
Parameters
----------
s: str
A string matching ``'Columns (\d.*)'``, where the numbers are the
columns of the reader to use"""
if not self._change_reader:
return
match = re.search('Columns (\d.*)', txt)
if not match:
return
cols = set(map(int, re.findall('\d+', match.group(1))))
old = self.straditizer.data_reader
children = chain([old], old.children)
filter_func = filter if txt.startswith('exag') else filterfalse
children = filter_func(lambda c: c.is_exaggerated, children)
new = next(child for child in children if cols <= set(child.columns))
new.set_as_parent()
self.straditizer.data_reader = new
self.straditizer_widgets.refresh()
[docs] def enable_col_selection_for_new_reader(self):
"""Start the selection process to get a new reader for specific cols"""
reader = self.straditizer.data_reader
reader.start_column_selection()
self.connect2apply(
lambda: self.new_reader_for_selection(),
reader.end_column_selection,
reader.draw_figure)
self.connect2cancel(
reader.end_column_selection,
reader.draw_figure)
[docs] def find_samples(self):
kws = dict(pixel_tol=self.sp_pixel_tol.value())
if self.txt_min_len.text().strip():
kws['min_len'] = int(self.txt_min_len.text())
if self.txt_max_len.text().strip():
kws['max_len'] = int(self.txt_max_len.text())
self.straditizer.data_reader.add_samples(
*self.straditizer.data_reader.find_samples(**kws))
self.straditizer_widgets.refresh()
[docs] def load_samples(self, fname=None):
"""Load the samples of a text file
This method askes for a filename to update the samples. The first
column in this file is taken as the sample locations. If the
y-axis translation is already done, the new data is assumed to be in
this transformed unit.
Parameters
----------
fname: str
The path to the file to use. If None, a QFileDialog is opened and
we ask for a name"""
if fname is None or not isinstance(fname, six.string_types):
fname = QFileDialog.getOpenFileName(
self.straditizer_widgets, 'samples',
self.straditizer_widgets.menu_actions._start_directory,
'CSV files (*.csv);;'
'Excel files (*.xls *.xlsx);;'
'Straditize projects (*.nc *.nc4 *.pkl);;'
'All files (*)'
)
if with_qt5: # the filter is passed as well
fname = fname[0]
if not fname:
return
base, ext = osp.splitext(fname)
if ext in ['.nc', '.nc4']:
with xr.open_dataset(fname) as ds:
df = self.straditizer.from_dataset(ds, plot=False).final_df
elif ext == '.pkl':
with open(fname, 'rb') as f:
df = pickle.load(f).final_df
elif ext in ['.xls', '.xlsx']:
df = pd.read_excel(fname, index_col=0)
else:
df = pd.read_csv(fname, index_col=0)
samples = df.index.values
try:
samples = self.straditizer.data2px_y(samples)
except ValueError:
pass
# HACK: truncate to the available data region
# This could be better solved through a dialog with the user...
maxy = len(self.reader._full_df) - 1
samples[samples < 0] = 0
samples[samples > maxy] = maxy
self.straditizer.data_reader.add_samples(
np.unique(samples.astype(int)))
self.straditizer_widgets.refresh()
[docs] def edit_samples(self):
"""Enable the sample editing
This method opens a
:class:`straditize.widgets.samples_table.MultiCrossMarksEditor` or a
:class:`straditize.widgets.samples_table.SingleCrossMarksEditor` to
edit the samples in the GUI. Depending on whether the
:attr:`cb_edit_separate` is checked or not, we use the
:meth:`straditize.straditizer.Straditizer.marks_for_samples_sep` or
:meth:`straditize.straditizer.Straditizer.marks_for_samples`
method."""
from psyplot_gui.main import mainwindow
from straditize.widgets.samples_table import (
MultiCrossMarksEditor, SingleCrossMarksEditor)
draw_sep = self.cb_edit_separate.isChecked()
ref = weakref.ref(self.straditizer)
if draw_sep:
fig, axes = self.straditizer.marks_for_samples_sep()
if mainwindow.figures: # using psyplot backend
fig_dock = fig.canvas.manager.window
stradi_dock = self.straditizer.ax.figure.canvas.manager.window
mainwindow.tabifyDockWidget(stradi_dock, fig_dock)
a = fig_dock.toggleViewAction()
if not a.isChecked():
a.trigger()
fig_dock.raise_()
self._samples_editor = editor = MultiCrossMarksEditor(
ref, axes=axes)
else:
self.straditizer.marks_for_samples()
self._samples_editor = editor = SingleCrossMarksEditor(ref)
editor.to_dock(
mainwindow, title='Samples editor')
editor.show_plugin()
editor.maybe_tabify()
editor.raise_()
# zoom to the first 3 samples
ncols = editor.table.model().columnCount() - 1
nrows = min(3, editor.table.model().rowCount())
if draw_sep:
editor.table.zoom_to_cells(
chain.from_iterable([i] * ncols for i in range(nrows)),
list(range(ncols)) * nrows)
self._draw_sep = draw_sep
self.connect2apply(
self._update_samples,
self._close_samples_fig, self.straditizer_widgets.refresh)
self.connect2cancel(
self.straditizer.remove_marks, self._close_samples_fig)
self.maybe_show_btn_reset_samples()
def _update_samples(self):
if self._draw_sep:
self.straditizer.update_samples_sep()
else:
self.straditizer.update_samples()
def _close_samples_fig(self):
import matplotlib.pyplot as plt
for l in self._samples_editor.table.model().lines:
try:
l.remove()
except ValueError:
pass
try:
fig = self._samples_fig()
except (AttributeError, RuntimeError):
self.straditizer.draw_figure()
else:
if fig is not None:
plt.close(fig.number)
del self._samples_fig
self._samples_editor.dock.close()
try:
self.straditizer.mark_added.disconnect(
self._samples_editor.table.model().load_new_marks)
except ValueError:
pass
try:
self.straditizer.mark_removed.disconnect(
self._samples_editor.table.model().remove_mark)
except ValueError:
pass
del self._samples_editor
try:
del self.straditizer._plotted_full_df
except AttributeError:
pass
[docs] def digitize(self):
"""Digitize the data
This method uses the :meth:`straditize.binary.DataReader.digitize`
method to digitize the data of the current reader"""
reader = self.reader
if self.txt_tolerance and self.txt_tolerance.isEnabled():
reader.tolerance = int(self.txt_tolerance.text())
if reader.is_exaggerated:
reader = reader.non_exaggerated_reader
reader.digitize()
pc = self.straditizer_widgets.plot_control.table
if pc.can_plot_full_df():
if pc.get_full_df_lines():
pc.remove_full_df_plot()
pc.plot_full_df()
pc.refresh()
[docs] def digitize_exaggerations(self):
"""Digitize the data
This method uses the
:meth:`straditize.binary.DataReader.digitize_exaggerated` method to
digitize the exaggerated data of the current reader and merge it into
the data obtained by the :meth:`digitize` method."""
reader = self.reader
fraction = float(self.txt_exag_percentage.text().strip() or 0) / 100.
absolute = int(self.txt_exag_absolute.text().strip() or 0)
reader.digitize_exaggerated(fraction=fraction, absolute=absolute)
[docs] def remove_xaxes(self):
"""Remove x-axes in the plot
This method uses the
:meth:`straditize.binary.DataReader.recognize_xaxes` method to identify
x-axes in the plot"""
fraction = float(self.txt_line_fraction.text().strip() or 0) / 100.
max_lw = self.sp_max_lw.value() if self.cb_max_lw.isChecked() else None
min_lw = int(self.sp_min_lw.text().strip() or 1)
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.recognize_xaxes(fraction=fraction, min_lw=min_lw,
max_lw=max_lw)
self.apply_button.clicked.connect(
lambda: self.reader.set_hline_locs_from_selection())
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.set_row_wand_mode()
self.straditizer.draw_figure()
[docs] def remove_hlines(self):
"""Remove horizontal lines
This method uses the
:meth:`straditize.binary.DataReader.recognize_hlines` method to
identify horizontal lines in the plot"""
fraction = float(self.txt_line_fraction.text().strip() or 0) / 100.
max_lw = self.sp_max_lw.value() if self.cb_max_lw.isChecked() else None
min_lw = int(self.sp_min_lw.text().strip() or 1)
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.recognize_hlines(fraction=fraction, min_lw=min_lw,
max_lw=max_lw)
self.apply_button.clicked.connect(
lambda: self.reader.set_hline_locs_from_selection())
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.set_row_wand_mode()
self.straditizer.draw_figure()
[docs] def remove_yaxes(self):
"""Remove y-axes in the plot
This method uses the
:meth:`straditize.binary.DataReader.recognize_yaxes` method to identify
y-axes in the plot"""
fraction = float(self.txt_line_fraction.text().strip() or 0) / 100.
max_lw = self.sp_max_lw.value() if self.cb_max_lw.isChecked() else None
min_lw = self.sp_min_lw.value()
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.recognize_yaxes(fraction=fraction, min_lw=min_lw,
max_lw=max_lw)
self.apply_button.clicked.connect(
lambda: self.reader.set_vline_locs_from_selection())
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.set_col_wand_mode()
self.straditizer.draw_figure()
[docs] def remove_vlines(self):
"""Remove vertical lines
This method uses the
:meth:`straditize.binary.DataReader.recognize_vlines` method to
identify vertical lines in the plot"""
fraction = float(self.txt_line_fraction.text().strip() or 0) / 100.
max_lw = self.sp_max_lw.value() if self.cb_max_lw.isChecked() else None
min_lw = self.sp_min_lw.value()
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.recognize_vlines(fraction=fraction, min_lw=min_lw,
max_lw=max_lw)
self.apply_button.clicked.connect(
lambda: self.reader.set_vline_locs_from_selection())
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.set_col_wand_mode()
self.straditizer.draw_figure()
[docs] def enable_occurences_selection(self):
"""Enable the selection of occurences
This method starts the selection of features in the data image and
connects the :meth:`select_occurences` to the :attr:`apply_button`."""
tb = self.selection_toolbar
tb.data_obj = 'Reader'
reader = tb.data_obj
self.apply_button.clicked.connect(self.select_occurences)
self.cancel_button.clicked.connect(
partial(self.cb_remove_occurences.setVisible, False))
tb.start_selection(reader.labels, rgba=reader.image_array(),
remove_on_apply=False)
self.cb_remove_occurences.setVisible(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.straditizer.draw_figure()
[docs] def select_occurences(self):
"""Save (and potentially remove) the selected occurences
Save the occurences with the
:meth:`straditize.binary.DataReader.get_occurences` method and remove
them if the :attr:`cb_remove_occurences` is checked"""
self.reader.get_occurences()
if self.cb_remove_occurences.isChecked():
self.reader.remove_selected_labels(disable=False)
self.cb_remove_occurences.setVisible(False)
[docs] def show_disconnected_parts(self):
"""Remove disconnected parts
This method uses the
:meth:`straditize.binary.DataReader.show_disconnected_parts` to
highlight and remove disconnected features in the diagram part. The
algorithm can be modified by the :attr:`txt_fromlast` and
:attr:`txt_from0` text editors"""
tb = self.selection_toolbar
tb.data_obj = 'Reader'
if self.cb_fromlast.isChecked():
fromlast = int(self.txt_fromlast.text() or 0)
else:
fromlast = 0
if self.cb_from0.isChecked():
from0 = int(self.txt_from0.text() or 0)
else:
from0 = 0
self.reader.show_disconnected_parts(
fromlast=fromlast, from0=from0)
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.straditizer.draw_figure()
[docs] def show_cross_column_features(self):
"""Remove cross column features
This method highlights features that span multiple columns using the
:meth:`straditize.binary.DataReader.show_cross_column_features` method.
The algorithm can be modified with the :attr:`txt_cross_column_px`
line editor"""
tb = self.selection_toolbar
tb.data_obj = 'Reader'
min_px = int(self.txt_cross_column_px.text().strip() or 0)
self.reader.show_cross_column_features(min_px)
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.straditizer.draw_figure()
[docs] def show_parts_at_column_ends(self):
"""Remove parts that touch the column ends
This method highlights features that touch the column ends using the
:meth:`straditize.binary.DataReader.show_parts_at_column_ends` method.
"""
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.show_parts_at_column_ends()
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.straditizer.draw_figure()
[docs] def show_small_parts(self):
"""Remove parts that touch the column ends
This method highlights small features in the data image using the
:meth:`straditize.binary.DataReader.show_small_parts` method.
The maximal size of the small features can is taken from the
:attr:`txt_max_small_size` line editor"""
tb = self.selection_toolbar
tb.data_obj = 'Reader'
self.reader.show_small_parts(int(self.txt_max_small_size.text()))
tb.start_selection(rgba=tb.data_obj.image_array())
tb.remove_select_action.setChecked(True)
if not tb.wand_action.isChecked():
tb.wand_action.setChecked(True)
tb.toggle_selection()
self.straditizer.draw_figure()
def _update_btn_show_small_parts(self, txt):
if txt:
self.btn_show_small_parts.setToolTip(
'Remove parts smaller than %s pixels' % txt)
self.btn_show_small_parts.setEnabled(
self.should_be_enabled(self.btn_show_small_parts))
else:
self.btn_show_small_parts.setEnabled(False)
self.btn_show_small_parts.setToolTip('')
def _update_btn_highlight_small_selection(self, txt):
if txt:
self.btn_highlight_small_selection.setToolTip(
'Highlight selected features smaller than %s pixels' % txt)
self.btn_show_small_parts.setEnabled(
self.should_be_enabled(self.btn_highlight_small_selection))
else:
self.btn_highlight_small_selection.setEnabled(False)
self.btn_highlight_small_selection.setToolTip('')
[docs] def toggle_btn_highlight_small_selection(self):
"""Enable or disable the :attr:`btn_highlight_small_selection`
This method enables the :attr:`btn_highlight_small_selection` button
if we are selecting something at the moment"""
obj = self.selection_toolbar.data_obj
if obj is not None and self.btn_highlight_small_selection.isChecked():
obj.highlight_small_selections(
n=int(self.txt_min_highlight.text()))
elif obj is not None:
obj.remove_small_selection_ellipses()
else:
return
if obj._select_img is not None:
obj._select_img.axes.figure.canvas.draw()
[docs] def select_data_part(self, guess_lims=True):
"""Enable the selection of the diagram part
This method uses the
:meth:`straditize.straditizer.Straditizer.marks_for_data_selection`
method to draw cross marks on the image for the diagram part"""
self.straditizer.marks_for_data_selection(guess_lims=guess_lims)
self.straditizer.draw_figure()
self.connect2apply(self.straditizer.update_data_part,
self.straditizer.draw_figure,
self.straditizer_widgets.refresh)
self.connect2cancel(self.straditizer.remove_marks,
self.straditizer.draw_figure)
[docs] def edit_occurences(self):
"""Enable the editing of occurences
This enables the editing of occurences using the
:meth:`straditize.straditizer.Straditizer.marks_for_occurences`
method for the occurences selected by the
:meth:`select_occurences` method"""
self.straditizer.marks_for_occurences()
self.straditizer.draw_figure()
self.connect2apply(lambda: self.straditizer.update_occurences(True),
self.straditizer.draw_figure,
self.straditizer_widgets.refresh)
self.connect2cancel(self.straditizer.remove_marks,
self.straditizer.draw_figure)
[docs] def align_vertical(self):
"""Create marks for vertical alignment of the columns
See Also
--------
straditize.straditizer.Straditizer.marks_for_vertical_alignment
straditize.straditizer.Straditizer.align_columns"""
self.straditizer.marks_for_vertical_alignment()
self.straditizer.draw_figure()
self.connect2apply(self.straditizer.align_columns,
self.straditizer.draw_figure)
self.connect2cancel(self.straditizer.remove_marks,
self.straditizer.draw_figure)
def _ask_for_column_modification(self):
answer = QMessageBox.Yes
sw = self.straditizer_widgets
if not sw.always_yes:
msg = None
if self.reader.children:
msg = 'Column specific readers have already been created!'
elif self.reader._full_df is not None:
msg = 'The data has already been digitized!'
if msg:
answer = QMessageBox.question(
sw, 'Really modify columns?',
msg + ' Are you sure you want to conitnue? This might '
'result in unexpected behaviour')
return answer == QMessageBox.Yes
[docs] def select_column_starts(self):
"""Estimate the column starts and draw marks
This methods estimates the column starts (if they are not yet set)
based on the threshold in the :attr:`txt_column_thresh` and draws
:class:`straditize.cross_marks.DraggableVLine` marks on the plot.
See Also
--------
straditize.straditizer.Straditizer.marks_for_column_starts
straditize.straditizer.Straditizer.update_column_starts"""
if not self._ask_for_column_modification():
return
threshold = self.txt_column_thresh.text()
threshold = float(threshold) / 100. if threshold else None
self.straditizer.marks_for_column_starts(threshold)
self.straditizer.draw_figure()
self.connect2apply(self.straditizer.update_column_starts,
self.straditizer_widgets.refresh,
self.straditizer.draw_figure)
self.connect2cancel(self.straditizer.remove_marks,
self.straditizer.draw_figure)
[docs] def modify_column_ends(self):
"""Modify the column ends
After having selected the :meth:`column starts <select_column_starts>`,
this method enables the modification of the column ends
See Also
--------
select_column_starts
straditize.straditizer.Straditizer.marks_for_column_ends
straditize.straditizer.Straditizer.update_column_ends"""
if not self._ask_for_column_modification():
return
threshold = self.txt_column_thresh.text()
threshold = float(threshold) / 100. if threshold else None
self.straditizer.marks_for_column_ends(threshold)
self.straditizer.draw_figure()
self.connect2apply(self.straditizer.update_column_ends,
self.straditizer.draw_figure)
self.connect2cancel(self.straditizer.remove_marks,
self.straditizer.draw_figure)
[docs]class BarSplitter(QTreeWidget, StraditizerControlBase):
"""A widget for splitting bars"""
#: The QTreeWidgetItem that is currently shown in the plot
selected_child = None
#: The action in the matplotlib toolbar to go to the previous bar to split
prev_action = None
#: A figure to show the other columns
suggestions_fig = None
@property
def previous_item(self):
"""The QTreeWidgetItem for the previous bar to split"""
child = self.selected_child
top = child.parent()
idx_child = top.indexOfChild(child)
# for the first child, go to the previous topLevelItem (if possible)
if idx_child == 0:
idx_top = self.indexOfTopLevelItem(top)
if idx_top == 0:
return None
previous_top = self.topLevelItem(idx_top - 1)
return previous_top.child(previous_top.childCount() - 1)
return top.child(idx_child - 1)
@property
def next_item(self):
"""The QTreeWidgetItem for the next bar to split"""
child = self.selected_child
top = child.parent()
idx_child = top.indexOfChild(child)
# for the last child, go to the next topLevelItem (if possible)
if top.childCount() == idx_child + 1:
idx_top = self.indexOfTopLevelItem(top)
if self.topLevelItemCount() == idx_top + 1:
return None
next_top = self.topLevelItem(idx_top + 1)
return next_top.child(0)
return top.child(idx_child + 1)
def __init__(self, straditizer_widgets, *args, **kwargs):
super(BarSplitter, self).__init__(*args, **kwargs)
self.init_straditizercontrol(straditizer_widgets)
self.setColumnCount(1)
self.setSelectionMode(QTreeWidget.SingleSelection)
self.filled = False
self._enable_doubleclick = False
self.itemDoubleClicked.connect(self.start_splitting)
self.source = 'too-long'
try:
if self.straditizer.data_reader._splitted:
self.fill_table()
except AttributeError:
pass
[docs] def get_overlapping_bars(self):
"""Get the bars the overlap with multiple bars in another column"""
reader = self.straditizer.data_reader
all_indices = list(map(np.array, reader._all_indices))
ret = [[] for _ in range(len(all_indices))]
for col, l in enumerate(all_indices):
for (imin, imax) in l:
for col2, l2 in enumerate(all_indices):
if col2 != col and len(l2) and \
((l2 > imin) & (l2 < imax)).any(axis=1).sum() >= 2:
ret[col].append([imin, imax])
break
return ret
[docs] def fill_table(self, source='too-long'):
"""Fill the table with the bars that should be splitted
Parameters
----------
source: { 'too-long' | 'overlaps' | 'all' }
The source with what to fill the table.
too-long
Only display the bars that are considered as *too long*
overlap
Only display the bars that overlap with multiple bars in
another column (see :meth:`get_overlapping_bars`)
all
Display all bars"""
self.clear()
self.filled = self._enable_doubleclick = False
self.source = source
if source == 'too-long':
try: # use the bars that should be splitted
items = self.straditizer.data_reader._splitted.items()
except AttributeError:
return
elif source == 'overlaps':
try: # use overlapping bars
items = enumerate(self.get_overlapping_bars())
except AttributeError:
return
else: # use all bars
try:
items = enumerate(self.straditizer.data_reader._all_indices)
except AttributeError:
return
self.filled = True
for col, lists in sorted(items):
if not lists:
continue
top = QTreeWidgetItem(0)
top.setText(0, 'Column %i - %i bars to split' % (col, len(lists)))
for indices in lists:
child = QTreeWidgetItem(0)
child.setText(0, ', '.join(map(str, range(*indices))))
top.addChild(child)
self.addTopLevelItem(top)
self._enable_doubleclick = True
[docs] def refresh(self):
"""Reimplemented to use the :meth:`fill_table` method"""
currently_expanded = [
self._get_col(item)
for item in map(self.topLevelItem, range(self.topLevelItemCount()))
if item.isExpanded()]
self.fill_table(self.source)
for item in map(self.topLevelItem, range(self.topLevelItemCount())):
if self._get_col(item) in currently_expanded:
item.setExpanded(True)
[docs] def start_splitting(self, item, *args, **kwargs):
"""Enable the splitting for the selected item"""
parent = item.parent()
found = False
for top in map(self.topLevelItem, range(self.topLevelItemCount())):
if parent is top:
found = True
break
if not found:
return
# if we are already in the selection, we just switch to another
# bar. Otherwise we check if this widget is enabled and if yes, zoom
# to the item
if self.selected_child is not None:
self.remove_lines()
self.disconnect()
self.clearSelection()
item.setSelected(True)
elif not self._enable_doubleclick:
return
self._col = col = int(top.text(0).split()[1])
reader = self.straditizer.data_reader
extent = reader.extent
if extent is not None:
x0 = extent[0]
y0 = min(extent[2:])
else:
x0 = y0 = 0
indices = list(map(int, item.text(0).split(', ')))
bounds = x0 + reader.column_bounds
idx_col = reader.columns.index(col)
ax = reader.ax
ax.set_xlim(*bounds[idx_col])
ylim = (y0 + max(indices) + len(indices) * 0.5,
y0 + min(indices) - len(indices) * 0.5)
ax.set_ylim(*ylim)
self.lines = [ax.plot(
bounds[idx_col, 0] + reader._full_df_orig.loc[indices, col].values,
y0 + np.array(indices) + 0.5, marker='+', lw=0)[0]]
self.selected_child = item
self.selected_col = col
self.selected_indices = list(indices)
self.split_cid = ax.figure.canvas.mpl_connect(
'button_press_event', self.prepare_for_split)
if self.prev_action is None:
self.add_toolbar_widgets()
if self._enable_doubleclick:
self.connect2apply(self.split_bars, self.remove_lines,
self.disconnect, self.remove_actions,
reader.draw_figure,
self.straditizer_widgets.digitizer.refresh)
self.connect2cancel(self.remove_lines, self.disconnect,
self.remove_actions, reader.draw_figure,
self.remove_split_children)
xmax = np.shape(self.straditizer.image)[1]
suggestions = self.suggest_splits()
# plot suggestions
if self.suggestions_fig is None:
import matplotlib.pyplot as plt
self.suggestions_fig = fig = plt.figure()
fig.add_subplot('131', sharey=ax)
fig.add_subplot('132', sharey=ax)
fig.add_subplot('133', sharey=ax)
for ax in fig.axes:
ax.callbacks.connect(
'xlim_changed', self.set_suggestions_fig_titles)
from psyplot_gui.main import mainwindow
if mainwindow.figures:
mainwindow.splitDockWidget(
self.straditizer.fig.canvas.manager.window,
fig.canvas.manager.window, Qt.Vertical)
else:
fig = self.suggestions_fig
for im in self.images:
im.remove()
plotted_cols = []
plot_suggestions = set(chain.from_iterable(suggestions.values()))
for col, l in suggestions.items():
if plot_suggestions.intersection(l):
plotted_cols.append(col)
plot_suggestions.difference_update(l)
ys_10p = int(len(reader.binary) * 0.1)
miny = max(0, indices[0] - ys_10p)
maxy = indices[-1] + ys_10p
im = reader.binary[miny:maxy]
extent = [x0, reader.binary.shape[1] + x0, y0 + miny, y0 + maxy]
self.images = [ax.imshow(im, cmap='binary', extent=extent)
for ax in fig.axes]
for col, ax in zip(plotted_cols, fig.axes):
ax.set_xlim(*bounds[col])
ax.set_ylim(*ylim)
# draw lines
if not item.childCount():
for y in unique_everseen(
chain.from_iterable(suggestions.values())):
self.new_split(y, y0, draw_figure=False)
else:
axes = [reader.ax] + self.suggestions_fig.axes
if reader.magni is not None:
axes.append(reader.magni.ax)
for i, child in enumerate(map(item.child,
range(item.childCount()-1))):
y = int(child.text(0).split(', ')[-1])
for ax in axes:
self.lines.append(ax.hlines(
y0 + y + 1, 0, xmax, color='red'))
reader.draw_figure()
self.suggestions_fig.canvas.draw()
[docs] def set_suggestions_fig_titles(self, ax):
"""Set the title in the suggestion figure with the displayed column
"""
x = np.mean(ax.get_xlim())
col = next(
(i for i, (s, e) in enumerate(
self.straditizer.data_reader.all_column_bounds)
if x >= s and x <= e), None)
ax.set_title(('Column %i' % col) if col else '')
[docs] def suggest_splits(self):
"""Find overlaps for the current selected bar in other columns"""
reader = self.straditizer.data_reader
imin, imax = np.asarray(self.selected_indices)[[0, -1]]
suggestions = {}
for col, l in enumerate(map(np.ravel, reader._all_indices)):
if col != self.selected_col:
l[1::2] -= 1
suggestions[col] = l[(l > imin) & (l < imax)]
return suggestions
[docs] def remove_lines(self):
"""Remove the plotted lines"""
for l in self.lines:
try:
l.remove()
except ValueError:
pass
self.lines.clear()
[docs] def prepare_for_split(self, event):
"""Select or deselect a split location
LeftButton selects the given location for a new split (see
:meth:`new_split`), RightButton deselects it (see :meth:`revert_split`)
"""
reader = self.straditizer.data_reader
if (event.inaxes != reader.ax or event.button not in [1, 3] or
reader.fig.canvas.manager.toolbar.mode != ''):
return
y = int(np.floor(event.ydata - 0.5))
extent = reader.extent or [0] * 4
idx_col = reader.columns.index(self._col)
start, end = reader.column_bounds[idx_col] + min(extent[:2])
indices = self.selected_indices
if extent is not None:
y0 = min(extent[2:])
y -= y0
if y not in indices or y in [indices[0], indices[-1]]:
return
if event.button == 1:
self.new_split(y, y0)
elif self.selected_child.childCount():
self.revert_split(y, y0)
[docs] @docstrings.get_sectionsf('BarSplitter.new_split')
def new_split(self, y, y0, draw_figure=True):
"""Mark the current item to be splitted at `y`
This method draws a horizontal line at `y` and adds a new
child QTreeWidgetItem to the :attr:`selected_child` to mark the split
Parameters
----------
y: int
The vertical position of the split in the data image coordinate
system
y0: int
The vertical start of the data image (see
:attr:`straditize.binary.DataReader.extent`)
draw_figure: bool
If True, draw the figure
"""
reader = self.straditizer.data_reader
item = self.selected_child
draw_line = False
x0, x1 = self.straditizer.data_xlim
if not item.childCount():
indices = self.selected_indices
c1 = QTreeWidgetItem(0)
c2 = QTreeWidgetItem(1)
item.addChildren([c1, c2])
c1.setText(0, ', '.join(map(str, indices[:indices.index(y) + 1])))
c2.setText(0, ', '.join(map(
str, indices[indices.index(y) + 1:])))
self.expandItem(item)
draw_line = True
else:
for i, child in enumerate(map(item.child,
range(item.childCount()))):
indices = list(map(int, child.text(0).split(', ')))
if y in indices:
if y in [indices[0], indices[-1]]:
return
c1 = QTreeWidgetItem(0)
c2 = child
item.insertChild(i, c1)
c1.setText(0, ', '.join(map(
str, indices[:indices.index(y) + 1])))
c2.setText(0, ', '.join(map(
str, indices[indices.index(y) + 1:])))
draw_line = True
if draw_line:
axes = [reader.ax] + self.suggestions_fig.axes
if reader.magni is not None:
axes.append(reader.magni.ax)
for i, child in enumerate(map(item.child,
range(item.childCount()-1))):
y = int(child.text(0).split(', ')[-1])
for ax in axes:
self.lines.append(ax.hlines(
y0 + y + 1, x0, x1, color='red'))
if draw_figure:
reader.draw_figure()
self.suggestions_fig.canvas.draw()
docstrings.delete_params('BarSplitter.new_split.parameters', 'draw_figure')
[docs] @docstrings.with_indent(8)
def revert_split(self, y, y0):
"""Revert the split
Parameters
----------
%(BarSplitter.new_split.parameters.no_draw_figure)s"""
item = self.selected_child
previous = None
for i, child in enumerate(map(item.child,
range(item.childCount()))):
indices = list(map(int, child.text(0).split(', ')))
if previous:
previous.setText(0, previous.text(0) + ', ' + child.text(0))
item.removeChild(child)
if item.childCount() == 1:
item.removeChild(previous)
break
if y == indices[-1]:
previous = child
if previous: # remove the drawn line
y += y0 + 1
for hline in self.lines[1:]:
if y == hline.get_segments()[0][0, 1]:
hline.remove()
self.lines.remove(hline)
self.straditizer.draw_figure()
self.suggestions_fig.canvas.draw()
def _get_col(self, item):
"""Convenience method for getting the column of a toplevel item"""
return int(item.text(0).split()[1])
[docs] def split_bars(self):
"""Split the bars after they have been separated manually"""
def reset_values(col, indices):
reader._full_df.loc[indices, col] = reader._full_df_orig.loc[
indices, col].max()
reader = self.straditizer.data_reader
for item in map(self.topLevelItem, range(self.topLevelItemCount())):
col = self._get_col(item)
for child in map(item.child, range(item.childCount())):
nchildren = child.childCount()
if not nchildren:
continue
all_indices = list(map(int, child.text(0).split(', ')))
for i, l in enumerate(reader._all_indices[col]):
if l[0] == all_indices[0]:
indices = list(map(int, child.child(
nchildren - 1).text(0).split(', ')))
reader._all_indices[col][i] = [indices[0],
indices[-1] + 1]
reset_values(col, indices)
for child2 in map(child.child, range(nchildren - 1)):
indices = list(map(int,
child2.text(0).split(', ')))
reader._all_indices[col].insert(
i, [indices[0], indices[-1] + 1])
reset_values(col, indices)
break
for i, l in enumerate(reader._splitted.get(col, [])):
if l[0] == all_indices[0]:
del reader._splitted[col][i]
break
[docs] def remove_split_children(self):
"""Remove all the child items that mark a split"""
for item in map(self.topLevelItem, range(self.topLevelItemCount())):
for child in map(item.child, range(item.childCount())):
nchildren = child.childCount()
if nchildren:
for child2 in list(map(child.child, range(nchildren))):
child.removeChild(child2)
[docs] def disconnect(self):
"""Disconnect the events to split an item"""
try:
canvas = self.straditizer.data_reader.ax.figure.canvas
except AttributeError:
pass
else:
canvas.mpl_disconnect(self.split_cid)
del self.selected_child, self.selected_col, self.selected_indices
[docs] def remove_actions(self):
"""Remove the actions added by :meth:`add_toolbar_widgets`"""
if self.prev_action is not None:
try:
tb = self.straditizer.data_reader.fig.canvas.toolbar
except AttributeError:
pass
else:
tb.removeAction(self.next_action)
tb.removeAction(self.prev_action)
tb.removeAction(self.tb_separator)
del self.prev_action
import matplotlib.pyplot as plt
plt.close(self.suggestions_fig)
del self.suggestions_fig, self.images
[docs] def go_to_next_bar(self):
"""Go to the :attr:`next_item`"""
self._go_to_item(self.next_item)
[docs] def go_to_prev_bar(self):
"""Go to the :attr:`previous_item`"""
self._go_to_item(self.previous_item)
def _go_to_item(self, item):
self.start_splitting(item)
self.expandItem(item.parent())
self.expandItem(item)
self.scrollToItem(item)
self.next_action.setEnabled(self.next_item is not None)
self.prev_action.setEnabled(self.previous_item is not None)