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 MyObj(props.HasProperties): val = props.String()
class LutLabel(props.HasProperties): """This class represents a mapping from a value to a colour and name. ``LutLabel`` instances are created and managed by :class:`LookupTable` instances. Listeners may be registered on the :attr:`name`, :attr:`colour`, and :attr:`enabled` properties to be notified when they change. """ name = props.String(default='Label') """The display name for this label. Internally (for comparison), the :meth:`internalName` is used, which is simply this name, converted to lower case. """ colour = props.Colour(default=(0, 0, 0)) """The colour for this label. """ enabled = props.Boolean(default=True) """Whether this label is currently enabled or disabled. """ def __init__(self, value, name=None, colour=None, enabled=None): """Create a ``LutLabel``. :arg value: The label value. :arg name: The label name. :arg colour: The label colour. :arg enabled: Whether the label is enabled/disabled. """ if value is None: raise ValueError('LutLabel value cannot be None') if name is None: name = LutLabel.getProp('name').getAttribute(None, 'default') if colour is None: colour = LutLabel.getProp('colour').getAttribute(None, 'default') if enabled is None: enabled = LutLabel.getProp('enabled').getAttribute(None, 'default') self.__value = value self.name = name self.colour = colour self.enabled = enabled @property def value(self): """Returns the value of this ``LutLabel``. """ return self.__value @property def internalName(self): """Returns the *internal* name of this ``LutLabel``, which is just its :attr:`name`, converted to lower-case. This is used by :meth:`__eq__` and :meth:`__hash__`, and by the :class:`LookupTable` class. """ return self.name.lower() def __eq__(self, other): """Equality operator - returns ``True`` if this ``LutLabel`` has the same value as the given one. """ return self.value == other.value def __lt__(self, other): """Less-than operator - compares two ``LutLabel`` instances based on their value. """ return self.value < other.value def __hash__(self): """The hash of a ``LutLabel`` is a combination of its value, name, and colour, but not its enabled state. """ return (hash(self.value) ^ hash(self.internalName) ^ hash(self.colour)) def __str__(self): """Returns a string representation of this ``LutLabel``.""" return '{}: {} / {} ({})'.format(self.value, self.internalName, self.colour, self.enabled) def __repr__(self): """Returns a string representation of this ``LutLabel``.""" return self.__str__()
class DataSeries(props.HasProperties): """A ``DataSeries`` instance encapsulates some data to be plotted by a :class:`PlotPanel`, with the data extracted from an overlay in the :class:`.OverlayList`. Sub-class implementations must: - Accept an overlay object, :class:`.OverlayList`, :class:`.DisplayContext`, and :class:`.PlotPanel` in their ``__init__`` method, and pass these through to :meth:`.DataSeries.__init__`. - Override the :meth:`getData` method - Override the :meth:`redrawProperties` method if necessary The overlay is accessible as an instance attribute, confusingly called ``overlay``. .. note:: Some ``DataSeries`` instances may not be associated with an overlay (e.g. series imported loaded a text file). In this case, the ``overlay`` attribute will be ``None``. Each``DataSeries`` instance is plotted as a line, with the line style defined by properties on the ``DataSeries`` instance, such as :attr:`colour`, :attr:`lineWidth` etc. """ colour = props.Colour() """Line colour. """ enabled = props.Boolean(default=True) """Draw or not draw?""" alpha = props.Real(minval=0.0, maxval=1.0, default=1.0, clamped=True) """Line transparency.""" label = props.String() """Line label (used in the plot legend).""" lineWidth = props.Choice((0.5, 1, 2, 3, 4, 5)) """Line width. """ lineStyle = props.Choice( ('-', '--', '-.', ':', (0, (5, 7)), (0, (1, 7)), (0, (4, 10, 1, 10)), (0, (4, 1, 1, 1, 1, 1)), (0, (4, 1, 4, 1, 1, 1)))) """Line style. See https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html """ def __init__(self, overlay, overlayList, displayCtx, plotPanel): """Create a ``DataSeries``. :arg overlay: The overlay from which the data to be plotted is retrieved. May be ``None``. :arg overlayList: The :class:`.OverlayList` instance. :arg displayCtx: The :class:`.DisplayContext` instance. :arg plotPanel: The :class:`.PlotPanel` that owns this ``DataSeries``. """ self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.__overlay = overlay self.__overlayList = overlayList self.__displayCtx = displayCtx self.__plotPanel = plotPanel self.setData([], []) log.debug('{}.init ({})'.format(type(self).__name__, id(self))) def __del__(self): """Prints a log message. """ if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self))) def __hash__(self): """Returns a hash for this ``DataSeries`` instance.""" return hash(id(self)) @property def name(self): """Returns a unique name for this ``DataSeries`` instance. """ return self.__name @property def overlay(self): """Returns the overlay associated with this ``DataSeries`` instance. """ return self.__overlay @property def overlayList(self): """Returns the :class:`.OverlayList`. """ return self.__overlayList @property def displayCtx(self): """Returns the :class:`.DisplayContext`. """ return self.__displayCtx @property def plotPanel(self): """Returns the :class:`.PlotPanel` that owns this ``DataSeries`` instance. """ return self.__plotPanel def destroy(self): """This method must be called when this ``DataSeries`` instance is no longer needed. This implementation may be overridden by sub-classes which need to perform any clean-up operations. Sub-class implementations should call this implementation. """ self.__overlay = None self.__overlayList = None self.__displayCtx = None self.__plotPanel = None def redrawProperties(self): """Returns a list of all properties which, when their values change, should result in this ``DataSeries`` being re-plotted. This method may be overridden by sub-classes. """ return self.getAllProperties()[0] def extraSeries(self): """Some ``DataSeries`` types have additional ``DataSeries`` associated with them (see e.g. the :class:`.FEATTimeSeries` class). This method can be overridden to return a list of these extra ``DataSeries`` instances. The default implementation returns an empty list. """ return [] def setData(self, xdata, ydata): """Set the data to be plotted. This method is irrelevant if a ``DataSeries`` sub-class has overridden :meth:`getData`. """ self.__xdata = xdata self.__ydata = ydata def getData(self): """This method should be overridden by sub-classes. It must return the data to be plotted, as a tuple of the form: ``(xdata, ydata)`` where ``xdata`` and ``ydata`` are sequences containing the x/y data to be plotted. The default implementation returns the data that has been set via the :meth:`setData` method. """ return self.__xdata, self.__ydata
class DataSeries(props.HasProperties): """A ``DataSeries`` instance encapsulates some data to be plotted by a :class:`PlotPanel`, with the data extracted from an overlay in the :class:`.OverlayList`. Sub-class implementations must: - Accept an overlay object in their ``__init__`` method - Pass this overlay to meth:`.DataSeries.__init__` - Override the :meth:`getData` method - Override the :meth:`redrawProperties` method if necessary The overlay is accessible as an instance attribute, confusingly called ``overlay``. .. note:: Some ``DataSeries`` instances may not be associated with an overlay (e.g. series imported loaded a text file). In this case, the ``overlay`` attribute will be ``None``. Each``DataSeries`` instance is plotted as a line, with the line style defined by properties on the ``DataSeries`` instance, such as :attr:`colour`, :attr:`lineWidth` etc. """ colour = props.Colour() """Line colour. """ enabled = props.Boolean(default=True) """Draw or not draw?""" alpha = props.Real(minval=0.0, maxval=1.0, default=1.0, clamped=True) """Line transparency.""" label = props.String() """Line label (used in the plot legend).""" lineWidth = props.Choice((0.5, 1, 2, 3, 4, 5)) """Line width. """ lineStyle = props.Choice(('-', '--', '-.', ':')) """Line style. """ def __init__(self, overlay): """Create a ``DataSeries``. :arg overlay: The overlay from which the data to be plotted is retrieved. May be ``None``. """ self.__overlay = overlay self.setData([], []) log.debug('{}.init ({})'.format(type(self).__name__, id(self))) def __del__(self): """Prints a log message. """ if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self))) def __hash__(self): """Returns a hash for this ``DataSeries`` instance.""" return hash(id(self)) @property def overlay(self): """Returns the overlay associated with this ``DataSeries`` instance. """ return self.__overlay def destroy(self): """This method must be called when this ``DataSeries`` instance is no longer needed. This implementation does nothing, but it should be overridden by sub-classes which need to perform any clean-up operations. """ pass def redrawProperties(self): """Returns a list of all properties which, when their values change, should result in this ``DataSeries`` being re-plotted. This method may be overridden by sub-classes. """ return self.getAllProperties()[0] def setData(self, xdata, ydata): """Set the data to be plotted. This method is irrelevant if a ``DataSeries`` sub-class has overridden :meth:`getData`. """ self.__xdata = xdata self.__ydata = ydata def getData(self): """This method should be overridden by sub-classes. It must return the data to be plotted, as a tuple of the form: ``(xdata, ydata)`` where ``xdata`` and ``ydata`` are sequences containing the x/y data to be plotted. The default implementation returns the data that has been set via the :meth:`setData` method. """ return self.__xdata, self.__ydata
class Display(props.SyncableHasProperties): """The ``Display`` class contains display settings which are common to all overlay types. A ``Display`` instance is also responsible for managing a single :class:`DisplayOpts` instance, which contains overlay type specific display options. Whenever the :attr:`overlayType` property of a ``Display`` instance changes, the old ``DisplayOpts`` instance (if any) is destroyed, and a new one, of the correct type, created. """ name = props.String() """The overlay name. """ overlayType = props.Choice() """This property defines the overlay type - how the data is to be displayed. The options for this property are populated in the :meth:`__init__` method, from the :attr:`.displaycontext.OVERLAY_TYPES` dictionary. A :class:`DisplayOpts` sub-class exists for every possible value that this property may take. """ enabled = props.Boolean(default=True) """Should this overlay be displayed at all? """ alpha = props.Percentage(default=100.0) """Opacity - 100% is fully opaque, and 0% is fully transparent.""" brightness = props.Percentage() """Brightness - 50% is normal brightness.""" contrast = props.Percentage() """Contrast - 50% is normal contrast.""" def __init__(self, overlay, overlayList, displayCtx, parent=None, overlayType=None): """Create a :class:`Display` for the specified overlay. :arg overlay: The overlay object. :arg overlayList: The :class:`.OverlayList` instance which contains all overlays. :arg displayCtx: A :class:`.DisplayContext` instance describing how the overlays are to be displayed. :arg parent: A parent ``Display`` instance - see :mod:`props.syncable`. :arg overlayType: Initial overlay type - see the :attr:`overlayType` property. """ self.__overlay = overlay self.__overlayList = overlayList self.__displayCtx = displayCtx self.name = overlay.name # Populate the possible choices # for the overlayType property from . import getOverlayTypes ovlTypes = getOverlayTypes(overlay) ovlTypeProp = self.getProp('overlayType') log.debug('Enabling overlay types for {}: '.format(overlay, ovlTypes)) ovlTypeProp.setChoices(ovlTypes, instance=self) # Override the default overlay # type if it has been specified if overlayType is not None: self.overlayType = overlayType # Call the super constructor after our own # initialisation, in case the provided parent # has different property values to our own, # and our values need to be updated props.SyncableHasProperties.__init__( self, parent=parent, # These properties cannot be unbound, as # they affect the OpenGL representation. # The name can't be unbound either, # because it would be silly to allow # different names for the same overlay. nounbind=['overlayType', 'name'], # Initial sync state between this # Display and the parent Display # (if this Display has a parent) state=displayCtx.syncOverlayDisplay) # When the overlay type changes, the property # values of the DisplayOpts instance for the # old overlay type are stored in this dict. # If the overlay is later changed back to the # old type, its previous values are restored. # # The structure of the dictionary is: # # { (type(DisplayOpts), propName) : propValue } # # This also applies to the case where the # overlay type is changed from one type to # a related type (e.g. from VolumeOpts to # LabelOpts) - the values of all common # properties are copied to the new # DisplayOpts instance. self.__oldOptValues = td.TypeDict() # Set up listeners after caling Syncable.__init__, # so the callbacks don't get called during # synchronisation self.addListener( 'overlayType', 'Display_{}'.format(id(self)), self.__overlayTypeChanged) # The __overlayTypeChanged method creates # a new DisplayOpts instance - for this, # it needs to be able to access this # Display instance's parent (so it can # subsequently access a parent for the # new DisplayOpts instance). Therefore, # we do this after calling Syncable.__init__. self.__displayOpts = None self.__overlayTypeChanged() log.debug('{}.init ({})'.format(type(self).__name__, id(self))) def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self))) def destroy(self): """This method must be called when this ``Display`` instance is no longer needed. When a ``Display`` instance is destroyed, the corresponding :class:`DisplayOpts` instance is also destroyed. """ if self.__displayOpts is not None: self.__displayOpts.destroy() self.removeListener('overlayType', 'Display_{}'.format(id(self))) self.detachAllFromParent() self.__displayOpts = None self.__overlayList = None self.__displayCtx = None self.__overlay = None @deprecated.deprecated('0.14.3', '1.0.0', 'Use overlay instead') def getOverlay(self): """Deprecated - use :meth:`overlay` instead.""" return self.__overlay @property def overlay(self): """Returns the overlay associated with this ``Display`` instance.""" return self.__overlay @property def opts(self): """Return the :class:`.DisplayOpts` instance associated with this ``Display``, which contains overlay type specific display settings. If a ``DisplayOpts`` instance has not yet been created, or the :attr:`overlayType` property no longer matches the type of the existing ``DisplayOpts`` instance, a new ``DisplayOpts`` instance is created (and the old one destroyed if necessary). See the :meth:`__makeDisplayOpts` method. """ if (self.__displayOpts is None) or \ (self.__displayOpts.overlayType != self.overlayType): if self.__displayOpts is not None: self.__displayOpts.destroy() self.__displayOpts = self.__makeDisplayOpts() return self.__displayOpts @deprecated.deprecated('0.16.0', '1.0.0', 'Use opts instead') def getDisplayOpts(self): """Return the :class:`.DisplayOpts` instance associated with this ``Display``, which contains overlay type specific display settings. If a ``DisplayOpts`` instance has not yet been created, or the :attr:`overlayType` property no longer matches the type of the existing ``DisplayOpts`` instance, a new ``DisplayOpts`` instance is created (and the old one destroyed if necessary). See the :meth:`__makeDisplayOpts` method. """ if (self.__displayOpts is None) or \ (self.__displayOpts.overlayType != self.overlayType): if self.__displayOpts is not None: self.__displayOpts.destroy() self.__displayOpts = self.__makeDisplayOpts() return self.__displayOpts def __makeDisplayOpts(self): """Creates a new :class:`DisplayOpts` instance. The specific ``DisplayOpts`` sub-class that is created is dictated by the current value of the :attr:`overlayType` property. The :data:`.displaycontext.DISPLAY_OPTS_MAP` dictionary defines the mapping between overlay types and :attr:`overlayType` values, and ``DisplayOpts`` sub-class types. """ if self.getParent() is None: oParent = None else: oParent = self.getParent().opts from . import DISPLAY_OPTS_MAP optType = DISPLAY_OPTS_MAP[self.__overlay, self.overlayType] log.debug('Creating {} instance (synced: {}) for overlay ' '{} ({})'.format(optType.__name__, self.__displayCtx.syncOverlayDisplay, self.__overlay, self.overlayType)) volProps = optType.getVolumeProps() allProps = optType.getAllProperties()[0] initState = {} for p in allProps: if p in volProps: initState[p] = self.__displayCtx.syncOverlayVolume else: initState[p] = self.__displayCtx.syncOverlayDisplay return optType(self.__overlay, self, self.__overlayList, self.__displayCtx, parent=oParent, state=initState) def __findOptBaseType(self, optType, optName): """Finds the class, in the hierarchy of the given ``optType`` (a :class:`.DisplayOpts` sub-class) in which the given ``optName`` is defined. This method is used by the :meth:`__saveOldDisplayOpts` method, and is an annoying necessity caused by the way that the :class:`.TypeDict` class works. A ``TypeDict`` does not allow types to be used as keys - they must be strings containing the type names. Furthermore, in order for the property values of a common ``DisplayOpts`` base type to be shared across sub types (e.g. copying the :attr:`.NiftiOpts.transform` property between :class:`.VolumeOpts` and :class:`.LabelOpts` instances), we need to store the name of the common base type in the dictionary. """ for base in inspect.getmro(optType): if optName in base.__dict__: return base return None def __saveOldDisplayOpts(self): """Saves the value of every property on the current :class:`DisplayOpts` instance, so they can be restored later if needed. """ opts = self.__displayOpts if opts is None: return for propName in opts.getAllProperties()[0]: base = self.__findOptBaseType(type(opts), propName) base = base.__name__ val = getattr(opts, propName) log.debug('Saving {}.{} = {} [{} {}]'.format( base, propName, val, type(opts).__name__, id(self))) self.__oldOptValues[base, propName] = val def __restoreOldDisplayOpts(self): """Restores any cached values for all of the properties on the current :class:`DisplayOpts` instance. """ opts = self.__displayOpts if opts is None: return for propName in opts.getAllProperties()[0]: try: value = self.__oldOptValues[opts, propName] if not hasattr(opts, propName): continue log.debug('Restoring {}.{} = {} [{}]'.format( type(opts).__name__, propName, value, id(self))) setattr(opts, propName, value) except KeyError: pass def __overlayTypeChanged(self, *a): """Called when the :attr:`overlayType` property changes. Makes sure that the :class:`DisplayOpts` instance is of the correct type. """ self.__saveOldDisplayOpts() self.opts self.__restoreOldDisplayOpts()
class MyObj(props.HasProperties): myint = props.Int() mybool = props.Boolean(default=False) mystr = props.String()
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)