class Thing(props.HasProperties):

        myobject     = props.Object()
        mybool       = props.Boolean()
        myint        = props.Int()
        myreal       = props.Real()
        mypercentage = props.Percentage()
        mystring     = props.String()
        mychoice     = props.Choice(('1', '2', '3', '4', '5'))
        myfilepath   = props.FilePath()
        mylist       = props.List()
        mycolour     = props.Colour()
        mycolourmap  = props.ColourMap()
        mybounds     = props.Bounds(ndims=2)
        mypoint      = props.Point(ndims=2)
        myarray      = props.Array()
Esempio n. 2
0
class VectorOpts(niftiopts.NiftiOpts):
    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts`,
    :class:`RGBVectorOpts`, :class:`.TensorOpts`, and :class:`.SHOpts`. It
    contains display settings which are common to each of them.


    *A note on orientation*


    The :attr:`orientFlip` property allows you to flip the left-right
    orientation of line vectors, tensors, and SH functions. This option is
    necessary, because different tools may output vector data in different
    ways, depending on the image orientation.


    For images which are stored radiologically (with the X axis increasaing
    from right to left), the FSL tools (e.g. `dtifit`) will generate vectors
    which are oriented according to the voxel coordinate system. However, for
    neurologically stored images (X axis increasing from left to right), FSL
    tools generate vectors which are radiologically oriented, and thus are
    inverted with respect to the X axis in the voxel coordinate system.
    Therefore, in order to correctly display vectors from such an image, we
    must flip each vector about the X axis.


    This issue is also applicable to ``tensor`` and ``sh`` overlays.
    """


    xColour = props.Colour(default=(1.0, 0.0, 0.0))
    """Colour used to represent the X vector magnitude."""


    yColour = props.Colour(default=(0.0, 1.0, 0.0))
    """Colour used to represent the Y vector magnitude."""


    zColour = props.Colour(default=(0.0, 0.0, 1.0))
    """Colour used to represent the Z vector magnitude."""


    suppressX = props.Boolean(default=False)
    """Do not use the X vector magnitude to colour vectors."""


    suppressY = props.Boolean(default=False)
    """Do not use the Y vector magnitude to colour vectors."""


    suppressZ = props.Boolean(default=False)
    """Do not use the Z vector magnitude to colour vectors."""


    suppressMode = props.Choice(('white', 'black', 'transparent'))
    """How vector direction colours should be suppressed. """


    orientFlip = props.Boolean(default=True)
    """If ``True``, individual vectors are flipped along the x-axis. This
    property is only applicable to the :class:`.LineVectorOpts`,
    :class:`.TensorOpts`, and :class:`.SHOpts` classes. See the
    :meth:`.NiftiOpts.getTransform` method for more information.

    This value defaults to ``True`` for images which have a neurological
    storage order, and ``False`` for radiological images.
    """


    cmap = props.ColourMap()
    """If an image is selected as the :attr:`colourImage`, this colour map
    is used to colour the vector voxels.
    """


    colourImage = props.Choice()
    """Colour vector voxels by the values contained in this image. Any image which
    is in the :class:`.OverlayList`, and which has the same voxel dimensions as
    the vector image can be selected for modulation. If a ``colourImage`` is
    selected, the :attr:`xColour`, :attr:`yColour`, :attr:`zColour`,
    :attr:`suppressX`, :attr:`suppressY`, and :attr:`suppressZ` properties are
    all ignored.
    """


    modulateImage  = props.Choice()
    """Modulate the vector colour brightness by another image. Any image which
    is in the :class:`.OverlayList`, and which has the same voxel dimensions as
    the vector image can be selected for modulation.
    """


    clipImage = props.Choice()
    """Clip voxels from the vector image according to another image. Any image
    which is in the :class:`.OverlayList`, and which has the same voxel
    dimensions as the vector image can be selected for clipping. The
    :attr:`clippingRange` dictates the value below which vector voxels are
    clipped.
    """


    clippingRange = props.Bounds(ndims=1)
    """Hide voxels for which the :attr:`clipImage` value is outside of this
    range.
    """


    modulateRange = props.Bounds(ndims=1)
    """Data range used in brightness modulation, when a :attr:`modulateImage`
    is in use.
    """


    def __init__(self, image, *args, **kwargs):
        """Create a ``VectorOpts`` instance for the given image.  All
        arguments are passed through to the :class:`.NiftiOpts`
        constructor.
        """

        # The orientFlip property defaults to True
        # for neurologically stored images. We
        # give it this vale before calling __init__,
        # because  if this VectorOptse instance has
        # a parent, we want to inherit the parent's
        # value.
        self.orientFlip = image.isNeurological()

        niftiopts.NiftiOpts.__init__(self, image, *args, **kwargs)

        self.__registered = self.getParent() is not None

        if self.__registered:

            self.overlayList.addListener('overlays',
                                         self.name,
                                         self.__overlayListChanged)
            self            .addListener('clipImage',
                                         self.name,
                                         self.__clipImageChanged)
            self            .addListener('modulateImage',
                                         self.name,
                                         self.__modulateImageChanged)

            if not self.isSyncedToParent('modulateImage'):
                self.__refreshAuxImage('modulateImage')
            if not self.isSyncedToParent('clipImage'):
                self.__refreshAuxImage('clipImage')
            if not self.isSyncedToParent('colourImage'):
                self.__refreshAuxImage('colourImage')

        else:
            self.__overlayListChanged()
            self.__clipImageChanged()
            self.__modulateImageChanged()


    def destroy(self):
        """Removes some property listeners, and calls the
        :meth:`.NiftiOpts.destroy` method.
        """
        if self.__registered:
            self.overlayList.removeListener('overlays',      self.name)
            self            .removeListener('clipImage',     self.name)
            self            .removeListener('modulateImage', self.name)

        niftiopts.NiftiOpts.destroy(self)


    def __clipImageChanged(self, *a):
        """Called when the :attr:`clipImage` property changes. Updates
        the range of the :attr:`clippingRange` property.
        """

        image = self.clipImage

        if image is None:
            self.clippingRange.xmin = 0
            self.clippingRange.xmax = 1
            self.clippingRange.x    = [0, 1]
            return

        minval, maxval = image.dataRange

        # Clipping works with <= and >=, so
        # we add an offset allowing the user
        # to configure the overlay such that
        # no voxels are clipped.
        distance = (maxval - minval) / 100.0

        self.clippingRange.xmin =  minval - distance
        self.clippingRange.xmax =  maxval + distance
        self.clippingRange.x    = [minval, maxval + distance]


    def __modulateImageChanged(self, *a):
        """Called when the :attr:`modulateImage` property changes. Updates
        the range of the :attr:`modulateRange` property.
        """

        image = self.modulateImage

        if image is None: minval, maxval = 0, 1
        else:             minval, maxval = image.dataRange

        self.modulateRange.xmin = minval
        self.modulateRange.xmax = maxval
        self.modulateRange.x    = [minval, maxval]


    def __overlayListChanged(self, *a):
        """Called when the overlay list changes. Updates the :attr:`modulateImage`,
        :attr:`colourImage` and :attr:`clipImage` properties so that they
        contain a list of overlays which could be used to modulate the vector
        image.
        """

        overlays = self.displayCtx.getOrderedOverlays()

        # the image for this VectorOpts
        # instance has been removed
        if self.overlay not in overlays:
            return

        self.__refreshAuxImage('modulateImage')
        self.__refreshAuxImage('clipImage')
        self.__refreshAuxImage('colourImage')


    def __refreshAuxImage(self, imageName):
        """Updates the named image property (:attr:`modulateImage`,
        :attr:`colourImage` or :attr:`clipImage`) so that it contains a list
        of overlays which could be used to modulate the vector image.
        """

        prop     = self.getProp(imageName)
        val      = getattr(self, imageName)
        overlays = self.displayCtx.getOrderedOverlays()

        options = [None]

        for overlay in overlays:

            # It doesn't make sense to
            # modulate/clip/colour the
            # image by itself.
            if overlay is self.overlay:
                continue

            # The modulate/clip/colour
            # images must be images.
            if not isinstance(overlay, fslimage.Image):
                continue

            options.append(overlay)

        prop.setChoices(options, instance=self)

        if val in options: setattr(self, imageName, val)
        else:              setattr(self, imageName, None)
Esempio n. 3
0
class ColourMapOpts(object):
    """The ``ColourMapOpts`` class is a mixin for use with
    :class:`.DisplayOpts` sub-classes. It provides properties and logic
    for displaying overlays which are coloured according to some data values.
    See the :class:`.MeshOpts` and :class:`.VolumeOpts` classes for examples
    of classes which inherit from this class.


    To use the ``ColourMapOpts`` class, you must:

      1. Define your class to inherit from both :class:`.DisplayOpts` and
         ``ColourMapOpts``::

             class MyOpts(DisplayOpts, ColourMapOpts):
                 ...

      2. Call the ``ColourMapOpts.__init__`` method *after*
         :meth:`.DisplayOpts.__init__`::

             def __init__(self, *args, **kwargs):
                 DisplayOpts.__init__(self, *args, **kwargs)
                 ColourMapOpts.__init__(self)

      3. Implement the :meth:`getDataRange` and (if necessary)
         :meth:`getClippingRange` methods.

      4. Call :meth:`updateDataRange` whenever the data driving the colouring
         changes.


    The ``ColourMapOpts`` class links the :attr:`.Display.brightness` and
    :attr:`.Display.contrast` properties to its own :attr:`displayRange`
    property, so changes in either of the former will result in a change to
    the latter, and vice versa. This relationship is defined by the
    :func:`~.colourmaps.displayRangeToBricon` and
    :func:`~.colourmaps.briconToDisplayRange` functions, in the
    :mod:`.colourmaps` module.


    ``ColourMapOpts`` instances provide the following methods:

    .. autosummary::
       :nosignatures:

       updateDataRange
       getDataRange
       getClippingRange
    """


    displayRange = props.Bounds(ndims=1, clamped=False)
    """Values which map to the minimum and maximum colour map colours.

    .. note:: The values that this property can take are unbound because of
              the interaction between it and the :attr:`.Display.brightness`
              and :attr:`.Display.contrast` properties.  The
              :attr:`displayRange` and :attr:`clippingRange` properties are
              not clamped (they can take values outside of their
              minimum/maximum values) because the data range for large NIFTI
              images may not be known, and may change as more data is read
              from disk.
    """


    clippingRange = props.Bounds(ndims=1, clamped=False)
    """Values outside of this range are not shown.  Clipping works as follows:

     - Values less than or equal to the minimum clipping value are
       clipped.

     - Values greater than or equal to the maximum clipping value are
       clipped.

    Because of this, a small amount of padding is added to the low and high
    clipping range limits, to make it possible for all values to be
    displayed.
    """


    invertClipping = props.Boolean(default=False)
    """If ``True``, the behaviour of :attr:`clippingRange` is inverted, i.e.
    values inside the clipping range are clipped, instead of those outside
    the clipping range.
    """


    cmap = props.ColourMap()
    """The colour map, a :class:`matplotlib.colors.Colourmap` instance."""


    gamma = props.Real(minval=-1, maxval=1, clamped=True, default=0)
    """Gamma correction factor - exponentially weights the :attr:`cmap`
    and :attr:`negCmap` towards the low or high ends.

    This property takes values between -1 and +1. The exponential weight
    that should actually be used to apply gamma correction should be derived
    as follows:

      - -1 corresponds to a gamma of 0.01
      -  0 corresponds to a gamma of 1
      - +1 corresponds to a gamma of 10

    The :meth:`realGamma` method will apply this scaling and return the
    exponent to be used.
    """


    cmapResolution = props.Int(minval=2, maxval=1024, default=256)
    """Resolution for the colour map, i.e. the number of colours to use. """


    interpolateCmaps = props.Boolean(default=False)
    """If ``True``, the colour maps are applied using linear interpolation.
    Otherwise they are applied using nearest neighbour interpolation.
    """


    negativeCmap = props.ColourMap()
    """A second colour map, used if :attr:`useNegativeCmap` is ``True``.
    When active, the :attr:`cmap` is used to colour positive values, and
    the :attr:`negativeCmap` is used to colour negative values.
    """


    useNegativeCmap = props.Boolean(default=False)
    """When ``True``, the :attr:`cmap` is used to colour positive values,
    and the :attr:`negativeCmap` is used to colour negative values.
    When this property is enabled, the minimum value for both the
    :attr:`displayRange` and :attr:`clippingRange` is set to zero. Both
    ranges are applied to positive values, and negated/inverted for negative
    values.

    .. note:: When this property is set to ``True``, the
              :attr:`.Display.brightness` and :attr:`.Display.contrast`
              properties are disabled, as managing the interaction between
              them would be far too complicated.
    """


    invert = props.Boolean(default=False)
    """Use an inverted version of the current colour map (see the :attr:`cmap`
    property).
    """


    linkLowRanges = props.Boolean(default=True)
    """If ``True``, the low bounds on both the :attr:`displayRange` and
    :attr:`clippingRange` ranges will be linked together.
    """


    linkHighRanges = props.Boolean(default=False)
    """If ``True``, the high bounds on both the :attr:`displayRange` and
    :attr:`clippingRange` ranges will be linked together.
    """


    @staticmethod
    def realGamma(gamma):
        """Return the value of ``gamma`` property, scaled appropriately.
        for use as an exponent.
        """

        # a gamma in the range [-1, 0]
        # gets scaled to [0.01, 1]
        if gamma < 0:
            return (gamma + 1.01) * 0.99

        # a gamma in the range [0, 1]
        # gets scaled to [1, 10]
        else:
            return 1 + 9 * gamma


    def __init__(self):
        """Create a ``ColourMapOpts`` instance. This must be called
        *after* the :meth:`.DisplayOpts.__init__` method.
        """

        # The displayRange property of every child ColourMapOpts
        # instance is linked to the corresponding
        # Display.brightness/contrast properties, so changes
        # in one are reflected in the other. This interaction
        # complicates the relationship between parent and child
        # ColourMapOpts instances, so we only implement it on
        # children.
        #
        # NOTE: This means that if we use a parent-less
        #       DisplayContext for display, this bricon-display
        #       range relationship will break.
        #
        self.__registered = self.getParent() is not None

        if self.__registered:

            name    = self.getColourMapOptsListenerName()
            display = self.display

            display    .addListener('brightness',
                                    name,
                                    self.__briconChanged,
                                    immediate=True)
            display    .addListener('contrast',
                                    name,
                                    self.__briconChanged,
                                    immediate=True)
            self       .addListener('displayRange',
                                    name,
                                    self.__displayRangeChanged,
                                    immediate=True)
            self       .addListener('useNegativeCmap',
                                    name,
                                    self.__useNegativeCmapChanged,
                                    immediate=True)
            self       .addListener('linkLowRanges',
                                    name,
                                    self.__linkLowRangesChanged,
                                    immediate=True)
            self       .addListener('linkHighRanges',
                                    name,
                                    self.__linkHighRangesChanged,
                                    immediate=True)

            # Because displayRange and bri/con are intrinsically
            # linked, it makes no sense to let the user sync/unsync
            # them independently. So here we are binding the boolean
            # sync properties which control whether the dRange/bricon
            # properties are synced with their parent. So when one
            # property is synced/unsynced, the other ones are too.
            self.bindProps(self   .getSyncPropertyName('displayRange'),
                           display,
                           display.getSyncPropertyName('brightness'))
            self.bindProps(self   .getSyncPropertyName('displayRange'),
                           display,
                           display.getSyncPropertyName('contrast'))

            # If useNegativeCmap, linkLowRanges or linkHighRanges
            # have been set to True (this will happen if they
            # are true on the parent VolumeOpts instance), make
            # sure the property / listener states are up to date.
            if self.linkLowRanges:   self.__linkLowRangesChanged()
            if self.linkHighRanges:  self.__linkHighRangesChanged()
            if self.useNegativeCmap:
                self.__useNegativeCmapChanged(updateDataRange=False)

        # If this is the parent ColourMapOpts
        # instance, its properties need to be
        # initialised. Child instance properties
        # should inherit the current parent
        # values, unless they are not synced
        # to the parent.
        if (not self.__registered) or \
           (not self.isSyncedToParent('displayRange')):
            self.updateDataRange(False, False)


    def getColourMapOptsListenerName(self):
        """Returns the name used by this ``ColourMapOpts`` instance for
        registering internal property listeners.

        Sibling ``ColourMapOpts``
        instances need to toggle each other's property listeners (see the
        :meth:`__toggleListeners` method), so they use this method to
        retrieve each other's listener names.
        """
        return 'ColourMapOpts_{}'.format(id(self))


    def destroy(self):
        """Must be called when this ``ColourMapOpts`` is no longer needed,
        and before :meth:`.DisplayOpts.destroy` is called. Removes property
        listeners.
        """

        if not self.__registered:
            return

        display = self.display
        name    = self.getColourMapOptsListenerName()

        display.removeListener('brightness',      name)
        display.removeListener('contrast',        name)
        self   .removeListener('displayRange',    name)
        self   .removeListener('useNegativeCmap', name)
        self   .removeListener('linkLowRanges',   name)
        self   .removeListener('linkHighRanges',  name)

        self.unbindProps(self   .getSyncPropertyName('displayRange'),
                         display,
                         display.getSyncPropertyName('brightness'))
        self.unbindProps(self   .getSyncPropertyName('displayRange'),
                         display,
                         display.getSyncPropertyName('contrast'))

        self.__linkRangesChanged(False, 0)
        self.__linkRangesChanged(False, 1)


    def getDataRange(self):
        """Must be overridden by sub-classes. Must return the range of the
        data used for colouring as a ``(min, max)`` tuple.  Note that, even

        if there is no effective data range, you should return two different
        values for ``min`` and ``max`` (e.g. ``(0, 1)``), because otherwise
        the relationship between the :attr:`displayRange` and the
        :attr:`.Display.brightness` and :attr:`.Display.contrast` properties
        will be corrupted.
        """

        raise NotImplementedError('ColourMapOpts.getDataRange must be '
                                  'implemented by sub-classes.')


    def getClippingRange(self):
        """Can be overridden by sub-classes if necessary. If the clipping
        range is always the same as the data range, this method does not
        need to be overridden.

        Otherwise, if the clipping range differs from the data range
        (see e.g. the :attr:`.VolumeOpts.clipImage` property), this method
        must return the clipping range as a ``(min, max)`` tuple.

        When a sub-class implementation wishes to use the default clipping
        range/behaviour, it should return the value returned by this
        base-class implementation.
        """
        return None


    @actions.action
    def resetDisplayRange(self):
        """Resets the :attr:`displayRange` and :attr:`clippingRange` to their
        initial values.
        """
        self.updateDataRange(True, True)


    def updateDataRange(self, resetDR=True, resetCR=True):
        """Must be called by sub-classes whenever the ranges of the underlying
        data or clipping values change.  Configures the minimum/maximum bounds
        of the :attr:`displayRange` and :attr:`clippingRange` properties.

        :arg resetDR: If ``True`` (the default), the :attr:`displayRange`
                      property will be reset to the data range returned
                      by :meth:`getDataRange`. Otherwise the existing
                      value will be preserved.

        :arg resetCR: If ``True`` (the default), the :attr:`clippingRange`
                      property will be reset to the clipping range returned
                      by :meth:`getClippingRange`. Otherwise the existing
                      value will be preserved.

        Note that both of these flags will be ignored if the existing low/high
        :attr:`displayRange`/:attr:`clippingRange` values and limits are equal
        to each other.
        """

        dataMin, dataMax = self.getDataRange()
        clipRange        = self.getClippingRange()

        absolute = self.useNegativeCmap
        drmin    = dataMin
        drmax    = dataMax

        if absolute:
            drmin = min((0,            abs(dataMin)))
            drmax = max((abs(dataMin), abs(dataMax)))

        if clipRange is not None: crmin, crmax = clipRange
        else:                     crmin, crmax = drmin, drmax

        # Clipping works on >= and <=, so we add
        # a small offset to the display range limits
        # (which are equal to the clipping limiits)
        # so the user can configure the scene such
        # that no values are clipped.
        droff  = abs(drmax - drmin) / 100.0
        croff  = abs(crmax - crmin) / 100.0
        crmin -= croff
        crmax += croff
        drmin -= droff
        drmax += droff

        # Execute on the PV call queue,
        # so that property updates occur
        # in the correct order.
        def doUpdate():

            # If display/clipping limit range
            # is 0, we assume that they haven't
            # yet been set
            drUnset = (self.displayRange .xmin == self.displayRange .xmax and
                       self.displayRange .xlo  == self.displayRange .xhi)
            crUnset = (self.clippingRange.xmin == self.clippingRange.xmax and
                       self.clippingRange.xlo  == self.clippingRange.xhi)
            crGrow  =  self.clippingRange.xhi  == self.clippingRange.xmax
            drUnset =  resetDR or drUnset
            crUnset =  resetCR or crUnset

            log.debug('[{}] Updating range limits [dr: {} - {}, ''cr: '
                      '{} - {}]'.format(id(self), drmin, drmax, crmin, crmax))

            self.displayRange .xlim = drmin, drmax
            self.clippingRange.xlim = crmin, crmax

            # If the ranges have not yet been set,
            # initialise them to the min/max.
            # Also, if the high clipping range
            # was previously equal to the max
            # clipping range, keep that relationship,
            # otherwise high values will be clipped.
            if drUnset: self.displayRange .x   = drmin + droff, dataMax
            if crUnset: self.clippingRange.x   = crmin + croff, crmax
            if crGrow:  self.clippingRange.xhi = crmax

            # If using absolute range values, the low
            # display/clipping should be set to 0
            if absolute and self.displayRange .xlo < 0:
                self.displayRange.xlo  = 0
            if absolute and self.clippingRange.xlo < 0:
                self.clippingRange.xlo = 0

        props.safeCall(doUpdate)


    def __toggleListeners(self, enable=True):
        """This method enables/disables the property listeners which
        are registered on the :attr:`displayRange` and
        :attr:`.Display.brightness`/:attr:`.Display.contrast`/properties.

        Because these properties are linked via the
        :meth:`__displayRangeChanged` and :meth:`__briconChanged` methods,
        we need to be careful about avoiding recursive callbacks.

        Furthermore, because the properties of both :class:`ColourMapOpts` and
        :class:`.Display` instances are possibly synchronised to a parent
        instance (which in turn is synchronised to other children), we need to
        make sure that the property listeners on these other sibling instances
        are not called when our own property values change. So this method
        disables/enables the property listeners on all sibling
        ``ColourMapOpts`` and ``Display`` instances.
        """

        parent = self.getParent()

        # this is the parent instance
        if parent is None:
            return

        # The parent.getChildren() method will
        # contain this ColourMapOpts instance,
        # so the below loop toggles listeners
        # for this instance and all of the other
        # children of the parent
        peers  = parent.getChildren()

        for peer in peers:

            name = peer.getColourMapOptsListenerName()
            bri  = peer.display.hasListener('brightness',   name)
            con  = peer.display.hasListener('contrast',     name)
            dr   = peer        .hasListener('displayRange', name)

            if enable:
                if bri: peer.display.enableListener('brightness',   name)
                if con: peer.display.enableListener('contrast',     name)
                if dr:  peer        .enableListener('displayRange', name)
            else:
                if bri: peer.display.disableListener('brightness',   name)
                if con: peer.display.disableListener('contrast',     name)
                if dr:  peer        .disableListener('displayRange', name)


    def __briconChanged(self, *a):
        """Called when the ``brightness``/``contrast`` properties of the
        :class:`.Display` instance change.

        Updates the :attr:`displayRange` property accordingly.

        See :func:`.colourmaps.briconToDisplayRange`.
        """

        dataRange = self.getDataRange()

        dlo, dhi = fslcm.briconToDisplayRange(
            dataRange,
            self.display.brightness / 100.0,
            self.display.contrast   / 100.0)

        self.__toggleListeners(False)
        self.displayRange.x = [dlo, dhi]
        self.__toggleListeners(True)


    def __displayRangeChanged(self, *a):
        """Called when the `attr:`displayRange` property changes.

        Updates the :attr:`.Display.brightness` and :attr:`.Display.contrast`
        properties accordingly.

        See :func:`.colourmaps.displayRangeToBricon`.
        """

        if self.useNegativeCmap:
            return

        dataRange = self.getDataRange()

        brightness, contrast = fslcm.displayRangeToBricon(
            dataRange, self.displayRange.x)

        self.__toggleListeners(False)

        # update bricon
        self.display.brightness = brightness * 100
        self.display.contrast   = contrast   * 100

        self.__toggleListeners(True)


    def __useNegativeCmapChanged(self, *a, **kwa):
        """Called when the :attr:`useNegativeCmap` property changes.
        Enables/disables the :attr:`.Display.brightness` and
        :attr:`.Display.contrast` properties, and calls
        :meth:`updateDataRange`.

        :arg updateDatRange: Must be passed as a keyword argument.
                             If ``True`` (the default), calls
                             :meth:`updateDataRange`.
        """

        if self.useNegativeCmap:
            self.display.disableProperty('brightness')
            self.display.disableProperty('contrast')
        else:
            self.display.enableProperty('brightness')
            self.display.enableProperty('contrast')

        if kwa.pop('updateDataRange', True):
            self.updateDataRange(resetDR=False, resetCR=False)


    def __linkLowRangesChanged(self, *a):
        """Called when the :attr:`linkLowRanges` property changes. Calls the
        :meth:`__linkRangesChanged` method.
        """
        self.__linkRangesChanged(self.linkLowRanges, 0)


    def __linkHighRangesChanged(self, *a):
        """Called when the :attr:`linkHighRanges` property changes. Calls the
        :meth:`__linkRangesChanged` method.
        """
        self.__linkRangesChanged(self.linkHighRanges, 1)


    def __linkRangesChanged(self, val, idx):
        """Called when either the :attr:`linkLowRanges` or
        :attr:`linkHighRanges` properties change. Binds/unbinds the specified
        range properties together.

        :arg val: Boolean indicating whether the range values should be
                  linked or unlinked.

        :arg idx: Range value index - 0 corresponds to the low range value,
                  and 1 to the high range value.
        """

        dRangePV = self.displayRange .getPropertyValueList()[idx]
        cRangePV = self.clippingRange.getPropertyValueList()[idx]

        if props.propValsAreBound(dRangePV, cRangePV) == val:
            return

        props.bindPropVals(dRangePV,
                           cRangePV,
                           bindval=True,
                           bindatt=False,
                           unbind=not val)

        if val:
            cRangePV.set(dRangePV.get())
Esempio n. 4
0
class ColourBarCanvas(props.HasProperties):
    """Contains logic to render a colour bar as an OpenGL texture.
    """

    cmap = props.ColourMap()
    """The :mod:`matplotlib` colour map to use."""

    negativeCmap = props.ColourMap()
    """Negative colour map to use, if :attr:`useNegativeCmap` is ``True``."""

    useNegativeCmap = props.Boolean(default=False)
    """Whether or not to use the :attr:`negativeCmap`.
    """

    cmapResolution = props.Int(minval=2, maxval=1024, default=256)
    """Number of discrete colours to use in the colour bar. """

    invert = props.Boolean(default=False)
    """Invert the colour map(s). """

    vrange = props.Bounds(ndims=1)
    """The minimum/maximum values to display."""

    label = props.String()
    """A label to display under the centre of the colour bar."""

    orientation = props.Choice(('horizontal', 'vertical'))
    """Whether the colour bar should be vertical or horizontal. """

    labelSide = props.Choice(('top-left', 'bottom-right'))
    """Whether the colour bar labels should be on the top/left, or bottom/right
    of the colour bar (depending upon whether the colour bar orientation is
    horizontal/vertical).
    """

    textColour = props.Colour(default=(1, 1, 1, 1))
    """Colour to use for the colour bar label. """

    bgColour = props.Colour(default=(0, 0, 0, 1))
    """Colour to use for the background. """
    def __init__(self):
        """Adds a few listeners to the properties of this object, to update
        the colour bar when they change.
        """

        self._tex = None
        self._name = '{}_{}'.format(self.__class__.__name__, id(self))

        self.addGlobalListener(self._name, self.__updateTexture)

    def __updateTexture(self, *a):
        self._genColourBarTexture()
        self.Refresh()

    def _initGL(self):
        """Called automatically by the OpenGL canvas target superclass (see the
        :class:`.WXGLCanvasTarget` and :class:`.OSMesaCanvasTarget` for
        details).

        Generates the colour bar texture.
        """
        self._genColourBarTexture()

    def destroy(self):
        """Should be called when this ``ColourBarCanvas`` is no longer needed.
        Destroys the :class:`.Texture2D` instance used to render the colour
        bar.
        """
        self.removeGlobalListener(self._name)
        self._tex.destroy()
        self._tex = None

    def _genColourBarTexture(self):
        """Generates a texture containing an image of the colour bar,
        according to the current property values.
        """

        if not self._setGLContext():
            return

        w, h = self.GetSize()

        if w < 50 or h < 50:
            return

        if self.orientation == 'horizontal':
            if self.labelSide == 'top-left': labelSide = 'top'
            else: labelSide = 'bottom'
        else:
            if self.labelSide == 'top-left': labelSide = 'left'
            else: labelSide = 'right'

        if self.cmap is None:
            bitmap = np.zeros((w, h, 4), dtype=np.uint8)
        else:

            if self.useNegativeCmap:
                negCmap = self.negativeCmap
                ticks = [0.0, 0.49, 0.51, 1.0]
                ticklabels = [
                    '{:0.2f}'.format(-self.vrange.xhi),
                    '{:0.2f}'.format(-self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xhi)
                ]
                tickalign = ['left', 'right', 'left', 'right']
            else:
                negCmap = None
                ticks = [0.0, 1.0]
                tickalign = ['left', 'right']
                ticklabels = [
                    '{:0.2f}'.format(self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xhi)
                ]

            bitmap = cbarbmp.colourBarBitmap(
                cmap=self.cmap,
                negCmap=negCmap,
                invert=self.invert,
                ticks=ticks,
                ticklabels=ticklabels,
                tickalign=tickalign,
                width=w,
                height=h,
                label=self.label,
                orientation=self.orientation,
                labelside=labelSide,
                textColour=self.textColour,
                cmapResolution=self.cmapResolution)

        if self._tex is None:
            self._tex = textures.Texture2D(
                '{}_{}'.format(type(self).__name__, id(self)), gl.GL_LINEAR)

        # The bitmap has shape W*H*4, but the
        # Texture2D instance needs it in shape
        # 4*W*H
        bitmap = np.fliplr(bitmap).transpose([2, 0, 1])

        self._tex.setData(bitmap)
        self._tex.refresh()

    def _draw(self):
        """Renders the colour bar texture using all available canvas space."""

        if self._tex is None or not self._setGLContext():
            return

        width, height = self.GetSize()

        # viewport
        gl.glViewport(0, 0, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()
        gl.glOrtho(0, 1, 0, 1, -1, 1)
        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glLoadIdentity()

        gl.glClearColor(*self.bgColour)
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        gl.glEnable(gl.GL_BLEND)
        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
        gl.glShadeModel(gl.GL_FLAT)

        self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1)