#
# atlasinfopanel.py - The AtlasInfoPanel class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`AtlasInfoPanel`, which is a sub-panel
that is used by the :class:`.AtlasPanel`.
"""
import logging
import numpy as np
import wx
import wx.html as wxhtml
import fsleyes_widgets.elistbox as elistbox
import fsleyes.panel as fslpanel
import fsleyes.strings as strings
import fsl.utils.idle as idle
from fsl.utils.platform import platform as fslplatform
import fsl.data.atlases as atlases
import fsl.data.constants as constants
log = logging.getLogger(__name__)
[docs]class AtlasInfoPanel(fslpanel.FSLeyesPanel):
"""The ``AtlasInfoPanel`` displays region information about the current
:attr:`.DisplayContext.location` from a set of :mod:`.atlases` chosen
by the user.
An ``AtlasInfoPanel`` looks something like this:
.. image:: images/atlasinfopanel.png
:scale: 50%
:align: center
The ``AtlasInfoPanel`` contains two main sections:
- A :class:`fsleyes_widgets.elistbox.EditableListBox` filled with
:class:`AtlasListWidget` controls, one for each available atlas.
The user is able to choose which atlases to show information for.
- A ``wx.html.HtmlWindow`` which contains information for each
selected atlas. The information contains hyperlinks for each atlas,
and each region which, when clicked, toggles on/off relevant
atlas overlays (see the :meth:`.AtlasPanel.toggleOverlay` method).
"""
[docs] def __init__(self, parent, overlayList, displayCtx, atlasPanel):
"""Create an ``AtlasInfoPanel``.
:arg parent: the :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg atlasPanel: The :class:`.AtlasPanel` instance that has created
this ``AtlasInfoPanel``.
"""
fslpanel.FSLeyesPanel.__init__(
self, parent, overlayList, displayCtx, atlasPanel.frame)
self.__enabledAtlases = {}
self.__atlasPanel = atlasPanel
self.__contentPanel = wx.SplitterWindow(self,
style=wx.SP_LIVE_UPDATE)
self.__infoPanel = wxhtml.HtmlWindow(self.__contentPanel)
self.__atlasList = elistbox.EditableListBox(
self.__contentPanel,
vgap=5,
style=(elistbox.ELB_NO_ADD |
elistbox.ELB_NO_REMOVE |
elistbox.ELB_NO_MOVE))
# Force the HTML info panel to
# use the default font size
self.__infoPanel.SetStandardFonts(self.GetFont().GetPointSize())
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
self.__contentPanel.SetMinimumPaneSize(50)
self.__contentPanel.SplitVertically(self.__atlasList,
self.__infoPanel)
self.__contentPanel.SetSashGravity(0.4)
# The info panel contains clickable links
# for the currently displayed regions -
# when a link is clicked, the location
# is centred at the corresponding region
self.__infoPanel.Bind(wxhtml.EVT_HTML_LINK_CLICKED,
self.__infoPanelLinkClicked)
fslplatform .register(self.name,
self.__fslDirChanged)
atlases.registry.register(self.name,
self.__atlasAdded,
topic='add')
atlases.registry.register(self.name,
self.__atlasRemoved,
topic='remove')
overlayList.addListener('overlays',
self.name,
self.__selectedOverlayChanged)
displayCtx .addListener('selectedOverlay',
self.name,
self.__selectedOverlayChanged)
displayCtx .addListener('location',
self.name,
self.__locationChanged)
self.__atlasList.SetMinSize((50, 60))
self.__infoPanel.SetMinSize((50, 60))
self.SetMinSize(self.__sizer.GetMinSize())
self.__buildAtlasList()
self.__selectedOverlayChanged()
# Enable the Harvard/Oxford
# atlases by default. We do this
# asynchronously because methods
# on the AtlasPanel will be called,
# and the AtlasPanel may not have
# finished initialising itself.
enable = ['harvardoxford-cortical', 'harvardoxford-subcortical']
enable = [e for e in enable if atlases.hasAtlas(e)]
for i, e in enumerate(enable):
refresh = i == len(enable) - 1
idle.idle(self.enableAtlasInfo, e, refresh=refresh)
[docs] def destroy(self):
"""Must be called when this :class:`AtlasInfoPanel` is to be
destroyed. De-registers various property listeners and calls
:meth:`FSLeyesPanel.destroy`.
"""
self.overlayList.removeListener('overlays', self.name)
self.displayCtx .removeListener('location', self.name)
self.displayCtx .removeListener('selectedOverlay', self.name)
atlases.registry.deregister(self.name, 'add')
atlases.registry.deregister(self.name, 'remove')
fslplatform .deregister(self.name)
fslpanel.FSLeyesPanel.destroy(self)
[docs] def enableAtlasInfo(self, atlasID, refresh=True):
"""Enables information display for the atlas with the specified ID
(see the :mod:`.atlases` module for details on atlas IDs).
If ``refresh`` is ``True`` (the default), the HTML information panel
is updated.
"""
listWidget = self.__atlasList.GetItemWidget(
self.__atlasList.IndexOf(atlasID))
def onLoad(atlas):
if not self or self.destroyed:
return
self.__enabledAtlases[atlasID] = atlas
listWidget.SetValue(True)
if refresh:
self.__locationChanged()
self.__atlasPanel.enableAtlasPanel()
def onError(e):
if not self or self.destroyed:
return
message = strings.messages[self, 'loadAtlasError']
message = message.format(
atlasID, '{} ({})'.format(type(e).__name__, str(e)))
wx.MessageDialog(
self.GetTopLevelParent(),
message=message,
style=(wx.ICON_EXCLAMATION | wx.OK)).ShowModal()
listWidget.SetValue(False)
self.__atlasPanel.enableAtlasPanel()
self.__atlasPanel.enableAtlasPanel(False)
self.__atlasPanel.loadAtlas(atlasID,
summary=False,
onLoad=onLoad,
onError=onError,
matchResolution=False)
[docs] def disableAtlasInfo(self, atlasID):
"""Disables information display for the atlas with the specified ID.
"""
# We set the elistbox client data
# to the atlas ID, so we can get
# the item index by atlasID here
listWidget = self.__atlasList.GetItemWidget(
self.__atlasList.IndexOf(atlasID))
self.__enabledAtlases.pop(atlasID)
self.__locationChanged()
listWidget.SetValue(False)
def __fslDirChanged(self, *a):
"""Called when the :attr:`.Platform.fsldir` changes. Refreshes
the atlas list.
"""
self.__buildAtlasList()
self.__locationChanged()
def __atlasAdded(self, registry, topic, atlasDesc):
"""Called when a new atlas is added to the :class:`.AtlasRegistry`.
Re-generates the atlas list.
"""
self.__buildAtlasList()
self.__locationChanged()
def __atlasRemoved(self, registry, topic, atlasDesc):
"""Called when an atlas is removed from the :class:`.AtlasRegistry`.
Re-generates the atlas list.
"""
self.__buildAtlasList()
self.__locationChanged()
def __buildAtlasList(self):
"""Clears and then builds the list of available atlases. The
This is performed asynchronously, via the :func:`.idle.run` function,
although the atlas list widget is updated on the ``wx`` idle loop.
"""
# This method gets called whenever atlases
# are added to/from the list.
# We want to preserve the 'enabled' state of
# any atlases that are still present in the
# atlas registry.
enabledAtlases = dict(self.__enabledAtlases)
self.__enabledAtlases = {}
self.__atlasList.Clear()
atlasDescs = atlases.listAtlases()
for i, atlasDesc in enumerate(atlasDescs):
atlasID = atlasDesc.atlasID
self.__atlasList.Append(atlasDesc.name, atlasID)
enabled = atlasID in enabledAtlases
widget = AtlasListWidget(self.__atlasList,
i,
self,
atlasID,
enabled=enabled)
self.__atlasList.SetItemWidget(i, widget)
if enabled:
self.__enabledAtlases[atlasID] = enabledAtlases[atlasID]
def __locationChanged(self, *a):
"""Called when the :attr:`.DisplayContext.location` property changes.
Updates the information shown in the HTML window.
"""
text = self.__infoPanel
overlay = self.displayCtx.getReferenceImage(
self.displayCtx.getSelectedOverlay())
topText = None
if self.__atlasList.GetCount() == 0:
text.SetPage(strings.messages['AtlasInfoPanel.atlasDisabled'])
return
if len(self.overlayList) == 0:
text.SetPage(strings.messages['AtlasInfoPanel.noOverlays'])
return
if overlay is None or \
overlay.getXFormCode() != constants.NIFTI_XFORM_MNI_152:
topText = strings.messages['AtlasInfoPanel.notMNISpace']
topText = '<font color="red">{}</font>'.format(topText)
if len(self.__enabledAtlases) == 0:
text.SetPage(strings.messages['AtlasInfoPanel.chooseAnAtlas'])
return
lines = []
if topText is not None:
lines.append(topText)
if overlay is None:
text.SetPage('<br>'.join(lines))
text.Refresh()
return
opts = self.displayCtx.getOpts(overlay)
loc = self.displayCtx.location
loc = opts.transformCoords([loc], 'display', 'world')[0]
# Three types of hyperlink:
# - one for complete (summary) label atlases,
# - one for a region label mask image
# - one for a region statistic/probability image
#
# The hrefs are formatted as:
#
# imageType atlasID labelIdx
#
# where "imageType" is one of "summary", "label", or "stat"
# and "labelIdx" is "None" for summary atlases.
titleTemplate = '<b>{}</b> (<a href="summary {} {}">Show/Hide</a>)'
labelTemplate = '{} (<a href="label {} {}">Show/Hide</a>)'
statTemplate = '{}{} {} (<a href="stat {} {}">Show/Hide</a>)'
for atlasID in self.__enabledAtlases:
atlas = self.__enabledAtlases[atlasID]
lines.append(titleTemplate.format(atlas.desc.name, atlasID, None))
if isinstance(atlas, atlases.StatisticAtlas):
values = atlas.values(loc)
if len(values) == 0:
continue
vallabels = zip(values, atlas.desc.labels)
for val, label in reversed(sorted(vallabels)):
if np.abs(val) < atlas.desc.lower:
continue
fmt = '{{:0.{}f}}'.format(atlas.desc.precision)
val = fmt.format(val)
lines.append(statTemplate.format(val,
atlas.desc.units,
label.name,
atlasID,
label.index))
elif isinstance(atlas, atlases.LabelAtlas):
labelVal = atlas.label(loc)
if labelVal is None:
continue
label = atlas.find(value=labelVal)
lines.append(labelTemplate.format(label.name,
atlasID,
label.index))
text.SetPage('<br>'.join(lines))
text.Refresh()
def __infoPanelLinkClicked(self, ev):
"""Called when a hyperlink is clicked in the HTML window. Toggles
the respective atlas overlay - see the
:meth:`.AtlasPanel.toggleOverlay` method.
"""
# Decode the href - see comments
# inside __locationChanged method
showType, atlasID, labelIndex = ev.GetLinkInfo().GetHref().split()
try: labelIndex = int(labelIndex)
except ValueError: labelIndex = None
# showType is one of 'stat', 'label', or
# 'summary'; the summary parameter controls
# whether a probabilstic/stat or label image
# is loaded
summary = showType != 'stat'
def onLoad():
self.__atlasPanel.Enable()
def onError(e):
message = strings.messages[self, 'loadAtlasError']
message = message.format(
atlasID, '{} ({})'.format(type(e).__name__, str(e)))
wx.MessageDialog(
self.GetTopLevelParent(),
message=message,
style=(wx.ICON_EXCLAMATION | wx.OK)).ShowModal()
self.__atlasPanel.Enable()
self.__atlasPanel.Disable()
self.__atlasPanel.toggleOverlay(atlasID,
labelIndex,
summary,
onLoad=onLoad,
onError=onError)
def __selectedOverlayChanged(self, *a):
"""Called when the :class:`.OverlayList` or the
:attr:`.DisplayContext.location` changes. Refreshes the displayed
atlas information (see :meth:`__locationChanged`), and adds a listener
to the :attr:`.DisplayOpts.bounds` property so that, when it changes,
the atlas information is refreshed.
"""
if len(self.overlayList) == 0:
self.__locationChanged()
return
selOverlay = self.displayCtx.getSelectedOverlay()
for ovl in self.overlayList:
opts = self.displayCtx.getOpts(ovl)
# Add a listener to the bounds property for
# the selected overlay. Bounds is used as a
# proxy for the overlay referene image (e.g.
# Mesh overlays) - if the reference image
# changes, the overlay may have been moved
# into MNI152 space, so we can display
# atlas info.
if ovl == selOverlay:
opts.addListener('bounds',
self.name,
self.__locationChanged,
overwrite=True)
else:
opts.removeListener('bounds', self.name)
self.__locationChanged()