# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-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.
#
# ###########################################################################*/
"""This module provides mix-in classes for :class:`Item3D`.
"""
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
import collections
import numpy
from silx.math.combo import min_max
from ...plot.items.core import ItemMixInBase
from ...plot.items.core import ColormapMixIn as _ColormapMixIn
from ...plot.items.core import SymbolMixIn as _SymbolMixIn
from ...colors import rgba
from ..scene import primitives
from .core import Item3DChangedType, ItemChangedType
class InterpolationMixIn(ItemMixInBase):
"""Mix-in class for image interpolation mode
:param str mode: 'linear' (default) or 'nearest'
:param primitive:
scene object for which to sync interpolation mode.
This object MUST have an interpolation property that is updated.
"""
NEAREST_INTERPOLATION = 'nearest'
"""Nearest interpolation mode (see :meth:`setInterpolation`)"""
LINEAR_INTERPOLATION = 'linear'
"""Linear interpolation mode (see :meth:`setInterpolation`)"""
INTERPOLATION_MODES = NEAREST_INTERPOLATION, LINEAR_INTERPOLATION
"""Supported interpolation modes for :meth:`setInterpolation`"""
def __init__(self, mode=NEAREST_INTERPOLATION, primitive=None):
self.__primitive = primitive
self._syncPrimitiveInterpolation()
self.__interpolationMode = None
self.setInterpolation(mode)
def _setPrimitive(self, primitive):
"""Set the scene object for which to sync interpolation"""
self.__primitive = primitive
self._syncPrimitiveInterpolation()
def _syncPrimitiveInterpolation(self):
"""Synchronize scene object's interpolation"""
if self.__primitive is not None:
self.__primitive.interpolation = self.getInterpolation()
def setInterpolation(self, mode):
"""Set image interpolation mode
:param str mode: 'nearest' or 'linear'
"""
mode = str(mode)
assert mode in self.INTERPOLATION_MODES
if mode != self.__interpolationMode:
self.__interpolationMode = mode
self._syncPrimitiveInterpolation()
self._updated(Item3DChangedType.INTERPOLATION)
def getInterpolation(self):
"""Returns the interpolation mode set by :meth:`setInterpolation`
:rtype: str
"""
return self.__interpolationMode
class ColormapMixIn(_ColormapMixIn):
"""Mix-in class for Item3D object with a colormap
:param sceneColormap:
The plot3d scene colormap to sync with Colormap object.
"""
def __init__(self, sceneColormap=None):
super(ColormapMixIn, self).__init__()
self._dataRange = None
self.__sceneColormap = sceneColormap
self._syncSceneColormap()
self.sigItemChanged.connect(self.__colormapUpdated)
def __colormapUpdated(self, event):
"""Handle colormap updates"""
if event == ItemChangedType.COLORMAP:
self._syncSceneColormap()
def _setRangeFromData(self, data=None):
"""Compute the data range the colormap should use from provided data.
:param data: Data set from which to compute the range or None
"""
if data is None or len(data) == 0:
dataRange = None
else:
dataRange = min_max(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 self.getColormap().isAutoscale():
self._syncSceneColormap()
def _setSceneColormap(self, sceneColormap):
"""Set the scene colormap to sync with Colormap object.
:param sceneColormap:
The plot3d scene colormap to sync with Colormap object.
"""
self.__sceneColormap = sceneColormap
self._syncSceneColormap()
def _getSceneColormap(self):
"""Returns scene colormap that is sync"""
return self.__sceneColormap
def _syncSceneColormap(self):
"""Synchronizes scene's colormap with Colormap object"""
if self.__sceneColormap is not None:
colormap = self.getColormap()
self.__sceneColormap.colormap = colormap.getNColors()
self.__sceneColormap.norm = colormap.getNormalization()
range_ = colormap.getColormapRange(data=self._dataRange)
self.__sceneColormap.range_ = range_
class SymbolMixIn(_SymbolMixIn):
"""Mix-in class for symbol and symbolSize properties for Item3D"""
_DEFAULT_SYMBOL = 'o'
_DEFAULT_SYMBOL_SIZE = 7.0
_SUPPORTED_SYMBOLS = collections.OrderedDict((
('o', 'Circle'),
('d', 'Diamond'),
('s', 'Square'),
('+', 'Plus'),
('x', 'Cross'),
('*', 'Star'),
('|', 'Vertical Line'),
('_', 'Horizontal Line'),
('.', 'Point'),
(',', 'Pixel')))
def _getSceneSymbol(self):
"""Returns a symbol name and size suitable for scene primitives.
:return: (symbol, size)
"""
symbol = self.getSymbol()
size = self.getSymbolSize()
if symbol == ',': # pixel
return 's', 1.
elif symbol == '.': # point
# Size as in plot OpenGL backend, mimic matplotlib
return 'o', numpy.ceil(0.5 * size) + 1.
else:
return symbol, size
class PlaneMixIn(ItemMixInBase):
"""Mix-in class for plane items (based on PlaneInGroup primitive)"""
def __init__(self, plane):
assert isinstance(plane, primitives.PlaneInGroup)
self.__plane = plane
self.__plane.alpha = 1.
self.__plane.addListener(self._planeChanged)
self.__plane.plane.addListener(self._planePositionChanged)
def _getPlane(self):
"""Returns plane primitive
:rtype: primitives.PlaneInGroup
"""
return self.__plane
def _planeChanged(self, source, *args, **kwargs):
"""Handle events from the plane primitive"""
# Sync visibility
if source.visible != self.isVisible():
self.setVisible(source.visible)
def _planePositionChanged(self, source, *args, **kwargs):
"""Handle update of cut plane position and normal"""
if self.__plane.visible: # TODO send even if hidden? or send also when showing if moved while hidden
self._updated(ItemChangedType.POSITION)
# Plane position
def moveToCenter(self):
"""Move cut plane to center of data set"""
self.__plane.moveToCenter()
def isValid(self):
"""Returns whether the cut plane is defined or not (bool)"""
return self.__plane.isValid
def getNormal(self):
"""Returns the normal of the plane (as a unit vector)
:return: Normal (nx, ny, nz), vector is 0 if no plane is defined
:rtype: numpy.ndarray
"""
return self.__plane.plane.normal
def setNormal(self, normal):
"""Set the normal of the plane
:param normal: 3-tuple of float: nx, ny, nz
"""
self.__plane.plane.normal = normal
def getPoint(self):
"""Returns a point on the plane
:return: (x, y, z)
:rtype: numpy.ndarray
"""
return self.__plane.plane.point
def setPoint(self, point):
"""Set a point contained in the plane.
Warning: The plane might not intersect the bounding box of the data.
:param point: (x, y, z) position
:type point: 3-tuple of float
"""
self.__plane.plane.point = point # TODO rework according to PR #1303
def getParameters(self):
"""Returns the plane equation parameters: a*x + b*y + c*z + d = 0
:return: Plane equation parameters: (a, b, c, d)
:rtype: numpy.ndarray
"""
return self.__plane.plane.parameters
def setParameters(self, parameters):
"""Set the plane equation parameters: a*x + b*y + c*z + d = 0
Warning: The plane might not intersect the bounding box of the data.
The given parameters will be normalized.
:param parameters: (a, b, c, d) equation parameters
"""
self.__plane.plane.parameters = parameters
# Border stroke
def _setForegroundColor(self, color):
"""Set the color of the plane border.
:param color: RGBA color as 4 floats in [0, 1]
"""
self.__plane.color = rgba(color)
if hasattr(super(PlaneMixIn, self), '_setForegroundColor'):
super(PlaneMixIn, self)._setForegroundColor(color)