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()
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)
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())
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)