From 731437527f9ef0e43c96e4ffdfa231cc792e2bcb Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Tue, 27 Oct 2015 09:00:31 +0000
Subject: [PATCH] Basic (buggy) support for MelodicImage overlays in
 TimeSeriesPanel. Things are going to change.

---
 fsl/data/melodicimage.py                    |  40 ++++++-
 fsl/data/melodicresults.py                  |  87 ++++++++++++--
 fsl/fsleyes/controls/timeserieslistpanel.py |  25 ++--
 fsl/fsleyes/overlay.py                      |  20 +++-
 fsl/fsleyes/views/plotpanel.py              |   3 +-
 fsl/fsleyes/views/timeseriespanel.py        | 122 +++++++++++---------
 6 files changed, 215 insertions(+), 82 deletions(-)

diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py
index d0c4b5f8a..6bdc25480 100644
--- a/fsl/data/melodicimage.py
+++ b/fsl/data/melodicimage.py
@@ -8,6 +8,7 @@
 """This module provides the :class:`MelodicImage` class, an :class:`.Image`
 sub-class which encapsulates data from a MELODIC analysis.
 """
+import os.path as op
 
 import image          as fslimage
 import melodicresults as melresults
@@ -17,7 +18,42 @@ class MelodicImage(fslimage.Image):
     """
     """
 
-    def __init__(self, path):
+    def __init__(self, image, *args, **kwargs):
         """
         """
-        pass
+
+
+        if op.isdir(image):
+
+            dirname  = image
+            filename = 'melodic_IC'
+
+
+        else:
+            dirname  = op.dirname( image)
+            filename = op.basename(image)
+
+        dirname = dirname.rstrip(op.sep)
+
+        if not melresults.isMelodicDir(dirname):
+            raise ValueError('{} does not appear to be a '
+                             'MELODIC directory'.format(dirname)) 
+        
+        if not filename.startswith('melodic_IC'):
+            raise ValueError('{} does not appear to be a MELODIC '
+                             'component file'.format(filename))
+            
+        fslimage.Image.__init__(self,
+                                op.join(dirname, filename),
+                                *args,
+                                **kwargs)
+
+        self.__melmix = melresults.getComponentTimeSeries(dirname)
+
+        
+    def getComponentTimeSeries(self, component):
+        return self.__melmix[:, component]
+
+
+    def numComponents(self):
+        return self.shape[3]
diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py
index 492a76e62..d05a25ab3 100644
--- a/fsl/data/melodicresults.py
+++ b/fsl/data/melodicresults.py
@@ -13,21 +13,90 @@ following functions are provided:
 .. autosummary::
    nosignatures:
 
-   isMELODICDir
-   getMELODICDir
-
+   isMelodicDir
+   getMelodicDir
    getICFile
