From b48b94273ccf8d2be70fe6c3fb2484d6979166a0 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Thu, 28 May 2015 15:57:43 +0100
Subject: [PATCH] Overlay-specific location preservation logic in
 DisplayContext is now in *Opts classes.

---
 fsl/fslview/displaycontext/display.py        |  6 ++
 fsl/fslview/displaycontext/displaycontext.py | 93 +++++++++-----------
 fsl/fslview/displaycontext/modelopts.py      | 48 +++++++++-
 fsl/fslview/displaycontext/volumeopts.py     | 41 ++++-----
 4 files changed, 113 insertions(+), 75 deletions(-)

diff --git a/fsl/fslview/displaycontext/display.py b/fsl/fslview/displaycontext/display.py
index e84be0f70..534ac88e2 100644
--- a/fsl/fslview/displaycontext/display.py
+++ b/fsl/fslview/displaycontext/display.py
@@ -77,6 +77,12 @@ class DisplayOpts(props.SyncableHasProperties):
             'The getDisplayBounds method must be implemented by subclasses')
 
 
+    def transformDisplayLocation(self, propName, oldLoc):
+        """
+        """
+        return oldLoc
+
+
 class Display(props.SyncableHasProperties):
     """
     """
diff --git a/fsl/fslview/displaycontext/displaycontext.py b/fsl/fslview/displaycontext/displaycontext.py
index 751a8784d..a12108765 100644
--- a/fsl/fslview/displaycontext/displaycontext.py
+++ b/fsl/fslview/displaycontext/displaycontext.py
@@ -10,9 +10,7 @@ import logging
 
 import props
 
-import fsl.utils.transform as transform
-import display             as fsldisplay
-import                        volumeopts
+import display as fsldisplay
 
 
 log = logging.getLogger(__name__)
@@ -88,9 +86,6 @@ class DisplayContext(props.SyncableHasProperties):
         overlayList.addListener('overlays',
                                 self.__name,
                                 self.__overlayListChanged)
-        self.addListener(       'bounds',
-                                self.__name,
-                                self.__boundsChanged)
 
         
     def getDisplay(self, overlay):
@@ -261,33 +256,35 @@ class DisplayContext(props.SyncableHasProperties):
             
     def __displayOptsChanged(self, value, valid, opts, name):
         """Called when the :class:`.DisplayOpts` properties of any overlay
-        change. Updates the :attr:`bounds` property.
+        change. If the bounds o the Updates the :attr:`bounds` property and,
+        if the currently selected 
         """
 
         # This check is ugly, and is required due to
         # an ugly circular relationship which exists
-        # between parent/child DCs and the transform/
+        # between parent/child DCs and the *Opts/
         # location properties:
         # 
-        # 1. When the transform property of a child DC
-        #    Display object changes (this should always 
-        #    be due to user input), that change is 
-        #    propagated to the parent DC Display object.
+        # 1. When a property of a child DC DisplayOpts
+        #    object changes (e.g. ImageOpts.transform)
+        #    this should always be due to user input),
+        #    that change is propagated to the parent DC
+        #    DisplayOpts object.
         #
-        # 2. This results in the DC._transformChanged
+        # 2. This results in the DC._displayOptsChanged
         #    method (this method) being called on the
         #    parent DC.
         #
         # 3. Said method correctly updates the DC.location
         #    property, so that the world location of the
-        #    selected image is preserved.
+        #    selected overlay is preserved.
         #
         # 4. This location update is propagated back to
         #    the child DC.location property, which is
         #    updated to have the new correct location
         #    value.
         #
-        # 5. Then, the child DC._transformChanged method
+        # 5. Then, the child DC._displayOpteChanged method
         #    is called, which goes and updates the child
         #    DC.location property to contain a bogus
         #    value.
@@ -301,37 +298,40 @@ class DisplayContext(props.SyncableHasProperties):
             if self.getParent().location == self.location:
                 return
 
