summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/ScalarFieldView.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
committerPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
commitbfa4dba15485b4192f8bbe13345e9658c97ecf76 (patch)
treefb9c6e5860881fbde902f7cbdbd41dc4a3a9fb5d /silx/gui/plot3d/ScalarFieldView.py
parentf7bdc2acff3c13a6d632c28c4569690ab106eed7 (diff)
New upstream version 0.6.0+dfsg
Diffstat (limited to 'silx/gui/plot3d/ScalarFieldView.py')
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py270
1 files changed, 130 insertions, 140 deletions
diff --git a/silx/gui/plot3d/ScalarFieldView.py b/silx/gui/plot3d/ScalarFieldView.py
index 2eb54a3..6a4d9d4 100644
--- a/silx/gui/plot3d/ScalarFieldView.py
+++ b/silx/gui/plot3d/ScalarFieldView.py
@@ -41,15 +41,17 @@ from collections import deque
import numpy
-from silx.gui import qt
+from silx.gui import qt, icons
from silx.gui.plot.Colors import rgba
+from silx.gui.plot.Colormap import Colormap
from silx.math.marchingcubes import MarchingCubes
+from silx.math.combo import min_max
-from .scene import axes, cutplane, function, interaction, primitives, transform
+from .scene import axes, cutplane, interaction, primitives, transform
from . import scene
from .Plot3DWindow import Plot3DWindow
-
+from .tools import InteractiveModeToolBar
_logger = logging.getLogger(__name__)
@@ -245,7 +247,7 @@ class Isosurface(qt.QObject):
self._level = level
self.sigLevelChanged.emit(level)
- if numpy.isnan(self._level):
+ if not numpy.isfinite(self._level):
return
st = time.time()
@@ -265,48 +267,6 @@ class Isosurface(qt.QObject):
self._group.children = [mesh]
-class Colormap(object):
- """Description of a colormap
-
- :param str name: Name of the colormap
- :param str norm: Normalization: 'linear' (default) or 'log'
- :param float vmin:
- Lower bound of the colormap or None for autoscale (default)
- :param float vmax:
- Upper bounds of the colormap or None for autoscale (default)
- """
-
- def __init__(self, name, norm='linear', vmin=None, vmax=None):
- assert name in function.Colormap.COLORMAPS
- self._name = str(name)
-
- assert norm in ('linear', 'log')
- self._norm = str(norm)
-
- self._vmin = float(vmin) if vmin is not None else None
- self._vmax = float(vmax) if vmax is not None else None
-
- def isAutoscale(self):
- """True if both min and max are in autoscale mode"""
- return self._vmin is None or self._vmax is None
-
- def getName(self):
- """Return the name of the colormap (str)"""
- return self._name
-
- def getNorm(self):
- """Return the normalization of the colormap (str)"""
- return self._norm
-
- def getVMin(self):
- """Return the lower bound of the colormap or None"""
- return self._vmin
-
- def getVMax(self):
- """Return the upper bounds of the colormap or None"""
- return self._vmax
-
-
class SelectedRegion(object):
"""Selection of a 3D region aligned with the axis.
@@ -391,7 +351,7 @@ class CutPlane(qt.QObject):
sigPlaneChanged = qt.Signal()
"""Signal emitted when the cut plane has moved"""
- sigColormapChanged = qt.Signal(object)
+ sigColormapChanged = qt.Signal(Colormap)
"""Signal emitted when the colormap has changed
This signal provides the new colormap.
@@ -406,11 +366,7 @@ class CutPlane(qt.QObject):
def __init__(self, sfView):
super(CutPlane, self).__init__(parent=sfView)
- self._colormap = Colormap(
- name='gray', norm='linear', vmin=None, vmax=None)
-
self._dataRange = None
- self._positiveMin = None
self._plane = cutplane.CutPlane(normal=(0, 1, 0))
self._plane.alpha = 1.
@@ -418,6 +374,11 @@ class CutPlane(qt.QObject):
self._plane.addListener(self._planeChanged)
self._plane.plane.addListener(self._planePositionChanged)
+ self._colormap = Colormap(
+ name='gray', normalization='linear', vmin=None, vmax=None)
+ self.getColormap().sigChanged.connect(self._colormapChanged)
+ self._updateSceneColormap()
+
sfView.sigDataChanged.connect(self._sfViewDataChanged)
def _get3DPrimitive(self):
@@ -427,13 +388,15 @@ class CutPlane(qt.QObject):
def _sfViewDataChanged(self):
"""Handle data change in the ScalarFieldView this plane belongs to"""
self._plane.setData(self.sender().getData(), copy=False)
+
+ # Store data range info as 3-tuple of values
self._dataRange = self.sender().getDataRange()
- self._positiveMin = None
+
self.sigDataChanged.emit()
# Update colormap range when autoscale
if self.getColormap().isAutoscale():
- self._updateColormapRange()
+ self._updateSceneColormap()
def _planeChanged(self, source, *args, **kwargs):
"""Handle events from the plane primitive"""
@@ -565,7 +528,7 @@ class CutPlane(qt.QObject):
# self._plane.alpha = alpha
def getColormap(self):
- """Returns the colormap set by :meth:`getColormap`.
+ """Returns the colormap set by :meth:`setColormap`.
:return: The colormap
:rtype: Colormap
@@ -574,25 +537,38 @@ class CutPlane(qt.QObject):
def setColormap(self,
name='gray',
- norm='linear',
+ norm=None,
vmin=None,
vmax=None):
"""Set the colormap to use.
- :param str name: Name of the colormap in
+ By either providing a :class:`Colormap` object or
+ its name, normalization and range.
+
+ :param name: Name of the colormap in
'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
+ Or Colormap object.
+ :type name: str or Colormap
:param str norm: Colormap mapping: 'linear' or 'log'.
:param float vmin: The minimum value of the range or None for autoscale
:param float vmax: The maximum value of the range or None for autoscale
"""
_logger.debug('setColormap %s %s (%s, %s)',
- name, norm, str(vmin), str(vmax))
+ name, str(norm), str(vmin), str(vmax))
- self._colormap = Colormap(
- name=name, norm=norm, vmin=vmin, vmax=vmax)
+ self._colormap.sigChanged.disconnect(self._colormapChanged)
- self._updateColormapRange()
- self.sigColormapChanged.emit(self.getColormap())
+ if isinstance(name, Colormap): # Use it as it is
+ assert (norm, vmin, vmax) == (None, None, None)
+ self._colormap = name
+ else:
+ if norm is None:
+ norm = 'linear'
+ self._colormap = Colormap(
+ name=name, normalization=norm, vmin=vmin, vmax=vmax)
+
+ self._colormap.sigChanged.connect(self._colormapChanged)
+ self._colormapChanged()
def getColormapEffectiveRange(self):
"""Returns the currently used range of the colormap.
@@ -604,35 +580,29 @@ class CutPlane(qt.QObject):
"""
return self._plane.colormap.range_
- def _updateColormapRange(self):
- """Update the colormap range"""
+ def _updateSceneColormap(self):
+ """Synchronizes scene's colormap with Colormap object"""
colormap = self.getColormap()
-
- self._plane.colormap.name = colormap.getName()
- if colormap.isAutoscale():
- range_ = self._dataRange
- if range_ is None: # No data, use a default range
- range_ = 1., 10.
- else:
- range_ = colormap.getVMin(), colormap.getVMax()
-
- if colormap.getNorm() == 'linear':
- self._plane.colormap.norm = 'linear'
- self._plane.colormap.range_ = range_
-
- else: # Log
- # Make sure range is strictly positive
- if range_[0] <= 0.:
- data = self._plane.getData(copy=False)
- if data is not None:
- if self._positiveMin is None:
- # TODO compute this with the range as a combo operation
- self._positiveMin = numpy.min(data[data > 0.])
- range_ = (self._positiveMin,
- max(range_[1], self._positiveMin))
-
- self._plane.colormap.range_ = range_
- self._plane.colormap.norm = colormap.getNorm()
+ sceneCMap = self._plane.colormap
+
+ indices = numpy.linspace(0., 1., 256)
+ colormapDisp = Colormap(name=colormap.getName(),
+ normalization=Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=colormap.getColormapLUT())
+ colors = colormapDisp.applyToData(indices)
+ sceneCMap.colormap = colors
+
+ sceneCMap.norm = colormap.getNormalization()
+ range_ = colormap.getColormapRange(data=self._dataRange)
+ sceneCMap.range_ = range_
+
+ def _colormapChanged(self):
+ """Handle update of Colormap object"""
+ self._updateSceneColormap()
+ # Forward colormap changed event
+ self.sigColormapChanged.emit(self.getColormap())
class _CutPlaneImage(object):
@@ -766,7 +736,7 @@ class ScalarFieldView(Plot3DWindow):
def __init__(self, parent=None):
super(ScalarFieldView, self).__init__(parent)
self._colormap = Colormap(
- name='gray', norm='linear', vmin=None, vmax=None)
+ name='gray', normalization='linear', vmin=None, vmax=None)
self._selectedRange = None
# Store iso-surfaces
@@ -815,7 +785,7 @@ class ScalarFieldView(Plot3DWindow):
self._bbox.children = [self._group]
self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
- self._initInteractionToolBar()
+ self._initPanPlaneAction()
self._updateColors()
@@ -958,81 +928,71 @@ class ScalarFieldView(Plot3DWindow):
raise ValueError('Unknown entry tag {0}.'
''.format(itemId))
- def _initInteractionToolBar(self):
- self._interactionToolbar = qt.QToolBar()
- self._interactionToolbar.setEnabled(False)
-
- group = qt.QActionGroup(self._interactionToolbar)
- group.setExclusive(True)
-
- self._cameraAction = qt.QAction(None)
- self._cameraAction.setText('camera')
- self._cameraAction.setCheckable(True)
- self._cameraAction.setToolTip('Control camera')
- self._cameraAction.setChecked(True)
- group.addAction(self._cameraAction)
-
- self._planeAction = qt.QAction(None)
- self._planeAction.setText('plane')
- self._planeAction.setCheckable(True)
- self._planeAction.setToolTip('Control cutting plane')
- group.addAction(self._planeAction)
- group.triggered.connect(self._interactionChanged)
-
- self._interactionToolbar.addActions(group.actions())
- self.addToolBar(self._interactionToolbar)
+ def _initPanPlaneAction(self):
+ """Creates and init the pan plane action"""
+ self._panPlaneAction = qt.QAction(self)
+ self._panPlaneAction.setIcon(icons.getQIcon('3d-plane-pan'))
+ self._panPlaneAction.setText('plane')
+ self._panPlaneAction.setCheckable(True)
+ self._panPlaneAction.setToolTip('pan the cutting plane')
+ self._panPlaneAction.setEnabled(False)
+
+ self._panPlaneAction.triggered[bool].connect(self._planeActionTriggered)
+ self.getPlot3DWidget().sigInteractiveModeChanged.connect(
+ self._interactiveModeChanged)
+
+ toolbar = self.findChild(InteractiveModeToolBar)
+ if toolbar is not None:
+ toolbar.addAction(self._panPlaneAction)
+
+ def _planeActionTriggered(self, checked=False):
+ self._panPlaneAction.setChecked(True)
+ self.setInteractiveMode('plane')
+
+ def _interactiveModeChanged(self):
+ self._panPlaneAction.setChecked(self.getInteractiveMode() == 'plane')
+ self._updateColors()
def _planeVisibilityChanged(self, visible):
"""Handle visibility events from the plane"""
- if visible != self._interactionToolbar.isEnabled():
+ if visible != self._panPlaneAction.isEnabled():
+ self._panPlaneAction.setEnabled(visible)
if visible:
- self._interactionToolbar.setEnabled(True)
self.setInteractiveMode('plane')
- else:
- self._interactionToolbar.setEnabled(False)
- self.setInteractiveMode('camera')
-
- def _interactionChanged(self, action):
- self.setInteractiveMode(action.text())
+ elif self._panPlaneAction.isChecked():
+ self.setInteractiveMode('rotate')
def setInteractiveMode(self, mode):
"""Choose the current interaction.
- :param str mode: Either plane or camera
+ :param str mode: Either rotate, pan or plane
"""
if mode == self.getInteractiveMode():
return
sceneScale = self.getPlot3DWidget().viewport.scene.transforms[0]
if mode == 'plane':
+ self.getPlot3DWidget().setInteractiveMode(None)
+
self.getPlot3DWidget().eventHandler = \
- interaction.PanPlaneRotateCameraControl(
+ interaction.PanPlaneZoomOnWheelControl(
self.getPlot3DWidget().viewport,
self._cutPlane._get3DPrimitive(),
mode='position',
scaleTransform=sceneScale)
- self._planeAction.setChecked(True)
- elif mode == 'camera':
- self.getPlot3DWidget().eventHandler = interaction.CameraControl(
- self.getPlot3DWidget().viewport, orbitAroundCenter=False,
- mode='position', scaleTransform=sceneScale,
- selectCB=None)
- self._cameraAction.setChecked(True)
else:
- raise ValueError('Unsupported interactive mode %s', str(mode))
+ self.getPlot3DWidget().setInteractiveMode(mode)
self._updateColors()
def getInteractiveMode(self):
"""Returns the current interaction mode, see :meth:`setInteractiveMode`
"""
- if isinstance(self.getPlot3DWidget().eventHandler,
- interaction.PanPlaneRotateCameraControl):
+ if (isinstance(self.getPlot3DWidget().eventHandler,
+ interaction.PanPlaneZoomOnWheelControl) or
+ self.getPlot3DWidget().eventHandler is None):
return 'plane'
- elif isinstance(self.getPlot3DWidget().eventHandler,
- interaction.CameraControl):
- return 'camera'
else:
- raise RuntimeError('Unknown interactive mode')
+ return self.getPlot3DWidget().getInteractiveMode()
# Handle scalar field
@@ -1063,7 +1023,18 @@ class ScalarFieldView(Plot3DWindow):
previousSelectedRegion = self.getSelectedRegion()
self._data = data
- self._dataRange = self._data.min(), self._data.max()
+
+ # Store data range info
+ dataRange = min_max(self._data, min_positive=True, finite=True)
+ if dataRange.minimum is None: # Only non-finite data
+ dataRange = None
+
+ if dataRange is not None:
+ min_positive = dataRange.min_positive
+ if min_positive is None:
+ min_positive = float('nan')
+ dataRange = dataRange.minimum, min_positive, dataRange.maximum
+ self._dataRange = dataRange
if previousSelectedRegion is not None:
# Update selected region to ensure it is clipped to array range
@@ -1094,7 +1065,12 @@ class ScalarFieldView(Plot3DWindow):
return numpy.array(self._data, copy=copy)
def getDataRange(self):
- """Return the range of the data as a 2-tuple (min, max)"""
+ """Return the range of the data as a 3-tuple of values.
+
+ positive min is NaN if no data is positive.
+
+ :return: (min, positive min, max) or None.
+ """
return self._dataRange
# Transformations
@@ -1135,6 +1111,20 @@ class ScalarFieldView(Plot3DWindow):
# Axes labels
+ def isBoundingBoxVisible(self):
+ """Returns axes labels, grid and bounding box visibility.
+
+ :rtype: bool
+ """
+ return self._bbox.boxVisible
+
+ def setBoundingBoxVisible(self, visible):
+ """Set axes labels, grid and bounding box visibility.
+
+ :param bool visible: True to show axes, False to hide
+ """
+ self._bbox.boxVisible = bool(visible)
+
def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
"""Set the text labels of the axes.