diff --git a/fsl/fslview/orthopanel.py b/fsl/fslview/orthopanel.py index f5475bacfb47c4d9aee8f45c9b47c39cd45df4bb..c6ca3c2e07580ac81c4274cb176fcac02002a1f6 100644 --- a/fsl/fslview/orthopanel.py +++ b/fsl/fslview/orthopanel.py @@ -8,24 +8,36 @@ # import wx -import wx.lib.newevent as wxevent import fsl.props as props import fsl.data.fslimage as fslimage import fsl.fslview.slicecanvas as slicecanvas -# The OrthoPanel emits a LocationEvent whenever the 'cursor' location -# changes. It contains three attributes, x, y, and z, corresponding to -# the current cursor location in the image space. -LocationEvent, EVT_LOCATION_EVENT = wxevent.NewEvent() - class OrthoPanel(wx.Panel, props.HasProperties): - displayXCanvas = props.Boolean(default=True) - displayYCanvas = props.Boolean(default=True) - displayZCanvas = props.Boolean(default=True) - + showXCanvas = props.Boolean(default=True) + showYCanvas = props.Boolean(default=True) + showZCanvas = props.Boolean(default=True) + showCursor = props.Boolean(default=True) + + xpos = props.Double(clamped=True) + ypos = props.Double(clamped=True) + zpos = props.Double(clamped=True) + + xCanvasZoom = props.Double(minval=1.0, + maxval=20.0, + default=1.0, + clamped=True) + yCanvasZoom = props.Double(minval=1.0, + maxval=20.0, + default=1.0, + clamped=True) + zCanvasZoom = props.Double(minval=1.0, + maxval=20.0, + default=1.0, + clamped=True) + def __init__(self, parent, imageList): """ Creates three SliceCanvas objects, each displaying the images @@ -37,6 +49,7 @@ class OrthoPanel(wx.Panel, props.HasProperties): 'imageList must be a fsl.data.fslimage.ImageList instance') self.imageList = imageList + self.name = 'OrthoPanel_{}'.format(id(self)) wx.Panel.__init__(self, parent) self.SetMinSize((300, 100)) @@ -63,38 +76,136 @@ class OrthoPanel(wx.Panel, props.HasProperties): self.ycanvas.Bind(wx.EVT_MOTION, self._setCanvasPosition) self.zcanvas.Bind(wx.EVT_MOTION, self._setCanvasPosition) + xmin = imageList.minBounds[0] + xmax = imageList.maxBounds[0] + ymin = imageList.minBounds[1] + ymax = imageList.maxBounds[1] + zmin = imageList.minBounds[2] + zmax = imageList.maxBounds[2] + + self.xpos = xmin + abs(xmax - xmin) / 2.0 + self.ypos = ymin + abs(ymax - ymin) / 2.0 + self.zpos = zmin + abs(zmax - zmin) / 2.0 + + self.imageList.addListener(lambda il: self._updateImageBounds()) + self._updateImageBounds() + + self._configShowListeners() + self._configZoomListeners() + + + def _configShowListeners(self): + def showCursor(ctx, value, valid): + self.xcanvas.showCursor = value + self.ycanvas.showCursor = value + self.zcanvas.showCursor = value + + def showXCanvas(ctx, value, valid): + self.sizer.Show(self.xcanvas, value) + self.Layout() + def showYCanvas(ctx, value, valid): + self.sizer.Show(self.ycanvas, value) + self.Layout() + def showZCanvas(ctx, value, valid): + self.sizer.Show(self.zcanvas, value) + self.Layout() + + self.addListener('showCursor', self.name, showCursor) + self.addListener('showXCanvas', self.name, showXCanvas) + self.addListener('showYCanvas', self.name, showYCanvas) + self.addListener('showZCanvas', self.name, showZCanvas) + + + def _configZoomListeners(self): + + + def xzoom(ctx, value, valid): + value = 1.0 / value + xlen = value * abs(self.imageList.maxBounds[1] - + self.imageList.minBounds[1]) + ylen = value * abs(self.imageList.maxBounds[2] - + self.imageList.minBounds[2]) + + if value == 1: + xcentre = self.imageList.minBounds[1] + 0.5 * xlen + ycentre = self.imageList.minBounds[2] + 0.5 * ylen + else: + xcentre = self.ypos + ycentre = self.zpos + + self.xcanvas.xmin = xcentre - 0.5 * xlen + self.xcanvas.xmax = xcentre + 0.5 * xlen + self.xcanvas.ymin = ycentre - 0.5 * ylen + self.xcanvas.ymax = ycentre + 0.5 * ylen + + self.addListener('xCanvasZoom', self.name, xzoom) + - def setLocation(self, x, y, z): + def _updateImageBounds(self): """ - Programmatically set the currently displayed location - on each of the canvases. This does not trigger an - EVT_LOCATION_EVENT. """ - - self.xcanvas.xpos = y - self.xcanvas.ypos = z - self.xcanvas.zpos = x - - self.ycanvas.xpos = x - self.ycanvas.ypos = z - self.ycanvas.zpos = y - self.zcanvas.xpos = x - self.zcanvas.ypos = y - self.zcanvas.zpos = z - - self.xcanvas.Refresh() - self.ycanvas.Refresh() - self.zcanvas.Refresh() - - def setXLocation(self, x): - self.setLocation(x, self.ycanvas.zpos, self.zcanvas.zpos) - - def setYLocation(self, y): - self.setLocation(self.xcanvas.zpos, y, self.zcanvas.zpos) + xmin = self.imageList.minBounds[0] + xmax = self.imageList.maxBounds[0] + ymin = self.imageList.minBounds[1] + ymax = self.imageList.maxBounds[1] + zmin = self.imageList.minBounds[2] + zmax = self.imageList.maxBounds[2] + + self.setConstraint('xpos', 'minval', xmin) + self.setConstraint('xpos', 'maxval', xmax) + self.setConstraint('ypos', 'minval', ymin) + self.setConstraint('ypos', 'maxval', ymax) + self.setConstraint('zpos', 'minval', zmin) + self.setConstraint('zpos', 'maxval', zmax) + + # reset the cursor and min/max values in + # case the old values were out of bounds + self.xpos = self.xpos + self.ypos = self.ypos + self.zpos = self.zpos + + + def _shiftCanvas(self, canvas, xax, yax, newx, newy): + + if newx >= canvas.xmin and \ + newx <= canvas.xmax and \ + newy >= canvas.ymin and \ + newy <= canvas.ymax: + return + + xshift = 0 + yshift = 0 + + imgxmin = self.imageList.minBounds[xax] + imgxmax = self.imageList.maxBounds[xax] + imgymin = self.imageList.minBounds[yax] + imgymax = self.imageList.maxBounds[yax] + + if newx < canvas.xmin: xshift = newx - canvas.xmin + elif newx > canvas.xmax: xshift = newx - canvas.xmax + if newy < canvas.ymin: yshift = newy - canvas.ymin + elif newy > canvas.ymax: yshift = newy - canvas.ymax + + newxmin = canvas.xmin + xshift + newxmax = canvas.xmax + xshift + newymin = canvas.ymin + xshift + newymax = canvas.ymax + xshift + + if newxmin < imgxmin: xshift = newxmin - imgxmin + elif newxmax > imgxmax: xshift = newxmax - imgxmax + + if newymin < imgymin: yshift = newymin - imgymin + elif newymax > imgymax: yshift = newymax - imgymax - def setZLocation(self, z): - self.setLocation(self.xcanvas.zpos, self.ycanvas.zpos, z) + if xshift != 0: + print 'xshift {}'.format(xshift) + canvas.xmin = canvas.xmin + xshift + canvas.xmax = canvas.xmax + xshift + if yshift != 0: + print 'yshift {}'.format(yshift) + canvas.ymin = canvas.ymin + yshift + canvas.ymax = canvas.ymax + yshift def _setCanvasPosition(self, ev): @@ -115,23 +226,67 @@ class OrthoPanel(wx.Panel, props.HasProperties): my = h - my - x = self.xcanvas.zpos - y = self.ycanvas.zpos - z = self.zcanvas.zpos - - mx = source.canvasToWorldX(mx) - my = source.canvasToWorldY(my) - - if source == self.xcanvas: y, z = mx, my - elif source == self.ycanvas: x, z = mx, my - elif source == self.zcanvas: x, y = mx, my - - self.setLocation(x, y, z) - - evt = LocationEvent(x=x, y=y, z=z) - wx.PostEvent(self, evt) - - + xpos = source.canvasToWorldX(mx) + ypos = source.canvasToWorldY(my) + + if source == self.xcanvas: + self.ypos = xpos + self.zpos = ypos + + self.xcanvas.xpos = xpos + self.xcanvas.ypos = ypos + + self.ycanvas.ypos = ypos + self.ycanvas.zpos = xpos + + self.zcanvas.ypos = xpos + self.zcanvas.zpos = ypos + + if self.yCanvasZoom != 1: + self._shiftCanvas(self.ycanvas, 0, 2, self.ycanvas.xpos, ypos) + + if self.zCanvasZoom != 1: + self._shiftCanvas(self.zcanvas, 0, 1, self.zcanvas.xpos, xpos) + + elif source == self.ycanvas: + self.xpos = xpos + self.zpos = ypos + + self.ycanvas.xpos = xpos + self.ycanvas.ypos = ypos + + self.xcanvas.ypos = ypos + self.xcanvas.zpos = xpos + + self.zcanvas.xpos = xpos + self.zcanvas.zpos = ypos + + if self.xCanvasZoom != 1: + self._shiftCanvas(self.xcanvas, 1, 2, self.xcanvas.xpos, ypos) + + if self.zCanvasZoom != 1: + self._shiftCanvas(self.zcanvas, 0, 1, xpos, self.zcanvas.ypos) + + elif source == self.zcanvas: + self.xpos = xpos + self.ypos = ypos + + self.zcanvas.xpos = xpos + self.zcanvas.ypos = ypos + + self.xcanvas.xpos = ypos + self.xcanvas.zpos = xpos + + self.ycanvas.xpos = xpos + self.ycanvas.zpos = ypos + + if self.xCanvasZoom != 1: + self._shiftCanvas(self.xcanvas, 1, 2, ypos, self.xcanvas.ypos) + + if self.yCanvasZoom != 1: + self._shiftCanvas(self.ycanvas, 0, 2, xpos, self.ycanvas.ypos) + + class OrthoFrame(wx.Frame): """ Convenience class for displaying an OrthoPanel in a standalone window. diff --git a/fsl/fslview/slicecanvas.py b/fsl/fslview/slicecanvas.py index 53476af868c199cd084b918cb7924111ac6ff9b9..4069541fa1617652d2f696c539a81b8b58105bbd 100644 --- a/fsl/fslview/slicecanvas.py +++ b/fsl/fslview/slicecanvas.py @@ -158,16 +158,19 @@ class SliceCanvas(wxgl.GLCanvas, props.HasProperties): # when any of the xyz properties of # this canvas change, we need to redraw - self.addListener('xpos', self.name, self._refresh) - self.addListener('ypos', self.name, self._refresh) - self.addListener('zpos', self.name, self._refresh) - self.addListener('xmin', self.name, self._refresh) - self.addListener('xmax', self.name, self._refresh) - self.addListener('ymin', self.name, self._refresh) - self.addListener('ymax', self.name, self._refresh) - self.addListener('zmin', self.name, self._refresh) - self.addListener('zmax', self.name, self._refresh) - self.addListener('showCursor', self.name, self._refresh) + def posRefresh( *a): self._refresh() + def boundRefresh(*a): self._refresh(True) + + self.addListener('xpos', self.name, posRefresh) + self.addListener('ypos', self.name, posRefresh) + self.addListener('zpos', self.name, posRefresh) + self.addListener('xmin', self.name, boundRefresh) + self.addListener('xmax', self.name, boundRefresh) + self.addListener('ymin', self.name, boundRefresh) + self.addListener('ymax', self.name, boundRefresh) + self.addListener('zmin', self.name, boundRefresh) + self.addListener('zmax', self.name, boundRefresh) + self.addListener('showCursor', self.name, boundRefresh) # When drawn, the slice does not necessarily take # up the entire canvas size, as its aspect ratio @@ -272,12 +275,14 @@ class SliceCanvas(wxgl.GLCanvas, props.HasProperties): glData = glimagedata.GLImageData(image, self.xax, self.yax) image.setAttribute(self.name, glData) - image.display.addListener('enabled', self.name, self._refresh) - image.display.addListener('alpha', self.name, self._refresh) - image.display.addListener('displayMin', self.name, self._refresh) - image.display.addListener('displayMax', self.name, self._refresh) - image.display.addListener('rangeClip', self.name, self._refresh) - image.display.addListener('cmap', self.name, self._refresh) + def refresh(*a): self._refresh() + + image.display.addListener('enabled', self.name, refresh) + image.display.addListener('alpha', self.name, refresh) + image.display.addListener('displayMin', self.name, refresh) + image.display.addListener('displayMax', self.name, refresh) + image.display.addListener('rangeClip', self.name, refresh) + image.display.addListener('cmap', self.name, refresh) self.Refresh() @@ -380,13 +385,13 @@ class SliceCanvas(wxgl.GLCanvas, props.HasProperties): self.glReady = True - def _refresh(self, *a): + def _refresh(self, constraints=False): """ Called when a display property changes. Updates x/y/z property values, updates the canvas bounding box, and triggers a redraw. """ - self._setPropertyConstraints() + if constraints: self._setPropertyConstraints() self._calculateCanvasBBox(None) self.Refresh() diff --git a/fsl/tools/fslview.py b/fsl/tools/fslview.py index 11fd296ba8fc03d1bed1dbd6e1b0901a4ec79e41..c10c2ac852642bc2b3017fb34f3c26e157f8646a 100644 --- a/fsl/tools/fslview.py +++ b/fsl/tools/fslview.py @@ -11,6 +11,7 @@ import fsl.fslview.orthopanel as orthopanel import fsl.fslview.imagelistpanel as imagelistpanel import fsl.data.fslimage as fslimage +import fsl.props as props class FslViewPanel(wx.Panel): @@ -21,11 +22,13 @@ class FslViewPanel(wx.Panel): self.imageList = imageList self.orthoPanel = orthopanel .OrthoPanel( self, imageList) + self.ctrlPanel = props.buildGUI(self, self.orthoPanel) self.listPanel = imagelistpanel.ImageListPanel(self, imageList) self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) + self.sizer.Add(self.ctrlPanel, flag=wx.EXPAND) self.sizer.Add(self.orthoPanel, flag=wx.EXPAND, proportion=1) self.sizer.Add(self.listPanel, flag=wx.EXPAND)