From eded900fcbae8cda5393a89ded7018290d22d81d Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 28 Oct 2015 18:12:40 +0000
Subject: [PATCH] Refactored TimeSeriesPanel preprocessing - the
 scaling/offsets controlled by the usePixdim and plotMode properties are now
 performed by the TimeSeriesPanel, instead of the TimeSeries class. This was
 done because TimeSeries instances added to the list already had
 scaling/offsetting applied, so it was potentially being doubly applied. This
 is no longer the case, due to a new 'preproc' option in the
 PlotPanel.drawDataSeries method.

Also added some melodic specific stuff to the OverlayInfoPanel.

Also fixed the overlay.guessDataSourceType to handle melodic analyses
that are located inside other melodic/feat analysis directories.

Also tweaked the new colourmaps.randomDarkColour function to be slightly
less dark.
---
 fsl/data/featresults.py                     |  2 +-
 fsl/data/melodicimage.py                    | 10 +++
 fsl/data/melodicresults.py                  |  4 ++
 fsl/data/strings.py                         | 28 ++++++--
 fsl/fsleyes/colourmaps.py                   |  2 +-
 fsl/fsleyes/controls/overlayinfopanel.py    | 40 ++++++++++--
 fsl/fsleyes/controls/timeserieslistpanel.py |  2 +-
 fsl/fsleyes/overlay.py                      | 71 ++++++++++++++-------
 fsl/fsleyes/plotting/timeseries.py          | 28 ++------
 fsl/fsleyes/views/plotpanel.py              | 21 ++++--
 fsl/fsleyes/views/timeseriespanel.py        | 46 +++++++++++--
 11 files changed, 180 insertions(+), 74 deletions(-)

diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py
index 719092a03..138c0379e 100644
--- a/fsl/data/featresults.py
+++ b/fsl/data/featresults.py
@@ -134,7 +134,7 @@ def loadDesign(featdir):
             if line.strip() == '/Matrix':
                 break
 
