# -*- coding: utf-8 -*-
from guidata.configtools import get_icon
from guidata.qthelpers import add_actions, create_action
from guidata.utils.misc import assert_interfaces_valid
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from plotpy.config import _
from plotpy.constants import ID_LCS, ID_OCS, ID_XCS, ID_YCS
from plotpy.interfaces import IPanel
from plotpy.panels.base import PanelWidget
from plotpy.panels.csection.csplot import (
BaseCrossSectionPlot,
LineCrossSectionPlot,
ObliqueCrossSectionPlot,
XCrossSectionPlot,
YCrossSectionPlot,
)
from plotpy.tools import ExportItemDataTool
class CrossSectionWidget(PanelWidget):
"""Base class for cross section panels
Args:
parent: parent widget
"""
SIG_RESIZED = QC.Signal() # emitted when the widget is resized
PANEL_ID = None
PANEL_TITLE = _("Cross section tool")
PANEL_ICON = "csection.png"
CrossSectionPlotKlass = BaseCrossSectionPlot # to be overridden in subclasses
__implements__ = (IPanel,)
def __init__(self, parent=None):
super().__init__(parent)
self.export_ac = None
self.autoscale_ac = None
self.refresh_ac = None
self.autorefresh_ac = None
self.lockscales_ac = None
self.manager = None # manager for the associated image plot
# Avoid circular import
# pylint-disable=import-outside-toplevel
from plotpy.plot.manager import PlotManager
self.local_manager = PlotManager(self)
self.cs_plot = self.CrossSectionPlotKlass(parent)
self.cs_plot.SIG_CS_CURVE_CHANGED.connect(self.cs_curve_has_changed)
self.export_tool = None
self.setup_plot()
self.toolbar = QW.QToolBar(self)
self.toolbar.setOrientation(QC.Qt.Orientation.Vertical)
self.setup_widget()
# Reimplement Qt method for sending a signal when the widget is resized
def resizeEvent(self, event):
"""
:param event:
"""
super().resizeEvent(event)
self.SIG_RESIZED.emit()
def set_options(self, autoscale=None, autorefresh=None, lockscales=None):
"""
:param autoscale:
:param autorefresh:
:param lockscales:
"""
assert self.manager is not None, (
"Panel '%s' must be registered to plot manager before changing options"
% self.PANEL_ID
)
if autoscale is not None:
self.autoscale_ac.setChecked(autoscale)
if autorefresh is not None:
self.autorefresh_ac.setChecked(autorefresh)
if lockscales is not None:
self.lockscales_ac.setChecked(lockscales)
def setup_plot(self):
""" """
# Configure the local manager
lman = self.local_manager
lman.add_plot(self.cs_plot)
lman.register_all_curve_tools()
self.export_tool = lman.get_tool(ExportItemDataTool)
def setup_widget(self):
""" """
layout = QW.QHBoxLayout()
layout.addWidget(self.cs_plot)
layout.addWidget(self.toolbar)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def cs_curve_has_changed(self, curve):
"""Cross section curve has just changed"""
# Do something with curve's data for example
pass
def register_panel(self, manager):
"""Register panel to plot manager"""
self.manager = manager
for plot in manager.get_plots():
self.cs_plot.connect_plot(plot)
self.setup_actions()
self.add_actions_to_toolbar()
def configure_panel(self):
"""Configure panel"""
pass
def get_plot(self):
"""
:return:
"""
return self.manager.get_active_plot()
def setup_actions(self):
""" """
self.export_ac = self.export_tool.action
self.lockscales_ac = create_action(
self,
_("Lock scales"),
icon=get_icon("axes.png"),
toggled=self.cs_plot.toggle_lockscales,
tip=_("Lock scales to main plot axes"),
)
self.lockscales_ac.setChecked(self.cs_plot.lockscales)
self.autoscale_ac = create_action(
self,
_("Auto-scale"),
icon=get_icon("csautoscale.png"),
toggled=self.cs_plot.toggle_autoscale,
)
self.autoscale_ac.toggled.connect(self.lockscales_ac.setDisabled)
self.autoscale_ac.setChecked(self.cs_plot.autoscale_mode)
self.refresh_ac = create_action(
self,
_("Refresh"),
icon=get_icon("refresh.png"),
triggered=lambda: self.cs_plot.update_plot(),
)
self.autorefresh_ac = create_action(
self,
_("Auto-refresh"),
icon=get_icon("autorefresh.png"),
toggled=self.cs_plot.toggle_autorefresh,
)
self.autorefresh_ac.setChecked(self.cs_plot.autorefresh_mode)
def add_actions_to_toolbar(self):
""" """
add_actions(
self.toolbar,
(
self.export_ac,
None,
self.autoscale_ac,
self.lockscales_ac,
None,
self.refresh_ac,
self.autorefresh_ac,
),
)
def register_shape(self, shape, refresh=True):
"""
:param shape:
:param final:
:param refresh:
"""
plot = self.get_plot()
self.cs_plot.register_shape(plot, shape, refresh)
def unregister_shape(self, shape):
"""
:param shape:
"""
self.cs_plot.unregister_shape(shape)
def update_plot(self, obj=None):
"""
Update cross section curve(s) associated to object *obj*
*obj* may be a marker or a rectangular shape
(see :py:class:`.tools.CrossSectionTool`
and :py:class:`.tools.AverageCrossSectionTool`)
If obj is None, update the cross sections of the last active object
"""
self.cs_plot.update_plot(obj)
assert_interfaces_valid(CrossSectionWidget)
[docs]
class XCrossSection(CrossSectionWidget):
"""X-axis cross section panel
Args:
parent: parent widget
"""
PANEL_ID = ID_XCS
OTHER_PANEL_ID = ID_YCS
CrossSectionPlotKlass = XCrossSectionPlot
def __init__(self, parent=None):
super().__init__(parent)
self.peritem_ac = None
self.applylut_ac = None
def set_options(
self,
autoscale=None,
autorefresh=None,
peritem=None,
applylut=None,
lockscales=None,
):
"""
:param autoscale:
:param autorefresh:
:param peritem:
:param applylut:
:param lockscales:
"""
assert self.manager is not None, (
f"Panel '{self.PANEL_ID}' must be "
"registered to plot manager before changing options"
)
if autoscale is not None:
self.autoscale_ac.setChecked(autoscale)
if autorefresh is not None:
self.autorefresh_ac.setChecked(autorefresh)
if lockscales is not None:
self.lockscales_ac.setChecked(lockscales)
if peritem is not None:
self.peritem_ac.setChecked(peritem)
if applylut is not None:
self.applylut_ac.setChecked(applylut)
def add_actions_to_toolbar(self):
""" """
other = self.manager.get_panel(self.OTHER_PANEL_ID)
if other is None:
add_actions(
self.toolbar,
(
self.peritem_ac,
self.applylut_ac,
None,
self.export_ac,
None,
self.autoscale_ac,
self.lockscales_ac,
None,
self.refresh_ac,
self.autorefresh_ac,
),
)
else:
add_actions(
self.toolbar,
(
other.peritem_ac,
other.applylut_ac,
None,
self.export_ac,
None,
other.autoscale_ac,
other.lockscales_ac,
None,
other.refresh_ac,
other.autorefresh_ac,
),
)
other.peritem_ac.toggled.connect(self.cs_plot.toggle_perimage_mode)
other.applylut_ac.toggled.connect(self.cs_plot.toggle_apply_lut)
other.autoscale_ac.toggled.connect(self.cs_plot.toggle_autoscale)
other.refresh_ac.triggered.connect(lambda: self.cs_plot.update_plot())
other.autorefresh_ac.toggled.connect(self.cs_plot.toggle_autorefresh)
other.lockscales_ac.toggled.connect(self.cs_plot.toggle_lockscales)
def closeEvent(self, event):
"""
:param event:
"""
self.hide()
event.ignore()
def setup_actions(self):
""" """
CrossSectionWidget.setup_actions(self)
self.peritem_ac = create_action(
self,
_("Per image cross-section"),
icon=get_icon("csperimage.png"),
toggled=self.cs_plot.toggle_perimage_mode,
tip=_(
"Enable the per-image cross-section mode, "
"which works directly on image rows/columns.\n"
"That is the fastest method to compute "
"cross-section curves but it ignores "
"image transformations (e.g. rotation)"
),
)
self.applylut_ac = create_action(
self,
_("Apply LUT\n(contrast settings)"),
icon=get_icon("csapplylut.png"),
toggled=self.cs_plot.toggle_apply_lut,
tip=_(
"Apply LUT (Look-Up Table) contrast settings.\n"
"This is the easiest way to compare images "
"which have slightly different level ranges.\n\n"
"Note: LUT is coded over 1024 levels (0...1023)"
),
)
self.peritem_ac.setChecked(True)
self.applylut_ac.setChecked(False)
[docs]
class YCrossSection(XCrossSection):
"""Y-axis cross section panel
Args:
parent: parent widget
position: position of the panel ('left' or 'right')
xsection_pos: position of the cross section plot ('top' or 'bottom')
"""
PANEL_ID = ID_YCS
OTHER_PANEL_ID = ID_XCS
CrossSectionPlotKlass = YCrossSectionPlot
def __init__(self, parent=None, position="right", xsection_pos="top"):
self.xsection_pos = xsection_pos
self.spacer = QW.QSpacerItem(0, 0)
super().__init__(parent)
self.cs_plot.set_axis_direction("bottom", reverse=position == "left")
def setup_widget(self):
""" """
toolbar = self.toolbar
toolbar.setOrientation(QC.Qt.Orientation.Horizontal)
layout = QW.QVBoxLayout()
if self.xsection_pos == "top":
layout.addSpacerItem(self.spacer)
layout.addWidget(toolbar)
layout.addWidget(self.cs_plot)
if self.xsection_pos == "bottom":
layout.addSpacerItem(self.spacer)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def adjust_height(self, height):
"""
:param height:
"""
self.spacer.changeSize(0, height, QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed)
self.layout().invalidate()
# Oblique cross section panel
[docs]
class ObliqueCrossSection(CrossSectionWidget):
"""Oblique averaged cross section panel
Args:
parent: parent widget
"""
PANEL_ID = ID_OCS
CrossSectionPlotKlass = ObliqueCrossSectionPlot
PANEL_ICON = "csection_oblique.png"
def setup_actions(self):
""" """
super().setup_actions()
self.lockscales_ac.setChecked(False)
self.autoscale_ac.setChecked(True)
[docs]
class LineCrossSection(CrossSectionWidget):
"""Line cross section panel
Args:
parent: parent widget
"""
PANEL_ID = ID_LCS
CrossSectionPlotKlass = LineCrossSectionPlot
def __init__(self, parent=None):
super().__init__(parent)
self.cs_plot.set_axis_direction("bottom", reverse=False)