From ee87fd94900a3662b094ee4a43c88d8bb9bed788 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 28 Oct 2015 11:32:02 +0000
Subject: [PATCH] TimeSeriesControlPanel refactorings, should now be less
 buggy. New option on TimeSeriesPanel - plotMelodicICs, which allows melodic
 images to be treated as regular 4D images.

---
 fsl/data/strings.py                           |   2 +
 .../controls/timeseriescontrolpanel.py        | 139 +++++++++++-------
 fsl/fsleyes/plotting/timeseries.py            |   6 +-
 fsl/fsleyes/tooltips.py                       |   5 +
 fsl/fsleyes/views/timeseriespanel.py          |  52 +++++--
 5 files changed, 136 insertions(+), 68 deletions(-)

diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 6405b52ac..27ec5ed61 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -384,6 +384,8 @@ properties = TypeDict({
     
     'TimeSeriesPanel.plotMode'         : 'Plotting mode',
     'TimeSeriesPanel.usePixdim'        : 'Use pixdims',
+    'TimeSeriesPanel.plotMelodicICs'   : 'Plot component time courses for '
+                                         'Melodic images',
     'TimeSeriesPanel.showMode'         : 'Time series to plot',
     'TimeSeriesPanel.plotFullModelFit' : 'Plot full model fit',
     'TimeSeriesPanel.plotResiduals'    : 'Plot residuals',
diff --git a/fsl/fsleyes/controls/timeseriescontrolpanel.py b/fsl/fsleyes/controls/timeseriescontrolpanel.py
index 683a989ec..4d755f2b7 100644
--- a/fsl/fsleyes/controls/timeseriescontrolpanel.py
+++ b/fsl/fsleyes/controls/timeseriescontrolpanel.py
@@ -15,6 +15,7 @@ import                                    props
 import pwidgets.widgetlist             as widgetlist
 
 import fsl.fsleyes.panel               as fslpanel
+import fsl.fsleyes.displaycontext      as fsldisplay
 import fsl.fsleyes.plotting.timeseries as timeseries
 import fsl.fsleyes.tooltips            as fsltooltips
 import fsl.data.strings                as strings
@@ -23,7 +24,7 @@ import fsl.data.strings                as strings
 class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
     """The ``TimeSeriesControlPanel`` is a :class:`.FSLEyesPanel` which allows
     the user to configure a :class:`.TimeSeriesPanel`. It contains controls
-    which are linked to the properties of the :class:`.TImeSeriesPanel`,
+    which are linked to the properties of the ``TimeSeriesPanel``,
     (which include properties defined on the :class:`.PlotPanel` base class),
     and the :class:`.TimeSeries` class.
 
@@ -79,7 +80,8 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
 
         tsProps   = ['showMode',
                      'plotMode',
-                     'usePixdim']
+                     'usePixdim',
+                     'plotMelodicICs']
         plotProps = ['xLogScale',
                      'yLogScale',
                      'smooth',
@@ -162,8 +164,9 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
                                 self.__selectedOverlayChanged)
 
         # This attribute keeps track of the currently
-        # selected overlay, but only if said overlay
-        # is a FEATImage.
+        # selected overlay, so the widget list group
+        # names can be updated if the overlay name
+        # changes.
         self.__selectedOverlay = None
         self.__selectedOverlayChanged()
 
@@ -185,15 +188,21 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
 
     def __selectedOverlayNameChanged(self, *a):
         """Called when the :attr:`.Display.name` property for the currently
-        selected overlay changes. Only called if the current overlay is a
-        :class:`.FEATImage`. Updates the display name of the *FEAT plot
-        settings* section.
+        selected overlay changes. Updates the display name of the *FEAT plot
+        settings* and *current time course* sections if necessary.
         """
         display = self._displayCtx.getDisplay(self.__selectedOverlay)
-        self.__widgets.RenameGroup(
-            'currentFEATSettings',
-            strings.labels[self, 'currentFEATSettings'].format(
-                display.name))
+        
+        if self.__widgets.HasGroup('currentFEATSettings'):
+            self.__widgets.RenameGroup(
+                'currentFEATSettings',
+                strings.labels[self, 'currentFEATSettings'].format(
+                    display.name))
+
+        if self.__widgets.HasGroup('currentSettings'):
+            self.__widgets.RenameGroup(
+                'currentSettings',
+                strings.labels[self, 'currentSettings'].format(display.name)) 
 
     
     def __selectedOverlayChanged(self, *a):
@@ -205,95 +214,121 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
         # We're assuminbg that the TimeSeriesPanel has
         # already updated its current TimeSeries for
         # the newly selected overlay.
-
         if self.__selectedOverlay is not None:
-            display = self._displayCtx.getDisplay(self.__selectedOverlay)
-            display.removeListener('name', self._name)
+            try:
+                display = self._displayCtx.getDisplay(self.__selectedOverlay)
+                display.removeListener('name', self._name)
+                
+            # The overlay may have been
+            # removed from the overlay list
+            except fsldisplay.InvalidOverlayError:
+                pass
+                
             self.__selectedOverlay = None
 
+        if self.__widgets.HasGroup('currentSettings'):
+            self.__widgets.RemoveGroup('currentSettings') 
+ 
         if self.__widgets.HasGroup('currentFEATSettings'):
             self.__widgets.RemoveGroup('currentFEATSettings')
 
         overlay = self._displayCtx.getSelectedOverlay()
-        ts      = self.__tsPanel.getTimeSeries(overlay)
-
-        self.__showSettingsForCurrentTimeSeries()
 
-        if ts is None or not isinstance(ts, timeseries.FEATTimeSeries):
+        if overlay is None:
             return
 
-        display = self._displayCtx.getDisplay(overlay)
+        ts = self.__tsPanel.getTimeSeries(overlay)
+
+        if ts is None:
+            return
 
         self.__selectedOverlay = overlay
 
+        display = self._displayCtx.getDisplay(overlay)
+
         display.addListener('name',
                             self._name,
                             self.__selectedOverlayNameChanged)
 
-        self.__widgets.AddGroup(
+        self.__showSettingsForTimeSeries(ts)
+
+        if isinstance(ts, timeseries.FEATTimeSeries):
+            self.__showFEATSettingsForTimeSeries(ts)
+
+
+    def __showFEATSettingsForTimeSeries(self, ts):
+        """(Re-)crates the *FEAT settings* section for the given
+        :class:`.FEATTimeSeries` instance.
+        """
+
+        overlay = ts.overlay
+        display = self._displayCtx.getDisplay(overlay)
+        widgets = self.__widgets
+
+        widgets.AddGroup(
             'currentFEATSettings',
             displayName=strings.labels[self, 'currentFEATSettings'].format(
                 display.name))
 
-        full    = props.makeWidget(     self.__widgets, ts, 'plotFullModelFit')
-        res     = props.makeWidget(     self.__widgets, ts, 'plotResiduals')
-        evs     = props.makeListWidgets(self.__widgets, ts, 'plotEVs')
-        pes     = props.makeListWidgets(self.__widgets, ts, 'plotPEFits')
-        copes   = props.makeListWidgets(self.__widgets, ts, 'plotCOPEFits')
-        partial = props.makeWidget(     self.__widgets, ts, 'plotPartial')
-        data    = props.makeWidget(     self.__widgets, ts, 'plotData') 
+        full    = props.makeWidget(     widgets, ts, 'plotFullModelFit')
+        res     = props.makeWidget(     widgets, ts, 'plotResiduals')
+        evs     = props.makeListWidgets(widgets, ts, 'plotEVs')
+        pes     = props.makeListWidgets(widgets, ts, 'plotPEFits')
+        copes   = props.makeListWidgets(widgets, ts, 'plotCOPEFits')
+        partial = props.makeWidget(     widgets, ts, 'plotPartial')
+        data    = props.makeWidget(     widgets, ts, 'plotData') 
 
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             data,
             displayName=strings.properties[ts, 'plotData'],
             tooltip=fsltooltips.properties[ts, 'plotData'],
             groupName='currentFEATSettings') 
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             full,
             displayName=strings.properties[ts, 'plotFullModelFit'],
             tooltip=fsltooltips.properties[ts, 'plotFullModelFit'],
             groupName='currentFEATSettings')
         
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             res,
             displayName=strings.properties[ts, 'plotResiduals'],
             tooltip=fsltooltips.properties[ts, 'plotResiduals'],
             groupName='currentFEATSettings')
         
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             partial,
             displayName=strings.properties[ts, 'plotPartial'],
             tooltip=fsltooltips.properties[ts, 'plotPartial'],
             groupName='currentFEATSettings')
 
-        self.__widgets.AddSpace(groupName='currentFEATSettings')
+        widgets.AddSpace(groupName='currentFEATSettings')
 
         for i, ev in enumerate(evs):
 
-            evName = ts.overlay.evNames()[i]
-            self.__widgets.AddWidget(
+            evName = overlay.evNames()[i]
+            widgets.AddWidget(
                 ev,
                 displayName=strings.properties[ts, 'plotEVs'].format(
                     i + 1, evName),
                 tooltip=fsltooltips.properties[ts, 'plotEVs'],
                 groupName='currentFEATSettings')
 
-        self.__widgets.AddSpace(groupName='currentFEATSettings')
+        widgets.AddSpace(groupName='currentFEATSettings')
             
         for i, pe in enumerate(pes):
-            evName = ts.overlay.evNames()[i]
-            self.__widgets.AddWidget(
+            evName = overlay.evNames()[i]
+            widgets.AddWidget(
                 pe,
                 displayName=strings.properties[ts, 'plotPEFits'].format(
                     i + 1, evName),
                 tooltip=fsltooltips.properties[ts, 'plotPEFits'],
                 groupName='currentFEATSettings')
 
-        self.__widgets.AddSpace(groupName='currentFEATSettings')
+        widgets.AddSpace(groupName='currentFEATSettings')
 
         copeNames = overlay.contrastNames()
         for i, (cope, name) in enumerate(zip(copes, copeNames)):
-            self.__widgets.AddWidget(
+            widgets.AddWidget(
                 cope,
                 displayName=strings.properties[ts, 'plotCOPEFits'].format(
                     i + 1, name),
@@ -301,22 +336,14 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
                 groupName='currentFEATSettings') 
 
 
-    def __showSettingsForCurrentTimeSeries(self):
+    def __showSettingsForTimeSeries(self, ts):
         """Called by the :meth:`__selectedOverlayChanged` method. Refreshes
-        the *Settings for the current time course* section.
+        the *Settings for the current time course* section, for the given
+        :class:`.TimeSeries` instance.
         """
-        widgets = self.__widgets
-        tsPanel = self.__tsPanel
-        overlay = self._displayCtx.getSelectedOverlay()
-        ts      = tsPanel.getTimeSeries(overlay)
-
-        if widgets.HasGroup('currentSettings'):
-            widgets.RemoveGroup('currentSettings')
-
-        if ts is None:
-            return
-
+        overlay = ts.overlay
         display = self._displayCtx.getDisplay(overlay)
+        widgets = self.__widgets
 
         self.__widgets.AddGroup(
             'currentSettings',
@@ -332,22 +359,22 @@ class TimeSeriesControlPanel(fslpanel.FSLEyesPanel):
             'lineStyle',
             labels=strings.choices['DataSeries.lineStyle'])
 
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             colour,
             displayName=strings.properties[ts, 'colour'],
             tooltip=fsltooltips.properties[ts, 'colour'],
             groupName='currentSettings')
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             alpha,
             displayName=strings.properties[ts, 'alpha'],
             tooltip=fsltooltips.properties[ts, 'alpha'],
             groupName='currentSettings')
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             lineWidth,
             displayName=strings.properties[ts, 'lineWidth'],
             tooltip=fsltooltips.properties[ts, 'lineWidth'],
             groupName='currentSettings') 
-        self.__widgets.AddWidget(
+        widgets.AddWidget(
             lineStyle,
             displayName=strings.properties[ts, 'lineStyle'],
             tooltip=fsltooltips.properties[ts, 'lineStyle'],
diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py
index 32966e672..c60cfdb99 100644
--- a/fsl/fsleyes/plotting/timeseries.py
+++ b/fsl/fsleyes/plotting/timeseries.py
@@ -586,7 +586,7 @@ class FEATEVTimeSeries(TimeSeries):
         
     def getData(self):
         """Returns the time course of the EV specified in the constructor. """
-        data = self.overlay.getDesign()[:, self.idx]
+        data = np.array(self.overlay.getDesign()[:, self.idx])
         return TimeSeries.getData(self, ydata=data)
     
 
@@ -629,7 +629,7 @@ class FEATResidualTimeSeries(TimeSeries):
             return [], []
 
         x, y, z = voxel
-        data    = self.overlay.getResiduals().data[x, y, z, :]
+        data    = np.array(self.overlay.getResiduals().data[x, y, z, :])
         
         return TimeSeries.getData(self, ydata=data)
             
@@ -755,5 +755,5 @@ class MelodicTimeSeries(TimeSeries):
         """Returns the time course of the current Melodic component. """
         
         component = self.getComponent()
-        ydata     = self.overlay.getComponentTimeSeries(component)
+        ydata     = np.array(self.overlay.getComponentTimeSeries(component))
         return TimeSeries.getData(self, ydata=ydata)
diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py
index 2c61a79b6..ca71b292e 100644
--- a/fsl/fsleyes/tooltips.py
+++ b/fsl/fsleyes/tooltips.py
@@ -256,6 +256,11 @@ properties = TypeDict({
                                          'scaled by the time dimension pixdim '
                                          'value specified in the NIFTI1 '
                                          'header.',
+    'TimeSeriesPanel.plotMelodicICs'   : 'If checked, the component time '
+                                         'courses are plotted for Melodic '
+                                         'images. If not checked, Melodic '
+                                         'images are treated as regular 4D '
+                                         'images.',
     'TimeSeriesPanel.showMode'         : 'Choose which time series to plot - '
                                          'you can choose to plot the time '
                                          'series for the currently selected '
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index 37093abda..02d2c6dce 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -152,6 +152,14 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     """
 
 
+    plotMelodicICs = props.Boolean(default=True)
+    """If ``True``, the component time courses are plotted for
+    :class:`.MelodicImage` overlays (using a :class:`.MelodicTimeSeries`
+    instance). Otherwise, ``MelodicImage`` overlays are treated as regular
+    4D :class:`.Image` overlays (a :class:`.TimeSeries` instance is used).
+    """
+
+
     def __init__(self, parent, overlayList, displayCtx):
         """Create a ``TimeSeriesPanel``.
 
@@ -185,9 +193,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         self       .addListener('usePixdim',       self._name, self.draw)
         self       .addListener('showMode',        self._name, self.draw)
         displayCtx .addListener('selectedOverlay', self._name, self.draw)
+        self       .addListener('plotMelodicICs',
+                                self._name,
+                                self.__plotMelodicICsChanged)
         overlayList.addListener('overlays',
                                 self._name,
-                                self.__overlayListChanged)        
+                                self.__overlayListChanged) 
 
         # The currentTss attribute is a dictionary of
         #
@@ -304,18 +315,40 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
                 ds.destroy()
         
         for overlay in list(self.__currentTss.keys()):
-            
-            ts = self.__currentTss.pop(overlay)
+            if overlay not in self._overlayList:
+                self.__clearCacheForOverlay(overlay)
+
+        self.__updateCurrentTimeSeries()
+        self.draw()
+
+
+    def __clearCacheForOverlay(self, overlay):
+        """Destroys the internally cached :class:`.TimeSeries` for the given
+        overlay.
+        """
+        
+        ts                 = self.__currentTss  .pop(overlay, None)
+        targets, propNames = self.__refreshProps.pop(overlay, ([], []))
+
+        if ts is not None:
             ts.destroy()
 
-        for overlay, (targets, propNames) in list(self.__refreshProps.items()):
-            if overlay not in self._overlayList:
-                self.__refreshProps.pop(overlay)
+        for t, p in zip(targets, propNames):
+            t.removeListener(p, self._name)
+
+        
+    def __plotMelodicICsChanged(self, *a):
+        """Called when the :attr:`plotMelodicICs` property changes. Re-creates
+        the internally cached :class:`.TimeSeries` instances for all
+        :class:`.MelodicImage` overlays in the :class:`.OverlayList`.
+        """
 
-                for t, p in zip(targets, propNames):
-                    t.removeListener(p, self._name)
+        for overlay in self._overlayList:
+            if isinstance(overlay, fslmelimage.MelodicImage):
+                self.__clearCacheForOverlay(overlay)
 
         self.__updateCurrentTimeSeries()
+        self.draw()
 
         
     def __updateCurrentTimeSeries(self, *a):
@@ -373,7 +406,8 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             targets   = [self._displayCtx]
             propNames = ['location']
             
-        elif isinstance(overlay, fslmelimage.MelodicImage):
+        elif isinstance(overlay, fslmelimage.MelodicImage) and \
+             self.plotMelodicICs:
             ts = timeseries.MelodicTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx.getOpts(overlay)]
             propNames = ['volume'] 
-- 
GitLab