diff --git a/src/sas/qtgui/Plotting/Plotter2D.py b/src/sas/qtgui/Plotting/Plotter2D.py index 818fa1d732..c3f7af0f14 100644 --- a/src/sas/qtgui/Plotting/Plotter2D.py +++ b/src/sas/qtgui/Plotting/Plotter2D.py @@ -136,6 +136,7 @@ def plot(self, data=None, marker=None, show_colorbar=True, update=False): update=update) self.updateCircularAverage() + self.updateSlicer() def calculateDepth(self): """ @@ -338,8 +339,7 @@ def circularAverage(self): else: new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") - new_plot.group_id = "2daverage" + self.data0.name - new_plot.id = "Circ avg " + self.data0.name + new_plot.id = "2daverage" + self.data0.name new_plot.is_data = True return new_plot @@ -375,8 +375,8 @@ def updateCircularAverage(self): # See if current item plots contain 2D average plot has_plot = False for plot in plots: - if plot.group_id is None: continue - if ca_caption in plot.group_id: has_plot = True + if plot.id is None: continue + if ca_caption in plot.id: has_plot = True # return prematurely if no circular average plot found if not has_plot: return @@ -388,6 +388,32 @@ def updateCircularAverage(self): # Show the new plot, if already visible self.manager.communicator.plotUpdateSignal.emit([new_plot]) + def updateSlicer(self): + """ + Update slicer plot on Data2D change + """ + if not hasattr(self, '_item'): return + item = self._item + if self._item.parent() is not None: + item = self._item.parent() + + # Get all plots for current item + plots = GuiUtils.plotsFromModel("", item) + if plots is None: return + slicer_caption = 'Slicer' + self.data0.name + # See if current item plots contain slicer plot + has_plot = False + for plot in plots: + if not hasattr(plot, 'type_id') or plot.type_id is None: continue + if slicer_caption in plot.type_id: has_plot = True + # return prematurely if no slicer plot found + if not has_plot: return + + # Now that we've identified the right plot, update the 2D data the slicer uses + self.slicer.data = self.data0 + # Replot now that the 2D data is updated + self.slicer._post_data() + def setSlicer(self, slicer, reset=True): """ Clear the previous slicer and create a new one. @@ -396,6 +422,65 @@ def setSlicer(self, slicer, reset=True): # Clear current slicer if self.slicer is not None: self.slicer.clear() + + # Clear the old slicer plots so they don't reappear later + if hasattr(self, '_item'): + item = self._item + if self._item.parent() is not None: + item = self._item.parent() + + # Go through all items and see if they are a plot. The checks done here are not as thorough + # as GuiUtils.deleteRedundantPlots (which this takes a lot from). Will this cause problems? + # Primary concern is the check (plot_data.plot_role == DataRole.ROLE_DELETABLE) as I don't + # know what it does. The other checks seem to be related to keeping the new plots for that function + # TODO: generalize this and put it in GuiUtils so that we can use it elsewhere + tempPlotsToRemove = [] + slicer_type_id = 'Slicer' + self.data0.name + for itemIndex in range(item.rowCount()): + # GuiUtils.plotsFromModel tests if the data is of type Data1D or Data2D to determine + # if it is a plot, so let's try that + if isinstance(item.child(itemIndex).data(), (Data1D, Data2D)): + # First take care of this item, then we'll take care of its children + if hasattr(item.child(itemIndex).data(), 'type_id'): + if slicer_type_id in item.child(itemIndex).data().type_id: + # At the time of writing, this should never be the case, but at some point the slicers may + # have relevant children (e.g. plots). We don't want to delete these slicers. + tempHasImportantChildren = False + for tempChildCheck in range(item.child(itemIndex).rowCount()): + # The data explorer uses the "text" attribute to set the name. If this has text='' then + # it can be deleted. + if item.child(itemIndex).child(tempChildCheck).text(): + tempHasImportantChildren = True + if not tempHasImportantChildren: + # Store this plot to be removed later. Removing now + # will cause the next plot to be skipped + tempPlotsToRemove.append(item.child(itemIndex)) + # It looks like the slicers are children of items that do not have data of instance Data1D or Data2D. + # Now do the children (1 level deep as is done in GuiUtils.plotsFromModel). Note that the slicers always + # seem to be the first entry (index2 == 0) + for itemIndex2 in range(item.child(itemIndex).rowCount()): + # Repeat what we did above (these if statements could probably be combined + # into one, but I'm not confident enough with how these work to say it wouldn't + # have issues if combined) + if isinstance(item.child(itemIndex).child(itemIndex2).data(), (Data1D, Data2D)): + if hasattr(item.child(itemIndex).child(itemIndex2).data(), 'type_id'): + if slicer_type_id in item.child(itemIndex).child(itemIndex2).data().type_id: + # Check for children we might want to keep (see the above loop) + tempHasImportantChildren = False + for tempChildCheck in range(item.child(itemIndex).child(itemIndex2).rowCount()): + # The data explorer uses the "text" attribute to set the name. If this has text='' + # then it can be deleted. + if item.child(itemIndex).child(itemIndex2).child(tempChildCheck).text(): + tempHasImportantChildren = True + if not tempHasImportantChildren: + # Remove the parent since each slicer seems to generate a new entry in item + tempPlotsToRemove.append(item.child(itemIndex)) + # Remove all the parent plots with matching criteria + for plot in tempPlotsToRemove: + item.removeRow(plot.row()) + # Delete the temporary list of plots to remove + del tempPlotsToRemove + # Create a new slicer self.slicer_z += 1 self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z) diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index 2c7e07c077..6e2c1f07b0 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -138,8 +138,8 @@ def _post_data(self, nbins=None): new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") - new_plot.group_id = "AnnulusPhi" + self.data.name new_plot.id = "AnnulusPhi" + self.data.name + new_plot.type_id = "Slicer" + self.data.name # Used to remove plots after changing slicer so they don't keep showing up after closed new_plot.is_data = True new_plot.xtransform = "x" new_plot.ytransform = "y" diff --git a/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py b/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py index 5d23996837..20f9279e67 100755 --- a/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py @@ -1,3 +1,5 @@ +import logging + interface_color = 'black' disable_color = 'gray' active_color = 'red' @@ -138,8 +140,6 @@ def onDrag(self, ev): if inside: self.clickx, self.clicky = ev.xdata, ev.ydata self.move(ev.xdata, ev.ydata, ev) - else: - self.restore(ev) return True def onKey(self, ev): diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index 2c9047a687..c047765561 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -4,6 +4,7 @@ from sas.qtgui.Plotting.PlotterData import Data1D import sas.qtgui.Utilities.GuiUtils as GuiUtils from sas.qtgui.Plotting.SlicerModel import SlicerModel +import logging class BoxInteractor(BaseInteractor, SlicerModel): @@ -13,14 +14,14 @@ class BoxInteractor(BaseInteractor, SlicerModel): by manipulations.py This class uses two other classes, HorizontalLines and VerticalLines, - to define the rectangle area: -x, x ,y, -y. It is subclassed by + to define the rectangle area: x1, x2 ,y1, y2. It is subclassed by BoxInteractorX and BoxInteracgtorY which define the direction of the - average. BoxInteractorX averages all the points from -y to +y as a + average. BoxInteractorX averages all the points from y1 to y2 as a function of Q_x and BoxInteractorY averages all the points from - -x to +x as a function of Q_y + x1 to x2 as a function of Q_y """ - def __init__(self, base, axes, item=None, color='black', zorder=3): + def __init__(self, base, axes, item=None, color='black', zorder=3, direction=None): BaseInteractor.__init__(self, base, axes, color=color) SlicerModel.__init__(self) # Class initialization @@ -30,43 +31,69 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): # connecting artist self.connect = self.base.connect # which direction is the preferred interaction direction - self.direction = None + self.direction = direction # determine x y values - self.x = 0.5 * min(numpy.fabs(self.data.xmax), - numpy.fabs(self.data.xmin)) - self.y = 0.5 * min(numpy.fabs(self.data.xmax), - numpy.fabs(self.data.xmin)) - # when reach qmax reset the graph - self.qmax = max(self.data.xmax, self.data.xmin, - self.data.ymax, self.data.ymin) + if self.direction == "Y": + self.half_width = 0.1 * (self.data.xmax - self.data.xmin) / 2 + self.half_height = 1.0 * (self.data.ymax - self.data.ymin) / 2 + elif self.direction == "X": + self.half_width = 1.0 * (self.data.xmax - self.data.xmin) / 2 + self.half_height = 0.1 * (self.data.ymax - self.data.ymin) / 2 + else: + msg = "post data:no Box Average direction was supplied" + raise ValueError(msg) + + # center of the box + # puts the center of box at the middle of the data q-range + self.center_x = (self.data.xmin + self.data.xmax) /2 + self.center_y = (self.data.ymin + self.data.ymax) /2 + # Number of points on the plot self.nbins = 100 # If True, I(|Q|) will be return, otherwise, # negative q-values are allowed + # Default to true on initialize self.fold = True # reference of the current Slab averaging self.averager = None - # Create vertical and horizaontal lines for the rectangle - self.vertical_lines = VerticalLines(self, - self.axes, - color='blue', - zorder=zorder, - y=self.y, - x=self.x) - self.vertical_lines.qmax = self.qmax - - self.horizontal_lines = HorizontalLines(self, - self.axes, - color='green', - zorder=zorder, - x=self.x, - y=self.y) - self.horizontal_lines.qmax = self.qmax - # draw the rectangle and plost the data 1D resulting - # of averaging data2D - self.update() - self._post_data() - self.draw() + # Flag to determine if the current figure has moved + # set to False == no motion , set to True== motion + # NOTE: This is not currently ever used. All moves happen in the + # individual interactors not the whole slicker. Thus the move(ev) + # currently does a pass. Default to False at initialize anyway + # (nothing has moved yet) for possible future implementation. + self.has_move = False + # Create vertical and horizontal lines for the rectangle + self.horizontal_lines = HorizontalDoubleLine(self, + self.axes, + color='blue', + zorder=zorder, + half_height=self.half_height, + half_width=self.half_width, + center_x=self.center_x, + center_y=self.center_y) + + self.vertical_lines = VerticalDoubleLine(self, + self.axes, + color='black', + zorder=zorder, + half_height=self.half_height, + half_width=self.half_width, + center_x=self.center_x, + center_y=self.center_y) + + # PointInteractor determines the center of the box + self.center = PointInteractor(self, + self.axes, color='grey', + zorder=zorder, + center_x=self.center_x, + center_y=self.center_y) + + # draw the rectangle and plot the data 1D resulting + # from averaging of the data2D + self.update_and_post() + # Set up the default slicer parameters for the parameter editor + # window (in SlicerModel.py) self.setModelFromParams() def update_and_post(self): @@ -96,20 +123,34 @@ def clear(self): self.horizontal_lines.clear() self.vertical_lines.clear() self.base.connect.clearall() + self.center.clear() + def update(self): """ Respond to changes in the model by recalculating the profiles and resetting the widgets. """ - # #Update the slicer if an horizontal line is dragged + # check if the center point has moved and update the figure accordingly + if self.center.has_move: + self.center.update() + self.horizontal_lines.update(center=self.center) + self.vertical_lines.update(center=self.center) + + # check if the horizontal lines have moved and + # update the figure accordingly if self.horizontal_lines.has_move: self.horizontal_lines.update() - self.vertical_lines.update(y=self.horizontal_lines.y) - # #Update the slicer if a vertical line is dragged + self.vertical_lines.update(y1=self.horizontal_lines.y1, + y2=self.horizontal_lines.y2, + half_height=self.horizontal_lines.half_height) + # check if the vertical lines have moved and + # update the figure accordingly if self.vertical_lines.has_move: self.vertical_lines.update() - self.horizontal_lines.update(x=self.vertical_lines.x) + self.horizontal_lines.update(x1=self.vertical_lines.x1, + x2=self.vertical_lines.x2, + half_width=self.vertical_lines.half_width) def save(self, ev): """ @@ -118,6 +159,7 @@ def save(self, ev): """ self.vertical_lines.save(ev) self.horizontal_lines.save(ev) + self.center.save(ev) def _post_data(self, new_slab=None, nbins=None, direction=None): """ @@ -131,10 +173,10 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): if self.direction is None: self.direction = direction - x_min = -1 * numpy.fabs(self.vertical_lines.x) - x_max = numpy.fabs(self.vertical_lines.x) - y_min = -1 * numpy.fabs(self.horizontal_lines.y) - y_max = numpy.fabs(self.horizontal_lines.y) + x_min = self.vertical_lines.x2 + x_max = self.vertical_lines.x1 + y_min = self.horizontal_lines.y2 + y_max = self.horizontal_lines.y1 if nbins is not None: self.nbins = nbins @@ -143,27 +185,64 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): msg = "post data:cannot average , averager is empty" raise ValueError(msg) self.averager = new_slab + # Calculate the bin width from number of points. The only tricky part + # is when the box stradles 0 but 0 is not the center. + # + # todo: This should probably NOT be calculated here. Instead it should + # be calculated as part of manipulations.py which already does + # almost the same math to calculate the bins anyway. See for + # example under "Build array of Q intervals" in the _avg method + # of the _Slab class. Moreover, scripts would more likely prefer + # to pass number of points than bin width anyway. This will + # however be an API change! + # Added by PDB -- 3/31/2024 if self.direction == "X": - if self.fold: + if self.fold and (x_max * x_min <= 0): x_low = 0 + x_high = max(abs(x_min),abs(x_max)) else: - x_low = numpy.fabs(x_min) - bin_width = (x_max + x_low) / self.nbins + x_low = x_min + x_high = x_max + bin_width = (x_high - x_low) / self.nbins elif self.direction == "Y": - if self.fold: + if self.fold and (y_max * y_min >= 0): y_low = 0 + y_high = max(abs(y_min),abs(y_max)) else: - y_low = numpy.fabs(y_min) - bin_width = (y_max + y_low) / self.nbins + y_low = y_min + y_high = y_max + bin_width = (y_high - y_low) / self.nbins else: msg = "post data:no Box Average direction was supplied" raise ValueError(msg) - # # Average data2D given Qx or Qy + + # Average data2D given Qx or Qy box = self.averager(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, bin_width=bin_width) box.fold = self.fold - boxavg = box(self.data) - # 3 Create Data1D to plot + # Check for data inside ROI. A bit of a kludge but faster than + # checking twice: once to check and once to do the calculation + # Adding a function to manipulations.py that returns the maksed data + # set as an object which can then be checked for not being empty before + # being passed back to the calculation in manipulations.py? + # + # Note that it no simple way to ensure that data is in the ROI without + # checking (unless one can guarantee a perfect grid in x,y). + try: + boxavg = box(self.data) + except ValueError as ve: + logging.warning(str(ve)) + self.restore(ev=None) + self.update() + self.draw() + self.setModelFromParams() + return + + # Now that we know the move valid, update the half_width and half_height + self.half_width = numpy.fabs(x_max - x_min)/2 + self.half_height = numpy.fabs(y_max - y_min)/2 + + # Create Data1D to plot if hasattr(boxavg, "dxl"): dxl = boxavg.dxl else: @@ -176,14 +255,18 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): new_plot.dxl = dxl new_plot.dxw = dxw new_plot.name = str(self.averager.__name__) + \ - "(" + self.data.name + ")" + "(" + self.data.name + ")" new_plot.title = str(self.averager.__name__) + \ - "(" + self.data.name + ")" + "(" + self.data.name + ")" new_plot.source = self.data.source new_plot.interactive = True new_plot.detector = self.data.detector - # If the data file does not tell us what the axes are, just assume... - new_plot.xaxis("\\rm{Q}", "A^{-1}") + if self.direction == "X": + new_plot.xaxis("\\rm{Q_x}", "A^{-1}") + elif self.direction == "Y": + new_plot.xaxis("\\rm{Q_y}", "A^{-1}") + else: + new_plot.xaxis("\\rm{Q}", "A^{-1}") new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") data = self.data @@ -193,7 +276,9 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): new_plot.yaxis("\\rm{Residuals} ", "/") new_plot.id = (self.averager.__name__) + self.data.name - new_plot.group_id = new_plot.id + # Create id to remove plots after changing slicer so they don't keep + # showing up after being closed + new_plot.type_id = "Slicer" + self.data.name new_plot.is_data = True item = self._item if self._item.parent() is not None: @@ -214,10 +299,19 @@ def moveend(self, ev): def restore(self, ev): """ - Restore the roughness for this layer. + Restore the roughness for this layer. Only restores things that have + moved. Otherwise you are restoring too far back. + + Save is only done when the mouse is clicked not when it is released. + Thus, if vertical lines have changed, they will move horizontal lines + also, but the original state of those horizontal lines has not been + saved (there was no click event on the horizontal lines). However, + restoring the vertical lines and then doing an updated will take care + of the related values in horizontal lines. """ - self.horizontal_lines.restore(ev) - self.vertical_lines.restore(ev) + if self.horizontal_lines.has_move: self.horizontal_lines.restore(ev) + if self.vertical_lines.has_move: self.vertical_lines.restore(ev) + if self.center.has_move: self.center.restore(ev) def move(self, x, y, ev): """ @@ -236,9 +330,11 @@ def getParams(self): """ params = {} - params["x_max"] = numpy.fabs(self.vertical_lines.x) - params["y_max"] = numpy.fabs(self.horizontal_lines.y) + params["half_width"] = self.vertical_lines.half_width + params["half_height"] = self.horizontal_lines.half_height params["nbins"] = self.nbins + params["center_x"] = self.center.x + params["center_y"] = self.center.y params["fold"] = self.fold return params @@ -250,101 +346,160 @@ def setParams(self, params): :param params: a dictionary containing name of slicer parameters and values the user assigned to the slicer. """ - self.x = float(numpy.fabs(params["x_max"])) - self.y = float(numpy.fabs(params["y_max"])) + self.half_width = params["half_width"] + self.half_height = params["half_height"] self.nbins = params["nbins"] self.fold = params["fold"] - - self.horizontal_lines.update(x=self.x, y=self.y) - self.vertical_lines.update(x=self.x, y=self.y) + self.center_x = params["center_x"] + self.center_y = params["center_y"] + + # save current state of the ROI in case the change leaves no data in + # the ROI and thus a disallowed move. Also set the has_move flags to + # true in case we have to restore this saved state. + self.save(ev=None) + self.center.has_move = True + self.horizontal_lines.has_move = True + self.vertical_lines.has_move = True + # Now update the ROI based on the change + self.center.update(center_x=self.center_x, center_y=self.center_y) + self.horizontal_lines.update(center=self.center, + half_width=self.half_width, half_height=self.half_height) + self.vertical_lines.update(center=self.center, + half_width=self.half_width, half_height=self.half_height) + # Compute and plot the 1D average based on these parameters self._post_data() + # Now move is over so turn off flags + self.center.has_move = False + self.horizontal_lines.has_move = False + self.vertical_lines.has_move = False self.draw() + def validate(self, param_name, param_value): + """ + Validate input from user. + Values get checked at apply time. + * nbins cannot be zero or samller + * The full ROI should stay within the data. thus center_x and center_y + are restricted such that the center +/- width (or height) cannot be + greate or smaller than data max/min. + * The width/height should not be set so small as to leave no data in + the ROI. Here we only make sure that the width/height is not zero + as done when dragging the vertical or horizontal lines. We let the + call to _post_data capture the ValueError of no points in ROI + raised by manipulations.py, log the message and negate the entry + at that point. + """ + isValid = True + + if param_name =='half_width': + # Can't be negative for sure. Also, it should not be so small that + # there remains no points to average in the ROI. We leave this + # second check to manipulations.py + if param_value <= 0: + logging.warning("The box width is too small. Please adjust.") + isValid = False + elif param_name =='half_height': + # Can't be negative for sure. Also, it should not be so small that + # there remains no points to average in the ROI. We leave this + # second check to manipulations.py + if param_value <= 0: + logging.warning("The box height is too small. Please adjust.") + isValid = False + elif param_name == 'nbins': + # Can't be negative or 0 + if param_value < 1: + logging.warning("Number of bins cannot be less than or equal"\ + "to 0. Please adjust.") + isValid = False + elif param_name == 'center_x': + # Keep the full ROI box within the data (only moving x here) + if (param_value + self.half_width) >= self.data.xmax or \ + (param_value- self.half_width) <= self.data.xmin: + logging.warning("The ROI must be fully contained within the"\ + "2D data. Please adjust") + isValid = False + elif param_name == 'center_y': + # Keep the full ROI box within the data (only moving y here) + if (param_value + self.half_height) >= self.data.ymax or \ + (param_value - self.half_height) <= self.data.ymin: + logging.warning("The ROI must be fully contained within the"\ + "2D data. Please adjust") + isValid = False + return isValid + def draw(self): """ - Draws the Canvas using the canvas.draw from the calling class - that instatiated this object. + Draws the Canvas using the canvas.Draw from the calling class + that instantiated this object. """ self.base.draw() -class HorizontalLines(BaseInteractor): + +class PointInteractor(BaseInteractor): """ - Draw 2 Horizontal lines centered on (0,0) that can move - on the x direction. The two lines move symmetrically (in opposite - directions). It also defines the x and -x position of a box. + Draw a point that can be dragged with the marker. + this class controls the motion the center of the BoxSum """ - - def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5): - """ - """ + def __init__(self, base, axes, color='black', zorder=5, center_x=0.0, + center_y=0.0): BaseInteractor.__init__(self, base, axes, color=color) - # Class initialization + # Initialization the class self.markers = [] self.axes = axes - # Saving the end points of two lines - self.x = x - self.save_x = x - - self.y = y - self.save_y = y - # Creating a marker - # Inner circle marker - self.inner_marker = self.axes.plot([0], [self.y], linestyle='', - marker='s', markersize=10, - color=self.color, alpha=0.6, - pickradius=5, label="pick", - zorder=zorder, - visible=True)[0] - # Define 2 horizontal lines - self.top_line = self.axes.plot([self.x, -self.x], [self.y, self.y], - linestyle='-', marker='', - color=self.color, visible=True)[0] - self.bottom_line = self.axes.plot([self.x, -self.x], [-self.y, -self.y], - linestyle='-', marker='', - color=self.color, visible=True)[0] - # Flag to check the motion of the lines + # center coordinates + self.x = center_x + self.y = center_y + # saved value of the center coordinates + self.save_x = center_x + self.save_y = center_y + # Create a marker + self.center_marker = self.axes.plot([self.x], [self.y], linestyle='', + marker='s', markersize=10, + color=self.color, alpha=0.6, + pickradius=5, label="pick", + zorder=zorder, + visible=True)[0] + # Draw a point + self.center = self.axes.plot([self.x], [self.y], + linestyle='-', marker='', + color=self.color, + visible=True)[0] + # Flag to determine if this point has moved self.has_move = False - # Connecting markers to mouse events and draw - self.connect_markers([self.top_line, self.inner_marker]) + # Flag to verify if the last move was valid + self.valid_move = True + # connecting the marker to allow it to be moved + self.connect_markers([self.center_marker]) + # Update the figure self.update() - def set_layer(self, n): + def setLayer(self, n): """ Allow adding plot to the same panel - - :param n: the number of layer - + @param n: the number of layer """ self.layernum = n self.update() def clear(self): """ - Clear this slicer and its markers + Clear this figure and its markers """ self.clear_markers() - self.inner_marker.remove() - self.top_line.remove() - self.bottom_line.remove() + self.center.remove() + self.center_marker.remove() - def update(self, x=None, y=None): + def update(self, center_x=None, center_y=None): """ Draw the new roughness on the graph. - - :param x: x-coordinates to reset current class x - :param y: y-coordinates to reset current class y - """ - # Reset x, y- coordinates if send as parameters - if x is not None: - self.x = numpy.sign(self.x) * numpy.fabs(x) - if y is not None: - self.y = numpy.sign(self.y) * numpy.fabs(y) - # Draw lines and markers - self.inner_marker.set(xdata=[0], ydata=[self.y]) - self.top_line.set(xdata=[self.x, -self.x], ydata=[self.y, self.y]) - self.bottom_line.set(xdata=[self.x, -self.x], ydata=[-self.y, -self.y]) + if center_x is not None: + self.x = center_x + if center_y is not None: + self.y = center_y + self.center_marker.set(xdata=[self.x], ydata=[self.y]) + self.center.set(xdata=[self.x], ydata=[self.y]) def save(self, ev): """ @@ -356,8 +511,6 @@ def save(self, ev): def moveend(self, ev): """ - Called after a dragging this edge and set self.has_move to False - to specify the end of dragging motion """ self.has_move = False self.base.moveend(ev) @@ -366,66 +519,96 @@ def restore(self, ev): """ Restore the roughness for this layer. """ - self.x = self.save_x self.y = self.save_y + self.x = self.save_x def move(self, x, y, ev): """ - Process move to a new position, making sure that the move is allowed. + Process move to a new position. BaseInteractor checks that the center + is within the data. Here we check to make sure that the center move + does not cause any part of the ROI box to move outside the data. """ - self.y = y + if x - self.base.half_width < self.base.data.xmin: + self.x = self.base.data.xmin + self.base.half_width + elif x + self.base.half_width > self.base.data.xmax: + self.x = self.base.data.xmax - self.base.half_width + else: + self.x = x + if y - self.base.half_height < self.base.data.ymin: + self.y = self.base.data.ymin + self.base.half_height + elif y + self.base.half_height > self.base.data.ymax: + self.y = self.base.data.ymax - self.base.half_height + else: + self.y = y self.has_move = True self.base.update() self.base.draw() + def setCursor(self, x, y): + """ + ..todo:: the cursor moves are currently being captured somewhere upstream + of BaseInteractor so this never gets called. + """ + self.move(x, y, None) + self.update() -class VerticalLines(BaseInteractor): +class VerticalDoubleLine(BaseInteractor): """ - Draw 2 vertical lines centered on (0,0) that can move - on the y direction. The two lines move symmetrically (in opposite - directions). It also defines the y and -y position of a box. + Draw 2 vertical lines that can move symmetrically in opposite directions in x and centered on + a point (PointInteractor). It also defines the top and bottom y positions of a box. """ - - def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5): - """ - """ + def __init__(self, base, axes, color='black', zorder=5, half_width=0.5, half_height=0.5, + center_x=0.0, center_y=0.0): BaseInteractor.__init__(self, base, axes, color=color) + # Initialization of the class self.markers = [] self.axes = axes - self.x = numpy.fabs(x) - self.save_x = self.x - self.y = numpy.fabs(y) - self.save_y = y - # Inner circle marker - self.inner_marker = self.axes.plot([self.x], [0], linestyle='', + # the height of the rectangle + self.half_height = half_height + self.save_half_height = self.half_height + # the width of the rectangle + self.half_width = half_width + self.save_half_width = self.half_width + # Center coordinates + self.center_x = center_x + self.center_y = center_y + # defined end points vertical and horizontal lines and their saved values + self.y1 = self.center_y + self.half_height + self.save_y1 = self.y1 + self.y2 = self.center_y - self.half_height + self.save_y2 = self.y2 + self.x1 = self.center_x + self.half_width + self.save_x1 = self.x1 + self.x2 = self.center_x - self.half_width + self.save_x2 = self.x2 + # save the color of the line + self.color = color + # Create marker + self.right_marker = self.axes.plot([self.x1], [0], linestyle='', marker='s', markersize=10, color=self.color, alpha=0.6, pickradius=5, label="pick", zorder=zorder, visible=True)[0] - self.right_line = self.axes.plot([self.x, self.x], - [self.y, -self.y], + + # Define the left and right lines of the rectangle + self.right_line = self.axes.plot([self.x1, self.x1], [self.y1, self.y2], linestyle='-', marker='', color=self.color, visible=True)[0] - self.left_line = self.axes.plot([-self.x, -self.x], - [self.y, -self.y], + self.left_line = self.axes.plot([self.x2, self.x2], [self.y1, self.y2], linestyle='-', marker='', color=self.color, visible=True)[0] + # Flag to determine if the lines have moved self.has_move = False - self.connect_markers([self.right_line, self.inner_marker]) + # Flag to verify if the last move was valid + self.valid_move = True + # Connect the marker and draw the picture + self.connect_markers([self.right_marker, self.right_line]) self.update() - def validate(self, param_name, param_value): - """ - Validate input from user - """ - return True - - def set_layer(self, n): + def setLayer(self, n): """ Allow adding plot to the same panel - :param n: the number of layer - """ self.layernum = n self.update() @@ -435,59 +618,304 @@ def clear(self): Clear this slicer and its markers """ self.clear_markers() - self.inner_marker.remove() - self.left_line.remove() + self.right_marker.remove() self.right_line.remove() + self.left_line.remove() - def update(self, x=None, y=None): + def update(self, x1=None, x2=None, y1=None, y2=None, half_width=None, + half_height=None, center=None): """ Draw the new roughness on the graph. + :param x1: new maximum value of x coordinates + :param x2: new minimum value of x coordinates + :param y1: new maximum value of y coordinates + :param y2: new minimum value of y coordinates + :param half_ width: is the half width of the new rectangle + :param half_height: is the half height of the new rectangle + :param center: provided x, y coordinates of the center point + """ + # Save the new height, width of the rectangle if given as a param + if half_width is not None: + self.half_width = half_width + if half_height is not None: + self.half_height = half_height + # If new center coordinates are given draw the rectangle + # given these value + if center is not None: + self.center_x = center.x + self.center_y = center.y + self.x1 = self.center_x + self.half_width + self.x2 = self.center_x - self.half_width + self.y1 = self.center_y + self.half_height + self.y2 = self.center_y - self.half_height + + self.right_marker.set(xdata=[self.x1], ydata=[self.center_y]) + self.right_line.set(xdata=[self.x1, self.x1], + ydata=[self.y1, self.y2]) + self.left_line.set(xdata=[self.x2, self.x2], + ydata=[self.y1, self.y2]) + return + # if x1, y1, x2, y2 are given draw the rectangle with these values + if x1 is not None: + self.x1 = x1 + if x2 is not None: + self.x2 = x2 + if y1 is not None: + self.y1 = y1 + if y2 is not None: + self.y2 = y2 + # Draw 2 vertical lines and a marker + self.right_marker.set(xdata=[self.x1], ydata=[self.center_y]) + self.right_line.set(xdata=[self.x1, self.x1], ydata=[self.y1, self.y2]) + self.left_line.set(xdata=[self.x2, self.x2], ydata=[self.y1, self.y2]) + + def save(self, ev): + """ + Remember the roughness for this layer and the next so that we + can restore on Esc. This save is run on mouse click (not a drag event) + by BaseInteractor + """ + self.save_x2 = self.x2 + self.save_y2 = self.y2 + self.save_x1 = self.x1 + self.save_y1 = self.y1 + self.save_half_height = self.half_height + self.save_half_width = self.half_width + + def moveend(self, ev): + """ + After a dragging motion update the 1D average plot and then reset the + flag self.has_move to False. + """ + self.base.moveend(ev) + self.has_move = False + + def restore(self, ev): + """ + Restore the roughness for this layer. + """ + self.y2 = self.save_y2 + self.x2 = self.save_x2 + self.y1 = self.save_y1 + self.x1 = self.save_x1 + self.half_height = self.save_half_height + self.half_width = self.save_half_width + + def move(self, x, y, ev): + """ + Process move to a new position, making sure that the move is allowed. + In principle, the move must not create a box without any data points + in it. For the dragging (continuous move), we make sure that the width + or height are not negative and that the entire ROI resides withing the + data. We leave the check of whether there are any data in that ROI to + the manipulations.py which is called from _post_data, itself being + called on moveend(ev). + """ + if x - self.center_x > 0: + self.valid_move = True + if self.center_x - (x - self.center_x) < self.base.data.xmin: + self.x1 = self.center_x - (self.base.data.xmin - self.center_x) + else: + self.x1 = x + self.half_width = self.x1 - self.center_x + self.x2 = self.center_x - self.half_width + self.has_move = True + self.base.update() + self.base.draw() + else: + if self.valid_move == True: + self.valid_move = False + logging.warning("the ROI cannot be negative") + + def setCursor(self, x, y): + """ + Update the figure given x and y + """ + self.move(x, y, None) + self.update() - :param x: x-coordinates to reset current class x - :param y: y-coordinates to reset current class y +class HorizontalDoubleLine(BaseInteractor): + """ + Draw 2 vertical lines that can move symmetrically in opposite directions in y and centered on + a point (PointInteractor). It also defines the left and right x positions of a box. + """ + def __init__(self, base, axes, color='black', zorder=5, half_width=0.5, half_height=0.5, + center_x=0.0, center_y=0.0): + + BaseInteractor.__init__(self, base, axes, color=color) + # Initialization of the class + self.markers = [] + self.axes = axes + # Center coordinates + self.center_x = center_x + self.center_y = center_y + # Box half width and height and horizontal and vertical limits + self.half_height = half_height + self.save_half_height = self.half_height + self.half_width = half_width + self.save_half_width = self.half_width + self.y1 = self.center_y + half_height + self.save_y1 = self.y1 + self.y2 = self.center_y - half_height + self.save_y2 = self.y2 + self.x1 = self.center_x + self.half_width + self.save_x1 = self.x1 + self.x2 = self.center_x - self.half_width + self.save_x2 = self.x2 + # Color + self.color = color + self.top_marker = self.axes.plot([0], [self.y1], linestyle='', + marker='s', markersize=10, + color=self.color, alpha=0.6, + pickradius=5, label="pick", + zorder=zorder, visible=True)[0] + + # Define 2 horizontal lines + self.top_line = self.axes.plot([self.x1, -self.x1], [self.y1, self.y1], + linestyle='-', marker='', + color=self.color, visible=True)[0] + self.bottom_line = self.axes.plot([self.x1, -self.x1], + [self.y2, self.y2], + linestyle='-', marker='', + color=self.color, visible=True)[0] + # Flag to determine if the lines have moved + self.has_move = False + # Flag to verify if the last move was valid + self.valid_move = True + # connect the marker and draw the picture + self.connect_markers([self.top_marker, self.top_line]) + self.update() + def setLayer(self, n): """ - # Reset x, y -coordinates if given as parameters - if x is not None: - self.x = numpy.sign(self.x) * numpy.fabs(x) - if y is not None: - self.y = numpy.sign(self.y) * numpy.fabs(y) - # Draw lines and markers - self.inner_marker.set(xdata=[self.x], ydata=[0]) - self.left_line.set(xdata=[-self.x, -self.x], ydata=[self.y, -self.y]) - self.right_line.set(xdata=[self.x, self.x], ydata=[self.y, -self.y]) + Allow adding plot to the same panel + @param n: the number of layer + """ + self.layernum = n + self.update() + + def clear(self): + """ + Clear this figure and its markers + """ + self.clear_markers() + self.top_marker.remove() + self.bottom_line.remove() + self.top_line.remove() + + def update(self, x1=None, x2=None, y1=None, y2=None, + half_width=None, half_height=None, center=None): + """ + Draw the new roughness on the graph. + :param x1: new maximum value of x coordinates + :param x2: new minimum value of x coordinates + :param y1: new maximum value of y coordinates + :param y2: new minimum value of y coordinates + :param half_width: is the half width of the new rectangle + :param half_height: is the half height of the new rectangle + :param center: provided x, y coordinates of the center point + """ + # Save the new height, width of the rectangle if given as a param + if half_width is not None: + self.half_width = half_width + if half_height is not None: + self.half_height = half_height + # If new center coordinates are given draw the rectangle + # given these value + if center is not None: + self.center_x = center.x + self.center_y = center.y + self.x1 = self.center_x + self.half_width + self.x2 = self.center_x - self.half_width + + self.y1 = self.center_y + self.half_height + self.y2 = self.center_y - self.half_height + + self.top_marker.set(xdata=[self.center_x], ydata=[self.y1]) + self.top_line.set(xdata=[self.x1, self.x2], + ydata=[self.y1, self.y1]) + self.bottom_line.set(xdata=[self.x1, self.x2], + ydata=[self.y2, self.y2]) + return + # if x1, y1, x2, y2 are given draw the rectangle with these values + if x1 is not None: + self.x1 = x1 + if x2 is not None: + self.x2 = x2 + if y1 is not None: + self.y1 = y1 + if y2 is not None: + self.y2 = y2 + # Draw 2 vertical lines and a marker + self.top_marker.set(xdata=[self.center_x], ydata=[self.y1]) + self.top_line.set(xdata=[self.x1, self.x2], ydata=[self.y1, self.y1]) + self.bottom_line.set(xdata=[self.x1, self.x2], ydata=[self.y2, self.y2]) def save(self, ev): """ Remember the roughness for this layer and the next so that we - can restore on Esc. + can restore on Esc. This save is run on mouse click (not a drag event) + by BaseInteractor """ - self.save_x = self.x - self.save_y = self.y + self.save_x2 = self.x2 + self.save_y2 = self.y2 + self.save_x1 = self.x1 + self.save_y1 = self.y1 + self.save_half_height = self.half_height + self.save_half_width = self.half_width def moveend(self, ev): """ - Called after a dragging this edge and set self.has_move to False - to specify the end of dragging motion + After a dragging motion update the 1D average plot and then reset the + flag self.has_move to False. """ - self.has_move = False self.base.moveend(ev) + self.has_move = False def restore(self, ev): """ Restore the roughness for this layer. """ - self.x = self.save_x - self.y = self.save_y + self.y2 = self.save_y2 + self.x2 = self.save_x2 + self.y1 = self.save_y1 + self.x1 = self.save_x1 + self.half_height = self.save_half_height + self.half_width = self.save_half_width def move(self, x, y, ev): """ Process move to a new position, making sure that the move is allowed. + In principle, the move must not create a box without any data points + in it. For the dragging (continuous move), we make sure that the width + or height are not negative and that the entire ROI resides withing the + data. We leave the check of whether there are any data in that ROI to + the manipulations.py which is called from _post_data, itself being + called on moveend(ev). + """ + if y - self.center_y > 0: + self.valid_move = True + if self.center_y - (y - self.center_y) < self.base.data.ymin: + self.y1 = self.center_y - (self.base.data.ymin - self.center_y) + else: + self.y1 = y + self.half_height = self.y1 - self.center_y + self.y2 = self.center_y - self.half_height + self.has_move = True + self.base.update() + self.base.draw() + else: + if self.valid_move == True: + self.valid_move = False + logging.warning("the ROI cannot be negative") + + def setCursor(self, x, y): """ - self.has_move = True - self.x = x - self.base.update() - self.base.draw() + Update the figure given x and y + """ + self.move(x, y, None) + self.update() + class BoxInteractorX(BoxInteractor): @@ -498,9 +926,8 @@ class BoxInteractorX(BoxInteractor): """ def __init__(self, base, axes, item=None, color='black', zorder=3): - BoxInteractor.__init__(self, base, axes, item=item, color=color) + BoxInteractor.__init__(self, base, axes, item=item, color=color, direction="X") self.base = base - super()._post_data() def _post_data(self, new_slab=None, nbins=None, direction=None): """ @@ -509,20 +936,6 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): from sasdata.data_util.manipulations import SlabX super()._post_data(SlabX, direction="X") - def validate(self, param_name, param_value): - """ - Validate input from user. - Values get checked at apply time. - """ - isValid = True - - if param_name == 'nbins': - # Can't be 0 - if param_value < 1: - print("Number of bins cannot be less than or equal to 0. Please adjust.") - isValid = False - return isValid - class BoxInteractorY(BoxInteractor): """ @@ -532,9 +945,8 @@ class BoxInteractorY(BoxInteractor): """ def __init__(self, base, axes, item=None, color='black', zorder=3): - BoxInteractor.__init__(self, base, axes, item=item, color=color) + BoxInteractor.__init__(self, base, axes, item=item, color=color, direction="Y") self.base = base - super()._post_data() def _post_data(self, new_slab=None, nbins=None, direction=None): """ @@ -542,17 +954,3 @@ def _post_data(self, new_slab=None, nbins=None, direction=None): """ from sasdata.data_util.manipulations import SlabY super()._post_data(SlabY, direction="Y") - - def validate(self, param_name, param_value): - """ - Validate input from user - Values get checked at apply time. - """ - isValid = True - - if param_name == 'nbins': - # Can't be 0 - if param_value < 1: - print("Number of bins cannot be less than or equal to 0. Please adjust.") - isValid = False - return isValid diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 41a3751cb8..2c653ba73a 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -67,6 +67,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): phi=self.phi, theta2=self.theta2) self.left_line.update(left=True) self.left_line.qmax = self.qmax + self.fold = True # draw the sector self.update() self._post_data() @@ -151,6 +152,7 @@ def _post_data(self, nbins=None): phi_min=phimin + numpy.pi, phi_max=phimax + numpy.pi, nbins=nbins) + sect.fold = self.fold sector = sect(self.data) # Create 1D data resulting from average @@ -178,8 +180,8 @@ def _post_data(self, nbins=None): new_plot.ytransform = 'y' new_plot.yaxis("\\rm{Residuals} ", "/") - new_plot.group_id = "2daverage" + self.data.name new_plot.id = "SectorQ" + self.data.name + new_plot.type_id = "Slicer" + self.data.name # Used to remove plots after changing slicer so they don't keep showing up after closed new_plot.is_data = True item = self._item if self._item.parent() is not None: @@ -250,6 +252,7 @@ def getParams(self): params["Phi [deg]"] = self.main_line.theta * 180 / numpy.pi params["Delta_Phi [deg]"] = numpy.fabs(self.left_line.phi * 180 / numpy.pi) params["nbins"] = self.nbins + params["fold"] = self.fold return params def setParams(self, params): @@ -269,6 +272,7 @@ def setParams(self, params): params["Delta_Phi [deg]"] = MIN_PHI self.nbins = int(params["nbins"]) + self.fold =params["fold"] self.main_line.theta = main # Reset the slicer parameters self.main_line.update() @@ -381,13 +385,13 @@ def update(self, phi=None, delta=None, mline=None, else: self.phi = numpy.fabs(self.phi) if side: - self.theta = mline.alpha + self.phi + self.theta = mline.theta + self.phi if mline is not None: if delta != 0: self.theta2 = mline + delta else: - self.theta2 = mline.alpha + self.theta2 = mline.theta if delta == 0: theta3 = self.theta + delta else: diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index fe5e4c871f..6f75c6f850 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -212,7 +212,7 @@ def _post_data(self, new_sector=None, nbins=None): new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.id = str(self.averager.__name__) + self.data.name - new_plot.group_id = new_plot.id + new_plot.type_id = "Slicer" + self.data.name # Used to remove plots after changing slicer so they don't keep showing up after closed new_plot.is_data = True item = self._item if self._item.parent() is not None: