Example #1
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

    pos = copy.copy(SliceCanvasOpts.pos)
    """Current cursor position in the display coordinate system. The dimensions
    are in the same ordering as the display coordinate system, in contrast
    to the :attr:`SliceCanvasOpts.pos` property.
    """


    showCursor   = copy.copy(SliceCanvasOpts.showCursor)
    cursorColour = copy.copy(SliceCanvasOpts.cursorColour)
    bgColour     = copy.copy(SliceCanvasOpts.bgColour)
    zoom         = copy.copy(SliceCanvasOpts.zoom)


    showLegend = props.Boolean(default=True)
    """If ``True``, an orientation guide will be shown on the canvas. """


    legendColour = props.Colour(default=(0, 1, 0))
    """Colour to use for the legend text."""


    occlusion = props.Boolean(default=True)
    """If ``True``, objects closer to the camera will occlude objects
    further away. Toggles ``gl.DEPTH_TEST``.
    """


    light = props.Boolean(default=True)
    """If ``True``, a lighting effect is applied to compatible overlays
    in the scene.
    """


    lightPos = props.Point(ndims=3)
    """Light position in the display coordinate system. """


    offset = props.Point(ndims=2)
    """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the
    centre of the ``Scene3DCanvas``.
    """


    rotation = props.Array(
        dtype=np.float64,
        shape=(3, 3),
        resizable=False,
        default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view
    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()
Example #3
0
class LocationInfoPanel(fslpanel.FSLeyesPanel):
    """The ``LocationInfoPanel`` is a panel which is embedded in the
    :class:`LocationPanel`, and which contains controls allowing the user to
    view and modify the :attr:`.DisplayContext.location` property.


    The ``LocationInfoPanel`` contains two main sections:

      - A collection of controls which show the current
        :attr:`.DisplayContext.location`

      - A ``wx.html.HtmlWindow`` which displays information about the current
        :attr:`.DisplayContext.location` for all overlays in the
        :class:`.OverlayList`.


    **NIFTI overlays**


    The ``LocationInfoPanel`` is primarily designed to work with
    :class:`.Image` overlays. If the :attr:`.DisplayContext.selectedOverlay`
    is an :class:`.Image`, or has an associated reference image (see
    :meth:`.DisplayOpts.referenceImage`), the ``LocationInfoPanel`` will
    display the current :class:`.DisplayContext.location` in both the the
    voxel coordinates and world coordinates of the ``Image`` instance.


    **Other overlays**


    If the :attr:`.DisplayContext.selectedOverlay` is not an :class:`.Image`,
    or does not have an associated reference image, the ``LocationInfoPanel``
    will display the current :attr:`.DisplayContext.location` as-is (i.e. in
    the display coordinate system); furthermore, the voxel location controls
    will be disabled.


    **Location updates**


    The :data:`DISPLAYOPTS_BOUNDS` and :data:`DISPLAYOPTS_INFO` dictionaries
    contain lists of property names that the :class:`.LocationInfoPanel`
    listens on for changes, so it knows when the location widgets, and
    information about the currenty location, need to be refreshed. For
    example, when the :attr`.NiftiOpts.volume` property of a :class:`.Nifti`
    overlay changes, the volume index, and potentially the overlay
    information, needs to be updated.
    """


    voxelLocation = props.Point(ndims=3, real=False)
    """If the currently selected overlay is a :class:`.Image` instance , this
    property tracks the current :attr:`.DisplayContext.location` in voxel
    coordinates.
    """


    worldLocation = props.Point(ndims=3, real=True)
    """For :class:`.Image` overlays, this property tracks the current
    :attr:`.DisplayContext.location` in the image world coordinates. For other
    overlay types, this property tracks the current location in display
    coordinates.
    """


    def __init__(self, parent, overlayList, displayCtx, frame):

        fslpanel.FSLeyesPanel.__init__(self,
                                       parent,
                                       overlayList,
                                       displayCtx,
                                       frame,
                                       kbFocus=True)

        # Whenever the selected overlay changes,
        # a reference to it and its DisplayOpts
        # instance is stored, as property listeners
        # are registered on it (and need to be
        # de-registered later on).
        self.__registeredOverlay = None
        self.__registeredDisplay = None
        self.__registeredOpts    = None

        self.__column1 = wx.Panel(self)
        self.__column2 = wx.Panel(self)
        self.__info    = wxhtml.HtmlWindow(self)

        # HTMLWindow does not use
        # the parent font by default,
        # so we force it to at least
        # have the parent font size
        self.__info.SetStandardFonts(self.GetFont().GetPointSize())

        self.__worldLabel = wx.StaticText(
            self.__column1, label=strings.labels[self, 'worldLocation'])
        self.__volumeLabel = wx.StaticText(
            self.__column1, label=strings.labels[self, 'volume'])
        self.__voxelLabel = wx.StaticText(
            self.__column2, label=strings.labels[self, 'voxelLocation'])

        worldX, worldY, worldZ = props.makeListWidgets(
            self.__column1,
            self,
            'worldLocation',
            slider=False,
            spin=True,
            showLimits=False,
            mousewheel=True)

        voxelX, voxelY, voxelZ = props.makeListWidgets(
            self.__column2,
            self,
            'voxelLocation',
            slider=False,
            spin=True,
            spinWidth=7,
            showLimits=False,
            mousewheel=True)

        self.__worldX = worldX
        self.__worldY = worldY
        self.__worldZ = worldZ
        self.__voxelX = voxelX
        self.__voxelY = voxelY
        self.__voxelZ = voxelZ
        self.__volume = floatspin.FloatSpinCtrl(
            self.__column2,
            width=7,
            style=floatspin.FSC_MOUSEWHEEL | floatspin.FSC_INTEGER)

        self.__column1Sizer = wx.BoxSizer(wx.VERTICAL)
        self.__column2Sizer = wx.BoxSizer(wx.VERTICAL)
        self.__sizer        = wx.BoxSizer(wx.HORIZONTAL)

        self.__column1Sizer.Add(self.__worldLabel,  flag=wx.EXPAND)
        self.__column1Sizer.Add(self.__worldX,      flag=wx.EXPAND)
        self.__column1Sizer.Add(self.__worldY,      flag=wx.EXPAND)
        self.__column1Sizer.Add(self.__worldZ,      flag=wx.EXPAND)
        self.__column1Sizer.Add(self.__volumeLabel, flag=wx.ALIGN_RIGHT)

        self.__column2Sizer.Add(self.__voxelLabel, flag=wx.EXPAND)
        self.__column2Sizer.Add(self.__voxelX,     flag=wx.EXPAND)
        self.__column2Sizer.Add(self.__voxelY,     flag=wx.EXPAND)
        self.__column2Sizer.Add(self.__voxelZ,     flag=wx.EXPAND)
        self.__column2Sizer.Add(self.__volume,     flag=wx.EXPAND)

        self.__sizer.Add(self.__column1, flag=wx.EXPAND)
        self.__sizer.Add((5, -1))
        self.__sizer.Add(self.__column2, flag=wx.EXPAND)
        self.__sizer.Add((5, -1))
        self.__sizer.Add(self.__info,    flag=wx.EXPAND, proportion=1)

        self.__column1.SetSizer(self.__column1Sizer)
        self.__column2.SetSizer(self.__column2Sizer)
        self          .SetSizer(self.__sizer)

        self.overlayList.addListener('overlays',
                                     self.name,
                                     self.__selectedOverlayChanged)
        self.displayCtx .addListener('selectedOverlay',
                                     self.name,
                                     self.__selectedOverlayChanged)
        self.displayCtx .addListener('overlayOrder',
                                     self.name,
                                     self.__overlayOrderChanged)
        self.displayCtx .addListener('location',
                                     self.name,
                                     self.__displayLocationChanged)
        self.addListener(            'voxelLocation',
                                     self.name,
                                     self.__voxelLocationChanged)
        self.addListener(            'worldLocation',
                                     self.name,
                                     self.__worldLocationChanged)

        self.__selectedOverlayChanged()

        self.__worldLabel .SetMinSize(self.__calcWorldLabelMinSize())
        self.__voxelLabel .SetMinSize(self.__voxelLabel .GetBestSize())
        self.__volumeLabel.SetMinSize(self.__volumeLabel.GetBestSize())
        self.__column1    .SetMinSize(self.__column1    .GetBestSize())
        self.__column2    .SetMinSize(self.__column2    .GetBestSize())
        self.__info       .SetMinSize((100, 100))

        # Keyboard navigation - see FSLeyesPanel
        self.setNavOrder((self.__worldX,
                          self.__worldY,
                          self.__worldZ,
                          self.__voxelX,
                          self.__voxelY,
                          self.__voxelZ,
                          self.__volume))

        self.Layout()

        self.__minSize = self.__sizer.GetMinSize()
        self.SetMinSize(self.__minSize)


    def destroy(self):
        """Must be called when this ``LocationInfoPanel`` is no longer needed.
        Removes property listeners and calls :meth:`.FSLeyesPanel.destroy`.
        """

        self.overlayList.removeListener('overlays',        self.name)
        self.displayCtx .removeListener('selectedOverlay', self.name)
        self.displayCtx .removeListener('location',        self.name)

        self.__deregisterOverlay()

        fslpanel.FSLeyesPanel.destroy(self)


    def GetMinSize(self):
        """Returns the minimum size for this ``LocationInfoPanel``.

        Under Linux/GTK, the ``wx.agw.lib.aui`` layout manager seems to
        arbitrarily adjust the minimum sizes of some panels. Therefore, The
        minimum size of the ``LocationInfoPanel`` is calculated in
        :meth:`__init__`, and is fixed.
        """
        return self.__minSize


    def DoGetBestClientSize(self):
        """Returns the best size for this ``LocationInfoPanel``.
        """
        return self.__minSize


    def __calcWorldLabelMinSize(self):
        """Calculates the minimum size that the world label (the label which
        shows the coordinate space of the currently selected overlay) needs.
        Called by the :meth:`__init__` method.

        The world label displays different things depending on the currently
        selected overlay. But we want it to be a fixed size. So this method
        calculates the size of all possible values that the world label will
        display, and returns the maximum size. This is then used as the
        minimum size for the world label.
        """

        dc = wx.ClientDC(self.__worldLabel)

        width, height = 0, 0

        labelPref = strings.labels[self, 'worldLocation']
        labelSufs = [
            strings.anatomy['Nifti',
                            'space',
                            constants.NIFTI_XFORM_UNKNOWN],
            strings.anatomy['Nifti',
                            'space',
                            constants.NIFTI_XFORM_SCANNER_ANAT],
            strings.anatomy['Nifti',
                            'space',
                            constants.NIFTI_XFORM_ALIGNED_ANAT],
            strings.anatomy['Nifti',
                            'space',
                            constants.NIFTI_XFORM_TALAIRACH],
            strings.anatomy['Nifti',
                            'space',
                            constants.NIFTI_XFORM_MNI_152],
            strings.labels[self, 'worldLocation', 'unknown']
        ]

        for labelSuf in labelSufs:

            w, h = dc.GetTextExtent(labelPref + labelSuf)

            if w > width:  width  = w
            if h > height: height = h

        return width + 5, height + 5


    def __selectedOverlayChanged(self, *a):
        """Called when the :attr:`.DisplayContext.selectedOverlay` or
        :class:`.OverlayList` is changed. Registered with the new overlay,
        and refreshes the ``LocationInfoPanel`` interface accordingly.
        """

        self.__deregisterOverlay()

        if len(self.overlayList) == 0:
            self.__updateWidgets()
            self.__updateLocationInfo()

        else:
            self.__registerOverlay()
            self.__updateWidgets()
            self.__displayLocationChanged()


    def __registerOverlay(self):
        """Registers property listeners with the :class:`.Display` and
        :class:`.DisplayOpts` instances associated with the currently
        selected overlay.
        """

        overlay = self.displayCtx.getSelectedOverlay()

        if overlay is None:
            return

        display = self.displayCtx.getDisplay(overlay)
        opts    = display.opts

        self.__registeredOverlay = overlay
        self.__registeredDisplay = display
        self.__registeredOpts    = opts

        # The properties that we need to
        # listen for are specified in the
        # DISPLAYOPTS_BOUNDS and
        # DISPLAYOPTS_INFO dictionaries.
        boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True)
        infoPropNames  = DISPLAYOPTS_INFO  .get(opts, [], allhits=True)
        boundPropNames = it.chain(*boundPropNames)
        infoPropNames  = it.chain(*infoPropNames)

        # DisplayOpts instances get re-created
        # when an overlay type is changed, so
        # we need to re-register when this happens.
        display.addListener('overlayType',
                            self.name,
                            self.__selectedOverlayChanged)

        for n in boundPropNames:
            opts.addListener(n, self.name, self.__boundsOptsChanged)
        for n in infoPropNames:
            opts.addListener(n, self.name, self.__infoOptsChanged)

        # Enable the volume widget if the
        # overlay is a NIFTI image with more
        # than three dimensions, and bind
        # the widget to the volume property
        # of  the associated NiftiOpts instance
        if isinstance(overlay, fslimage.Nifti) and overlay.ndim > 3:

            props.bindWidget(self.__volume,
                             opts,
                             'volume',
                             floatspin.EVT_FLOATSPIN)

            opts.addListener('volumeDim', self.name, self.__volumeDimChanged)

            self.__volume     .Enable()
            self.__volumeLabel.Enable()
            self.__volumeDimChanged()

        # Or, if the overlay is a mesh which
        # has some time series data associated
        # with it
        elif isinstance(overlay, fslmesh.Mesh):

            props.bindWidget(self.__volume,
                             opts,
                             'vertexDataIndex',
                             floatspin.EVT_FLOATSPIN)

            opts.addListener('vertexData', self.name, self.__vertexDataChanged)
            self.__vertexDataChanged()

        else:
            self.__volume.SetRange(0, 0)
            self.__volume.SetValue(0)
            self.__volume.Disable()


    def __deregisterOverlay(self):
        """De-registers property listeners with the :class:`.Display` and
        :class:`.DisplayOpts` instances associated with the previously
        registered overlay.
        """

        opts    = self.__registeredOpts
        display = self.__registeredDisplay
        overlay = self.__registeredOverlay

        if overlay is None:
            return

        self.__registeredOpts    = None
        self.__registeredDisplay = None
        self.__registeredOverlay = None

        boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True)
        infoPropNames  = DISPLAYOPTS_INFO  .get(opts, [], allhits=True)
        boundPropNames = it.chain(*boundPropNames)
        infoPropNames  = it.chain(*infoPropNames)

        if display is not None:
            display.removeListener('overlayType', self.name)

        for p in boundPropNames: opts.removeListener(p, self.name)
        for p in infoPropNames:  opts.removeListener(p, self.name)

        if isinstance(overlay, fslimage.Nifti) and overlay.ndim > 3:
            props.unbindWidget(self.__volume,
                               opts,
                               'volume',
                               floatspin.EVT_FLOATSPIN)
            opts.removeListener('volumeDim', self.name)

        elif isinstance(overlay, fslmesh.Mesh):
            props.unbindWidget(self.__volume,
                               opts,
                               'vertexDataIndex',
                               floatspin.EVT_FLOATSPIN)
            opts.removeListener('vertexData', self.name)


    def __volumeDimChanged(self, *a):
        """Called when the selected overlay is a :class:`.Nifti`, and its
        :attr:`.NiftiOpts.volumeDim` property changes. Updates the volume
        widget.
        """
        overlay = self.__registeredOverlay
        opts    = self.__registeredOpts
        volume  = opts.volume
        vdim    = opts.volumeDim + 3

        self.__volume.SetRange(0, overlay.shape[vdim] - 1)
        self.__volume.SetValue(volume)
        self.__infoOptsChanged()


    def __vertexDataChanged(self, *a):
        """Called when the selected overlay is a :class:`.Mesh`, and
        its :attr:`.MeshOpts.vertexData` property changes. Updates the volume
        widget.
        """

        opts    = self.__registeredOpts
        vd      = opts.getVertexData()
        vdi     = opts.vertexDataIndex
        enabled = vd is not None and vd.shape[1] > 1

        self.__volume     .Enable(enabled)
        self.__volumeLabel.Enable(enabled)

        if enabled:
            self.__volume.SetRange(0, vd.shape[1] - 1)
            self.__volume.SetValue(vdi)

        self.__infoOptsChanged()


    def __boundsOptsChanged(self, *a):
        """Called when a :class:`.DisplayOpts` property associated
        with the currently selected overlay, and listed in the
        :data:`DISPLAYOPTS_BOUNDS` dictionary, changes. Refreshes the
        ``LocationInfoPanel`` interface accordingly.
        """
        self.__updateWidgets()
        self.__displayLocationChanged()


    def __infoOptsChanged(self, *a):
        """Called when a :class:`.DisplayOpts` property associated
        with the currently selected overlay, and listed in the
        :data:`DISPLAYOPTS_INFO` dictionary, changes. Refreshes the
        ``LocationInfoPanel`` interface accordingly.
        """
        self.__displayLocationChanged()


    def __overlayOrderChanged(self, *a):
        """Called when the :attr:`.DisplayContext.overlayOrder` changes,
        Refreshes the information panel.
        """
        self.__displayLocationChanged()


    def __updateWidgets(self):
        """Called by the :meth:`__selectedOverlayChanged` and
        :meth:`__displayOptsChanged` methods.  Enables/disables the
        voxel/world location and volume controls depending on the currently
        selected overlay (or reference image).
        """

        overlay = self.__registeredOverlay
        opts    = self.__registeredOpts

        if overlay is not None: refImage = opts.referenceImage
        else:                   refImage = None

        haveRef = refImage is not None

        self.__voxelX     .Enable(haveRef)
        self.__voxelY     .Enable(haveRef)
        self.__voxelZ     .Enable(haveRef)
        self.__voxelLabel .Enable(haveRef)

        ######################
        # World location label
        ######################

        label = strings.labels[self, 'worldLocation']

        if haveRef: label += strings.anatomy[refImage,
                                             'space',
                                             refImage.getXFormCode()]
        else:       label += strings.labels[ self,
                                             'worldLocation',
                                             'unknown']

        self.__worldLabel.SetLabel(label)

        ####################################
        # Voxel/world location widget limits
        ####################################

        # Figure out the limits for the
        # voxel/world location widgets
        if haveRef:
            opts     = self.displayCtx.getOpts(refImage)
            v2w      = opts.getTransform('voxel', 'world')
            shape    = refImage.shape[:3]
            vlo      = [0, 0, 0]
            vhi      = np.array(shape) - 1
            wlo, whi = transform.axisBounds(shape, v2w)
            wstep    = refImage.pixdim[:3]
        else:
            vlo     = [0, 0, 0]
            vhi     = [0, 0, 0]
            wbounds = self.displayCtx.bounds[:]
            wlo     = wbounds[0::2]
            whi     = wbounds[1::2]
            wstep   = [1, 1, 1]

        log.debug('Setting voxelLocation limits: {} - {}'.format(vlo, vhi))
        log.debug('Setting worldLocation limits: {} - {}'.format(wlo, whi))

        # Update the voxel and world location limits,
        # but don't trigger a listener callback, as
        # this would change the display location.
        widgets = [self.__worldX, self.__worldY, self.__worldZ]
        with props.suppress(self, 'worldLocation'), \
             props.suppress(self, 'voxelLocation'):

            for i in range(3):
                self.voxelLocation.setLimits(i, vlo[i], vhi[i])
                self.worldLocation.setLimits(i, wlo[i], whi[i])
                widgets[i].SetIncrement(wstep[i])


    def __displayLocationChanged(self, *a):
        """Called when the :attr:`.DisplayContext.location` changes.
        Propagates the change on to the :attr:`voxelLocation`
        and :attr:`worldLocation` properties.

        .. note:: Because the :attr:`.DisplayContext.location`,
                  :attr:`voxelLocation` and :attr:`worldLocation` properties
                  are all linked through property listeners (see
                  :meth:`props.HasProperties.addListener`), we need to be a
                  bit careful to avoid circular updates. Therefore, each of
                  the :meth:`__displayLocationChanged`,
                  :meth:`__worldLocationChanged` and
                  :meth:`__voxelLocationChanged` methods use the
                  :meth:`__prePropagate`, :meth:`__propagate`, and
                  :meth:`__postPropagate` methods to propagate changes
                  between the three location properties.
        """

        if not self or self.destroyed():
            return

        if len(self.overlayList) == 0:       return
        if self.__registeredOverlay is None: return

        self.__prePropagate()
        self.__propagate('display', 'voxel')
        self.__propagate('display', 'world')
        self.__postPropagate()
        self.__updateLocationInfo()


    def __worldLocationChanged(self, *a):
        """Called when the :attr:`worldLocation` changes.  Propagates the
        change on to the :attr:`voxelLocation` and
        :attr:`.DisplayContext.location` properties.
        """

        if len(self.overlayList) == 0:       return
        if self.__registeredOverlay is None: return

        self.__prePropagate()
        self.__propagate('world', 'voxel')
        self.__propagate('world', 'display')
        self.__postPropagate()
        self.__updateLocationInfo()


    def __voxelLocationChanged(self, *a):
        """Called when the :attr:`voxelLocation` changes.  Propagates the
        change on to the :attr:`worldLocation` and
        :attr:`.DisplayContext.location` properties.
        """

        if len(self.overlayList) == 0:       return
        if self.__registeredOverlay is None: return

        self.__prePropagate()
        self.__propagate('voxel', 'world')
        self.__propagate('voxel', 'display')
        self.__postPropagate()
        self.__updateLocationInfo()


    def __prePropagate(self):
        """Called by the :meth:`__displayLocationChanged`,
        :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged`
        methods.

        Disables notification of all location property listeners, so
        circular updates do not occur.
        """

        self           .disableNotification('voxelLocation')
        self           .disableNotification('worldLocation')
        self.displayCtx.disableListener(    'location', self.name)

        self.Freeze()


    def __propagate(self, source, target):
        """Called by the :meth:`__displayLocationChanged`,
        :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged`
        methods. Copies the coordinates from the ``source`` location to the
        ``target`` location. Valid values for the ``source`` and ``target``
        are:

        =========== ==============================================
        ``display`` The :attr:`.DisplayContext.location` property.
        ``voxel``   The :attr:`voxelLocation` property.
        ``world``   The :attr:`worldLocation` property.
        =========== ==============================================
        """

        if   source == 'display': coords = self.displayCtx.location.xyz
        elif source == 'voxel':   coords = self.voxelLocation.xyz
        elif source == 'world':   coords = self.worldLocation.xyz

        refImage = self.__registeredOpts.referenceImage

        if refImage is not None:
            opts    = self.displayCtx.getOpts(refImage)
            xformed = opts.transformCoords([coords],
                                           source,
                                           target,
                                           vround=target == 'voxel')[0]
        else:
            xformed = coords

        log.debug('Updating location ({} {} -> {} {})'.format(
            source, coords, target, xformed))

        if   target == 'display': self.displayCtx.location.xyz = xformed
        elif target == 'voxel':   self.voxelLocation      .xyz = xformed
        elif target == 'world':   self.worldLocation      .xyz = xformed


    def __postPropagate(self):
        """Called by the :meth:`__displayLocationChanged`,
        :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged`
        methods.

        Re-enables the property listeners that were disabled by the
        :meth:`__postPropagate` method.
        """
        self           .enableNotification('voxelLocation')
        self           .enableNotification('worldLocation')
        self.displayCtx.enableListener(    'location', self.name)

        self.Thaw()
        self.Refresh()
        self.Update()


    def __updateLocationInfo(self):
        """Called whenever the :attr:`.DisplayContext.location` changes.
        Updates the HTML panel which displays information about all overlays
        in the :class:`.OverlayList`.
        """

        if len(self.overlayList) == 0 or self.__registeredOverlay is None:
            self.__info.SetPage('')
            return

        # Reverse the overlay order so they
        # are ordered the same on the info
        # page as in the overlay list panel
        displayCtx = self.displayCtx
        overlays   = reversed(displayCtx.getOrderedOverlays())
        selOvl     = displayCtx.getSelectedOverlay()
        lines      = []

        for overlay in overlays:

            display = displayCtx.getDisplay(overlay)
            opts    = display.opts

            if not display.enabled:
                continue

            info = None
            title = '<b>{}</b>'.format(display.name)

            # For mesh overlays, if the current location
            # corresponds to a vertex, show some info
            # about that vertex
            if isinstance(overlay, fslmesh.Mesh):
                vidx  = opts.getVertex()
                vd    = opts.getVertexData()
                vdidx = opts.vertexDataIndex

                if vidx is None:
                    info = '[no vertex]'

                else:
                    # some vertex data has been
                    # loaded for this mesh.
                    if vd is not None:

                        # time series/multiple data points per
                        # vertex - display the time/data index
                        # as well
                        if vd.shape[1] > 1:
                            info = '[{}, {}]: {}'.format(vidx,
                                                         vdidx,
                                                         vd[vidx, vdidx])

                        # Only one scalar value per vertex -
                        # don't bother showing the vertex
                        # data index
                        else:
                            info = '[{}]: {}'.format(vidx, vd[vidx, vdidx])

                    else:
                        info = '[{}]'.format(vidx)

            elif isinstance(overlay, fslimage.Image):

                vloc = opts.getVoxel()

                if vloc is not None:
                    vloc = tuple(int(v) for v in vloc)
                    vloc = opts.index(vloc)
                    vval = overlay[vloc]
                    vloc = ' '.join(map(str, vloc))

                    if not np.isscalar(vval):
                        vval = np.asscalar(vval)

                    if opts.overlayType == 'label':
                        lbl = opts.lut.get(int(vval))
                        if lbl is None: lbl = 'no label'
                        else:           lbl = lbl.name
                        info = '[{}]: {} / {}'.format(vloc, vval, lbl)

                    else:
                        info = '[{}]: {}'.format(vloc, vval)

                else:
                    info = strings.labels[self, 'outOfBounds']
            else:
                info = '{}'.format(strings.labels[self, 'noData'])

            # Indent info for unselected overlays,
            # to make the info for the selected
            # overlay a bit more obvious.
            colourFmt = '<span style="color: #6060ff">{}</span>'
            if overlay is selOvl:
                title = colourFmt.format(title)
                if info is not None:
                    info = colourFmt.format(info)

            lines.append(title)

            if info is not None:
                lines.append(info)

        self.__info.SetPage('<br>'.join(lines))
        self.__info.Refresh()