-        matrix = np.loadtxt(f)
+        matrix = np.loadtxt(f, ndmin=2)
 
     if matrix is None or matrix.size == 0:
         raise RuntimeError('{} does not appear to be a '
diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py
index 1456386c9..3a2087da9 100644
--- a/fsl/data/melodicimage.py
+++ b/fsl/data/melodicimage.py
@@ -31,6 +31,7 @@ class MelodicImage(fslimage.Image):
        tr
        getComponentTimeSeries
        numComponents
+       getTopLevelAnalysisDir
        getDataFile
     """
 
@@ -98,6 +99,15 @@ class MelodicImage(fslimage.Image):
         return self.shape[3]
 
 
+    def getTopLevelAnalysisDir(self):
+        """Returns the top level analysis, if the melodic analysis for this
+        ``MelodicImage`` is contained within another analysis. Otherwise,
+        returnsa ``None``. See the
+        :func:`.melodicresults.getTopLevelAnalysisDir` function.
+        """
+        return melresults.getTopLevelAnalysisDir(self.__meldir)
+
+
     def getDataFile(self):
         """Returns the file name of the data image from which this
         ``MelodicImage`` was generated, if possible. See the
diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py
index f020003fb..c31811723 100644
--- a/fsl/data/melodicresults.py
+++ b/fsl/data/melodicresults.py
@@ -142,6 +142,10 @@ def getMixFile(meldir):
     return op.join(meldir, 'melodic_mix')
 
 
+def getReportFile(meldir):
+    pass
+
+
 def getNumComponents(meldir):
     """Returns the number of components generated in the melodic analysis
     contained in the given directrory.
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 27ec5ed61..ba3d48926 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -325,13 +325,17 @@ labels = TypeDict({
     'OverlayInfoPanel.Image.transform'    : 'Transform/space',
     'OverlayInfoPanel.Image.orient'       : 'Orientation',
     
-    'OverlayInfoPanel.Image'              : 'NIFTI1 image',
-    'OverlayInfoPanel.FEATImage'          : 'NIFTI1 image (FEAT analysis)',
-    'OverlayInfoPanel.FEATImage.featInfo' : 'FEAT information',
-    'OverlayInfoPanel.Model'              : 'VTK model',
-    'OverlayInfoPanel.Model.numVertices'  : 'Number of vertices',
-    'OverlayInfoPanel.Model.numIndices'   : 'Number of indices',
-    'OverlayInfoPanel.dataSource'         : 'Data source',
+    'OverlayInfoPanel.Image'                    : 'NIFTI1 image',
+    'OverlayInfoPanel.FEATImage'                : 'NIFTI1 image '
+                                                  '(FEAT analysis)',
+    'OverlayInfoPanel.FEATImage.featInfo'       : 'FEAT information',
+    'OverlayInfoPanel.MelodicImage'             : 'NIFTI1 image '
+                                                  '(MELODIC analysis)', 
+    'OverlayInfoPanel.MelodicImage.melodicInfo' : 'MELODIC information',
+    'OverlayInfoPanel.Model'                    : 'VTK model',
+    'OverlayInfoPanel.Model.numVertices'        : 'Number of vertices',
+    'OverlayInfoPanel.Model.numIndices'         : 'Number of indices',
+    'OverlayInfoPanel.dataSource'               : 'Data source',
 })
 
 
@@ -708,4 +712,14 @@ feat = TypeDict({
     'numPoints'    : 'Number of volumes',
     'numEVs'       : 'Number of EVs',
     'numContrasts' : 'Number of contrasts',
+    'report'       : 'Link to report',
+})
+
+
+melodic = TypeDict({
+    'dataFile'       : 'Data file',
+    'partOfAnalysis' : 'Part of analysis',
+    'numComponents'  : 'Number of ICs',
+    'tr'             : 'TR time',
+    'report'         : 'Link to report',
 })
diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py
index 46e1a2f1b..0589151de 100644
--- a/fsl/fsleyes/colourmaps.py
+++ b/fsl/fsleyes/colourmaps.py
@@ -626,7 +626,7 @@ def randomBrightColour():
 def randomDarkColour():
     """Generates a random saturated and darkened RGB colour."""
 
-    return applyBricon(randomBrightColour(), 0.25, 0.5)
+    return applyBricon(randomBrightColour(), 0.35, 0.5)
 
 
 def complementaryColour(rgb):
diff --git a/fsl/fsleyes/controls/overlayinfopanel.py b/fsl/fsleyes/controls/overlayinfopanel.py
index 8f15f8534..f44baf590 100644
--- a/fsl/fsleyes/controls/overlayinfopanel.py
+++ b/fsl/fsleyes/controls/overlayinfopanel.py
@@ -30,14 +30,15 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
        :scale: 50%
        :align: center
 
-    Slightly different informtion is shown depending on the overlay type,
+    Slightly different information is shown depending on the overlay type,
     and is generated by the following methods:
 
-    =================== ==========================
-    :class:`.Image`     :meth:`__getImageInfo`
-    :class:`.FEATImage` :meth:`__getFEATImageInfo`
-    :class:`.Model`     :meth:`__getModelInfo`
-    =================== ==========================
+    ====================== =============================
+    :class:`.Image`        :meth:`__getImageInfo`
+    :class:`.FEATImage`    :meth:`__getFEATImageInfo`
+    :class:`.MelodicImage` :meth:`__getMelodicImageInfo`
+    :class:`.Model`        :meth:`__getModelInfo`
+    ====================== =============================
     """
 
 
@@ -293,6 +294,33 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
 
         return info
 
+
+    def __getMelodicImageInfo(self, overlay, display):
+        """Creates and returns an :class:`OverlayInfo` object containing
+        information about the given :class:`.MelodicImage` overlay.
+
+        :arg overlay: A :class:`.MelodicImage` instance.
+        :arg display: The :class:`.Display` instance assocated with the
+                      ``MelodicImage``.
+        """
+
+        info = self.__getImageInfo(overlay, display)
+
+        melInfo = collections.OrderedDict([
+            ('tr',             overlay.tr),
+            ('dataFile',       overlay.getDataFile()),
+            ('partOfAnalysis', overlay.getTopLevelAnalysisDir()),
+            ('numComponents',  overlay.numComponents()),
+        ])
+
+        secName = strings.labels[self, overlay, 'melodicInfo']
+        info.addSection(secName)
+
+        for k, v in melInfo.items():
+            info.addInfo(strings.melodic[k], v, section=secName)
+
+        return info
+
     
     def __getModelInfo(self, overlay, display):
         """Creates and returns an :class:`OverlayInfo` object containing
diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py
index c3d500fb4..c211d35b9 100644
--- a/fsl/fsleyes/controls/timeserieslistpanel.py
+++ b/fsl/fsleyes/controls/timeserieslistpanel.py
@@ -183,7 +183,7 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel):
             copy.setData(*ts.getData())
 
             # This is hacky, and is here in order to
-            # make the __onLIstSelect method work.
+            # make the __onListSelect method work.
             if isinstance(ts, timeseries.MelodicTimeSeries):
                 copy.tsLoc = 'volume'
                 copy.coord = ts.getComponent()
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index 62d6c1890..053801d5f 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -182,12 +182,12 @@ class OverlayList(props.HasProperties):
         return self.overlays.insertAll(index, items) 
 
 
-def guessDataSourceType(filename):
+def guessDataSourceType(path):
     """A convenience function which, given the name of a file or directory,
     figures out a suitable overlay type.
 
     Returns a tuple containing two values - a type which should be able to
-    load the filename, and the filename, possibly adjusted. If the file type
+    load the path, and the path itself, possibly adjusted. If the type
     is unrecognised, the first tuple value will be ``None``.
     """
 
@@ -198,30 +198,53 @@ def guessDataSourceType(filename):
     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:
-            
-            try:               filename = fslimage.addExt(filename, True)
-            except ValueError: return None, filename
+    path = op.abspath(path)
+
+    # VTK files are easy
+    if path.endswith('.vtk'):
+        return fslmodel.Model, path
+
+    # Now, we check to see if the given
+    # path is part of a FEAT or MELODIC
+    # analysis. The way we go about this is
+    # a bit silly, but is necessary due to
+    # the fact thet a melodic analysis can
+    # be contained  within a feat analysis
+    # (or another melodic analysis). So we
+    # check for all analysis types and, if
+    # more than one analysis type matches,
+    # we return the one with the longest
+    # path name.
+    analyses = [
+        (fslfeatimage.FEATImage,    featresults.getFEATDir(   path)),
+        (fslmelimage .MelodicImage, melresults .getMelodicDir(path))]
+
+    # Remove the analysis types that didn't match
+    # (the get*Dir function returned None)
+    analyses = [(t, d) for (t, d) in analyses if d is not None]
+
+    # If we have one or more matches for
+    # an analysis directory, we return
+    # the one with the longest path
+    if len(analyses) > 0:
+
+        dirlens = map(len, [d for (t, d) in analyses])
+        maxidx  = dirlens.index(max(dirlens))
+        
+        return analyses[maxidx]
 
-            if featresults.isFEATDir(filename):
-                return fslfeatimage.FEATImage, filename
-            elif melresults.isMelodicDir(filename):
-                return fslmelimage.MelodicImage, filename
-            else:
-                return fslimage.Image, filename
+    # If the path is not an analysis directory,
+    # see if it is a regular nifti image
+    try:
+        path = fslimage.addExt(path, mustExist=True)
+        return fslimage.Image, path
+    
+    except ValueError:
+        pass
 
-    return None, filename
+    # Otherwise, I don't
+    # know what to do
+    return None, path
 
 
 def makeWildcard():
diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py
index 094d450c8..64c163109 100644
--- a/fsl/fsleyes/plotting/timeseries.py
+++ b/fsl/fsleyes/plotting/timeseries.py
@@ -26,9 +26,8 @@ import numpy as np
 
 import props
 
-import                          dataseries
-import fsl.data.strings      as strings
-import fsl.data.melodicimage as fslmelimage
+import                     dataseries
+import fsl.data.strings as strings
 
 
 class TimeSeries(dataseries.DataSeries):
@@ -72,8 +71,7 @@ class TimeSeries(dataseries.DataSeries):
         
     def getData(self, xdata=None, ydata=None):
         """Overrides :meth:`.DataSeries.getData`. Returns the data associated
-        with this ``TimeSeries`` instance, pre-processed according to the
-        current :class:`.TimeSeriesPanel` settings.
+        with this ``TimeSeries`` instance.
 
         The ``xdata`` and ``ydata`` arguments may be used by sub-classes to
         override the x/y data in the event that they have already performed
@@ -89,25 +87,7 @@ class TimeSeries(dataseries.DataSeries):
 
         xdata = np.array(xdata, dtype=np.float32)
         ydata = np.array(ydata, dtype=np.float32)
-
-        if self.tsPanel.usePixdim:
-            if isinstance(self.overlay, fslmelimage.MelodicImage):
-                xdata *= self.overlay.tr
-            else:
-                xdata *= self.overlay.pixdim[3]
         
-        if self.tsPanel.plotMode == 'demean':
-            ydata = ydata - ydata.mean()
-
-        elif self.tsPanel.plotMode == 'normalise':
-            ymin  = ydata.min()
-            ymax  = ydata.max()
-            ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1
-            
-        elif self.tsPanel.plotMode == 'percentChange':
-            mean  = ydata.mean()
-            ydata =  100 * (ydata / mean) - 100
-            
         return xdata, ydata
 
 
@@ -503,7 +483,7 @@ class FEATTimeSeries(VoxelTimeSeries):
                     copenum)
 
 
