function h_group = impositionrect(h_parent, position)
%IMPOSITIONRECT Create draggable position rectangle.
%   H = IMPOSITIONRECT(HPARENT, POSITION) creates a position rectangle on
%   the object specified by HPARENT.  The function returns H, a handle to
%   the position rectangle, which is an hggroup object.  HPARENT
%   specifies the hggroup's parent, which is typically an axes object,
%   but can also be any other object that can be the parent of an
%   hggroup.  POSITION is a four-element vector that specifies the
%   initial location of the rectangle.  POSITION has the form [XMIN YMIN
%   WIDTH HEIGHT].
%
%   A position rectangle can be dragged interactively using the mouse.
%   When the position rectangle occupies a small number of screen pixels,
%   its appearance changes to aid visibility.
%
%   The position rectangle has a context menu associated with it that allows
%   you to copy the current position to the clipboard and change the color
%   used to display the rectangle.
%
%   API = IPTGETAPI(H) returns a structure of function handles that can
%   be used to manipulate the position rectangle.
%
%   API Function Syntaxes
%   ---------------------
%   A position rectangle contains a structure of function handles, called
%   an API, that can be used to manipulate it.  To retrieve this
%   structure from the position rectangle, use the IPTGETAPI function.
%
%       API = IPTGETAPI(H)
%
%   Functions in the API, listed in the order of the structure fields, include:
%
%   api.setPosition
%
%       Sets the position rectangle to a new position.
%
%           api.setPosition(new_position)
%
%       where new_position is a four-element position vector.
%
%   api.getPosition
%
%       Returns the current position of the position rectangle.
%
%           position = api.getPosition()
%
%   api.delete
%
%       Deletes the position rectangle associated with the API.
%
%           api.delete()
%
%   api.setColor
%
%       Sets the color used to draw the position rectangle.
%
%           api.setColor(new_color)
%
%       where new_color is a three-element vector specifying an RGB
%       value.
%
%   api.addNewPositionCallback
%
%       Adds the function handle FCN to the list of new-position callback
%       functions.
%
%           id = api.addNewPositionCallback(fcn)
%
%       Whenever the position rectangle changes its position, each
%       function in the list is called with the syntax:
%
%           fcn(position)
%
%       The return value, id, is used only with
%       removeNewPositionCallback.
%
%   api.removeNewPositionCallback
%
%       Removes the corresponding function from the new-position callback
%       list.
%
%           api.removeNewPositionCallback(id)
%
%       where id is the identifier returned by
%       api.addNewPositionCallback.
%
%   api.setDragConstraintCallback
%
%       Sets the drag constraint function to be the specified function
%       handle, fcn.
%
%           api.setDragConstraintCallback(fcn)
%
%       Whenever the position rectangle is moved because of a mouse drag,
%       the constraint function is called using the syntax:
%
%           constrained_position = fcn(new_position)
%
%       where new_position is a four-element position vector.  This
%       allows a client, for example, to control where the position
%       rectangle may be dragged.
%   
%   Examples
%   --------
%       % Display updated position in the command window.
%       close all, plot(1:10)
%       h = impositionrect(gca, [4 4 2 2]);
%       api = iptgetapi(h);
%       api.addNewPositionCallback(@(p) title(mat2str(p)));
%       % Now drag the position rectangle using the mouse.
%
%       % Constrain the position rectangle to move only up and down.
%       close all, plot(1:10)
%       h = impositionrect(gca, [4 4 2 2]);
%       api = iptgetapi(h);
%       api.setDragConstraintCallback(@(p) [4 p(2:4)]);
%       % Now drag the position rectangle using the mouse.
%
%       % Use a custom color for displaying the rectangle.
%       close all, plot(1:10)
%       h = impositionrect(gca, [4 4 2 2]);
%       api = iptgetapi(h);
%       api.setColor([1 0 0]);
%
%       % When the rectangle position occupies only a few pixels on the
%       % screen, the rectangle is drawn in a different style to increase
%       % its visibility.
%       close all, imshow cameraman.tif
%       h = impositionrect(gca, [100 100 10 10]);
%
%   See also IPTGETAPI.