-
    getNumComponents
    getComponentTimeSeries
 """
 
 
-def isMELODICDir(path):
+import            os 
+import os.path as op
+import numpy   as np
+
+import fsl.data.image as fslimage
+
+
+def isMelodicDir(path):
+    """
+    """
+
+    # Must be named *.ica or *.gica
+    meldir = getMelodicDir(path)
+
+    if meldir is None:
+        return False
+
+    # Must contain an image file called melodic_IC
+    try:
+        fslimage.addExt(op.join(meldir, 'melodic_IC'), mustExist=True)
+    except ValueError:
+        return False
+
+    # Must contain a file called melodic_mix
+    if not op.exists(op.join(meldir, 'melodic_mix')):
+        return False
+
+    return True
+
+    
+def getMelodicDir(path):
+    """
+    """
+
+    # TODO This code is identical to featresults.getFEATDir.
+    # Can you generalise it and put it somewhere in fsl.utils?
+
+    sufs     = ['.ica', '.gica']
+    idxs     = [(path.rfind(s), s) for s in sufs]
+    idx, suf = max(idxs, key=lambda (i, s): i)
+
+    if idx == -1:
+        return None
+
+    idx  += len(suf)
+    path  = path[:idx]
+
+    if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)):
+        return path
+                                           
+    return None 
+
+
+def getICFile(meldir):
+    """
+    """
+    return fslimage.addExt(op.join(meldir, 'melodic_IC'))
+
+
+def getMixFile(meldir):
+    """
+    """
+    return op.join(meldir, 'melodic_mix')
+
+
+def getNumComponents(meldir):
+    """
+    """
+
+    icImg = fslimage.Image(getICFile(meldir), loadData=False)
+    return icImg.shape[3]
+
+
+def getComponentTimeSeries(meldir):
     """
     """
 
-    # A MELODIC directory:
-    #   - Must be called *.ica
-    #   - Must contain melodic_IC.nii.gz
-    #   - Must contain melodic_mix
+    mixfile = getMixFile(meldir)
+    return np.loadtxt(mixfile)
diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py
index 7d94536c8..a4da0a512 100644
--- a/fsl/fsleyes/controls/timeserieslistpanel.py
+++ b/fsl/fsleyes/controls/timeserieslistpanel.py
@@ -15,12 +15,12 @@ import          copy
 import          wx
 import numpy as np
 
-import                           props
-import pwidgets.elistbox      as elistbox
-import fsl.fsleyes.panel      as fslpanel
-import fsl.fsleyes.tooltips   as fsltooltips
-import fsl.data.strings       as strings
-import fsl.fsleyes.colourmaps as fslcm
+import                                      props
+import pwidgets.elistbox                 as elistbox
+import fsl.fsleyes.panel                 as fslpanel
+import fsl.fsleyes.tooltips              as fsltooltips
+import fsl.data.strings                  as strings
+import fsl.fsleyes.colourmaps            as fslcm
 
 
 class TimeSeriesListPanel(fslpanel.FSLEyesPanel):
@@ -114,12 +114,17 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel):
         """Creates a label to use for the given :class:`.TimeSeries` instance.
         """
 
+        import fsl.fsleyes.views.timeseriespanel as tsp
+
         display = self._displayCtx.getDisplay(ts.overlay)
 
-        return '{} [{} {} {}]'.format(display.name,
-                                      ts.coords[0],
-                                      ts.coords[1],
-                                      ts.coords[2])
+        if isinstance(ts, tsp.MelodicTimeSeries):
+            return '{} [component {}]'.format(display.name, ts.coords)
+        else:
+            return '{} [{} {} {}]'.format(display.name,
+                                          ts.coords[0],
+                                          ts.coords[1],
+                                          ts.coords[2])
 
 
     def __makeFEATModelTSLabel(self, parentTs, modelTs):
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index c6d6fbf24..2ee9a540e 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -191,19 +191,24 @@ def guessDataSourceType(filename):
     is unrecognised, the first tuple value will be ``None``.
     """
 
-    import fsl.data.image       as fslimage
-    import fsl.data.model       as fslmodel
-    import fsl.data.featimage   as fslfeatimage
-    import fsl.data.featresults as featresults
+    import fsl.data.image          as fslimage
+    import fsl.data.model          as fslmodel
+    import fsl.data.featimage      as fslfeatimage
+    import fsl.data.melodicimage   as fslmelimage
+    import fsl.data.melodicresults as melresults
+    import fsl.data.featresults    as featresults
+
+    filename = op.abspath(filename)
 
     if filename.endswith('.vtk'):
         return fslmodel.Model, filename
 
     else:
-
         if op.isdir(filename):
             if featresults.isFEATDir(filename):
                 return fslfeatimage.FEATImage, filename
+            elif melresults.isMelodicDir(filename):
+                return fslmelimage.MelodicImage, filename 
         else:
         
             filename = fslimage.addExt(filename, False)
@@ -212,6 +217,8 @@ def guessDataSourceType(filename):
 
                 if featresults.isFEATDir(filename):
                     return fslfeatimage.FEATImage, filename
+                elif melresults.isMelodicDir(filename):
+                    return fslmelimage.MelodicImage, filename
                 else:
                     return fslimage.Image, filename
 
@@ -288,7 +295,8 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True):
         e     = str(e)
         msg   = strings.messages['overlay.loadOverlays.error'].format(s, e)
         title = strings.titles[  'overlay.loadOverlays.error']
-        log.debug('Error loading overlay ({}), ({})'.format(s, e))
+        log.debug('Error loading overlay ({}), ({})'.format(s, e),
+                  exc_info=True)
         wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) 
 
     # If loadFunc or errorFunc are explicitly set to
diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index d8079e8f0..c69b7c484 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -488,7 +488,8 @@ class PlotPanel(viewpanel.ViewPanel):
         if ds.alpha == 0:
             return (0, 0), (0, 0)
 
-        log.debug('Drawing plot for {}'.format(ds.overlay))
+        log.debug('Drawing {} for {}'.format(type(ds).__name__,
+                                             ds.overlay))
 
         xdata, ydata = ds.getData()
 
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index 7a39d1217..79bfcf41d 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -23,6 +23,7 @@ details):
 
    TimeSeries
    FEATTimeSeries