+        overlay = opts.display.getOverlay()
 
-        overlay  = opts.display.getOverlay()
-        refImage = self.getReferenceImage(overlay)
-
-        if refImage is not None:
-            opts = self.getOpts(refImage)
+        # Save a copy of the location before
+        # updating the bounds, as the change
+        # to the bounds may result in the
+        # location being modified
+        oldDispLoc = self.location.xyz
 
-        if (overlay != self.getSelectedOverlay()) or \
-           not isinstance(opts, volumeopts.ImageOpts):
-            self.__updateBounds()
+        # Update the display context bounds
+        # to take into account any changes 
+        # to individual overlay bounds
+        self.__updateBounds()
+ 
+        # The main purpose of this method is to preserve
+        # the current display location in terms of the
+        # currently selected overlay, when the overlay
+        # bounds have changed. We don't care about changes
+        # to the options for other overlays.
+        if (overlay != self.getSelectedOverlay()):
             return
 
-        # If the currently selected overlay is an Image, and its
-        # transformation matrix property has changed, we want the
-        # current display location to be preserved, in terms of
-        # the corresponding image world coordinates.
+        # Now we want to update the display location
+        # so that it is preserved with respect to the
+        # currently selected overlay.
+        newDispLoc = opts.transformDisplayLocation(name, oldDispLoc)
+
+        log.debug('Preserving display location in '
+                  'terms of overlay {} ({}.{}): {} -> {}'.format(
+                      overlay,
+                      type(opts).__name__,
+                      name,
+                      oldDispLoc,
+                      newDispLoc))
         
-        # Calculate the image world location using
-        # the old display<-> world transform, then
-        # transform it back to the new world->display
-        # transform. 
-        imgWorldLoc = transform.transform(
-            [self.location.xyz],
-            opts.getTransform(opts.getLastTransform(), 'world'))[0]
-        newDispLoc  = transform.transform(
-            [imgWorldLoc],
-            opts.getTransform('world', 'display'))[0]
-
-        # Update the display coordinate 
-        # system bounds, and the location
-        self.__updateBounds()
         self.location.xyz = newDispLoc
 
 
@@ -362,14 +362,9 @@ class DisplayContext(props.SyncableHasProperties):
         self.bounds[:] = [minBounds[0], maxBounds[0],
                           minBounds[1], maxBounds[1],
                           minBounds[2], maxBounds[2]]
- 
-            
-    def __boundsChanged(self, *a):
-        """Called when the :attr:`bounds` property changes.
-
-        Updates the constraints on the :attr:`location` property.
-        """
-
+        
+        # Update the constraints on the :attr:`location` 
+        # property to be aligned with the new bounds
         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)
diff --git a/fsl/fslview/displaycontext/modelopts.py b/fsl/fslview/displaycontext/modelopts.py
index e8b3a8064..f5b0dd8e3 100644
--- a/fsl/fslview/displaycontext/modelopts.py
+++ b/fsl/fslview/displaycontext/modelopts.py
@@ -112,14 +112,58 @@ class ModelOpts(fsldisplay.DisplayOpts):
         return opts.getTransform(self.coordSpace, self.transform)
 
 
-    def __refImageChanged(self, *a):
+    def transformDisplayLocation(self, propName, oldLoc):
+
+        newLoc = oldLoc
         
