diff options
Diffstat (limited to 'silx/gui/plot/tools/test')
-rw-r--r-- | silx/gui/plot/tools/test/__init__.py | 2 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testProfile.py | 673 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testROI.py | 191 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testScatterProfileToolBar.py | 137 |
4 files changed, 915 insertions, 88 deletions
diff --git a/silx/gui/plot/tools/test/__init__.py b/silx/gui/plot/tools/test/__init__.py index 9cede27..1429545 100644 --- a/silx/gui/plot/tools/test/__init__.py +++ b/silx/gui/plot/tools/test/__init__.py @@ -33,6 +33,7 @@ from . import testROI from . import testTools from . import testScatterProfileToolBar from . import testCurveLegendsWidget +from . import testProfile def suite(): @@ -42,6 +43,7 @@ def suite(): testTools.suite(), testScatterProfileToolBar.suite(), testCurveLegendsWidget.suite(), + testProfile.suite(), ]) return test_suite diff --git a/silx/gui/plot/tools/test/testProfile.py b/silx/gui/plot/tools/test/testProfile.py new file mode 100644 index 0000000..444cfe0 --- /dev/null +++ b/silx/gui/plot/tools/test/testProfile.py @@ -0,0 +1,673 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "28/06/2018" + + +import unittest +import contextlib +import numpy +import logging + +from silx.gui import qt +from silx.utils import deprecation +from silx.utils import testutils + +from silx.gui.utils.testutils import TestCaseQt +from silx.utils.testutils import ParametricTestCase +from silx.gui.plot import PlotWindow, Plot1D, Plot2D, Profile +from silx.gui.plot.StackView import StackView +from silx.gui.plot.tools.profile import rois +from silx.gui.plot.tools.profile import editors +from silx.gui.plot.items import roi as roi_items +from silx.gui.plot.tools.profile import manager +from silx.gui import plot as silx_plot + +_logger = logging.getLogger(__name__) + + +class TestRois(TestCaseQt): + + def test_init(self): + """Check that the constructor is not called twice""" + roi = rois.ProfileImageVerticalLineROI() + if qt.BINDING not in ["PySide", "PySide2"]: + # the profile ROI + the shape + self.assertEqual(roi.receivers(roi.sigRegionChanged), 2) + + +class TestInteractions(TestCaseQt): + + @contextlib.contextmanager + def defaultPlot(self): + try: + widget = silx_plot.PlotWidget() + widget.show() + self.qWaitForWindowExposed(widget) + yield widget + finally: + widget.close() + widget = None + self.qWait() + + @contextlib.contextmanager + def imagePlot(self): + try: + widget = silx_plot.Plot2D() + image = numpy.arange(10 * 10).reshape(10, -1) + widget.addImage(image) + widget.show() + self.qWaitForWindowExposed(widget) + yield widget + finally: + widget.close() + widget = None + self.qWait() + + @contextlib.contextmanager + def scatterPlot(self): + try: + widget = silx_plot.ScatterView() + + nbX, nbY = 7, 5 + yy = numpy.atleast_2d(numpy.ones(nbY)).T + xx = numpy.atleast_2d(numpy.ones(nbX)) + positionX = numpy.linspace(10, 50, nbX) * yy + positionX = positionX.reshape(nbX * nbY) + positionY = numpy.atleast_2d(numpy.linspace(20, 60, nbY)).T * xx + positionY = positionY.reshape(nbX * nbY) + values = numpy.arange(nbX * nbY) + + widget.setData(positionX, positionY, values) + widget.resetZoom() + widget.show() + self.qWaitForWindowExposed(widget) + yield widget.getPlotWidget() + finally: + widget.close() + widget = None + self.qWait() + + @contextlib.contextmanager + def stackPlot(self): + try: + widget = silx_plot.StackView() + image = numpy.arange(10 * 10).reshape(10, -1) + cube = numpy.array([image, image, image]) + widget.setStack(cube) + widget.resetZoom() + widget.show() + self.qWaitForWindowExposed(widget) + yield widget.getPlotWidget() + finally: + widget.close() + widget = None + self.qWait() + + def waitPendingOperations(self, proflie): + for _ in range(10): + if not proflie.hasPendingOperations(): + return + self.qWait(100) + _logger.error("The profile manager still have pending operations") + + def genericRoiTest(self, plot, roiClass): + profileManager = manager.ProfileManager(plot, plot) + profileManager.setItemType(image=True, scatter=True) + + try: + action = profileManager.createProfileAction(roiClass, plot) + action.triggered[bool].emit(True) + widget = plot.getWidgetHandle() + + # Do the mouse interaction + pos1 = widget.width() * 0.4, widget.height() * 0.4 + self.mouseMove(widget, pos=pos1) + self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1) + + if issubclass(roiClass, roi_items.LineROI): + pos2 = widget.width() * 0.6, widget.height() * 0.6 + self.mouseMove(widget, pos=pos2) + self.mouseClick(widget, qt.Qt.LeftButton, pos=pos2) + + self.waitPendingOperations(profileManager) + + # Test that something was computed + if issubclass(roiClass, rois._ProfileCrossROI): + self.assertEqual(profileManager._computedProfiles, 2) + elif issubclass(roiClass, roi_items.LineROI): + self.assertGreaterEqual(profileManager._computedProfiles, 1) + else: + self.assertEqual(profileManager._computedProfiles, 1) + + # Test the created ROIs + profileRois = profileManager.getRoiManager().getRois() + if issubclass(roiClass, rois._ProfileCrossROI): + self.assertEqual(len(profileRois), 3) + else: + self.assertEqual(len(profileRois), 1) + # The first one should be the expected one + roi = profileRois[0] + + # Test that something was displayed + if issubclass(roiClass, rois._ProfileCrossROI): + profiles = roi._getLines() + window = profiles[0].getProfileWindow() + self.assertIsNotNone(window) + window = profiles[1].getProfileWindow() + self.assertIsNotNone(window) + else: + window = roi.getProfileWindow() + self.assertIsNotNone(window) + finally: + profileManager.clearProfile() + + def testImageActions(self): + roiClasses = [ + rois.ProfileImageHorizontalLineROI, + rois.ProfileImageVerticalLineROI, + rois.ProfileImageLineROI, + rois.ProfileImageCrossROI, + ] + with self.imagePlot() as plot: + for roiClass in roiClasses: + with self.subTest(roiClass=roiClass): + self.genericRoiTest(plot, roiClass) + + def testScatterActions(self): + roiClasses = [ + rois.ProfileScatterHorizontalLineROI, + rois.ProfileScatterVerticalLineROI, + rois.ProfileScatterLineROI, + rois.ProfileScatterCrossROI, + rois.ProfileScatterHorizontalSliceROI, + rois.ProfileScatterVerticalSliceROI, + rois.ProfileScatterCrossSliceROI, + ] + with self.scatterPlot() as plot: + for roiClass in roiClasses: + with self.subTest(roiClass=roiClass): + self.genericRoiTest(plot, roiClass) + + def testStackActions(self): + roiClasses = [ + rois.ProfileImageStackHorizontalLineROI, + rois.ProfileImageStackVerticalLineROI, + rois.ProfileImageStackLineROI, + rois.ProfileImageStackCrossROI, + ] + with self.stackPlot() as plot: + for roiClass in roiClasses: + with self.subTest(roiClass=roiClass): + self.genericRoiTest(plot, roiClass) + + def genericEditorTest(self, plot, roi, editor): + if isinstance(editor, editors._NoProfileRoiEditor): + pass + elif isinstance(editor, editors._DefaultImageStackProfileRoiEditor): + # GUI to ROI + editor._lineWidth.setValue(2) + self.assertEqual(roi.getProfileLineWidth(), 2) + editor._methodsButton.setMethod("sum") + self.assertEqual(roi.getProfileMethod(), "sum") + editor._profileDim.setDimension(1) + self.assertEqual(roi.getProfileType(), "1D") + # ROI to GUI + roi.setProfileLineWidth(3) + self.assertEqual(editor._lineWidth.value(), 3) + roi.setProfileMethod("mean") + self.assertEqual(editor._methodsButton.getMethod(), "mean") + roi.setProfileType("2D") + self.assertEqual(editor._profileDim.getDimension(), 2) + elif isinstance(editor, editors._DefaultImageProfileRoiEditor): + # GUI to ROI + editor._lineWidth.setValue(2) + self.assertEqual(roi.getProfileLineWidth(), 2) + editor._methodsButton.setMethod("sum") + self.assertEqual(roi.getProfileMethod(), "sum") + # ROI to GUI + roi.setProfileLineWidth(3) + self.assertEqual(editor._lineWidth.value(), 3) + roi.setProfileMethod("mean") + self.assertEqual(editor._methodsButton.getMethod(), "mean") + elif isinstance(editor, editors._DefaultScatterProfileRoiEditor): + # GUI to ROI + editor._nPoints.setValue(100) + self.assertEqual(roi.getNPoints(), 100) + # ROI to GUI + roi.setNPoints(200) + self.assertEqual(editor._nPoints.value(), 200) + else: + assert False + + def testEditors(self): + roiClasses = [ + (rois.ProfileImageHorizontalLineROI, editors._DefaultImageProfileRoiEditor), + (rois.ProfileImageVerticalLineROI, editors._DefaultImageProfileRoiEditor), + (rois.ProfileImageLineROI, editors._DefaultImageProfileRoiEditor), + (rois.ProfileImageCrossROI, editors._DefaultImageProfileRoiEditor), + (rois.ProfileScatterHorizontalLineROI, editors._DefaultScatterProfileRoiEditor), + (rois.ProfileScatterVerticalLineROI, editors._DefaultScatterProfileRoiEditor), + (rois.ProfileScatterLineROI, editors._DefaultScatterProfileRoiEditor), + (rois.ProfileScatterCrossROI, editors._DefaultScatterProfileRoiEditor), + (rois.ProfileScatterHorizontalSliceROI, editors._NoProfileRoiEditor), + (rois.ProfileScatterVerticalSliceROI, editors._NoProfileRoiEditor), + (rois.ProfileScatterCrossSliceROI, editors._NoProfileRoiEditor), + (rois.ProfileImageStackHorizontalLineROI, editors._DefaultImageStackProfileRoiEditor), + (rois.ProfileImageStackVerticalLineROI, editors._DefaultImageStackProfileRoiEditor), + (rois.ProfileImageStackLineROI, editors._DefaultImageStackProfileRoiEditor), + (rois.ProfileImageStackCrossROI, editors._DefaultImageStackProfileRoiEditor), + ] + with self.defaultPlot() as plot: + profileManager = manager.ProfileManager(plot, plot) + editorAction = profileManager.createEditorAction(parent=plot) + for roiClass, editorClass in roiClasses: + with self.subTest(roiClass=roiClass): + roi = roiClass() + roi._setProfileManager(profileManager) + try: + # Force widget creation + menu = qt.QMenu(plot) + menu.addAction(editorAction) + widgets = editorAction.createdWidgets() + self.assertGreater(len(widgets), 0) + + editorAction.setProfileRoi(roi) + editorWidget = editorAction._getEditor(widgets[0]) + self.assertIsInstance(editorWidget, editorClass) + self.genericEditorTest(plot, roi, editorWidget) + finally: + editorAction.setProfileRoi(None) + menu.deleteLater() + menu = None + self.qapp.processEvents() + + +class TestProfileToolBar(TestCaseQt, ParametricTestCase): + """Tests for ProfileToolBar widget.""" + + def setUp(self): + super(TestProfileToolBar, self).setUp() + self.plot = PlotWindow() + self.toolBar = Profile.ProfileToolBar(plot=self.plot) + self.plot.addToolBar(self.toolBar) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.mouseMove(self.plot) # Move to center + self.qapp.processEvents() + deprecation.FORCE = True + + def tearDown(self): + deprecation.FORCE = False + self.qapp.processEvents() + profileManager = self.toolBar.getProfileManager() + profileManager.clearProfile() + profileManager = None + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + del self.toolBar + + super(TestProfileToolBar, self).tearDown() + + def testAlignedProfile(self): + """Test horizontal and vertical profile, without and with image""" + # Use Plot backend widget to submit mouse events + widget = self.plot.getWidgetHandle() + for method in ('sum', 'mean'): + with self.subTest(method=method): + # 2 positions to use for mouse events + pos1 = widget.width() * 0.4, widget.height() * 0.4 + pos2 = widget.width() * 0.6, widget.height() * 0.6 + + for action in (self.toolBar.hLineAction, self.toolBar.vLineAction): + with self.subTest(mode=action.text()): + # Trigger tool button for mode + action.trigger() + # Without image + self.mouseMove(widget, pos=pos1) + self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1) + + # with image + self.plot.addImage( + numpy.arange(100 * 100).reshape(100, -1)) + self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) + self.mouseMove(widget, pos=pos2) + self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) + + self.mouseMove(widget) + self.mouseClick(widget, qt.Qt.LeftButton) + + manager = self.toolBar.getProfileManager() + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + @testutils.test_logging(deprecation.depreclog.name, warning=4) + def testDiagonalProfile(self): + """Test diagonal profile, without and with image""" + # Use Plot backend widget to submit mouse events + widget = self.plot.getWidgetHandle() + + for method in ('sum', 'mean'): + for image in (False, True): + with self.subTest(method=method, image=image): + # 2 positions to use for mouse events + pos1 = widget.width() * 0.4, widget.height() * 0.4 + pos2 = widget.width() * 0.6, widget.height() * 0.6 + + if image: + self.plot.addImage( + numpy.arange(100 * 100).reshape(100, -1)) + + # Trigger tool button for diagonal profile mode + self.toolBar.lineAction.trigger() + + # draw profile line + widget.setFocus(qt.Qt.OtherFocusReason) + self.mouseMove(widget, pos=pos1) + self.qWait(100) + self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) + self.qWait(100) + self.mouseMove(widget, pos=pos2) + self.qWait(100) + self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) + self.qWait(100) + + manager = self.toolBar.getProfileManager() + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + roi = manager.getCurrentRoi() + self.assertIsNotNone(roi) + roi.setProfileLineWidth(3) + roi.setProfileMethod(method) + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + if image is True: + curveItem = self.toolBar.getProfilePlot().getAllCurves()[0] + if method == 'sum': + self.assertTrue(curveItem.getData()[1].max() > 10000) + elif method == 'mean': + self.assertTrue(curveItem.getData()[1].max() < 10000) + + # Remove the ROI so the profile window is also removed + roiManager = manager.getRoiManager() + roiManager.removeRoi(roi) + self.qWait(100) + + +class TestDeprecatedProfileToolBar(TestCaseQt): + """Tests old features of the ProfileToolBar widget.""" + + def setUp(self): + self.plot = None + super(TestDeprecatedProfileToolBar, self).setUp() + + def tearDown(self): + if self.plot is not None: + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + self.plot = None + self.qWait() + + super(TestDeprecatedProfileToolBar, self).tearDown() + + @testutils.test_logging(deprecation.depreclog.name, warning=2) + def testCustomProfileWindow(self): + from silx.gui.plot import ProfileMainWindow + + self.plot = PlotWindow() + profileWindow = ProfileMainWindow.ProfileMainWindow(self.plot) + toolBar = Profile.ProfileToolBar(parent=self.plot, + plot=self.plot, + profileWindow=profileWindow) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + profileWindow.show() + self.qWaitForWindowExposed(profileWindow) + self.qapp.processEvents() + + self.plot.addImage(numpy.arange(10 * 10).reshape(10, -1)) + profile = rois.ProfileImageHorizontalLineROI() + profile.setPosition(5) + toolBar.getProfileManager().getRoiManager().addRoi(profile) + toolBar.getProfileManager().getRoiManager().setCurrentRoi(profile) + + for _ in range(20): + self.qWait(200) + if not toolBar.getProfileManager().hasPendingOperations(): + break + + # There is a displayed profile + self.assertIsNotNone(profileWindow.getProfile()) + self.assertIs(toolBar.getProfileMainWindow(), profileWindow) + + # There is nothing anymore but the window is still there + toolBar.getProfileManager().clearProfile() + self.qapp.processEvents() + self.assertIsNone(profileWindow.getProfile()) + + +class TestProfile3DToolBar(TestCaseQt): + """Tests for Profile3DToolBar widget. + """ + def setUp(self): + super(TestProfile3DToolBar, self).setUp() + self.plot = StackView() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.plot.setStack(numpy.array([ + [[0, 1, 2], [3, 4, 5]], + [[6, 7, 8], [9, 10, 11]], + [[12, 13, 14], [15, 16, 17]] + ])) + deprecation.FORCE = True + + def tearDown(self): + deprecation.FORCE = False + profileManager = self.plot.getProfileToolbar().getProfileManager() + profileManager.clearProfile() + profileManager = None + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + self.plot = None + + super(TestProfile3DToolBar, self).tearDown() + + @testutils.test_logging(deprecation.depreclog.name, warning=2) + def testMethodProfile2D(self): + """Test that the profile can have a different method if we want to + compute then in 1D or in 2D""" + + toolBar = self.plot.getProfileToolbar() + + toolBar.vLineAction.trigger() + plot2D = self.plot.getPlotWidget().getWidgetHandle() + pos1 = plot2D.width() * 0.5, plot2D.height() * 0.5 + self.mouseClick(plot2D, qt.Qt.LeftButton, pos=pos1) + + manager = toolBar.getProfileManager() + roi = manager.getCurrentRoi() + roi.setProfileMethod("mean") + roi.setProfileType("2D") + roi.setProfileLineWidth(3) + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + # check 2D 'mean' profile + profilePlot = toolBar.getProfilePlot() + data = profilePlot.getAllImages()[0].getData() + expected = numpy.array([[1, 4], [7, 10], [13, 16]]) + numpy.testing.assert_almost_equal(data, expected) + + @testutils.test_logging(deprecation.depreclog.name, warning=2) + def testMethodSumLine(self): + """Simple interaction test to make sure the sum is correctly computed + """ + toolBar = self.plot.getProfileToolbar() + + toolBar.lineAction.trigger() + plot2D = self.plot.getPlotWidget().getWidgetHandle() + pos1 = plot2D.width() * 0.5, plot2D.height() * 0.2 + pos2 = plot2D.width() * 0.5, plot2D.height() * 0.8 + + self.mouseMove(plot2D, pos=pos1) + self.mousePress(plot2D, qt.Qt.LeftButton, pos=pos1) + self.mouseMove(plot2D, pos=pos2) + self.mouseRelease(plot2D, qt.Qt.LeftButton, pos=pos2) + + manager = toolBar.getProfileManager() + roi = manager.getCurrentRoi() + roi.setProfileMethod("sum") + roi.setProfileType("2D") + roi.setProfileLineWidth(3) + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + # check 2D 'sum' profile + profilePlot = toolBar.getProfilePlot() + data = profilePlot.getAllImages()[0].getData() + expected = numpy.array([[3, 12], [21, 30], [39, 48]]) + numpy.testing.assert_almost_equal(data, expected) + + +class TestGetProfilePlot(TestCaseQt): + + def setUp(self): + self.plot = None + super(TestGetProfilePlot, self).setUp() + + def tearDown(self): + if self.plot is not None: + manager = self.plot.getProfileToolbar().getProfileManager() + manager.clearProfile() + manager = None + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + self.plot = None + + super(TestGetProfilePlot, self).tearDown() + + def testProfile1D(self): + self.plot = Plot2D() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + self.plot.addImage([[0, 1], [2, 3]]) + + toolBar = self.plot.getProfileToolbar() + + manager = toolBar.getProfileManager() + roiManager = manager.getRoiManager() + + roi = rois.ProfileImageHorizontalLineROI() + roi.setPosition(0.5) + roiManager.addRoi(roi) + roiManager.setCurrentRoi(roi) + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + profileWindow = roi.getProfileWindow() + self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) + self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D) + + def testProfile2D(self): + """Test that the profile plot associated to a stack view is either a + Plot1D or a plot 2D instance.""" + self.plot = StackView() + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + self.plot.setStack(numpy.array([[[0, 1], [2, 3]], + [[4, 5], [6, 7]]])) + + toolBar = self.plot.getProfileToolbar() + + manager = toolBar.getProfileManager() + roiManager = manager.getRoiManager() + + roi = rois.ProfileImageStackHorizontalLineROI() + roi.setPosition(0.5) + roi.setProfileType("2D") + roiManager.addRoi(roi) + roiManager.setCurrentRoi(roi) + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + profileWindow = roi.getProfileWindow() + self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) + self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot2D) + + roi.setProfileType("1D") + + for _ in range(20): + self.qWait(200) + if not manager.hasPendingOperations(): + break + + profileWindow = roi.getProfileWindow() + self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) + self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D) + + +def suite(): + test_suite = unittest.TestSuite() + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loadTests(TestRois)) + test_suite.addTest(loadTests(TestInteractions)) + test_suite.addTest(loadTests(TestProfileToolBar)) + test_suite.addTest(loadTests(TestGetProfilePlot)) + test_suite.addTest(loadTests(TestProfile3DToolBar)) + test_suite.addTest(loadTests(TestDeprecatedProfileToolBar)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testROI.py b/silx/gui/plot/tools/test/testROI.py index 8aec1d9..33a0000 100644 --- a/silx/gui/plot/tools/test/testROI.py +++ b/silx/gui/plot/tools/test/testROI.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2018 European Synchrotron Radiation Facility +# Copyright (c) 2018-2020 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -60,7 +60,7 @@ class TestRoiItems(TestCaseQt): def testPoint_geometry(self): point = numpy.array([1, 2]) - item = roi_items.VerticalLineROI() + item = roi_items.PointROI() item.setPosition(point) numpy.testing.assert_allclose(item.getPosition(), point) @@ -108,6 +108,43 @@ class TestRoiItems(TestCaseQt): numpy.testing.assert_allclose(item.getCenter(), expectedCenter) numpy.testing.assert_allclose(item.getSize(), size) + def testCircle_geometry(self): + center = numpy.array([0, 0]) + radius = 10. + item = roi_items.CircleROI() + item.setGeometry(center=center, radius=radius) + numpy.testing.assert_allclose(item.getCenter(), center) + numpy.testing.assert_allclose(item.getRadius(), radius) + + def testCircle_setCenter(self): + center = numpy.array([0, 0]) + radius = 10. + item = roi_items.CircleROI() + item.setGeometry(center=center, radius=radius) + newCenter = numpy.array([-10, 0]) + item.setCenter(newCenter) + numpy.testing.assert_allclose(item.getCenter(), newCenter) + numpy.testing.assert_allclose(item.getRadius(), radius) + + def testCircle_setRadius(self): + center = numpy.array([0, 0]) + radius = 10. + item = roi_items.CircleROI() + item.setGeometry(center=center, radius=radius) + newRadius = 5.1 + item.setRadius(newRadius) + numpy.testing.assert_allclose(item.getCenter(), center) + numpy.testing.assert_allclose(item.getRadius(), newRadius) + + def testRectangle_isIn(self): + origin = numpy.array([0, 0]) + size = numpy.array([10, 20]) + item = roi_items.RectangleROI() + item.setGeometry(origin=origin, size=size) + self.assertTrue(item.contains(position=(0, 0))) + self.assertTrue(item.contains(position=(2, 14))) + self.assertFalse(item.contains(position=(14, 12))) + def testPolygon_emptyGeometry(self): points = numpy.empty((0, 2)) item = roi_items.PolygonROI() @@ -120,6 +157,17 @@ class TestRoiItems(TestCaseQt): item.setPoints(points) numpy.testing.assert_allclose(item.getPoints(), points) + def testPolygon_isIn(self): + points = numpy.array([[0, 0], [0, 10], [5, 10]]) + item = roi_items.PolygonROI() + item.setPoints(points) + self.assertTrue(item.contains((0, 0))) + self.assertFalse(item.contains((6, 2))) + self.assertFalse(item.contains((-2, 5))) + self.assertFalse(item.contains((2, -1))) + self.assertFalse(item.contains((8, 1))) + self.assertTrue(item.contains((1, 8))) + def testArc_getToSetGeometry(self): """Test that we can use getGeometry as input to setGeometry""" item = roi_items.ArcROI() @@ -147,7 +195,7 @@ class TestRoiItems(TestCaseQt): self.assertAlmostEqual(item.getInnerRadius(), innerRadius) self.assertAlmostEqual(item.getOuterRadius(), outerRadius) self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0) - self.assertAlmostEqual(item.isClosed(), True) + self.assertTrue(item.isClosed()) def testArc_special_donut(self): item = roi_items.ArcROI() @@ -158,7 +206,7 @@ class TestRoiItems(TestCaseQt): self.assertAlmostEqual(item.getInnerRadius(), innerRadius) self.assertAlmostEqual(item.getOuterRadius(), outerRadius) self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0) - self.assertAlmostEqual(item.isClosed(), True) + self.assertTrue(item.isClosed()) def testArc_clockwiseGeometry(self): """Test that we can use getGeometry as input to setGeometry""" @@ -186,6 +234,15 @@ class TestRoiItems(TestCaseQt): self.assertAlmostEqual(item.getEndAngle(), endAngle) self.assertAlmostEqual(item.isClosed(), False) + def testHRange_geometry(self): + item = roi_items.HorizontalRangeROI() + vmin = 1 + vmax = 3 + item.setRange(vmin, vmax) + self.assertAlmostEqual(item.getMin(), vmin) + self.assertAlmostEqual(item.getMax(), vmax) + self.assertAlmostEqual(item.getCenter(), 2) + class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): """Tests for RegionOfInterestManager class""" @@ -229,6 +286,9 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): (roi_items.VerticalLineROI, numpy.array((((10., 20.), (10., 30.)), ((30., 40.), (30., 50.))))), + (roi_items.HorizontalLineROI, + numpy.array((((10., 20.), (10., 30.)), + ((30., 40.), (30., 50.))))), ) for roiClass, points in tests: @@ -246,7 +306,9 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): manager.sigRoiChanged.connect(changedListener) # Add a point - manager.createRoi(roiClass, points[0]) + r = roiClass() + r.setFirstShapePoints(points[0]) + manager.addRoi(r) self.qapp.processEvents() self.assertTrue(len(manager.getRois()), 1) self.assertEqual(changedListener.callCount(), 1) @@ -257,9 +319,13 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): self.assertEqual(changedListener.callCount(), 2) # Add two point - manager.createRoi(roiClass, points[0]) + r = roiClass() + r.setFirstShapePoints(points[0]) + manager.addRoi(r) self.qapp.processEvents() - manager.createRoi(roiClass, points[1]) + r = roiClass() + r.setFirstShapePoints(points[1]) + manager.addRoi(r) self.qapp.processEvents() self.assertTrue(len(manager.getRois()), 2) self.assertEqual(changedListener.callCount(), 4) @@ -273,9 +339,13 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): changedListener.clear() # Add two point - manager.createRoi(roiClass, points[0]) + r = roiClass() + r.setFirstShapePoints(points[0]) + manager.addRoi(r) self.qapp.processEvents() - manager.createRoi(roiClass, points[1]) + r = roiClass() + r.setFirstShapePoints(points[1]) + manager.addRoi(r) self.qapp.processEvents() self.assertTrue(len(manager.getRois()), 2) self.assertEqual(changedListener.callCount(), 2) @@ -356,6 +426,10 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) rois.append(item) + # Horizontal Range + item = roi_items.HorizontalRangeROI() + item.setRange(-1, 3) + rois.append(item) manager = roi.RegionOfInterestManager(self.plot) self.roiTableWidget.setRegionOfInterestManager(manager) @@ -370,6 +444,25 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): manager.removeRoi(item) self.qapp.processEvents() + def testSelectionProxy(self): + item1 = roi_items.PointROI() + item1.setSelectable(True) + item2 = roi_items.PointROI() + item2.setSelectable(True) + item1.setFocusProxy(item2) + manager = roi.RegionOfInterestManager(self.plot) + manager.setCurrentRoi(item1) + self.assertIs(manager.getCurrentRoi(), item2) + + def testRemovedSelection(self): + item1 = roi_items.PointROI() + item1.setSelectable(True) + manager = roi.RegionOfInterestManager(self.plot) + manager.addRoi(item1) + manager.setCurrentRoi(item1) + manager.removeRoi(item1) + self.assertIs(manager.getCurrentRoi(), None) + def testMaxROI(self): """Test Max ROI""" origin1 = numpy.array([1., 10.]) @@ -443,6 +536,86 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): manager.clear() + def testLineInteraction(self): + """This test make sure that a ROI based on handles can be edited with + the mouse.""" + xlimit = self.plot.getXAxis().getLimits() + ylimit = self.plot.getYAxis().getLimits() + points = numpy.array([xlimit, ylimit]).T + center = numpy.mean(points, axis=0) + + # Create the line + manager = roi.RegionOfInterestManager(self.plot) + item = roi_items.LineROI() + item.setEndPoints(points[0], points[1]) + item.setEditable(True) + manager.addRoi(item) + self.qapp.processEvents() + + # Drag the center + widget = self.plot.getWidgetHandle() + mx, my = self.plot.dataToPixel(*center) + self.mouseMove(widget, pos=(mx, my)) + self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my)) + self.mouseMove(widget, pos=(mx, my+50)) + self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my)) + + result = numpy.array(item.getEndPoints()) + # x location is still the same + numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5) + # size is still the same + numpy.testing.assert_allclose(points[1] - points[0], + result[1] - result[0], atol=0.5) + # But Y is not the same + self.assertNotEqual(points[0, 1], result[0, 1]) + self.assertNotEqual(points[1, 1], result[1, 1]) + item = None + manager.clear() + self.qapp.processEvents() + + def testPlotWhenCleared(self): + """PlotWidget.clear should clean up the available ROIs""" + manager = roi.RegionOfInterestManager(self.plot) + item = roi_items.LineROI() + item.setEndPoints((0, 0), (1, 1)) + item.setEditable(True) + manager.addRoi(item) + self.qWait() + try: + # Make sure the test setup is fine + self.assertNotEqual(len(manager.getRois()), 0) + self.assertNotEqual(len(self.plot.getItems()), 0) + + # Call clear and test the expected state + self.plot.clear() + self.assertEqual(len(manager.getRois()), 0) + self.assertEqual(len(self.plot.getItems()), 0) + finally: + # Clean up + manager.clear() + + def testPlotWhenRoiRemoved(self): + """Make sure there is no remaining items in the plot when a ROI is removed""" + manager = roi.RegionOfInterestManager(self.plot) + item = roi_items.LineROI() + item.setEndPoints((0, 0), (1, 1)) + item.setEditable(True) + manager.addRoi(item) + self.qWait() + try: + # Make sure the test setup is fine + self.assertNotEqual(len(manager.getRois()), 0) + self.assertNotEqual(len(self.plot.getItems()), 0) + + # Call clear and test the expected state + manager.removeRoi(item) + self.assertEqual(len(manager.getRois()), 0) + self.assertEqual(len(self.plot.getItems()), 0) + finally: + # Clean up + manager.clear() + + def suite(): test_suite = unittest.TestSuite() diff --git a/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/silx/gui/plot/tools/test/testScatterProfileToolBar.py index 714746a..b9f4885 100644 --- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py +++ b/silx/gui/plot/tools/test/testScatterProfileToolBar.py @@ -34,8 +34,9 @@ from silx.gui import qt from silx.utils.testutils import ParametricTestCase from silx.gui.utils.testutils import TestCaseQt from silx.gui.plot import PlotWindow -from silx.gui.plot.tools import profile -import silx.gui.plot.items.roi as roi_items +from silx.gui.plot.tools.profile import manager +from silx.gui.plot.tools.profile import core +from silx.gui.plot.tools.profile import rois class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): @@ -45,40 +46,25 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): super(TestScatterProfileToolBar, self).setUp() self.plot = PlotWindow() - self.profile = profile.ScatterProfileToolBar(plot=self.plot) - - self.plot.addToolBar(self.profile) + self.manager = manager.ProfileManager(plot=self.plot) + self.manager.setItemType(scatter=True) + self.manager.setActiveItemTracking(True) self.plot.show() self.qWaitForWindowExposed(self.plot) def tearDown(self): - del self.profile + del self.manager self.qapp.processEvents() self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) self.plot.close() del self.plot super(TestScatterProfileToolBar, self).tearDown() - def testNoProfile(self): - """Test ScatterProfileToolBar without profile""" - self.assertEqual(self.profile.getPlotWidget(), self.plot) - - # Add a scatter plot - self.plot.addScatter( - x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.)) - self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) - self.qapp.processEvents() - - # Check that there is no profile - self.assertIsNone(self.profile.getProfileValues()) - self.assertIsNone(self.profile.getProfilePoints()) - def testHorizontalProfile(self): """Test ScatterProfileToolBar horizontal profile""" - nPoints = 8 - self.profile.setNPoints(nPoints) - self.assertEqual(self.profile.getNPoints(), nPoints) + + roiManager = self.manager.getRoiManager() # Add a scatter plot self.plot.addScatter( @@ -86,46 +72,39 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) self.qapp.processEvents() - # Activate Horizontal profile - hlineAction = self.profile.actions()[0] - hlineAction.trigger() - self.qapp.processEvents() - # Set a ROI profile - roi = roi_items.HorizontalLineROI() + roi = rois.ProfileScatterHorizontalLineROI() roi.setPosition(0.5) - self.profile._getRoiManager().addRoi(roi) + roi.setNPoints(8) + roiManager.addRoi(roi) # Wait for async interpolator init for _ in range(20): self.qWait(200) - if not self.profile.hasPendingOperations(): + if not self.manager.hasPendingOperations(): break self.qapp.processEvents() - self.assertIsNotNone(self.profile.getProfileValues()) - points = self.profile.getProfilePoints() - self.assertEqual(len(points), nPoints) + window = roi.getProfileWindow() + self.assertIsNotNone(window) + data = window.getProfile() + self.assertIsInstance(data, core.CurveProfileData) + self.assertEqual(len(data.coords), 8) # Check that profile has same limits than Plot xLimits = self.plot.getXAxis().getLimits() - self.assertEqual(points[0, 0], xLimits[0]) - self.assertEqual(points[-1, 0], xLimits[1]) + self.assertEqual(data.coords[0], xLimits[0]) + self.assertEqual(data.coords[-1], xLimits[1]) # Clear the profile - clearAction = self.profile.actions()[-1] - clearAction.trigger() + self.manager.clearProfile() self.qapp.processEvents() - - self.assertIsNone(self.profile.getProfileValues()) - self.assertIsNone(self.profile.getProfilePoints()) - self.assertEqual(self.profile.getProfileTitle(), '') + self.assertIsNone(roi.getProfileWindow()) def testVerticalProfile(self): """Test ScatterProfileToolBar vertical profile""" - nPoints = 8 - self.profile.setNPoints(nPoints) - self.assertEqual(self.profile.getNPoints(), nPoints) + + roiManager = self.manager.getRoiManager() # Add a scatter plot self.plot.addScatter( @@ -133,55 +112,52 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) self.qapp.processEvents() - # Activate vertical profile - vlineAction = self.profile.actions()[1] - vlineAction.trigger() - self.qapp.processEvents() - # Set a ROI profile - roi = roi_items.VerticalLineROI() + roi = rois.ProfileScatterVerticalLineROI() roi.setPosition(0.5) - self.profile._getRoiManager().addRoi(roi) + roi.setNPoints(8) + roiManager.addRoi(roi) # Wait for async interpolator init for _ in range(10): self.qWait(200) - if not self.profile.hasPendingOperations(): + if not self.manager.hasPendingOperations(): break - self.assertIsNotNone(self.profile.getProfileValues()) - points = self.profile.getProfilePoints() - self.assertEqual(len(points), nPoints) + window = roi.getProfileWindow() + self.assertIsNotNone(window) + data = window.getProfile() + self.assertIsInstance(data, core.CurveProfileData) + self.assertEqual(len(data.coords), 8) # Check that profile has same limits than Plot yLimits = self.plot.getYAxis().getLimits() - self.assertEqual(points[0, 1], yLimits[0]) - self.assertEqual(points[-1, 1], yLimits[1]) + self.assertEqual(data.coords[0], yLimits[0]) + self.assertEqual(data.coords[-1], yLimits[1]) # Check that profile limits are updated when changing limits self.plot.getYAxis().setLimits(yLimits[0] + 1, yLimits[1] + 10) - self.qapp.processEvents() + + # Wait for async interpolator init + for _ in range(10): + self.qWait(200) + if not self.manager.hasPendingOperations(): + break + yLimits = self.plot.getYAxis().getLimits() - points = self.profile.getProfilePoints() - self.assertEqual(points[0, 1], yLimits[0]) - self.assertEqual(points[-1, 1], yLimits[1]) + data = window.getProfile() + self.assertEqual(data.coords[0], yLimits[0]) + self.assertEqual(data.coords[-1], yLimits[1]) - # Clear the plot - self.plot.clear() + # Clear the profile + self.manager.clearProfile() self.qapp.processEvents() - self.assertIsNone(self.profile.getProfileValues()) - self.assertIsNone(self.profile.getProfilePoints()) + self.assertIsNone(roi.getProfileWindow()) def testLineProfile(self): """Test ScatterProfileToolBar line profile""" - nPoints = 8 - self.profile.setNPoints(nPoints) - self.assertEqual(self.profile.getNPoints(), nPoints) - # Activate line profile - lineAction = self.profile.actions()[2] - lineAction.trigger() - self.qapp.processEvents() + roiManager = self.manager.getRoiManager() # Add a scatter plot self.plot.addScatter( @@ -190,19 +166,22 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): self.qapp.processEvents() # Set a ROI profile - roi = roi_items.LineROI() + roi = rois.ProfileScatterLineROI() roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.])) - self.profile._getRoiManager().addRoi(roi) + roi.setNPoints(8) + roiManager.addRoi(roi) # Wait for async interpolator init for _ in range(10): self.qWait(200) - if not self.profile.hasPendingOperations(): + if not self.manager.hasPendingOperations(): break - self.assertIsNotNone(self.profile.getProfileValues()) - points = self.profile.getProfilePoints() - self.assertEqual(len(points), nPoints) + window = roi.getProfileWindow() + self.assertIsNotNone(window) + data = window.getProfile() + self.assertIsInstance(data, core.CurveProfileData) + self.assertEqual(len(data.coords), 8) def suite(): |