+   MelodicTimeSeries
 """
 
 
@@ -37,6 +38,7 @@ import                                                props
 
 import                                                plotpanel
 import fsl.data.featimage                          as fslfeatimage
+import fsl.data.melodicimage                       as fslmelimage
 import fsl.data.image                              as fslimage
 import fsl.fsleyes.displaycontext                  as fsldisplay
 import fsl.fsleyes.colourmaps                      as fslcmaps
@@ -66,8 +68,9 @@ class TimeSeries(plotpanel.DataSeries):
         plotpanel.DataSeries.__init__(self, overlay)
 
         self.tsPanel = tsPanel
-        self.coords  = map(int, coords)
-        self.data    = overlay.data[coords[0], coords[1], coords[2], :]
+        self.coords  = None
+        self.data    = None
+        self.update(coords)
 
 
     def __copy__(self):
@@ -91,15 +94,15 @@ class TimeSeries(plotpanel.DataSeries):
                      destroyed/recreated whenever the
                      :attr:`.DisplayContext.location` changes.
         """
-        
-        coords = map(int, coords)
-        if coords == self.coords:
-            return False
-        
+
         self.coords = coords
-        self.data   = self.overlay.data[coords[0], coords[1], coords[2], :]
+        self.data = self._getData(coords)
         return True
 
+
+    def _getData(self, coords):
+        return self.overlay.data[coords[0], coords[1], coords[2], :]
+    
         
     def getData(self, xdata=None, ydata=None):
         """Overrides :meth:`.DataSeries.getData` Returns the data associated
@@ -648,6 +651,17 @@ class FEATModelFitTimeSeries(TimeSeries):
         self.data = self.overlay.fit(contrast, xyz, fitType == 'full')
 
 
+
+class MelodicTimeSeries(TimeSeries):
+
+    def __init__(self, tsPanel, overlay, component):
+        TimeSeries.__init__(self, tsPanel, overlay, component)
+
+
+    def _getData(self, component):
+        return self.overlay.getComponentTimeSeries(component)
+
+
 class TimeSeriesPanel(plotpanel.PlotPanel):
     """A :class:`.PlotPanel` which plots time series data from
     :class:`.Image` overlays.
@@ -707,12 +721,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
 
     The ``TimeSeriesPanel`` has some extra functionality for
-    :class:`.FEATImage` overlays. For these overlays, a :class:`FEATTimeSeries`
-    instance is plotted, instead of a regular :class:`TimeSeries` instance. The
-    ``FEATTimeSeries`` class, in turn, has the ability to generate more
-    ``TimeSeries`` instances which represent various aspects of the FEAT model
-    fit. See the :class:`FEATTimeSeries` and the
-    :class:`.TimeSeriesControlPanel` classes for more details.
+    :class:`.FEATImage` overlays. For these overlays, a
+    :class:`.FEATTimeSeries` instance is plotted, instead of a regular
+    :class:`.TimeSeries` instance. The ``FEATTimeSeries`` class, in turn, has
+    the ability to generate more ``TimeSeries`` instances which represent
+    various aspects of the FEAT model fit. See the :class:`.FEATTimeSeries`
+    and the :class:`.TimeSeriesControlPanel` classes for more details.
     """
 
     
@@ -844,7 +858,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
         
     def getCurrent(self):
-        """Returns the :class:`TimeSeries` instance for the current time
+        """Returns the :class:`.TimeSeries` instance for the current time
         course. If :attr:`showCurrent` is ``False``, or the currently
         selected overlay is not a :class:`.Image` (see
         :attr:`.DisplayContext.selectedOverlay`) this method will return
@@ -881,34 +895,25 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
                 overlays = [o for o in self._overlayList
                             if o is not currOverlay]
 
-                # Remove overlays for which the
-                # current location is out of bounds
-                locs   = map(self.__getTimeSeriesLocation, overlays)
-                locovl = filter(lambda (l, o): l is not None,
-                                zip(locs, overlays))
-                
-                if len(locovl) > 0:
-                    locs, overlays = zip(*locovl)
-
-                    tss = map(self.__genTimeSeries, overlays, locs)
+                tss = map(self.__genTimeSeries, overlays)
 
-                    extras.extend([ts for ts in tss if ts is not None])
+                extras.extend([ts for ts in tss if ts is not None])
                     
-                    for ts in tss:
-                        ts.alpha     = 1
-                        ts.lineWidth = 0.5
-
-                        # Use a random colour for each overlay,
-                        # but use the same random colour each time
-                        colour = self.__overlayColours.get(
-                            ts.overlay,
-                            fslcmaps.randomBrightColour())
+                for ts in tss:
+                    ts.alpha     = 1
+                    ts.lineWidth = 0.5
+
+                    # Use a random colour for each overlay,
+                    # but use the same random colour each time
+                    colour = self.__overlayColours.get(
+                        ts.overlay,
+                        fslcmaps.randomBrightColour())
                         
-                        ts.colour = colour
-                        self.__overlayColours[ts.overlay] = colour
+                    ts.colour = colour
+                    self.__overlayColours[ts.overlay] = colour
                         
-                        if isinstance(ts, FEATTimeSeries):
-                            extras.extend(ts.getModelTimeSeries())
+                    if isinstance(ts, FEATTimeSeries):
+                        extras.extend(ts.getModelTimeSeries())
                 
             self.drawDataSeries(extras)
         else:
@@ -918,9 +923,9 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
     def __currentSettingsChanged(self, *a):
         """Called when the settings controlling the display of the current time
         course(s) changes.  If the current time course is a