%   Copyright 1993-2004 The MathWorks, Inc.
%   $Revision: 1.1.8.3 $  $Date: 2004/12/18 07:36:10 $

  iptchecknargin(2, 2, nargin, mfilename);
  if ~ishandle(h_parent)
    error('Images:impositionrect:invalidHandle', ...
          'HPARENT must be a valid graphics handle.');
  end
  if strcmp(get(h_parent, 'type'), 'axes')
    h_axes = h_parent;
  else
    h_axes = ancestor(h_parent, 'axes');
    if isempty(h_axes)
      error('Images:impositionrect:noAxesAncestor', ...
            'HPARENT must be a descendent of an axes object.');
    end
  end

  % constraint_function is used by dragMotion() to give a client the
  % opportunity to constraint where the position rectangle can be
  % dragged.
  drag_constraint_function = [];

  % outer_position is the position of the visual representation of the
  % rectangle, including any decorations.  It is used by nested function
  % cursorCheck() to determine when to change the figure's mouse
  % pointer.
  outer_position = position;
  
  % mouse_is_over_rect is a boolean used to indicate when the mouse
  % pointer has made a transition onto or off of the rectangle.  It is
  % used by nested function cursorCheck().
  mouse_is_over_rect = false;
  
  % saved_mouse_pointer is used to save the figure's mouse pointer before
  % changing it to fleur when the mouse moves over the rectangle.  It is
  % used by nested function cursorCheck().
  saved_mouse_pointer = [];
  
  % new_position_callback_functions is used by sendNewPosition() to
  % notify interested clients whenever the rectangle position changes.
  new_position_callback_functions = makeList;
  
  try
    h_group = hggroup('ButtonDownFcn', @startDrag, 'Parent', h_parent, ...
                      'HitTest', 'on');
  catch
    error('Images:impositionrect:failureToParent', ...
          'HPARENT must be able to have an hggroup object as a child.');
  end

  % The line objects should have a width of one screen pixel.
  line_width = ceil(getPointsPerScreenPixel);
  h_outer_line = line('Color', 'w', ...
            'LineStyle', '-', ...
            'LineWidth', line_width, ...
            'HitTest', 'off', ...
            'Parent', h_group);
  h_inner_line = line('Color', 'w', ...
            'LineStyle', '-', ...
            'LineWidth', line_width, ...
            'HitTest', 'off', ...
            'Parent', h_group);
  h_middle_line = line(...
            'LineStyle', '-', ...
            'LineWidth', line_width, ...
            'HitTest', 'off', ...
            'Parent', h_group);
  h_patch = patch('FaceColor', 'none', 'EdgeColor', 'none', ...
            'HitTest', 'off', ...
            'Parent', h_group);

  % is_in_drag_operation is a boolean flag set to true during a mouse drag of
  % the rectangle.  It is used inside the cursorCheck() function.
  is_in_drag_operation = false;

  % Pattern for set associated with callbacks that get called as a
  % result of the set.
  insideSetPosition = false;
  
  updateView;
  updateAncestorPositionListeners;
  
  fig = ancestor(h_axes, 'figure', 'toplevel');
  cmenu = uicontextmenu('Parent', fig);
  set(h_group, 'UIContextMenu', cmenu);
  uimenu(cmenu, ...
         'Label', 'Copy Position', ...
         'Callback', @copyPosition');
  set_color_menu = uimenu(cmenu, ...
                          'Label', 'Set Rectangle Color');
  color_choices = getColorChoices;
  for k = 1:numel(color_choices)
    uimenu(set_color_menu, 'Label', color_choices(k).Label, ...
           'Callback', @(varargin) setColor(color_choices(k).Color));
  end
  setColor(color_choices(1).Color);
  
  cursor_check_callback_id = iptaddcallback(fig, 'WindowButtonMotionFcn', ...
                                                @cursorCheck);
  
  set(h_group, 'DeleteFcn', ...
               @(src, varargin) iptremovecallback(ancestor(src, 'figure'), ...
                                                 'WindowButtonMotionFcn', ...
                                                 cursor_check_callback_id));
  
  api.setPosition                = @setPosition;
  api.getPosition                = @getPosition;
  api.delete                     = @deletePositionRect;
  api.setColor                   = @setColor;
  api.addNewPositionCallback     = @addNewPositionCallback;
  api.removeNewPositionCallback  = @removeNewPositionCallback;
  api.setDragConstraintCallback  = @setDragConstraintCallback;
  
  setappdata(h_group, 'API', api);

  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function setPosition(new_position)
  
    % Pattern to break recursion
    if insideSetPosition
        return
    else
        insideSetPosition = true;
    end     

    position = new_position;
    updateView;
    
    % Pattern to break recursion
    insideSetPosition = false;
    
  end
  %--------------------------------------------------------------------------

  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function pos = getPosition
    pos = position;
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function id = addNewPositionCallback(fun)
    id = new_position_callback_functions.appendItem(fun);
  end
  %--------------------------------------------------------------------------

  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function removeNewPositionCallback(id)
    new_position_callback_functions.removeItem(id);
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function setColor(c)
    if ishandle(h_middle_line)
      set(h_middle_line, 'Color', c);
    end
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function setDragConstraintCallback(fun)
    drag_constraint_function = fun;
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function deletePositionRect(src, varargin)
    if ishandle(h_group)
      iptremovecallback(ancestor(h_group, 'figure'), 'WindowButtonMotionFcn', ...
                       cursor_check_callback_id);
      delete(h_group);
    end
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function cursorCheck(src_fig, varargin)
  
    if ~ishandle(h_group)
        return;
    end
    
    h_axes = ancestor(h_group, 'axes');
  
    xmin = outer_position(1);
    xmax = xmin + outer_position(3);
    ymin = outer_position(2);
    ymax = ymin + outer_position(4);
    
    cp = get(h_axes, 'CurrentPoint');
    cpx = cp(1,1,1);
    cpy = cp(1,2,1);
    
    if (cpx >= xmin) && (cpx <= xmax) && ...
               (cpy >= ymin) && (cpy <= ymax)
      if ~mouse_is_over_rect
        mouse_is_over_rect = true;
        saved_mouse_pointer = get(src_fig, 'Pointer');
        set(src_fig, 'Pointer', 'fleur');
      end
    else
      if mouse_is_over_rect && ~is_in_drag_operation
        mouse_is_over_rect = false;
        set(src_fig, 'Pointer', saved_mouse_pointer);
      end
    end
    
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function copyPosition(varargin)
  
    clipboard('copy', position);
  
  end
  %--------------------------------------------------------------------------
    
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function updateView(varargin)
  
    if ~ishandle(h_group)
        return;
    end
    
    h_axes = ancestor(h_group, 'axes');
  
    xrange = get(h_axes,'XLim');
    yrange = get(h_axes,'YLim');
    
    [dx_per_screen_pixel, dy_per_screen_pixel] = getAxesScale(h_axes);

    min_decorated_rect_size = 30;
    x_left = position(1) / dx_per_screen_pixel;
    x_right = (position(1) + position(3)) / dx_per_screen_pixel;
    x_wing_size = max(ceil((min_decorated_rect_size - ...
                            (x_right - x_left)) / 2), 0);
    
    y_bottom = position(2) / dy_per_screen_pixel;
    y_top = (position(2) + position(4)) / dy_per_screen_pixel;
    y_wing_size = max(ceil((min_decorated_rect_size - ...
                            (y_top - y_bottom)) / 2), 0);
    
    x1 = x_left - x_wing_size;
    x2 = x_left;
    x3 = (x_left + x_right) / 2;
    x4 = x_right;
    x5 = x_right + x_wing_size;
    
    y1 = y_bottom - y_wing_size;
    y2 = y_bottom;
    y3 = (y_bottom + y_top) / 2;
    y4 = y_top;
    y5 = y_top + y_wing_size;

    % (x,y) is a polygon that strokes the middle line.  Here it is in
    % screen pixel units.
    x = [x1 x2 x2 x3 x3 x3 x4 x4 x5 x4 x4 x3 x3 x3 x2 x2 x1];
    y = [y3 y3 y2 y2 y1 y2 y2 y3 y3 y3 y4 y4 y5 y4 y4 y3 y3];
    
    % Convert the (x,y) polygon back to data units and clip it to be one
    % pixel inside the axes limits.
    x = x * dx_per_screen_pixel;
    y = y * dy_per_screen_pixel;
    
    x = max(x, xrange(1) + dx_per_screen_pixel);
    x = min(x, xrange(2) - 2*dx_per_screen_pixel);
    
    y = max(y, yrange(1) + dy_per_screen_pixel);
    y = min(y, yrange(2) - 2*dy_per_screen_pixel);
    
    xx1 = x1 - 1;
    xx2 = x2 - 1;
    xx3 = x3 - 1;
    xx4 = x3 + 1;
    xx5 = x4 + 1;
    xx6 = x5 + 1;
    
    yy1 = y1 - 1;
    yy2 = y2 - 1;
    yy3 = y3 - 1;
    yy4 = y3 + 1;
    yy5 = y4 + 1;
    yy6 = y5 + 1;
    
    % (xx,yy) is a polygon that strokes the outer line.  Here it is in
    % screen pixel units.
    xx = [xx1 xx2 xx2 xx3 xx3 xx4 xx4 xx5 xx5 xx6 xx6 xx5 xx5 ...
          xx4 xx4 xx3 xx3 xx2 xx2 xx1 xx1];
    yy = [yy3 yy3 yy2 yy2 yy1 yy1 yy2 yy2 yy3 yy3 yy4 yy4 yy5 ...
          yy5 yy6 yy6 yy5 yy5 yy4 yy4 yy3];
    
    % Convert the (xx,yy) polygon back to data units and clip it to the
    % axes limits.
    xx = xx * dx_per_screen_pixel;
    yy = yy * dy_per_screen_pixel;
    
    xx = max(xx, xrange(1));
    xx = min(xx, xrange(2) - dx_per_screen_pixel);
    
    yy = max(yy, yrange(1));
    yy = min(yy, yrange(2) - dy_per_screen_pixel);
    
    xi1 = x2 + 1;
    xi2 = x4 - 1;
    yi1 = y2 + 1;
    yi2 = y4 - 1;
    
    % (xi,yi) is a polygon that strokes the inner line.  Here it is in
    % screen pixel units.
    xi = [xi1 xi2 xi2 xi1 xi1];
    yi = [yi1 yi1 yi2 yi2 yi1];
    
    % Convert the (xi,yi) polygon back to data units and clip it to two
    % pixels inside the axes limits.
    
    xi = xi * dx_per_screen_pixel;
    yi = yi * dy_per_screen_pixel;
    
    xi = max(xi, xrange(1) + 2*dx_per_screen_pixel);
    xi = min(xi, xrange(2) - 3*dx_per_screen_pixel);
    
    yi = max(yi, yrange(1) + 2*dy_per_screen_pixel);
    yi = min(yi, yrange(2) - 3*dy_per_screen_pixel);
    
    % Set the output position to include the entire extent of the drawn
    % rectangle, including decorations.
    outer_left = min(xx);
    outer_right = max(xx);
    outer_bottom = min(yy);
    outer_top = max(yy);
    outer_position = [outer_left outer_bottom ...
                      (outer_right-outer_left) (outer_top-outer_bottom)];
    
    if ~isequal(get(h_middle_line, 'XData'), x) || ...
       ~isequal(get(h_middle_line, 'YData'), y)
      set(h_middle_line, 'XData', x, 'YData', y);
      set(h_outer_line, 'XData', xx, 'YData', yy);
      set(h_inner_line, 'XData', xi, 'YData', yi);
      set(h_patch, 'XData', xx, 'YData', yy);
      sendNewPosition;
    end
    
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function sendNewPosition(varargin)

    list = new_position_callback_functions.getList();
    for k = 1:numel(list)
      fun = list{k};
      fun(position);
    end

  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function startDrag(src, varargin)

    src_fig = ancestor(src, 'figure');
    src_axes = ancestor(src, 'axes');
  
    if strcmp(get(src_fig, 'SelectionType'), 'normal')
        is_in_drag_operation = true;
        % Get the mouse location in data space.
        start_point = get(src_axes, 'CurrentPoint');  
        start_position = position;
        start_x = start_point(1,1,1);
        start_y = start_point(1,2,1);

        drag_motion_callback_id = iptaddcallback(src_fig, ...
                                                'WindowButtonMotionFcn', ...
                                                @dragMotion);
        
        drag_up_callback_id = iptaddcallback(src_fig, ...
                                            'WindowButtonUpFcn', ...
                                            @stopDrag);
    end
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    function dragMotion(varargin)
    
      ax = ancestor(h_group, 'axes');
      if ~ishandle(ax)
          return;
      end
    
      new_point = get(ax, 'CurrentPoint');
      delta_x = new_point(1,1,1) - start_x;
      delta_y = new_point(1,2,1) - start_y;
      new_position = start_position + [delta_x delta_y 0 0];
      
      if ~isempty(drag_constraint_function)
          position = drag_constraint_function(new_position);
      else
          position = new_position;
      end
      
      updateView;
    end
    %------------------------------------------------------------------------
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    function stopDrag(varargin)
      dragMotion();
      is_in_drag_operation = false;
      iptremovecallback(src_fig, 'WindowButtonMotionFcn', ...
                       drag_motion_callback_id);
      iptremovecallback(src_fig, 'WindowButtonUpFcn', ...
                       drag_up_callback_id);
    end
    %------------------------------------------------------------------------
    
  end
  %--------------------------------------------------------------------------
  
  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  function updateAncestorPositionListeners
  % Set up listeners to all ancestors of h_group that have a Position
  % property.  Whenever the Position property of any ancestor changes, we
  % need to call updateView().

    % Clear the old listeners.
    setappdata(h_group, 'AncestorPositionListeners', []);

    h_parent = get(h_group, 'Parent');
    root = 0;
    listeners = [];
    while h_parent ~= root
      % Some ancestor objects might not have Position properties.
      properties = get(h_parent);
      if isfield(properties, 'Position')
        parent_handle = handle(h_parent);
        listener = handle.listener(parent_handle, ...
                                   parent_handle.findprop('Position'), ...
                                   'PropertyPostSet', @updateView);
        if isempty(listeners)
          listeners = listener;
        else
          listeners(end + 1) = listener;
        end
      end
      h_parent = get(h_parent, 'Parent');
    end
    
    setappdata(h_group, 'AncestorPositionListeners', listeners);
  end
  %--------------------------------------------------------------------------
  
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [dx_per_screen_pixel, dy_per_screen_pixel] = getAxesScale(h_axes)
% Compute the data units per screen pixel for both the x and y
% directions.

  axes_position = hgconvertunits(ancestor(h_axes, 'figure'), ...
                                 get(h_axes, 'Position'), ...
                                 get(h_axes, 'Units'), ...
                                 'pixels', ...
                                 get(h_axes, 'Parent'));

  x_limits = get(h_axes, 'XLim');
  y_limits = get(h_axes, 'YLim');
  
  if axes_position(3) <= 1
    % Degenerate case; return 1 (arbitrary choice).
    dx_per_screen_pixel = 1;
  else
    dx_per_screen_pixel = (x_limits(2) - x_limits(1)) / axes_position(3);
  end
  
  if axes_position(4) <= 1
    % Degenerate case; return 1 (arbitrary choice).
    dy_per_screen_pixel = 1;
  else
    dy_per_screen_pixel = (y_limits(2) - y_limits(1)) / axes_position(4);
  end
end
%-------------------------------------------------------------------------------

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function points_per_screen_pixel = getPointsPerScreenPixel

  points_per_inch = 72;
  pixels_per_inch = get(0, 'ScreenPixelsPerInch');
  
  points_per_screen_pixel = points_per_inch / pixels_per_inch;
end
%-------------------------------------------------------------------------------

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function colors = getColorChoices

  % First color in the list will be the default.
  temp = {'Medium Blue',   [ 72  72 248]/255,
          'Light Blue',    [ 72 136 248]/255,
          'Light Red',     [248  79  79]/255,
          'Green',         [ 72 248  72]/255,
          'Yellow',        [248 246  74]/255,
          'Magenta',       [248  72 248]/255,
          'Cyan',          [ 72 248 248]/255,
          'Light Gray',    [232 232 232]/255,
          'Black',         [  0   0   0]/255};
  
  colors = struct('Label', temp(:,1), 'Color', temp(:,2));
end
%-------------------------------------------------------------------------------
