#
# sampleline.py - The SampleLineAction
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`SampleLineAction` class, which allows
the user to open a :class:`SampleLinePanel`. The ``SampleLinePanel`` is
a FSLeyes control which allows the user to draw a line on the canvases of
an :class:`.OrthoPanel`, and plot the data along that line from the currently
selected :class:`.Image` overlay.
"""
import os
import copy
import numpy as np
import scipy.ndimage as ndimage
import wx
import fsl.data.image as fslimage
import fsl.utils.settings as fslsettings
import fsl.transform.affine as affine
import fsleyes_widgets.widgetlist as widgetlist
import fsleyes_widgets.utils.status as status
import fsleyes_props as props
import fsleyes.strings as strings
import fsleyes.tooltips as tooltips
import fsleyes.icons as fslicons
import fsleyes.actions as actions
import fsleyes.actions.screenshot as screenshot
import fsleyes.views.orthopanel as orthopanel
import fsleyes.controls.controlpanel as ctrlpanel
import fsleyes.plotting as plotting
import fsleyes.plotting.plotcanvas as plotcanvas
import fsleyes.plugins.profiles.samplelineprofile as samplelineprofile
[docs]def sampleAlongLine(data, start, end, resolution, order):
"""Samples from ``data``, along a line between ``start`` and ``end``.
:arg data: 3D array
:arg start: Start coordinate
:arg end: End coordinate
:arg resolution: Number of points to sample
:arg order: Interpolation (see ``scipy.ndimage.map_coordinates``)
:returns: Tuple containing:
- 1D Numpy array containing the sampled values
- ``(3, N)`` numpy array containing the coordinates for
each sample
"""
start = list(start)
end = list(end)
shape = data.shape
coords = np.linspace(start, end, resolution).T
# map_coordinates doesn't take
# kindly to dims of length 1
data = data.squeeze()
if any(s == 1 for s in shape):
drop = [i for i, s in enumerate(shape) if s == 1]
mapcoords = np.delete(coords, drop, axis=0)
else:
mapcoords = coords
# multi-channel data?
if len(data.dtype) > 1:
channels = []
for chan in data.dtype.fields.keys():
channels.append(data[chan])
data = channels
else:
data = [data]
ys = []
for arr in data:
ys.append(ndimage.map_coordinates(arr,
mapcoords,
order=order,
output=np.float64))
# For multi channel data, we currently
# just take the mean across all
# channels, but this might change in
# the future (if there is any need).
if len(ys) > 1: y = np.mean(ys, axis=0)
else: y = ys[0]
return y, coords
[docs]class SampleLineAction(actions.ToggleControlPanelAction):
"""The ``SampleLineAction`` simply shows/hides a :class:`SampleLinePanel`.
"""
[docs] @staticmethod
def supportedViews():
"""The ``SampleLineAction`` is restricted for use with
:class:`.OrthoPanel` views.
"""
return [orthopanel.OrthoPanel]
[docs] def __init__(self, overlayList, displayCtx, ortho):
"""Create a ``SampleLineAction``. """
super().__init__(overlayList, displayCtx, ortho, SampleLinePanel)
self.__ortho = ortho
self.__name = '{}_{}'.format(type(self).__name__, id(self))
displayCtx.addListener('selectedOverlay', self.__name,
self.__selectedOverlayChanged)
[docs] def destroy(self):
"""Called when the :class:`.OrthoPanel` that owns this action is
closed. Clears references, removes listeners, and calls the base
class ``destroy`` method.
"""
if self.destroyed:
return
self.__ortho = None
self.displayCtx.removeListener('selectedOverlay', self.__name)
super().destroy()
def __selectedOverlayChanged(self, *a):
"""Called when the selected overlay changes. Enables/disables this
action (and hence the bound Tools menu item) depending on whether the
overlay is an image.
"""
ovl = self.displayCtx.getSelectedOverlay()
self.enabled = isinstance(ovl, fslimage.Image)
def __run(self):
"""Open/close a :class:`SampleLinePanel`. """
self.viewPanel.togglePanel(SampleLinePanel)
[docs]class SampleLineDataSeries(plotting.DataSeries):
"""The ``SampleLineDataSeries`` represents data that is sampled along
a straight line through a 3D volume from an :class:`.Image` overlay.
``SampleLineDataSeries`` objects are created by the
:class:`SampleLinePanel`.
"""
interp = props.Choice((0, 1, 2, 3), default=0)
"""How to interpolate the sampled data when it is plotted. The value is
used directly as the ``order`` parameter to the
``scipy.ndimage.map_coordinates`` function.
"""
resolution = props.Int(minval=2, maxval=200, default=100, clamped=False)
"""The number of points (uniformly spaced) to sample along the line. """
normalise = props.Choice(('none', 'y', 'x', 'xy'))
"""Whether to normalise all plotted data along the x, or y, or both, axes.
"""
[docs] def __init__(self,
overlay,
overlayList,
displayCtx,
plotCanvas,
index,
start,
end):
"""Create a ``SampleLineDataSeries``.
:arg overlay: The :class:`.Image` overlay to sample from
:arg overlayList: The :class:`.OverlayList`
:arg displayCtx: A :class:`.DisplayContext` instance
:arg plotCanvas: The :class:`.PlotCanvas` that is plotting this
``DataSeries``.
:arg index: Volume index, for images with more than 3
dimensions (see :meth:`.NiftiOpts.index`)
:arg start: Start of sampling line, in voxel coordinates
:arg end: End of sampling line, in voxel coordinates
"""
plotting.DataSeries.__init__(
self, overlay, overlayList, displayCtx, plotCanvas)
self.__index = index
self.__start = start
self.__end = end
self.__coords = None
self.addListener('resolution', self.name, self.__refreshData)
self.addListener('interp', self.name, self.__refreshData)
self.addListener('normalise', self.name, self.__refreshData)
self.label = ('{}: [{:.2f} {:.2f} {:.2f}] -> '
'[{:.2f} {:.2f} {:.2f}]'.format(
overlay.name, *start, *end))
@property
def coords(self):
"""Return a ``(3, n)`` array containing the voxel coordinates of
each sampled point for the most recently generated data, or ``None``
if no data has been sampled yet.
"""
return self.__coords
def __refreshData(self, *a):
"""Called when :attr:`resolution`, :attr:`interp` or :attr:`normalise`
change. Re-samples the data from the image.
"""
overlay = self.overlay
opts = self.displayCtx.getOpts(overlay)
data = overlay[self.__index]
resolution = self.resolution
order = self.interp
normalisex = 'x' in self.normalise
normalisey = 'y' in self.normalise
start = self.__start
end = self.__end
y, coords = sampleAlongLine(data, start, end, resolution, order)
if normalisey:
y = (y - y.min()) / (y.max() - y.min())
if normalisex:
xmax = 1
else:
wstart = opts.transformCoords(start, 'voxel', 'world')
wend = opts.transformCoords(end, 'voxel', 'world')
xmax = affine.veclength(wstart - wend)[0]
x = np.linspace(0, xmax, resolution)
self.__coords = coords
self.setData(x, y)
[docs]class SampleLinePanel(ctrlpanel.ControlPanel):
"""The ``SampleLinePanel`` is a FSLeyes control which can be used in
conjunction with an :class:`.OrthoPanel` view. It allows the user to draw
a line on an ``OrthoPanel`` canvas, and plot the voxel intensities, along
that line, from the currently selected :class:`.Image` overlay.
The :class:`.SampleLineProfile` class implements user interaction, and the
:class:`.PlotCanvas` class is used for plotting.
"""
# Used to synchronise between GUI widgets
# and SampleLineDataSeries property values
# The GUI widgets are bound to the most
# recently added SampleLineDataSeries
# instance
interp = copy.copy(SampleLineDataSeries.interp)
resolution = copy.copy(SampleLineDataSeries.resolution)
normalise = copy.copy(SampleLineDataSeries.normalise)
colour = copy.copy(plotting.DataSeries.colour)
lineWidth = copy.copy(plotting.DataSeries.lineWidth)
lineStyle = copy.copy(plotting.DataSeries.lineStyle)
[docs] @staticmethod
def supportedViews():
"""Overrides :meth:`.ControlMixin.supportedViews`. The
``SampleLinePanel`` is only intended to be added to
:class:`.OrthoPanel` views.
"""
return [orthopanel.OrthoPanel]
[docs] @staticmethod
def ignoreControl():
"""Tells FSLeyes not to add the ``SampleLinePanel`` as an option to
the Settings menu. Instead, the :class:`SampleLineAction` is added as
an option to the Tools menu.
"""
return True
[docs] @staticmethod
def profileCls():
"""Returns the :class:`.SampleLineProfile` class, which needs to be
activated in conjunction with the ``SampleLinePanel``.
"""
return samplelineprofile.SampleLineProfile
[docs] @staticmethod
def defaultLayout():
"""Returns a dictionary containing layout settings to be passed to
:class:`.ViewPanel.togglePanel`.
"""
return {'floatPane' : True,
'floatOnly' : True}
[docs] def __init__(self, parent, overlayList, displayCtx, ortho):
"""Create a ``SampleLinePanel``.
:arg parent: The :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg ortho: The :class:`.OrthoPanel` instance.
"""
ctrlpanel.ControlPanel.__init__(
self, parent, overlayList, displayCtx, ortho)
profile = ortho.currentProfile
# plot which displays the sampled data
canvas = plotcanvas.PlotCanvas(self, drawFunc=self.__draw)
canvas.canvas.SetMinSize((-1, 150))
self.__ortho = ortho
self.__profile = profile
self.__canvas = canvas
self.__current = None
# initial settings
self.colour = '#000050'
self.lineWidth = 2
canvas.legend = False
# Controls which allow the user to select
# interpolation/resolution, line colour, etc
widgets = widgetlist.WidgetList(self)
legend = props.makeWidget(
widgets, canvas, 'legend',
labels=strings.properties['PlotCanvas.legend'])
interp = props.makeWidget(
widgets, self, 'interp', labels=strings.choices[self, 'interp'])
resolution = props.makeWidget(
widgets, self, 'resolution', slider=True, spin=True,
showLimits=False)
normalise = props.makeWidget(
widgets, self, 'normalise',
labels=strings.choices[self, 'normalise'])
colour = props.makeWidget(widgets, self, 'colour')
lineWidth = props.makeWidget(widgets, self, 'lineWidth')
lineStyle = props.makeWidget(
widgets, self, 'lineStyle',
labels=strings.choices['DataSeries.lineStyle'])
widgets.AddWidget(legend, strings.labels[self, 'legend'])
widgets.AddWidget(interp, strings.labels[self, 'interp'])
widgets.AddWidget(resolution, strings.labels[self, 'resolution'])
widgets.AddWidget(normalise, strings.labels[self, 'normalise'])
widgets.AddWidget(colour, strings.labels[self, 'colour'])
widgets.AddWidget(lineWidth, strings.labels[self, 'lineWidth'])
widgets.AddWidget(lineStyle, strings.labels[self, 'lineStyle'])
# Controls allowing the user to add/remove
# sample lines from the plot to save the
# data to a file, and to save a screenshot
# of the plot
ctrlSizer = wx.BoxSizer(wx.HORIZONTAL)
screenshot = actions.ActionButton(
'screenshot',
icon=fslicons.findImageFile('camera24'),
tooltip=tooltips.actions['PlotPanel.screenshot'])
export = actions.ActionButton(
'export',
icon=fslicons.findImageFile('exportDataSeries24'),
tooltip=tooltips.actions['PlotPanel.exportDataSeries'])
add = actions.ActionButton(
'addDataSeries',
icon=fslicons.findImageFile('add24'),
tooltip=tooltips.actions[self, 'addDataSeries'])
remove = actions.ActionButton(
'removeDataSeries',
icon=fslicons.findImageFile('remove24'),
tooltip=tooltips.actions[self, 'removeDataSeries'])
screenshot = props.buildGUI(self, self, screenshot)
export = props.buildGUI(self, self, export)
add = props.buildGUI(self, self, add)
remove = props.buildGUI(self, self, remove)
ctrlSizer.Add(screenshot, flag=wx.EXPAND)
ctrlSizer.Add(export, flag=wx.EXPAND)
ctrlSizer.Add(add, flag=wx.EXPAND)
ctrlSizer.Add(remove, flag=wx.EXPAND)
# Labels which show the start and end
# coordinates of the sample line in voxel
# ("v") and world ("w") coordinates, and
# the line length in [units] (probably mm)
vfromlbl = wx.StaticText(self)
vtolbl = wx.StaticText(self)
wfromlbl = wx.StaticText(self)
wtolbl = wx.StaticText(self)
vfromval = wx.StaticText(self)
vtoval = wx.StaticText(self)
wfromval = wx.StaticText(self)
wtoval = wx.StaticText(self)
lenlbl = wx.StaticText(self)
lenval = wx.StaticText(self)
self.__vfromval = vfromval
self.__vtoval = vtoval
self.__wfromval = wfromval
self.__wtoval = wtoval
self.__lenval = lenval
vfromlbl.SetLabel(strings.labels[self, 'voxelfrom'])
vtolbl .SetLabel(strings.labels[self, 'voxelto'])
wfromlbl.SetLabel(strings.labels[self, 'worldfrom'])
wtolbl .SetLabel(strings.labels[self, 'worldto'])
lenlbl .SetLabel(strings.labels[self, 'length'])
infoSizer = wx.FlexGridSizer(3, 4, 5, 5)
infoSizer.AddGrowableCol(1)
infoSizer.AddGrowableCol(3)
infoSizer.Add(vfromlbl)
infoSizer.Add(vfromval)
infoSizer.Add(vtolbl)
infoSizer.Add(vtoval)
infoSizer.Add(wfromlbl)
infoSizer.Add(wfromval)
infoSizer.Add(wtolbl)
infoSizer.Add(wtoval)
infoSizer.Add(lenlbl)
infoSizer.Add(lenval)
infoSizer.Add((1, 1))
infoSizer.Add((1, 1))
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(ctrlSizer, flag=wx.EXPAND)
mainSizer.Add(widgets, flag=wx.EXPAND)
mainSizer.Add(infoSizer, flag=wx.EXPAND)
mainSizer.Add(canvas.canvas, flag=wx.EXPAND, proportion=1)
self.SetSizer(mainSizer)
self.Layout()
profile.registerHandler('LeftMouseDown', self.name, self.__onMouseDown)
profile.registerHandler('LeftMouseDrag', self.name, self.__onMouseDrag)
profile.registerHandler('LeftMouseUp', self.name, self.__onMouseUp)
self.addListener('interp', self.name, canvas.asyncDraw)
self.addListener('resolution', self.name, canvas.asyncDraw)
self.addListener('normalise', self.name, canvas.asyncDraw)
self.addListener('colour', self.name, canvas.asyncDraw)
self.addListener('lineWidth', self.name, canvas.asyncDraw)
self.addListener('lineStyle', self.name, canvas.asyncDraw)
[docs] def destroy(self):
"""Called when this ``SampleLinePanel`` is no longer needed. Clears
references, and calls :meth:`.ControlPanel.destroy`.
"""
super().destroy()
self.__canvas.destroy()
self.__ortho = None
self.__profile = None
self.__canvas = None
self.__current = None
@property
def canvas(self):
"""Return a reference to the :class:`.PlotCanvas` that is used to
plot sampled data from lines that the user has drawn.
"""
return self.__canvas
[docs] @actions.action
def addDataSeries(self):
"""Holds/persists the most recently sampled line to the plot. """
if self.__current is not None:
self.__canvas.dataSeries.append(self.__current)
self.__bindToDataSeries(self.__current, False)
self.__current = None
[docs] @actions.action
def removeDataSeries(self):
"""Removes the most recently held/persisted line from the plot. """
canvas = self.__canvas
if len(canvas.dataSeries) > 0:
canvas.dataSeries.pop()
[docs] @actions.action
def export(self):
"""Prompts the user to save the sampled data to a file. """
# only one series can be saved - the
# user is asked to select which one
if self.__current is None: series = []
else: series = [self.__current]
series.extend(reversed(self.__canvas.dataSeries))
if len(series) == 0:
return
# ask what series, and whether they
# want coordinates of sample points
parent = self.GetParent()
dlg = ExportSampledDataDialog(parent, series)
if dlg.ShowModal() != wx.ID_OK:
return
series = dlg.GetSeries()
saveCoords = dlg.GetCoordinates()
# ask where to save the file
fromDir = fslsettings.read('loadSaveOverlayDir', os.getcwd())
msg = strings.titles[self, 'savefile']
dlg = wx.FileDialog(parent,
message=msg,
defaultDir=fromDir,
defaultFile='sample.txt',
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if dlg.ShowModal() != wx.ID_OK:
return
filename = dlg.GetPath()
# prepare the data
if saveCoords == 'none':
data = series.getData()[1]
else:
image = series.overlay
opts = self.displayCtx.getOpts(image)
samples = series.getData()[1]
coords = series.coords.T
data = np.zeros((len(samples), 4))
data[:, 0] = samples
if saveCoords == 'voxel':
data[:, 1:] = coords
else:
data[:, 1:] = opts.transformCoords(coords, 'voxel', 'world')
# save the file
errTitle = strings.titles[ self, 'exportError']
errMsg = strings.messages[self, 'exportError']
with status.reportIfError(errTitle, errMsg):
np.savetxt(filename, data, fmt='%0.8f')
[docs] @actions.action
def screenshot(self):
"""Creates and runs a :class:`.ScreenshotAction`, which propmts the
user to save the plot to a file.
"""
screenshot.ScreenshotAction(self.overlayList,
self.displayCtx,
self.__canvas)()
def __bindToDataSeries(self, ds, bind=True):
"""Binds/unbinds the GUI widgets (:attr:`colour`, :attr:`resolution`),
etc to/from the given :class:`SampleLineDataSeries` instance.
"""
ds.bindProps('colour', self, unbind=not bind)
ds.bindProps('lineWidth', self, unbind=not bind)
ds.bindProps('lineStyle', self, unbind=not bind)
ds.bindProps('resolution', self, unbind=not bind)
ds.bindProps('interp', self, unbind=not bind)
ds.bindProps('normalise', self, unbind=not bind)
def __updateInfo(self):
"""Called while the mouse is being dragged. Updates the information
about the sampling line (e.g. start/end voxel coordinates) that is
displayed at the top of the ``SampleLinePanel``.
"""
start = self.__profile.sampleStart
end = self.__profile.sampleEnd
if start is None or end is None:
self.__vfromval.SetLabel('')
self.__vtoval .SetLabel('')
self.__wfromval.SetLabel('')
self.__wtoval .SetLabel('')
self.__lenval .SetLabel('')
return
overlay = self.displayCtx.getSelectedOverlay()
opts = self.displayCtx.getOpts(overlay)
image = opts.referenceImage
# display world and voxel coordinates
if image is not None:
opts = self.displayCtx.getOpts(image)
units = image.xyzUnits
units = strings.nifti.get(('xyz_unit', units), '(unknown units)')
ws = opts.transformCoords(start, 'display', 'world')
we = opts.transformCoords(end, 'display', 'world')
vs = opts.transformCoords(start, 'display', 'voxel')
ve = opts.transformCoords(end, 'display', 'voxel')
length = affine.veclength(ws - we)[0]
vs = '[{:.2f}, {:.2f}, {:.2f}]'.format(*vs)
ve = '[{:.2f}, {:.2f}, {:.2f}]'.format(*ve)
ws = '[{:.2f}, {:.2f}, {:.2f}]'.format(*ws)
we = '[{:.2f}, {:.2f}, {:.2f}]'.format(*we)
length = f'{length:.2f} {units}'
self.__vfromval.SetLabel(vs)
self.__vtoval .SetLabel(ve)
self.__wfromval.SetLabel(ws)
self.__wtoval .SetLabel(we)
self.__lenval .SetLabel(length)
def __onMouseDown(self, *a):
"""Called on mouse down events on an :class:`.OrthoPanel` canvas.
Calls :meth:`__updateInfo`.
"""
self.__updateInfo()
def __onMouseDrag(self, *a):
"""Called on mouse drag events on an :class:`.OrthoPanel` canvas.
Calls :meth:`__updateInfo`.
"""
self.__updateInfo()
def __onMouseUp(self, *a):
"""Called on mouse up events on an :class:`.OrthoPanel` canvas.
Samples and plots data from the currently selected overlay along
the drawn sample line.
"""
start = self.__profile.sampleStart
end = self.__profile.sampleEnd
image = self.displayCtx.getSelectedOverlay()
if start is None or end is None:
return
if image is None or not isinstance(image, fslimage.Image):
return
if self.__current is not None:
self.__bindToDataSeries(self.__current, False)
# Round to avoid floating point imprecision
opts = self.displayCtx.getOpts(image)
vstart = opts.transformCoords(start, 'display', 'voxel').round(6)
vend = opts.transformCoords(end, 'display', 'voxel').round(6)
series = SampleLineDataSeries(image,
self.overlayList,
self.displayCtx,
self.__canvas,
opts.index(),
vstart,
vend)
self.__bindToDataSeries(series)
self.__current = series
self.__canvas.asyncDraw()
def __draw(self):
"""Passed as the ``drawFunc`` to the :class:`.PlotCanvas`. Calls
:meth:`.PlotCanvas.drawDataSeries`.
"""
if self.__current is None: extras = None
else: extras = [self.__current]
self.__canvas.drawDataSeries(extraSeries=extras, refresh=True)
[docs]class ExportSampledDataDialog(wx.Dialog):
"""The ``ExportSampledDataDialog`` is used by the
:meth:`SampleLinePanel.export` method to ask the user which sample data
they want to save, and whether they want to save the sample point
coordinates to file as well as the samples themselves.
"""
[docs] def __init__(self, parent, series):
"""Create an ``ExportSampledDataDialog``.
:arg parent: ``wx`` parent object
:arg series: Sequence of ``SampleLineDataSeries`` instances. Must
contain at least one series. If there is more than one
series, the user is asked to choose one.
"""
title = strings.titles[self]
coords = strings.choices[self, 'saveCoordinates']
seriesLabels = [s.label for s in series]
coordsLabels = list(coords.values())
coords = list(coords.keys())
wx.Dialog.__init__(self,
parent,
title=title,
style=wx.DEFAULT_DIALOG_STYLE)
self.__series = series
self.__coords = coords
self.__coordsChoice = wx.Choice(self, choices=coordsLabels)
self.__coordsChoice.SetSelection(0)
coordsLabel = wx.StaticText(self)
ok = wx.Button(self, id=wx.ID_OK)
cancel = wx.Button(self, id=wx.ID_CANCEL)
coordsLabel.SetLabel(strings.labels[self, 'coords'])
ok .SetLabel(strings.labels[self, 'ok'])
cancel .SetLabel(strings.labels[self, 'cancel'])
if len(series) > 1:
self.__seriesChoice = wx.Choice(self, choices=seriesLabels)
self.__seriesChoice.SetSelection(0)
seriesLabel = wx.StaticText(self)
seriesLabel.SetLabel(strings.labels[self, 'series'])
ok.SetDefault()
ok .Bind(wx.EVT_BUTTON, self.__onOk)
cancel.Bind(wx.EVT_BUTTON, self.__onCancel)
buttonSizer = wx.BoxSizer(wx.HORIZONTAL)
mainSizer = wx.BoxSizer(wx.VERTICAL)
buttonSizer.Add((10, 10), proportion=1)
buttonSizer.Add(ok)
buttonSizer.Add((10, 10))
buttonSizer.Add(cancel)
buttonSizer.Add((10, 10), proportion=1)
mainSizer.Add((10, 10), proportion=1)
if len(series) > 1:
mainSizer.Add(seriesLabel,
flag=wx.ALIGN_CENTRE | wx.ALL, border=10)
mainSizer.Add((10, 10))
mainSizer.Add(self.__seriesChoice,
flag=wx.ALIGN_CENTRE | wx.ALL, border=10)
mainSizer.Add((10, 10))
mainSizer.Add(coordsLabel, flag=wx.ALIGN_CENTRE | wx.ALL, border=10)
mainSizer.Add((10, 10))
mainSizer.Add(self.__coordsChoice,
flag=wx.ALIGN_CENTRE | wx.ALL, border=10)
mainSizer.Add((10, 10), proportion=1)
mainSizer.Add(buttonSizer, flag=wx.ALIGN_CENTRE | wx.ALL, border=10)
mainSizer.Add((10, 10))
self.SetSizer(mainSizer)
self.Layout()
self.Fit()
self.CentreOnParent()
def __onOk(self, ev):
"""Called when the ok button is pushed. Closes the dialog. """
self.EndModal(wx.ID_OK)
def __onCancel(self, ev):
"""Called when the cancel button is pushed. Closes the dialog. """
self.EndModal(wx.ID_CANCEL)
[docs] def GetCoordinates(self):
"""Return one of ``'none'``, ``'voxel'``, or ``'world'``, denoting
the user's preference for saving coordinates to the file.
"""
idx = self.__coordsChoice.GetSelection()
return self.__coords[idx]
[docs] def GetSeries(self):
"""Return the :class:`.SampleLineDataSeries` that was selected."""
if len(self.__series) == 1:
return self.__series[0]
idx = self.__seriesChoice.GetSelection()
return self.__series[idx]