# TODO: the line slicer should listen to all 2DREFRESH events, get the data and slice it # before pushing a new 1D data update. # # TODO: NEED MAJOR REFACTOR # import math import wx # from copy import deepcopy # Debug printout from sas.guiframe.events import NewPlotEvent from sas.guiframe.events import StatusEvent from sas.guiframe.events import SlicerParameterEvent from sas.guiframe.events import EVT_SLICER_PARS from BaseInteractor import _BaseInteractor from sas.guiframe.dataFitting import Data1D class AnnulusInteractor(_BaseInteractor): """ Select an annulus through a 2D plot. This interactor is used to average 2D data with the region defined by 2 radius. this class is defined by 2 Ringinterators. """ def __init__(self, base, axes, color='black', zorder=3): _BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes self.base = base self.qmax = min(math.fabs(self.base.data2D.xmax), math.fabs(self.base.data2D.xmin)) # must be positive self.connect = self.base.connect # # Number of points on the plot self.nbins = 36 # Cursor position of Rings (Left(-1) or Right(1)) self.xmaxd = self.base.data2D.xmax self.xmind = self.base.data2D.xmin if (self.xmaxd + self.xmind) > 0: self.sign = 1 else: self.sign = -1 # Inner circle self.inner_circle = RingInteractor(self, self.base.subplot, zorder=zorder, r=self.qmax / 2.0, sign=self.sign) self.inner_circle.qmax = self.qmax self.outer_circle = RingInteractor(self, self.base.subplot, zorder=zorder + 1, r=self.qmax / 1.8, sign=self.sign) self.outer_circle.qmax = self.qmax * 1.2 self.update() self._post_data() # Bind to slice parameter events self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS) def _onEVT_SLICER_PARS(self, event): """ receive an event containing parameters values to reset the slicer :param event: event of type SlicerParameterEvent with params as attribute """ wx.PostEvent(self.base, StatusEvent(status="AnnulusSlicer._onEVT_SLICER_PARS")) event.Skip() if event.type == self.__class__.__name__: self.set_params(event.params) self.base.update() def set_layer(self, n): """ Allow adding plot to the same panel :param n: the number of layer """ self.layernum = n self.update() def clear(self): """ Clear the slicer and all connected events related to this slicer """ self.clear_markers() self.outer_circle.clear() self.inner_circle.clear() self.base.connect.clearall() self.base.Unbind(EVT_SLICER_PARS) def update(self): """ Respond to changes in the model by recalculating the profiles and resetting the widgets. """ # Update locations self.inner_circle.update() self.outer_circle.update() def save(self, ev): """ Remember the roughness for this layer and the next so that we can restore on Esc. """ self.base.freeze_axes() self.inner_circle.save(ev) self.outer_circle.save(ev) def _post_data(self, nbins=None): """ Uses annulus parameters to plot averaged data into 1D data. :param nbins: the number of points to plot """ # Data to average data = self.base.data2D # If we have no data, just return if data == None: return from sas.dataloader.manipulations import Ring rmin = min(math.fabs(self.inner_circle.get_radius()), math.fabs(self.outer_circle.get_radius())) rmax = max(math.fabs(self.inner_circle.get_radius()), math.fabs(self.outer_circle.get_radius())) # if the user does not specify the numbers of points to plot # the default number will be nbins= 36 if nbins == None: self.nbins = 36 else: self.nbins = nbins # # create the data1D Q average of data2D sect = Ring(r_min=rmin, r_max=rmax, nbins=self.nbins) sector = sect(self.base.data2D) if hasattr(sector, "dxl"): dxl = sector.dxl else: dxl = None if hasattr(sector, "dxw"): dxw = sector.dxw else: dxw = None new_plot = Data1D(x=(sector.x - math.pi) * 180 / math.pi, y=sector.y, dy=sector.dy) new_plot.dxl = dxl new_plot.dxw = dxw new_plot.name = "AnnulusPhi" + "(" + self.base.data2D.name + ")" new_plot.source = self.base.data2D.source # new_plot.info=self.base.data2D.info new_plot.interactive = True new_plot.detector = self.base.data2D.detector # If the data file does not tell us what the axes are, just assume... new_plot.xaxis("\\rm{\phi}", 'degrees') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") if hasattr(data, "scale") and data.scale == 'linear' and \ self.base.data2D.name.count("Residuals") > 0: new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") new_plot.group_id = "AnnulusPhi" + self.base.data2D.name new_plot.id = "AnnulusPhi" + self.base.data2D.name new_plot.is_data = True new_plot.xtransform = "x" new_plot.ytransform = "y" self.base.parent.update_theory(data_id=data.id, theory=new_plot) wx.PostEvent(self.base.parent, NewPlotEvent(plot=new_plot, title="AnnulusPhi")) def moveend(self, ev): """ Called when any dragging motion ends. Post an event (type =SlicerParameterEvent) to plotter 2D with a copy slicer parameters Call _post_data method """ self.base.thaw_axes() # Post parameters to plotter 2D event = SlicerParameterEvent() event.type = self.__class__.__name__ event.params = self.get_params() wx.PostEvent(self.base, event) def restore(self): """ Restore the roughness for this layer. """ self.inner_circle.restore() self.outer_circle.restore() def move(self, x, y, ev): """ Process move to a new position, making sure that the move is allowed. """ pass def set_cursor(self, x, y): pass def get_params(self): """ Store a copy of values of parameters of the slicer into a dictionary. :return params: the dictionary created """ params = {} params["inner_radius"] = math.fabs(self.inner_circle._inner_mouse_x) params["outer_radius"] = math.fabs(self.outer_circle._inner_mouse_x) params["nbins"] = self.nbins return params def set_params(self, params): """ Receive a dictionary and reset the slicer with values contained in the values of the dictionary. :param params: a dictionary containing name of slicer parameters and values the user assigned to the slicer. """ inner = math.fabs(params["inner_radius"]) outer = math.fabs(params["outer_radius"]) self.nbins = int(params["nbins"]) # # Update the picture self.inner_circle.set_cursor(inner, self.inner_circle._inner_mouse_y) self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y) # # Post the data given the nbins entered by the user self._post_data(self.nbins) def freeze_axes(self): """ """ self.base.freeze_axes() def thaw_axes(self): """ """ self.base.thaw_axes() def draw(self): """ """ self.base.draw() class RingInteractor(_BaseInteractor): """ Draw a ring Given a radius """ def __init__(self, base, axes, color='black', zorder=5, r=1.0, sign=1): """ :param: the color of the line that defined the ring :param r: the radius of the ring :param sign: the direction of motion the the marker """ _BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes # Current radius of the ring self._inner_mouse_x = r # Value of the center of the ring self._inner_mouse_y = 0 # previous value of that radius self._inner_save_x = r # Save value of the center of the ring self._inner_save_y = 0 # Class instantiating RingIterator class self.base = base # the direction of the motion of the marker self.sign = sign # # Create a marker try: # Inner circle marker x_value = [self.sign * math.fabs(self._inner_mouse_x)] self.inner_marker = self.axes.plot(x_value, [0], linestyle='', marker='s', markersize=10, color=self.color, alpha=0.6, pickradius=5, label="pick", zorder=zorder, visible=True)[0] except: x_value = [self.sign * math.fabs(self._inner_mouse_x)] self.inner_marker = self.axes.plot(x_value, [0], linestyle='', marker='s', markersize=10, color=self.color, alpha=0.6, label="pick", visible=True)[0] message = "\nTHIS PROTOTYPE NEEDS THE LATEST" message += " VERSION OF MATPLOTLIB\n" message += "Get the SVN version that is at " message += " least as recent as June 1, 2007" owner = self.base.base.parent wx.PostEvent(owner, StatusEvent(status="AnnulusSlicer: %s" % message)) # Draw a circle [self.inner_circle] = self.axes.plot([], [], linestyle='-', marker='', color=self.color) # the number of points that make the ring line self.npts = 40 self.connect_markers([self.inner_marker]) self.update() def set_layer(self, n): """ Allow adding plot to the same panel :param n: the number of layer """ self.layernum = n self.update() def clear(self): """ Clear the slicer and all connected events related to this slicer """ self.clear_markers() try: self.inner_marker.remove() self.inner_circle.remove() except: # Old version of matplotlib for item in range(len(self.axes.lines)): del self.axes.lines[0] def get_radius(self): """ :return self._inner_mouse_x: the current radius of the ring """ return self._inner_mouse_x def update(self): """ Draw the new roughness on the graph. """ # Plot inner circle x = [] y = [] for i in range(self.npts): phi = 2.0 * math.pi / (self.npts - 1) * i xval = 1.0 * self._inner_mouse_x * math.cos(phi) yval = 1.0 * self._inner_mouse_x * math.sin(phi) x.append(xval) y.append(yval) self.inner_marker.set(xdata=[self.sign * math.fabs(self._inner_mouse_x)], ydata=[0]) self.inner_circle.set_data(x, y) def save(self, ev): """ Remember the roughness for this layer and the next so that we can restore on Esc. """ self._inner_save_x = self._inner_mouse_x self._inner_save_y = self._inner_mouse_y self.base.freeze_axes() def moveend(self, ev): """ Called after a dragging motion """ self.base.moveend(ev) def restore(self): """ Restore the roughness for this layer. """ self._inner_mouse_x = self._inner_save_x self._inner_mouse_y = self._inner_save_y def move(self, x, y, ev): """ Process move to a new position, making sure that the move is allowed. """ self._inner_mouse_x = x self._inner_mouse_y = y self.base.base.update() def set_cursor(self, x, y): """ draw the ring given x, y value """ self.move(x, y, None) self.update() def get_params(self): """ Store a copy of values of parameters of the slicer into a dictionary. :return params: the dictionary created """ params = {} params["radius"] = math.fabs(self._inner_mouse_x) return params def set_params(self, params): """ Receive a dictionary and reset the slicer with values contained in the values of the dictionary. :param params: a dictionary containing name of slicer parameters and values the user assigned to the slicer. """ x = params["radius"] self.set_cursor(x, self._inner_mouse_y) class CircularMask(_BaseInteractor): """ Draw a ring Given a radius """ def __init__(self, base, axes, color='grey', zorder=3, side=None): """ :param: the color of the line that defined the ring :param r: the radius of the ring :param sign: the direction of motion the the marker """ _BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes self.base = base self.is_inside = side self.qmax = min(math.fabs(self.base.data.xmax), math.fabs(self.base.data.xmin)) # must be positive self.connect = self.base.connect # Cursor position of Rings (Left(-1) or Right(1)) self.xmaxd = self.base.data.xmax self.xmind = self.base.data.xmin if (self.xmaxd + self.xmind) > 0: self.sign = 1 else: self.sign = -1 # Inner circle self.outer_circle = RingInteractor(self, self.base.subplot, 'blue', zorder=zorder + 1, r=self.qmax / 1.8, sign=self.sign) self.outer_circle.qmax = self.qmax * 1.2 self.update() self._post_data() # Bind to slice parameter events # self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS) def _onEVT_SLICER_PARS(self, event): """ receive an event containing parameters values to reset the slicer :param event: event of type SlicerParameterEvent with params as attribute """ wx.PostEvent(self.base, StatusEvent(status="AnnulusSlicer._onEVT_SLICER_PARS")) event.Skip() if event.type == self.__class__.__name__: self.set_params(event.params) self.base.update() def set_layer(self, n): """ Allow adding plot to the same panel :param n: the number of layer """ self.layernum = n self.update() def clear(self): """ Clear the slicer and all connected events related to this slicer """ self.clear_markers() self.outer_circle.clear() self.base.connect.clearall() # self.base.Unbind(EVT_SLICER_PARS) def update(self): """ Respond to changes in the model by recalculating the profiles and resetting the widgets. """ # Update locations self.outer_circle.update() # if self.is_inside != None: out = self._post_data() return out def save(self, ev): """ Remember the roughness for this layer and the next so that we can restore on Esc. """ self.base.freeze_axes() self.outer_circle.save(ev) def _post_data(self): """ Uses annulus parameters to plot averaged data into 1D data. :param nbins: the number of points to plot """ # Data to average data = self.base.data # If we have no data, just return if data == None: return mask = data.mask from sas.dataloader.manipulations import Ringcut rmin = 0 rmax = math.fabs(self.outer_circle.get_radius()) # # create the data1D Q average of data2D mask = Ringcut(r_min=rmin, r_max=rmax) if self.is_inside: out = (mask(data) == False) else: out = (mask(data)) # self.base.data.mask=out return out def moveend(self, ev): """ Called when any dragging motion ends. Post an event (type =SlicerParameterEvent) to plotter 2D with a copy slicer parameters Call _post_data method """ self.base.thaw_axes() # create a 1D data plot self._post_data() def restore(self): """ Restore the roughness for this layer. """ self.outer_circle.restore() def move(self, x, y, ev): """ Process move to a new position, making sure that the move is allowed. """ pass def set_cursor(self, x, y): pass def get_params(self): """ Store a copy of values of parameters of the slicer into a dictionary. :return params: the dictionary created """ params = {} params["outer_radius"] = math.fabs(self.outer_circle._inner_mouse_x) return params def set_params(self, params): """ Receive a dictionary and reset the slicer with values contained in the values of the dictionary. :param params: a dictionary containing name of slicer parameters and values the user assigned to the slicer. """ outer = math.fabs(params["outer_radius"]) # # Update the picture self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y) # # Post the data given the nbins entered by the user self._post_data() def freeze_axes(self): self.base.freeze_axes() def thaw_axes(self): self.base.thaw_axes() def draw(self): self.base.update()