Example #4
0
class SliceCanvasOpts(props.HasProperties):
    """The ``SliceCanvasOpts`` class defines all of the display settings
    for a :class:`.SliceCanvas`.
    """

    pos = props.Point(ndims=3)
    """The currently displayed position.

    The ``pos.x`` and ``pos.y`` positions denote the position of a *cursor*,
    which is highlighted with crosshairs (see the :attr:`showCursor`
    property). The ``pos.z`` position specifies the currently displayed slice.
    """

    zoom = props.Percentage(minval=100.0,
                            maxval=5000.0,
                            default=100.0,
                            clamped=False)
    """The :attr:`.DisplayContext.bounds` are divided by this zoom
    factor to produce the canvas display bounds.
    """

    displayBounds = props.Bounds(ndims=2, clamped=False)
    """The display bound x/y values specify the horizontal/vertical display
    range of the canvas, in display coordinates. This may be a larger area
    than the size of the displayed overlays, as it is adjusted to preserve
    the aspect ratio.
    """

    showCursor = props.Boolean(default=True)
    """If ``False``, the crosshairs which show the current cursor location
    will not be drawn.
    """

    cursorGap = props.Boolean(default=False)
    """If ``True``, and the currently selected overlay is a :class:`.Nifti`
    instance, a gap will be shown at the cursor centre (i.e. the current
    voxel).
    """

    zax = props.Choice((0, 1, 2),
                       alternates=[['x', 'X'], ['y', 'Y'], ['z', 'Z']],
                       allowStr=True)
    """The display coordinate system axis to be used as the screen *depth*
    axis. The :meth:`xax` and :meth:`yax` attributes are derived from this
    property:

     - If ``zax == 0``, ``xax, yax == 1, 2``
     - If ``zax == 1``, ``xax, yax == 0, 2``
     - If ``zax == 2``, ``xax, yax == 0, 1``
    """

    invertX = props.Boolean(default=False)
    """If ``True``, the display is inverted along the X (horizontal screen)
    axis.
    """

    invertY = props.Boolean(default=False)
    """If ``True``, the display is inverted along the Y (vertical screen)
    axis.
    """

    cursorColour = props.Colour(default=(0, 1, 0))
    """Canvas cursor colour."""

    bgColour = props.Colour(default=(0, 0, 0))
    """Canvas background colour."""

    renderMode = props.Choice(('onscreen', 'offscreen', 'prerender'))
    """How the :class:`.GLObject` instances are rendered to the canvas.

    See the :class:`.SliceCanvas` for more details.
    """

    highDpi = props.Boolean(default=False)
    """If FSLeyes is being displayed on a high-DPI screen, try to display
    the scene at full resolution.
    """
    def __init__(self):
        """Create a ``SliceCanvasOpts`` instance. """

        self.__name = '{}_{}'.format(type(self).__name__, id(self))
        self.__xax = 0
        self.__yax = 0

        self.addListener('zax', self.__name, self.__zaxChanged, immediate=True)
        self.__zaxChanged()

    def __zaxChanged(self, *a):
        """Calle when the :attr:`zax` property changes. Derives the
        ``xax`` and ``yax`` values.
        """

        dims = list(range(3))
        dims.pop(self.zax)
        self.__xax = dims[0]
        self.__yax = dims[1]

    @property
    def xax(self):
        """The display coordinate system axis which maps to the X (horizontal)
        canvas axis.
        """
        return self.__xax

    @property
    def yax(self):
        """The display coordinate system axis which maps to the Y (vertical)
        canvas axis.
        """
        return self.__yax