-    def __plotPEFitChanged(self, evnum):
+    def __plotPEFitChanged(self, *a):
         """Called when the :attr:`plotPEFits` setting changes.
 
         If necessary, creates and caches one or more
diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index 0a64d0e59..e80aa4b78 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -355,13 +355,16 @@ class PlotPanel(viewpanel.ViewPanel):
         self.Refresh()
 
 
-    def drawDataSeries(self, extraSeries=None, **plotArgs):
+    def drawDataSeries(self, extraSeries=None, preproc=None, **plotArgs):
         """Plots all of the :class:`.DataSeries` instances in the
         :attr:`dataSeries` list
 
         :arg extraSeries: A sequence of additional ``DataSeries`` to be
                           plotted.
 
+        :arg preproc:     An optional preprocessing function - passed to the
+                          :meth:`__drawOneDataSeries` method.
+
         :arg plotArgs:    Passed through to the :meth:`__drawOneDataSeries`
                           method.
         """
@@ -397,7 +400,7 @@ class PlotPanel(viewpanel.ViewPanel):
         ylims = []
 
         for ds in toPlot:
-            xlim, ylim = self.__drawOneDataSeries(ds, **plotArgs)
+            xlim, ylim = self.__drawOneDataSeries(ds, preproc, **plotArgs)
             xlims.append(xlim)
             ylims.append(ylim)
 
@@ -474,16 +477,26 @@ class PlotPanel(viewpanel.ViewPanel):
         self.Refresh()
 
         
-    def __drawOneDataSeries(self, ds, **plotArgs):
+    def __drawOneDataSeries(self, ds, preproc=None, **plotArgs):
         """Plots a single :class:`.DataSeries` instance. This method is called
         by the :meth:`drawDataSeries` method.
 
         :arg ds:       The ``DataSeries`` instance.
 
