function dragviewButtonDownFcn( hObject, eventData )
%dragviewButtonDownFcn( hObject, eventData )
%   Installing this as the ButtonDownFcn for an axes object or its children
%   will cause a click on the object to produce panning, zooming, or
%   trackball-like behaviour when the mouse is dragged.
%   This routine temporarily stores all the information it needs to process the
%   mouse movement, in the field 'clickdragData' of the guidata of the figure.
%   The field is deleted when the mouse button is released.
%   As a side-effect, all of the camera mode properties of the axes are set
%   to manual.
%
%   There is more than one way to map mouse movement to rotation, and the
%   choice is a matter of design.  Three mappings are implemented, and the choice
%   can be specified by setting the 'trackballMode' field of guidata.
%   'global' (the default) maps trackball coordinates [x,y] to a rotation about
%   an axis perpendicular to that vector in the (cameraright,cameraup) plane, by
%   an amount proportional to the length of the vector.
%   'local' maps incremental motion of the mouse to incremental rotation in
%   the same way.
%   'upright' maps trackball coordinates [x,y] to azimuth and elevation,
%   maintaining the camera up vector equal to [0 0 1].  This is equivalent
%   to the trackball mode available from the standard camera toolbar,
%   except that the scaling may be different.
%
%   Global and Upright mode have the advantage that mouse positions are uniquely
%   mapped to rotations: moving the mouse back to its starting point
%   restores the axes to its original orientation.
%   Local mode has the advantage that the current rotation axis is always
%   perpendicular to the current mouse velocity.
%   There is no mapping of trackball data to axes rotation that allows both
%   of these.
%
%   The scaling factor is currently set so that movement by a distance
%   equal to the smaller of the width and height of the axes object rotates
%   it by 2 radians.

    % Find the axes and the figure.
    hAxes = ancestor( hObject, 'axes' );
    if ~ishandle(hAxes), return; end
    hFigure = ancestor( hAxes, 'figure' );
    if ~ishandle(hFigure), return; end
    
    % Get all the information we need.
    camParams = getCameraParams( hAxes );
    hitPointParent = get( hFigure, 'CurrentPoint' );
    oldUnits = get( hAxes, 'Units' );
    set( hAxes, 'Units', 'pixels' );
    axespos = get( hAxes, 'Position' );
    set( hAxes, 'Units', oldUnits );
    axessizepixels = min(axespos([3 4]));
    axessizeunits = getViewWidth( camParams );
    setManualCamera( hAxes );

    [cameraLook, cameraUp, cameraRight] = cameraFrame( ...
        camParams.CameraPosition, ...
        camParams.CameraTarget, ...
        get( hAxes, 'CameraUpVector' ) );

    % Store it in guidata.
    gd = guidata(hFigure);
    if isfield( gd, 'trackballScale' )
        trackballScale = gd.trackballScale;
    else
        trackballScale = pi;
    end
    trackballMode = '';
    axesUserData = get( hAxes, 'UserData' );
    if (~isempty(axesUserData)) && isstruct(axesUserData) && isfield(axesUserData,'dragmode')
        dragmode = axesUserData.dragmode;
    else
        dragmode = 'rot';
    end
    switch dragmode
        case 'rot'
            trackballMode = 'global';
            buttonDownFcn = @trackballButtonMotionFcn;
        case 'rotupright'
            trackballMode = 'upright';
            buttonDownFcn = @trackballButtonMotionFcn;
        case 'pan'
            buttonDownFcn = @panButtonMotionFcn;
        case 'zoom'
            buttonDownFcn = @zoomButtonMotionFcn;
        otherwise
            buttonDownFcn = '';
    end
    gd.clickdragData = struct( 'axes', hAxes, ...
        'moved', false, ...
        'dragmode', dragmode, ...
        'startpoint', hitPointParent, ...
        'currentpoint', hitPointParent, ...
        'axessizepixels', axessizepixels, ...
        'axessizeunits', axessizeunits, ...
        'cameraLook', cameraLook, ...
        'cameraUp', cameraUp, ...
        'cameraRight', cameraRight, ...
        'cameraTarget', camParams.CameraTarget, ...
        'cameraPosition', camParams.CameraPosition, ...
        'startCameraPosition', camParams.CameraPosition, ...
        'startCameraTarget', camParams.CameraTarget, ...
        'startCameraViewAngle', get( hAxes, 'CameraViewAngle' ), ...
        'trackballScale', trackballScale, ...
        'trackballMode', trackballMode, ...
        'oldWindowButtonMotionFcn', get( hFigure, 'WindowButtonMotionFcn' ), ...
        'oldWindowButtonUpFcn', get( hFigure, 'WindowButtonUpFcn' ) );
    guidata( hFigure, gd );
    set( hFigure, ...
         'WindowButtonMotionFcn', buttonDownFcn, ...
         'WindowButtonUpFcn', @clickdragButtonUpFcn );
    if isfield( gd, 'stereodata' )
        stereoTransfer( hAxes, gd.stereodata.otheraxes, gd.stereodata.vergence );
    end