Example #5
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

    pos = copy.copy(SliceCanvasOpts.pos)
    """Current cursor position in the display coordinate system. The dimensions
    are in the same ordering as the display coordinate system, in contrast
    to the :attr:`SliceCanvasOpts.pos` property.
    """

    showCursor = copy.copy(SliceCanvasOpts.showCursor)
    cursorColour = copy.copy(SliceCanvasOpts.cursorColour)
    bgColour = copy.copy(SliceCanvasOpts.bgColour)
    zoom = copy.copy(SliceCanvasOpts.zoom)
    highDpi = copy.copy(SliceCanvasOpts.highDpi)

    showLegend = props.Boolean(default=True)
    """If ``True``, an orientation guide will be shown on the canvas. """

    legendColour = props.Colour(default=(0, 1, 0))
    """Colour to use for the legend text."""

    labelSize = props.Int(minval=4, maxval=96, default=12, clamped=True)
    """Font size used for the legend labels. """

    occlusion = props.Boolean(default=True)
    """If ``True``, objects closer to the camera will occlude objects
    further away. Toggles ``gl.DEPTH_TEST``.
    """

    light = props.Boolean(default=True)
    """If ``True``, a lighting effect is applied to compatible overlays
    in the scene.
    """

    showLight = props.Boolean(default=False)
    """If ``True``, a point is drawn at the current light position. """

    lightPos = props.Point(ndims=3)
    """Defines the light position in the display coordinate system. This
    property contains a set of three rotation values, in degrees.

    The lighting model uses a point source which is located a fixed distance
    away from the display coordinate system centre - the distance is set
    by the :attr:`lightDistance` property.

    The lightPos property defines how the light is rotated with respect to
    the centre of the display coordinate system.

    The :meth:`.Scene3DCanvas.lightPos` method can be used to calculate the
    actual position of the light in the display coordinate system.
    """

    lightDistance = props.Real(minval=0.5, maxval=10, default=2)
    """Distance of the light source from the centre of the display coordinate
    system. This is used as a multiplicative factor - a value of 2 set the
    light source a distance of twice the length of the display bounding box
    from the bounding box centre.
    """

    offset = props.Point(ndims=2)
    """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the
    centre of the ``Scene3DCanvas``.
    """

    rotation = props.Array(dtype=np.float64,
                           shape=(3, 3),
                           resizable=False,
                           default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view
Example #6
0
class DisplayContext(props.SyncableHasProperties):
    """A ``DisplayContext`` instance contains a number of properties defining
    how the overlays in an :class:`.OverlayList` are to be displayed, and
    related contextual information.


    A ``DisplayContext`` instance is responsible for creating and destroying
    :class:`.Display` instances for every overlay in the
    ``OverlayList``. These ``Display`` instances, and the corresponding
    :class:`.DisplayOpts` instances (which, in turn, are created/destroyed by
    ``Display`` instances) can be accessed with the :meth:`getDisplay` and
    :meth:`getOpts` method respectively.


    A number of other useful methods are provided by a ``DisplayContext``
    instance:

    .. autosummary::
       :nosignatures:

        getDisplay
        getOpts
        getReferenceImage
        displayToWorld
        worldToDisplay
        displaySpaceIsRadiological
        selectOverlay
        getSelectedOverlay
        getOverlayOrder
        getOrderedOverlays
        freeze
        freezeOverlay
        thawOverlay
        defaultDisplaySpace
        detachDisplaySpace
    """

    selectedOverlay = props.Int(minval=0, default=0, clamped=True)
    """Index of the currently 'selected' overlay.

    .. note:: The value of this index is in relation to the
              :class:`.OverlayList`, rather than to the :attr:`overlayOrder`
              list.

              If you're interested in the currently selected overlay, you must
              also listen for changes to the :attr:`.OverlayList.images` list
              as, if the list changes, the :attr:`selectedOverlay` index may
              not change, but the overlay to which it points may be different.
    """

    location = props.Point(ndims=3)
    """The location property contains the currently selected 3D location (xyz)
    in the display coordinate system. Different ``DisplayContext`` instances
    may be using different display coordinate systems - see the
    :attr:`displaySpace` property.
    """

    worldLocation = props.Point(ndims=3)
    """The location property contains the currently selected 3D location (xyz)
    in the world coordinate system. Whenever the :attr:`location` changes, it
    gets transformed into the world coordinate system, and propagated to this
    property. The location of different ``DisplayContext`` instances is
    synchronised through this property.

    .. note:: If any :attr:`.NiftiOpts.transform` properties have been modified
              independently of the :attr:`displaySpace`, this value will be
              invalid.
    """

    bounds = props.Bounds(ndims=3)
    """This property contains the min/max values of a bounding box (in display
    coordinates) which is big enough to contain all of the overlays in the
    :class:`.OverlayList`.

    .. warning:: This property shouid be treated as read-only.
    """

    overlayOrder = props.List(props.Int())
    """A list of indices into the :attr:`.OverlayList.overlays`
    list, defining the order in which the overlays are to be displayed.

    See the :meth:`getOrderedOverlays` method.
    """

    overlayGroups = props.List()
    """A list of :class:`.OverlayGroup` instances, each of which defines
    a group of overlays which share display properties.
    """

    syncOverlayDisplay = props.Boolean(default=True)
    """If this ``DisplayContext`` instance has a parent (see
    :mod:`props.syncable`), and this property is ``True``, the properties of
    the :class:`.Display` and :class:`.DisplayOpts` instances for every
    overlay managed by this ``DisplayContext`` instance will be synchronised
    to those of the parent instance. Otherwise, the display properties for
    every overlay will be unsynchronised from the parent.

    Synchronisation of the following properties between child and parent
    ``DisplayContext`` instances is also controlled by this flag:

      - :attr:`displaySpace`
      - :attr:`bounds`
      - :attr:`radioOrientation`

    .. note:: This property is accessed by the :class:`.Display` class, in its
              constructor, and when it creates new :class:`.DisplayOpts`
              instances, to set initial sync states.
    """

    displaySpace = props.Choice(('world', ))
    """The *space* in which overlays are displayed. This property defines the
    display coordinate system for this ``DisplayContext``. When it is changed,
    the :attr:`.NiftiOpts.transform` property of all :class:`.Nifti` overlays
    in the :class:`.OverlayList` is updated. It has two settings, described
    below. The options for this property are dynamically added by
    :meth:`__updateDisplaySpaceOptions`.

    1. **World** space (a.k.a. ``'world'``)

       All :class:`.Nifti` overlays are displayed in the space defined by
       their affine transformation matrix - the :attr:`.NiftiOpts.transform`
       property for every ``Nifti`` overlay is set to ``affine``.

    2. **Reference image** space

       A single :class:`.Nifti` overlay is selected as a *reference* image,
       and is displayed in scaled voxel space (with a potential L/R flip for
       neurological images - its :attr:`.NiftiOpts.transform` is set to
       ``pixdim-flip``). All other ``Nifti`` overlays are transformed into
       this reference space - their :attr:`.NiftiOpts.transform` property is
       set to ``reference``, which results in them being transformed into the
       scaled voxel space of the reference image.

    .. note:: The :attr:`.NiftiOpts.transform` property of any
              :class:`.Nifti` overlay can be set independently of this
              property. However, whenever *this* property changes, it will
              change the ``transform`` property for every ``Nifti``, in the
              manner described above.

    The :meth:`defaultDisplaySpace` can be used to control how the
    ``displaySpace`` is initialised.
    """

    radioOrientation = props.Boolean(default=True)
    """If ``True``, 2D views will show images in radiological convention
    (i.e.subject left on the right of the display). Otherwise, they will be
    shown in neurological convention (subject left on the left).

    .. note:: This setting is not enforced by the ``DisplayContext``. It is
              the responsibility of the :class:`.OrthoPanel` and
              :class:`.LightBoxPanel` (and other potential future 2D view
              panels) to implement the flip.
    """

    autoDisplay = props.Boolean(default=False)
    """If ``True``, whenever an overlay is added to the :class:`.OverlayList`,
    the :mod:`autodisplay` module will be used to automatically configure
    its display settings. Note that the ``DisplayContext`` does not perform
    this configuration - this flag is used by other modules (e.g. the
    :class:`.OverlayListPanel` and the :class:`.OpenFileAction`).
    """

    loadInMemory = props.Boolean(default=False)
    """If ``True``, all :class:`.Image` instances will be loaded into memory,
    regardless of their size. Otherwise (the default), large compressed
    ``Image`` overlays may be kept on disk.


    .. note:: Changing the value of this property will not affect existing
              ``Image`` overlays.


    .. note:: This property may end up being used in a more general sense by
              any code which needs to decide whether to do things in a more
              or less memory-intensive manner.
    """
    def __init__(self, overlayList, parent=None, defaultDs='ref', **kwargs):
        """Create a ``DisplayContext``.

        :arg overlayList: An :class:`.OverlayList` instance.

        :arg parent:      Another ``DisplayContext`` instance to be used
                          as the parent of this instance, passed to the
                          :class:`.SyncableHasProperties` constructor.

        :arg defaultDs:   Initial value for the :meth:`defaultDisplaySpace`.
                          Either ``'ref'`` or ``'world'``. If ``'ref'`` (the
                          default), when overlays are added to an empty list,
                          the :attr:`displaySpace` will be set to the first
                          :class:`.Nifti` overlay. Otherwise (``'world'``),
                          the display space will be set to ``'world'``.

        All other arguments are passed through to the ``SyncableHasProperties``
        constructor, in addition to the following:

          - The ``syncOverlayDisplay`` and ``location`` properties
            are added to the ``nobind`` argument

          - The ``selectedOverlay``, ``overlayGroups``,
            ``autoDisplay`` and ``loadInMemory`` properties
            are added to the ``nounbind`` argument.
        """

        kwargs = dict(kwargs)

        nobind = kwargs.pop('nobind', [])
        nounbind = kwargs.pop('nounbind', [])

        nobind.extend(['syncOverlayDisplay', 'location', 'bounds'])
        nounbind.extend([
            'selectedOverlay', 'overlayGroups', 'autoDisplay', 'loadInMemory'
        ])

        kwargs['parent'] = parent
        kwargs['nobind'] = nobind
        kwargs['nounbind'] = nounbind
        kwargs['state'] = {'overlayOrder': False}

        props.SyncableHasProperties.__init__(self, **kwargs)

        self.__overlayList = overlayList
        self.__name = '{}_{}'.format(self.__class__.__name__, id(self))
        self.__child = parent is not None

        # When the first overlay(s) is/are
        # added, the display space may get
        # set either to a reference image,
        # or to world. The defaultDisplaySpace
        # controls this behaviour.
        self.defaultDisplaySpace = defaultDs

        # The overlayOrder is unsynced by
        # default, but we will inherit the
        # current parent value.
        if self.__child: self.overlayOrder[:] = parent.overlayOrder[:]
        else: self.overlayOrder[:] = range(len(overlayList))

        # If this is the first child DC, we
        # need to initialise the display
        # space and location. If there is
        # already a child DC, then we have
        # (probably) inherited initial
        # settings.
        if self.__child:
            self.__initDS = (len(parent.getChildren()) - 1) == 0

        # This dict contains the Display
        # objects for every overlay in
        # the overlay list, as
        # {Overlay : Display} mappings
        self.__displays = {}

        overlayList.addListener('overlays',
                                self.__name,
                                self.__overlayListChanged,
                                immediate=True)

        if self.__child:
            self.addListener('syncOverlayDisplay', self.__name,
                             self.__syncOverlayDisplayChanged)
            self.addListener('displaySpace',
                             self.__name,
                             self.__displaySpaceChanged,
                             immediate=True)
            self.addListener('location',
                             self.__name,
                             self.__locationChanged,
                             immediate=True)
            self.addListener('worldLocation',
                             self.__name,
                             self.__worldLocationChanged,
                             immediate=True)

        # The overlayListChanged method
        # is important - check it out
        self.__overlayListChanged()

        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 ``DisplayContext`` is no
        longer needed.

        When a ``DisplayContext`` is destroyed, all of the :class:`.Display`
        instances managed by it are destroyed as well.
        """

        self.detachAllFromParent()

        overlayList = self.__overlayList
        displays = self.__displays

        self.__overlayList = None
        self.__displays = None

        overlayList.removeListener('overlays', self.__name)

        if self.__child:
            self.removeListener('syncOverlayDisplay', self.__name)
            self.removeListener('displaySpace', self.__name)
            self.removeListener('location', self.__name)
            self.removeListener('worldLocation', self.__name)

        for overlay, display in displays.items():
            display.destroy()

    def destroyed(self):
        """Returns ``True`` if this ``DisplayContext`` has been, or is being,
        destroyed, ``False`` otherwise.
        """
        return self.__overlayList is None

    def getDisplay(self, overlay, overlayType=None):
        """Returns the :class:`.Display` instance for the specified overlay
        (or overlay index).

        If the overlay is not in the ``OverlayList``, an
        :exc:`InvalidOverlayError` is raised.  Otheriwse, if a
        :class:`Display` object does not exist for the given overlay, one is
        created.

        If this ``DisplayContext`` has been destroyed, a ``ValueError`` is
        raised.

        :arg overlay:     The overlay to retrieve a ``Display`` instance for,
                          or an index into the ``OverlayList``.

        :arg overlayType: If a ``Display`` instance for the specified
                          ``overlay`` does not exist, one is created - in
                          this case, the specified ``overlayType`` is passed
                          to the :class:`.Display` constructor.
        """

        if overlay is None:
            raise ValueError('No overlay specified')

        if self.destroyed():
            raise ValueError('DisplayContext has been destroyed')

        if overlay not in self.__overlayList:
            raise InvalidOverlayError('Overlay {} is not in '
                                      'list'.format(overlay.name))

        if isinstance(overlay, int):
            overlay = self.__overlayList[overlay]

        try:
            display = self.__displays[overlay]

        except KeyError:

            if not self.__child:
                dParent = None
            else:
                dParent = self.getParent().getDisplay(overlay, overlayType)
                if overlayType is None:
                    overlayType = dParent.overlayType

            from .display import Display

            display = Display(overlay,
                              self.__overlayList,
                              self,
                              parent=dParent,
                              overlayType=overlayType)
            self.__displays[overlay] = display

        return display

    def getOpts(self, overlay, overlayType=None):
        """Returns the :class:`.DisplayOpts` instance associated with the
        specified overlay.  See :meth:`getDisplay` and :meth:`.Display.opts`
        for more details.
        """

        if overlay is None:
            raise ValueError('No overlay specified')

        if self.destroyed():
            raise ValueError('DisplayContext has been destroyed')

        if overlay not in self.__overlayList:
            raise InvalidOverlayError('Overlay {} is not in '
                                      'list'.format(overlay.name))

        return self.getDisplay(overlay, overlayType).opts

    def getReferenceImage(self, overlay):
        """Convenience method which returns the reference image associated
        with the given overlay, or ``None`` if there is no reference image.

        See the :class:`.DisplayOpts.referenceImage` method.
        """
        if overlay is None:
            return None

        return self.getOpts(overlay).referenceImage

    def displayToWorld(self, dloc):
        """Transforms the given coordinates from the display coordinate
        system into the world coordinate system.

        .. warning:: If any :attr:`.NiftiOpts.transform` properties have
                     been modified manually, this method will return invalid
                     results.
        """

        displaySpace = self.displaySpace

        if displaySpace == 'world' or len(self.__overlayList) == 0:
            return dloc

        opts = self.getOpts(displaySpace)

        return opts.transformCoords(dloc, 'display', 'world')

    def worldToDisplay(self, wloc):
        """Transforms the given coordinates from the world coordinate
        system into the display coordinate system.

        .. warning:: If any :attr:`.NiftiOpts.transform` properties have
                     been modified manually, this method will return invalid
                     results.
        """

        displaySpace = self.displaySpace

        if displaySpace == 'world' or len(self.__overlayList) == 0:
            return wloc

        opts = self.getOpts(displaySpace)

        return opts.transformCoords(wloc, 'world', 'display')

    def displaySpaceIsRadiological(self):
        """Returns ``True`` if the current :attr:`displaySpace` aligns with
        a radiological orientation. A radiological orientation is one in
        which anatomical right is shown on the left of the screen, i.e.:

          - The X axis corresponds to right -> left
          - The Y axis corresponds to posterior -> anterior
          - The Z axis corresponds to inferior -> superior
        """

        if len(self.__overlayList) == 0:
            return True

        space = self.displaySpace

        # Display space is either 'world', or an image.
        # We assume that 'world' is an RAS coordinate
        # system which, if transferred directly to a
        # display coordinate system, would result in a
        # neurological view (left on left, right on
        # right).
        if space == 'world':
            return False
        else:
            opts = self.getOpts(space)
            xform = opts.getTransform('pixdim-flip', 'display')

            return npla.det(xform) > 0

    def selectOverlay(self, overlay):
        """Selects the specified ``overlay``. Raises an :exc:`IndexError` if
        the overlay is not in the list.

        If you want to select an overlay by its index in the ``OverlayList``,
        you can just assign to the :attr:`selectedOverlay` property directly.
        """
        self.selectedOverlay = self.__overlayList.index(overlay)

    def getSelectedOverlay(self):
        """Returns the currently selected overlay object,
        or ``None`` if there are no overlays.
        """
        if len(self.__overlayList) == 0: return None
        if self.selectedOverlay >= len(self.__overlayList): return None

        return self.__overlayList[self.selectedOverlay]

    def getOverlayOrder(self, overlay):
        """Returns the order in which the given overlay (or an index into
        the :class:`.OverlayList` list) should be displayed
        (see the :attr:`overlayOrder` property).

        Raises an :exc:`IndexError` if the overlay is not in the list.
        """
        self.__syncOverlayOrder()

        if not isinstance(overlay, int):
            overlay = self.__overlayList.index(overlay)

        return self.overlayOrder.index(overlay)

    def getOrderedOverlays(self):
        """Returns a list of overlay objects from the :class:`.OverlayList`
        list, sorted into the order that they should be displayed, as defined
        by the :attr:`overlayOrder` property.
        """
        self.__syncOverlayOrder()

        return [self.__overlayList[idx] for idx in self.overlayOrder]

    @contextlib.contextmanager
    def freeze(self, overlay):
        """This method can be used as a context manager to suppress
        notification for all :class:`.Display` and :class:`.DisplayOpts`
        properties related to the given ``overlay``::

            with displayCtx.freeze(overlay):
                # Do stuff which might trigger unwanted
                # Display/DisplayOpts notifications

        See :meth:`freezeOverlay` and :meth:`thawOverlay`.
        """
        self.freezeOverlay(overlay)

        try:
            yield

        finally:
            self.thawOverlay(overlay)

    def freezeOverlay(self, overlay):
        """Suppresses notification for all :class:`.Display` and
        :class:`.DisplayOpts` properties associated with the given ``overlay``.
        Call :meth:`.thawOverlay` to re-enable notification.

        See also the :meth:`freeze` method, which can be used as a context
        manager to automatically call this method and ``thawOverlay``.
        """
        if self.__child:
            self.getParent().freezeOverlay(overlay)
            return

        dctxs = [self] + self.getChildren()

        for dctx in dctxs:
            display = dctx.getDisplay(overlay)
            opts = display.opts

            display.disableAllNotification()
            opts.disableAllNotification()

    def thawOverlay(self, overlay):
        """Enables notification for all :class:`.Display` and
        :class:`.DisplayOpts` properties associated with the given ``overlay``.
        """

        if self.__child:
            self.getParent().thawOverlay(overlay)
            return
        dctxs = [self] + self.getChildren()

        for dctx in dctxs:
            display = dctx.getDisplay(overlay)
            opts = display.opts

            display.enableAllNotification()
            opts.enableAllNotification()

    @property
    def defaultDisplaySpace(self):
        """This property controls how the :attr:`displaySpace` is initialised
        when overlays are added to a previously empty :class:`.OverlayList`.
        If the ``defaultDisplaySpace`` is set to ``'ref'``, the
        ``displaySpace`` will be initialised to the first :class:`.Nifti`
        overlay. Otherwise (the ``defaultDisplaySpace`` is set to ``'world'``),
        the ``displaySpace`` will be set to ``'world'``.
        """
        return self.__defaultDisplaySpace

    @defaultDisplaySpace.setter
    def defaultDisplaySpace(self, ds):
        """Sets the :meth:`defaultDisplaySpace`.

        :arg ds: Either ``'ref'`` or ``'world'``.
        """
        if ds not in ('world', 'ref'):
            raise ValueError('Invalid default display space: {}'.format(ds))
        self.__defaultDisplaySpace = ds

    def detachDisplaySpace(self):
        """Detaches the :attr:`displaySpace` and :attr:`bounds` properties,
        and all related :class:`.DisplayOpts` properties, from the parent
        ``DisplayContext``.

        This allows this ``DisplayContext`` to use a display coordinate
        system that is completely independent from other instances, and is not
        affected by changes to the parent properties.

        This is an irreversible operation.
        """

        self.detachFromParent('displaySpace')
        self.detachFromParent('bounds')

        for ovl in self.__overlayList:

            opts = self.getOpts(ovl)

            opts.detachFromParent('bounds')

            if isinstance(ovl, fslimage.Nifti):
                opts.detachFromParent('transform')

    def __overlayListChanged(self, *a):
        """Called when the :attr:`.OverlayList.overlays` property
        changes.

        Ensures that a :class:`.Display` and :class:`.DisplayOpts` object
        exists for every image, updates the :attr:`bounds` property, makes
        sure that the :attr:`overlayOrder` property is consistent, and updates
        constraints on the :attr:`selectedOverlay` property.
        """

        # Discard all Display instances
        # which refer to overlays that
        # are no longer in the list
        for overlay in list(self.__displays.keys()):
            if overlay not in self.__overlayList:

                display = self.__displays.pop(overlay)
                opts = display.opts

                display.removeListener('overlayType', self.__name)
                opts.removeListener('bounds', self.__name)

                # The display instance will destroy the
                # opts instance, so we don't do it here
                display.destroy()

        # Ensure that a Display object exists
        # for every overlay in the list
        for overlay in self.__overlayList:

            ovlType = self.__overlayList.initOverlayType(overlay)

            # The getDisplay method
            # will create a Display object
            # if one does not already exist
            display = self.getDisplay(overlay, ovlType)
            opts = display.opts

            # Register a listener on the overlay type,
            # because when it changes, the DisplayOpts
            # instance will change, and we will need
            # to re-register the DisplayOpts.bounds
            # listener (see the next statement)
            display.addListener('overlayType',
                                self.__name,
                                self.__overlayListChanged,
                                overwrite=True)

            # Register a listener on the DisplayOpts.bounds
            # property for every overlay - if the display
            # bounds for any overlay changes, we need to
            # update our own bounds property. This is only
            # done on child DCs, as the parent DC bounds
            # only gets used for synchronisation
            if self.__child:
                opts.addListener('bounds',
                                 self.__name,
                                 self.__overlayBoundsChanged,
                                 overwrite=True)

                # If detachDisplaySpace has been called,
                # make sure the opts bounds (and related)
                # properties are also detached
                if not self.canBeSyncedToParent('displaySpace'):
                    opts.detachFromParent('bounds')
                    if isinstance(overlay, fslimage.Nifti):
                        opts.detachFromParent('transform')

        # Ensure that the displaySpace
        # property options are in sync
        # with the overlay list.
        self.__updateDisplaySpaceOptions()

        # Stuff which only needs to
        # be done on the parent DC
        if not self.__child:

            # Limit the selectedOverlay property
            # so it cannot take a value greater
            # than len(overlayList)-1. selectedOverlay
            # is always synchronised, so we only
            # need to do this on the parent DC.
            nOverlays = len(self.__overlayList)
            if nOverlays > 0:
                self.setAttribute('selectedOverlay', 'maxval', nOverlays - 1)
            else:
                self.setAttribute('selectedOverlay', 'maxval', 0)

            return

        # Ensure that the overlayOrder
        # property is valid
        self.__syncOverlayOrder()

        # If the overlay list was empty,
        # and is now non-empty, we need
        # to initialise the display space
        # and the display location
        initDS        = self.__initDS                      and \
                        np.all(np.isclose(self.bounds, 0)) and \
                        len(self.__overlayList) > 0
        self.__initDS = len(self.__overlayList) == 0

        # Initialise the display space. We
        # have to do this before updating
        # image transforms, and updating
        # the display bounds
        if initDS:

            displaySpace = 'world'

            if self.defaultDisplaySpace == 'ref':
                for overlay in self.__overlayList:
                    if isinstance(overlay, fslimage.Nifti):
                        displaySpace = overlay
                        break

            with props.skip(self, 'displaySpace', self.__name):
                self.displaySpace = displaySpace

        # Initialise the transform property
        # of any Image overlays which have
        # just been added to the list,
        oldList = self.__overlayList.getLastValue('overlays')[:]
        for overlay in self.__overlayList:
            if isinstance(overlay, fslimage.Nifti) and \
               (overlay not in oldList):
                self.__setTransform(overlay)

        # Ensure that the bounds
        # property is accurate
        self.__updateBounds()

        # Initialise the display location to
        # the centre of the display bounds
        if initDS:
            b = self.bounds
            self.location.xyz = [
                b.xlo + b.xlen / 2.0, b.ylo + b.ylen / 2.0,
                b.zlo + b.zlen / 2.0
            ]
            self.__propagateLocation('world')
        else:
            self.__propagateLocation('display')

    def __updateDisplaySpaceOptions(self):
        """Updates the :attr:`displaySpace` property so it is synchronised with
        the current contents of the :class:`.OverlayList`

        This method is called by the :meth:`__overlayListChanged` method.
        """

        choiceProp = self.getProp('displaySpace')
        choices = []

        for overlay in self.__overlayList:
            if isinstance(overlay, fslimage.Nifti):
                choices.append(overlay)

        choices.append('world')

        choiceProp.setChoices(choices, instance=self)

    def __setTransform(self, image):
        """Sets the :attr:`.NiftiOpts.transform` property associated with
        the given :class:`.Nifti` overlay to a sensible value, given the
        current value of the :attr:`.displaySpace` property.

        Called by the :meth:`__displaySpaceChanged` method, and by
        :meth:`__overlayListChanged` for any :class:`.Image` overlays which
        have been newly added to the :class:`.OverlayList`.

        :arg image: An :class:`.Image` overlay.
        """

        space = self.displaySpace
        opts = self.getOpts(image)

        # Disable notification of the bounds
        # property so the __overlayBoundsChanged
        # method does not get called. Use
        # ignoreInvalid, because this method might
        # get called before we have registered a
        # listener on the bounds property.
        with props.skip(opts, 'bounds', self.__name, ignoreInvalid=True):
            if space == 'world': opts.transform = 'affine'
            elif image is space: opts.transform = 'pixdim-flip'
            else: opts.transform = 'reference'

    def __displaySpaceChanged(self, *a):
        """Called when the :attr:`displaySpace` property changes. Updates the
        :attr:`.NiftiOpts.transform` property for all :class:`.Nifti`
        overlays in the :class:`.OverlayList`.
        """

        selectedOverlay = self.getSelectedOverlay()

        if selectedOverlay is None:
            return

        # Update the transform property of all
        # Image overlays to put them into the
        # new display space
        for overlay in self.__overlayList:

            if not isinstance(overlay, fslimage.Nifti):
                continue

            self.__setTransform(overlay)

        # Update the display world bounds,
        # and then update the location
        self.__updateBounds()

        # Make sure that the location is
        # kept in the same place, relative
        # to the world coordinate system
        self.__propagateLocation('display')

    def __syncOverlayOrder(self):
        """Ensures that the :attr:`overlayOrder` property is up to date
        with respect to the :class:`.OverlayList`.
        """

        if len(self.overlayOrder) == len(self.__overlayList):
            return

        #
        # NOTE: The following logic assumes that operations
        #       which modify the overlay list will only do
        #       one of the following:
        #
        #        - Adding one or more overlays to the end of the list
        #        - Removing one or more overlays from the list
        #
        # More complex overlay list modifications
        # will cause this code to break.

        oldList = self.__overlayList.getLastValue('overlays')[:]
        oldOrder = self.overlayOrder[:]

        # If the overlay order was just the
        # list order, preserve that ordering
        if self.overlayOrder[:] == list(range(len(oldList))):
            self.overlayOrder[:] = list(range(len(self.__overlayList)))

        # If overlays have been added to
        # the overlay list, add indices
        # for them to the overlayOrder list
        elif len(self.overlayOrder) < len(self.__overlayList):

            newOrder = []
            newOverlayIdx = len(oldList)

            # The order of existing overlays is preserved,
            # and all new overlays added to the end of the
            # overlay order.
            for overlay in self.__overlayList:

                if overlay in oldList:
                    newOrder.append(oldOrder[oldList.index(overlay)])
                else:
                    newOrder.append(newOverlayIdx)
                    newOverlayIdx += 1

            self.overlayOrder[:] = newOrder

        # Otherwise, if overlays have been
        # removed from the overlay list ...
        elif len(self.overlayOrder) > len(self.__overlayList):

            # Remove the corresponding indices
            # from the overlayOrder list
            for i, overlay in enumerate(oldList):
                if overlay not in self.__overlayList:
                    oldOrder.remove(i)

            # Re-generate new indices,
            # preserving the order of
            # the remaining overlays
            newOrder = [sorted(oldOrder).index(idx) for idx in oldOrder]
            self.overlayOrder[:] = newOrder

    def __overlayBoundsChanged(self, value, valid, opts, name):
        """Called when the :attr:`.DisplayOpts.bounds` property of any
        overlay changes. Updates the :attr:`bounds` property and preserves
        the display :attr:`location` in terms of the :attr:`worldLocation`.
        """

        # This method might get called
        # after DisplayOpts instance
        # has been destroyed
        if opts.display is None:
            return

        # Update the display context bounds
        # to take into account any changes
        # to individual overlay bounds.
        # Inhibit notification on the location
        # property - it will be updated properly
        # below
        self.__updateBounds()

        # Make sure the display location
        # is consistent w.r.t. the world
        # coordinate location
        self.__propagateLocation('display')

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

        Synchronises or unsychronises the :class:`.Display` and
        :class:`.DisplayOpts` instances for every overlay to/from their
        parent instances.
        """

        dcProps = ['displaySpace', 'bounds', 'radioOrientation']

        if self.syncOverlayDisplay:
            for p in dcProps:
                if self.canBeSyncedToParent(p):
                    self.syncToParent(p)

        else:
            for p in dcProps:
                if self.canBeUnsyncedFromParent(p):
                    self.unsyncFromParent(p)

        for display in self.__displays.values():

            opts = display.opts

            if self.syncOverlayDisplay:
                display.syncAllToParent()
                opts.syncAllToParent()
            else:
                display.unsyncAllFromParent()
                opts.unsyncAllFromParent()

    def __updateBounds(self, *a):
        """Called when the overlay list changes, or when any overlay display
        transform is changed. Updates the :attr:`bounds` property so that it
        is big enough to contain all of the overlays (as defined by their
        :attr:`.DisplayOpts.bounds` properties).
        """

        if len(self.__overlayList) == 0:
            minBounds = [0.0, 0.0, 0.0]
            maxBounds = [0.0, 0.0, 0.0]

        else:
            minBounds = 3 * [sys.float_info.max]
            maxBounds = 3 * [-sys.float_info.max]

        for ovl in self.__overlayList:

            display = self.__displays[ovl]
            opts = display.opts
            lo = opts.bounds.getLo()
            hi = opts.bounds.getHi()

            for ax in range(3):

                if lo[ax] < minBounds[ax]: minBounds[ax] = lo[ax]
                if hi[ax] > maxBounds[ax]: maxBounds[ax] = hi[ax]

        self.bounds[:] = [
            minBounds[0], maxBounds[0], minBounds[1], maxBounds[1],
            minBounds[2], maxBounds[2]
        ]

        # Update the constraints on the location
        # property to be aligned with the new bounds
        with props.suppress(self, 'location'):
            self.location.setLimits(0, self.bounds.xlo, self.bounds.xhi)
            self.location.setLimits(1, self.bounds.ylo, self.bounds.yhi)
            self.location.setLimits(2, self.bounds.zlo, self.bounds.zhi)

    def __locationChanged(self, *a):
        """Called when the :attr:`location` property changes. Propagates
        the new location to the :attr:`worldLocation` property.
        """
        self.__propagateLocation('world')

    def __worldLocationChanged(self, *a):
        """Called when the :attr:`worldLocation` property changes. Propagates
        the new location to the :attr:`location` property.
        """

        self.__propagateLocation('display')

    def __propagateLocation(self, dest):
        """Called by the :meth:`__locationChanged` and
        :meth:`__worldLocationChanged` methods. The ``dest`` argument may be
        either ``'world'`` (the ``worldLocation`` is updated from the
        ``location``), or ``'display'`` (vice-versa).
        """

        if self.displaySpace == 'world':
            if dest == 'world':
                with props.skip(self, 'worldLocation', self.__name):
                    self.worldLocation = self.location
            else:
                with props.skip(self, 'location', self.__name):
                    self.location = self.worldLocation
            return

        ref = self.displaySpace
        opts = self.getOpts(ref)

        if dest == 'world':
            with props.skip(self, 'location', self.__name):
                self.worldLocation = opts.transformCoords(
                    self.location, 'display', 'world')
        else:
            with props.skip(self, 'worldLocation', self.__name):
                self.location = opts.transformCoords(self.worldLocation,
                                                     'world', 'display')
Example #7
0
class MyObj(props.HasProperties):

    mypointi = props.Point(ndims=2, real=False)
    mypointf = props.Point(ndims=2)