+        :arg preproc:  An optional preprocessing function which must accept
+                       the ``DataSeries`` instance as its sole argument, and
+                       must return the ``(xdata, ydata)`` with any required
+                       processing applied.  The default preprocessing function
+                       returns the result of a call to
+                       :meth:`.DataSeries.getData`.
+
         :arg plotArgs: May be used to customise the plot - these
                        arguments are all passed through to the
                        ``Axis.plot`` function.
         """
+        
+        if preproc is None:
+            preproc = lambda s: s.getData()
 
         if ds.alpha == 0:
             return (0, 0), (0, 0)
@@ -491,7 +504,7 @@ class PlotPanel(viewpanel.ViewPanel):
         log.debug('Drawing {} for {}'.format(type(ds).__name__,
                                              ds.overlay))
 
-        xdata, ydata = ds.getData()
+        xdata, ydata = preproc(ds)
 
         if len(xdata) != len(ydata) or len(xdata) == 0:
             return (0, 0), (0, 0)
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index 5a879e526..d83221c9a 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -20,7 +20,7 @@ import fsl.data.featimage                          as fslfeatimage
 import fsl.data.melodicimage                       as fslmelimage
 import fsl.data.image                              as fslimage
 import fsl.fsleyes.colourmaps                      as fslcmaps
-import fsl.fsleyes.plotting.timeseries             as timeseries
+import fsl.fsleyes.plotting                        as plotting
 import fsl.fsleyes.controls.timeseriescontrolpanel as timeseriescontrolpanel
 import fsl.fsleyes.controls.timeserieslistpanel    as timeserieslistpanel
 
@@ -281,14 +281,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         tss = [ts for ts in tss if ts is not None]
 
         for i, ts in enumerate(list(tss)):
-            if isinstance(ts, timeseries.FEATTimeSeries):
+            if isinstance(ts, plotting.FEATTimeSeries):
                 tss.pop(i)
                 tss = tss[:i] + ts.getModelTimeSeries() + tss[i:]
 
         for ts in tss:
             ts.label = ts.makeLabel()
 
-        self.drawDataSeries(extraSeries=tss)
+        self.drawDataSeries(extraSeries=tss,
+                            preproc=self.__prepareTimeSeriesData)
 
 
     def getTimeSeries(self, overlay):
@@ -298,6 +299,39 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
         return self.__currentTss.get(overlay)
 
 
+    def __prepareTimeSeriesData(self, ts):
+        """Given a :class:`.TimeSeries` instance, scales and normalises
+        the x and y data according to the current values of the
+        :attr:`usePixdim` and :attr:`plotMode` properties.
+
+        This method is used as a preprocessing function for all
+        :class:`.TimeSeries` instances that are plotted - see the
+        :meth:`.PlotPanel.drawDataSeries` method.
+        """
+
+        xdata, ydata = ts.getData()
+
+        if self.usePixdim:
+            if isinstance(ts.overlay, fslmelimage.MelodicImage):
+                xdata *= ts.overlay.tr
+            else:
+                xdata *= ts.overlay.pixdim[3]
+        
+        if self.plotMode == 'demean':
+            ydata = ydata - ydata.mean()
+
+        elif self.plotMode == 'normalise':
+            ymin  = ydata.min()
+            ymax  = ydata.max()
+            ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1
+            
+        elif self.plotMode == 'percentChange':
+            mean  = ydata.mean()
+            ydata =  100 * (ydata / mean) - 100
+            
+        return xdata, ydata 
+
+
     def __overlayListChanged(self, *a):
         """Called when the :class:`.OverlayList` changes. Makes sure that
         there are no :class:`.TimeSeries` instances in the
@@ -402,18 +436,18 @@ class TimeSeriesPanel(plotpanel.PlotPanel):
             return None, None, None
 
         if isinstance(overlay, fslfeatimage.FEATImage):
-            ts = timeseries.FEATTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx]
             propNames = ['location']
             
         elif isinstance(overlay, fslmelimage.MelodicImage) and \
              self.plotMelodicICs:
-            ts = timeseries.MelodicTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx.getOpts(overlay)]
             propNames = ['volume'] 
             
         else:
-            ts = timeseries.VoxelTimeSeries(self, overlay, self._displayCtx)
+            ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx]
             propNames = ['location'] 
 
-- 
GitLab