+        if propName == 'refImage':
+
+            refImage    = self.refImage
+            oldRefImage = self.getLastValue('refImage')
+
+            if oldRefImage == 'none':
+                refOpts = self.displayCtx.getOpts(refImage)
+                newLoc  = transform.transform(
+                    [oldLoc],
+                    refOpts.getTransform(self.coordSpace, 'display'))[0] 
+
+            elif refImage == 'none':
+                oldRefOpts = self.displayCtx.getOpts(oldRefImage)
+                newLoc = transform.transform(
+                    [oldLoc],
+                    oldRefOpts.getTransform('display', self.coordSpace))[0]
+
+        elif propName == 'coordSpace':
+            if self.refImage != 'none':
+                refOpts  = self.displayCtx.getOpts(self.refImage)
+                worldLoc = transform.transform(
+                    [oldLoc],
+                    refOpts.getTransform(
+                        self.getLastValue('coordSpace'),
+                        'world'))[0]
+                newLoc   = transform.transform(
+                    [worldLoc],
+                    refOpts.getTransform(
+                        'world',
+                        self.coordSpace))[0]
+
+        elif propName == 'transform':
+
+            if self.refImage != 'none':
+                refOpts = self.displayCtx.getOpts(self.refImage)
+                newLoc  = refOpts.transformDisplayLocation(propName, oldLoc)
+
+        return newLoc
+
+
+    def __refImageChanged(self, *a):
+
         if self.__oldRefImage != 'none':
             opts = self.displayCtx.getOpts(self.__oldRefImage)
             self.unbindProps('transform', opts)
 
         self.__oldRefImage = self.refImage
-        
+
         if self.refImage != 'none':
             opts = self.displayCtx.getOpts(self.refImage)
             self.bindProps('transform', opts)
diff --git a/fsl/fslview/displaycontext/volumeopts.py b/fsl/fslview/displaycontext/volumeopts.py
index 61a1630e8..0f24336fe 100644
--- a/fsl/fslview/displaycontext/volumeopts.py
+++ b/fsl/fslview/displaycontext/volumeopts.py
@@ -70,7 +70,7 @@ class ImageOpts(fsldisplay.DisplayOpts):
         overlay = self.overlay
 
         # The display<->* transformation matrices
-        # are created in the _transformChanged method
+        # are created in the _setupTransforms method
         self.__xforms = {}
         self.__setupTransforms()
 
@@ -78,19 +78,13 @@ class ImageOpts(fsldisplay.DisplayOpts):
         if self.overlay.is4DImage():
             self.setConstraint('volume', 'maxval', overlay.shape[3] - 1)
 
-        self.addListener('transform', self.name, self.__transformChanged) 
-
-        self.__oldTransform = None
-        self.__transform    = self.transform
-        self.__transformChanged()
-
         # limit resolution to the image dimensions
         self.resolution = min(overlay.pixdim[:3])
         self.setConstraint('resolution', 'minval', self.resolution)
 
 
     def destroy(self):
-        self.removeListener('transform',  self.name)
+        pass
 
 
     def getDisplayBounds(self):
@@ -185,24 +179,23 @@ class ImageOpts(fsldisplay.DisplayOpts):
         return self.__xforms[from_, to]
 
 
-    def getLastTransform(self):
-        """Returns the most recent value of the :attr:`transform` property,
-        before its current value.
-        """
-        return self.__oldTransform
-
+    def transformDisplayLocation(self, propName, oldLoc):
 
-    def __transformChanged(self, *a):
-        """Called when the :attr:`transform` property is changed."""
+        if propName != 'transform':
+            return oldLoc
 
-
-        # Store references to the previous display related transformation
-        # matrices, just in case anything (hint the DisplayContext object)
-        # needs them for any particular reason (hint: so the DisplayContext
-        # can preserve the current display location, in terms of image world
-        # space, when the transform of the selected image changes)
-        self.__oldTransform = self.__transform
-        self.__transform    = self.transform
+        # Calculate the image world location using the
+        # old display<-> world transform, then transform
+        # it back to the new world->display transform. 
+        worldLoc = transform.transform(
+            [oldLoc],
+            self.getTransform(self.getLastValue('transform'), 'world'))[0]
+        
+        newLoc  = transform.transform(
+            [worldLoc],
+            self.getTransform('world', 'display'))[0]
+        
+        return newLoc
 
 
 class VolumeOpts(ImageOpts):
-- 
GitLab