end

function panButtonMotionFcn( hObject, eventData )
    gd = guidata( hObject );
    if isempty( gd ) || ~isfield( gd, 'clickdragData' ), return; end
    currentpoint = get( hObject, 'CurrentPoint' );
    delta = (currentpoint - gd.clickdragData.startpoint) ...
            * gd.clickdragData.axessizeunits/gd.clickdragData.axessizepixels;
  % dp = (currentpoint - gd.clickdragData.startpoint)
  % asp = gd.clickdragData.axessizepixels
  % asu = gd.clickdragData.axessizeunits
    camParams = getCameraParams( gd.clickdragData.axes );
    [cameraLook, cameraUp, cameraRight] = cameraFrame( camParams );
    offset = delta(1)*cameraRight + delta(2)*cameraUp;
    newcampos = gd.clickdragData.startCameraPosition - offset;
    newcamtgt = gd.clickdragData.startCameraTarget - offset;
    set( gd.clickdragData.axes, ...
         'CameraPosition', newcampos, ...
         'CameraTarget', newcamtgt );
    if isfield( gd, 'stereodata' )
        stereoTransfer( gd.stereodata.otheraxes, ...
                        newcampos, ...
                        newcamtgt, ...
                        cameraUp, ...
                        gd.stereodata.vergence );
    end
    gd.clickdragData.moved = true;
    guidata( hObject, gd );
end

function zs = zoomScale( delta )
    zs = (delta + sqrt( delta*delta+4 ))/2;
end

function zoomButtonMotionFcn( hObject, eventData )
    gd = guidata( hObject );
    if isempty( gd ) ...
            || ~isfield( gd, 'clickdragData' ) ...
            || ~isfield( gd.clickdragData, 'startpoint' )
        return;
    end
    currentpoint = get( hObject, 'CurrentPoint' );
    ZOOMSCALING = 5;
    delta = (currentpoint - gd.clickdragData.startpoint) ...
            * ZOOMSCALING/gd.clickdragData.axessizepixels;
    camParams = getCameraParams( gd.clickdragData.axes );
    [cameraLook, cameraUp, cameraRight] = cameraFrame( camParams );
    newviewangle = trimnumber( 0, ...
                       gd.clickdragData.startCameraViewAngle * zoomScale( delta(2) ), ...
                       179.0 );
    set( gd.clickdragData.axes, ...
         'CameraViewAngle', newviewangle );
    if isfield( gd, 'stereodata' )
        stereoTransfer( gd.stereodata.otheraxes, ...
                        camParams.CameraPosition, ...
                        camParams.CameraTarget, ...
                        cameraUp, ...
                        gd.stereodata.vergence );
    end
    setscalebarsize( gd.mesh );
    gd.clickdragData.moved = true;
    guidata( hObject, gd );
end