-        :class:`FEATTimeSeries`, the display settings are propagated to all of
+        :class:`.FEATTimeSeries`, the display settings are propagated to all of
         its associated time courses (see the
-        :meth:`FEATTimeSeries.getModelTimeSeries` method).
+        :meth:`.FEATTimeSeries.getModelTimeSeries` method).
         """
         if self.__currentTs is None:
             return
@@ -948,7 +953,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
     def __overlaysChanged(self, *a):
         """Called when the :class:`.OverlayList` changes. Makes sure
-        that there are no :class:`TimeSeries` instances in the
+        that there are no :class:`.TimeSeries` instances in the
         :attr:`.PlotPanel.dataSeries` list which refer to overlays that
         no longer exist.
         """
@@ -962,7 +967,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
 
 
     def __bindCurrentProps(self, ts, bind=True):
-        """Binds or unbinds the properties of the given :class:`TimeSeries`
+        """Binds or unbinds the properties of the given :class:`.TimeSeries`
         instance with the current display settings (e.g.
         :attr:`currentColour`, :attr:`currentAlpha`, etc).
         """
@@ -992,6 +997,9 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
            not overlay.is4DImage():
             return None
 
+        if isinstance(overlay, fslmelimage.MelodicImage):
+            return opts.volume
+
         vox = opts.transformCoords([[x, y, z]], 'display', 'voxel')[0]
         vox = np.round(vox)
 
@@ -1006,15 +1014,25 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         return vox
 
     
-    def __genTimeSeries(self, overlay, vox):
-        """Creates and returns a :class:`TimeSeries` or :class:`FEATTimeSeries`
-        instance for the specified voxel of the specified overlay.
+    def __genTimeSeries(self, overlay):
+        """Creates and returns a :class:`.TimeSeries` or
+        :class:`.FEATTimeSeries` instance for the specified voxel of the
+        specified overlay.
         """
 
+        loc = self.__getTimeSeriesLocation(overlay)
+
+        if loc is None:
+            return None
+
         if isinstance(overlay, fslfeatimage.FEATImage):
-            ts = FEATTimeSeries(self, overlay, vox)
+            ts = FEATTimeSeries(self, overlay, loc)
+            
+        elif isinstance(overlay, fslmelimage.MelodicImage):
+            ts = MelodicTimeSeries(self, overlay, loc)
+            
         else:
-            ts = TimeSeries(self, overlay, vox)
+            ts = TimeSeries(self, overlay, loc)
 
         ts.colour    = self.currentColour
         ts.alpha     = self.currentAlpha
@@ -1043,18 +1061,14 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             return
 
         overlay = self._displayCtx.getSelectedOverlay()
-        vox     = self.__getTimeSeriesLocation(overlay)
-
-        if vox is None:
-            return
 
         if overlay is prevOverlay:
             self.__currentOverlay = prevOverlay
             self.__currentTs      = prevTs
-            prevTs.update(vox)
+            prevTs.update(self.__getTimeSeriesLocation(overlay))
 
         else:
-            ts                    = self.__genTimeSeries(overlay, vox)
+            ts                    = self.__genTimeSeries(overlay)
             self.__currentTs      = ts
             self.__currentOverlay = overlay
 
-- 
GitLab