function trackballButtonMotionFcn( hObject, eventData )
    gd = guidata( hObject );
    if isempty( gd ) || ~isfield( gd, 'clickdragData' ), return; end
    if ~isfield( gd.clickdragData, 'trackballMode' )
      % fprintf( 1, '** Warning: trackballButtonMotionFcn called but no trackball data available.\n' );
        return;
    end
    currentpoint = get( hObject, 'CurrentPoint' );
    switch gd.clickdragData.trackballMode
        case 'global'
            delta = (currentpoint - gd.clickdragData.startpoint)/gd.clickdragData.axessizepixels;
        otherwise % case { 'local', 'upright' }
            delta = (currentpoint - gd.clickdragData.currentpoint)/gd.clickdragData.axessizepixels;
    end
    [cameraPos,cameraUp,cameraRight] = ...
        trballView( gd.clickdragData, [-delta(1), delta(2)], ...
                    gd.clickdragData.trackballScale );
    if strcmp( gd.clickdragData.trackballMode, 'upright' )
        cameraUp = [0 0 1];
    end
    gd.clickdragData.currentpoint = currentpoint;
    switch gd.clickdragData.trackballMode
        case 'global'
            %nothing
        otherwise % case { 'local', 'upright' }
            gd.clickdragData.cameraPosition = cameraPos;
            gd.clickdragData.cameraUp = cameraUp;
            gd.clickdragData.cameraRight = cameraRight;
    end
    set( gd.clickdragData.axes, ...
        'CameraPosition', cameraPos, ...
        'CameraUpVector', cameraUp );
    if isfield( gd, 'stereodata' )
        stereoTransfer( gd.stereodata.otheraxes, ...
                        cameraPos, ...
                        get( gd.clickdragData.axes, 'CameraTarget' ), ...
                        cameraUp, ...
                        gd.stereodata.vergence );
    end
    gd = updateAziElScrollFromView( gd );
    gd.clickdragData.moved = true;
    guidata( hObject, gd );
end

function clickdragButtonUpFcn( hObject, eventData )
    % Process the final mouse position.
    % Tests indicate that this is not necessary -- the final mouse position
    % always generates a ButtonMotion event.
    % trackballButtonMotionFcn( hObject, eventData )

    % Restore the original ButtonMotion and ButtonUp functions.
    % Invoke the finalisation callback to notify the host program that the
    % tracking has ended, so that it can read the camera info and update
    % host-specific data.
    % Finally, delete the trackball data.
    gd = guidata( hObject );
    if ~isempty(gd) && isfield( gd, 'clickdragData' )
        if isfield( gd.clickdragData, 'oldWindowButtonMotionFcn' )
            set( hObject, 'WindowButtonMotionFcn', gd.clickdragData.oldWindowButtonMotionFcn );
        else
            set( hObject, 'WindowButtonMotionFcn', '' );
        end
        if isfield( gd.clickdragData, 'oldWindowButtonUpFcn' )
            set( hObject, 'WindowButtonUpFcn', gd.clickdragData.oldWindowButtonUpFcn );
        else
            set( hObject, 'WindowButtonUpFcn', '' );
        end
        gd.dragged = isfield( gd.clickdragData, 'moved' ) && gd.clickdragData.moved;
        gd.clickdragData = trimStruct( gd.clickdragData, ...
            { ...
                'cameraLook', ...
                'cameraUp', ...
                'cameraRight', ...
                'cameraTarget', ...
                'cameraPosition', ...
            } );
        guidata( hObject, gd );
        if isfield( gd, 'dragend_Callback' ) && ~isempty( gd.dragend_Callback )
            gd.dragend_Callback( hObject );
        end
    end
end

function [cameraPos,cameraUp,cameraRight] = trballView( trdata, trball, trballscale )
%[cameraPos,cameraUp] = trballView( initview, trball, trballscale )
%   initview is a structure containing
%       cameraRight
%       cameraUp
%   trball is a pair of real numbers indicating the amount of trackball
%   movement in two dimensions.  These represent a rotation about an axis
%   in the view plane perpendicular to the trball vector.  The scaling is 1
%   unit = trballscale radians. cameraPos and cameraUp are set to the new
%   camera position and up-vector specified by the trackball value.

    if all(trball==0)
        cameraPos = trdata.cameraPosition;
        cameraUp = trdata.cameraUp;
        cameraRight = trdata.cameraRight;
    else
        rotAxis = trball(2)*trdata.cameraRight + trball(1)*trdata.cameraUp;
        rotAxis = rotAxis/norm(rotAxis);
        rotAmount = norm(trball) * trballscale;

        % Rotate cameraPosition about rotAxis through cameraTarget by
        % rotAmount.
        cameraPos = rotVec( trdata.cameraPosition, trdata.cameraTarget, rotAxis, rotAmount );

        % Rotate cameraPosition about rotAxis by rotAmount.
        cameraUp = rotVec( trdata.cameraUp, [0 0 0], rotAxis, rotAmount );

        % Rotate cameraRight about rotAxis by rotAmount.
        cameraRight = rotVec( trdata.cameraRight, [0 0 0], rotAxis, rotAmount );
    end
end

