import javax.media.j3d.*;
import javax.vecmath.*;
import java.awt.event.*;

/**
 * A Mouse listener to be attached to each canvas.
 * This listener changes the viewing transform such that it appears
 * as though the object is rotating/translating, etc..<BR>
 * <BR>
 * Button assignments:<BR><UL>
 * <LI>Left - Rotate
 * <LI>Middle - Move in/out of scene along Z Axis
 * <LI>Right - Translate in X-Y plane
 * </UL>
 * Hold the SHIFT key down to activate vernier mode.<BR>
 * <BR>
 * Two modes of rotation are supported: WORLD and ORBIT.
 * WORLD mode orbits around the 
 * origin of the universe. ORBIT mode rotates around a specified
 * point. Default is WORLD mode.
 *
 * @see #WORLD_MODE
 * @see #ORBIT_MODE
 * @see #setViewMode(byte mode)
 * @see #setCenterOfRotation(TransformGroup center)
 *
 * @author Josh Richmond (2000)
 * @author Eric Reiss (orbit transformations)
 *
 */
public class ViewNavigator extends MouseAdapter implements MouseMotionListener
{
    private int lastX;
    private int lastY;

    // Scale for translational moves
    private final double COARSE_TRANS_SCALE = 0.08; //0.003;
    private final double VERNIER_TRANS_SCALE = 0.008; //0.0003;

    // Scale for rotation moves
    private final double COARSE_ROT_SCALE = 0.003;
    private final double VERNIER_ROT_SCALE = 0.0003;

    /** Center of rotation; nominally the origin **/
    private Transform3D centerOfRotation = new Transform3D();
    /** Center of an object - used to orbit **/
    private Transform3D objectCenter = new Transform3D();
    private TransformGroup viewTG;
    private Transform3D vpGhostT3D = new Transform3D();

    /** For screen-centered zoom **/
    private double zoomFactor = 2.0;
    
    private byte mode = WORLD_MODE;

    public static final byte WORLD_MODE = 0;
    public static final byte ORBIT_MODE = 2;

    /**
     * @param tg The TransformGroup for the viewer under control.
     */
    public ViewNavigator(TransformGroup tg)
    {
	viewTG = tg;
    }
	

    /**
     * Sets the rotation mode: WORLD_MODE or ORBIT_MODE.
     * WORLD_MODE rotates around the origin of the universe.
     * ORBIT_MODE rotates arounce the center of an object.
     *
     * @param mode WORLD_MODE or ORBIT_MODE.
     * @see #WORLD_MODE
     * @see #ORBIT_MODE
     */
    public void setViewMode(byte mode)
    {
	this.mode = mode;
	if (mode == WORLD_MODE)
	    centerOfRotation = new Transform3D(); // origin
	else if (mode == ORBIT_MODE)
	    centerOfRotation = new Transform3D(objectCenter);
    }

    /**
     * Sets the center of rotation (in the World frame)
     *
     * @param center Center of rotation in the world frame
     */
    public void setCenterOfRotation(TransformGroup center)
    {
	center.getTransform(objectCenter);
	// reset mode to transfer object center to CoR if neccessary
	this.setViewMode(this.mode);
    }

    /**
     * Sets the center of rotation (in the World frame)
     *
     * @param center Center of rotation in the world frame
     */
    public void setCenterOfRotation(Point3d center)
    {
	objectCenter = new Transform3D();
	objectCenter.set(new Vector3d(center));

	// reset mode to transfer object center to CoR if neccessary
	this.setViewMode(this.mode);
    }

    /**
     * When the mouse button is pressed, keep track of its position.
     */
    public void mousePressed(MouseEvent e)
    {
	lastX = e.getX();
	lastY = e.getY();
	updateZoom();
    }

    /**
     * Update zoom factor for screen mode
     */
    public void mouseReleased(MouseEvent e)
    {
	updateZoom();
    }

    /** 
     * Updates the zoom factor; used for screen mode only.
     */
    private void updateZoom()
    {
	// Get the camera's current transform
	Transform3D curXform = new Transform3D();
	viewTG.getTransform(curXform);
	
	// Get it's position in WCS
	Vector3d pos = new Vector3d();
	curXform.get(pos);

	zoomFactor = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
    }

    /**
     * Calls the appropriate function when the mouse is dragged.
     * Rotate:    Button 1
     * Zoom:      Button 2
     * Translate: Button 3
     * Vernier mode -- use SHIFT
     */
    public void mouseDragged(MouseEvent e)
    {
	if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == 
	    InputEvent.BUTTON1_MASK)
	    mouseRotate(e);
	else if ((e.getModifiers() & InputEvent.BUTTON2_MASK) == 
		 InputEvent.BUTTON2_MASK)
	    mouseZoom(e);
	else if ((e.getModifiers() & InputEvent.BUTTON3_MASK) == 
		 InputEvent.BUTTON3_MASK)
	    mouseTranslate(e);
    }
    
    public void mouseMoved(MouseEvent e) { /* do nothing */  }
    
    /*
     * Variables for rotate function. Pre-allocated for speed
     */
    /** World to Camera transform **/
    private Transform3D wTc = new Transform3D();
    /** rotation matrix **/
    private Transform3D mouseRot= new Transform3D();
    /** World to center-of-rotation **/
    private Transform3D wTn = new Transform3D();

    /**
     * Rotate the transform so that it is as if the scene is rotating
     * around the <code>centerOfRotation</code> point.
     *
     * @param e Mouse event from MouseDragged
     */
    public void mouseRotate(MouseEvent e)
    {
	int dx = e.getX() - lastX;
	int dy = e.getY() - lastY;
	Vector3d eulerAngles = new Vector3d();

	double scale = e.isShiftDown() ? VERNIER_ROT_SCALE : COARSE_ROT_SCALE;
	
	eulerAngles.x = -dy * scale;
	eulerAngles.y = -dx * scale;
	eulerAngles.z = 0.0;

	orbit(eulerAngles);
	
	lastX = e.getX();
	lastY = e.getY();
    }

    /**
     * Rotate the transform so that it is as if the scene is rotating
     * around the <code>centerOfRotation</code> point. Algorithm
     * based on MouseOrbit by Eric Reiss.
     * http://www.sigda.org/Eric/java3d/behaviors/index.htm
     *
     * @param eulerAngles Vector containing the scaled Euler angles
     */
    private void orbit(Vector3d eulerAngles)
    {
	// Store the rotations
	Transform3D orbitT3D = new Transform3D();
	orbitT3D.setEuler(eulerAngles);

	// Keep a working copy
	Transform3D ghostT3D = new Transform3D(centerOfRotation);

	// Get the translation to the center of Rotation
	Vector3d ghostTrans = new Vector3d();
	ghostT3D.get(ghostTrans);

	// Get the rotation of the center of Rotation
	Matrix4d ghostRot = new Matrix4d();
	ghostT3D.get(ghostRot);

	// Get view to center of rotation transgform
	Transform3D vpGhostT3D_NonInverted = new Transform3D();

	viewTG.getTransform(vpGhostT3D_NonInverted);
	vpGhostT3D_NonInverted.setTranslation(new Vector3d());

	Transform3D vpGhostT3D_Inverted = 
	    new Transform3D(vpGhostT3D_NonInverted);
	vpGhostT3D_Inverted.invert();

	// Remove the transform to the center of rotation
	ghostT3D.mul(vpGhostT3D_Inverted, ghostT3D);
	ghostT3D.setTranslation(new Vector3d());

	// orbit
	ghostT3D.mul(orbitT3D, ghostT3D);

	// Go back
	ghostT3D.mul(vpGhostT3D_NonInverted, ghostT3D);
	ghostT3D.setTranslation(ghostTrans);

	// Accumulate for dragging
	centerOfRotation = new Transform3D(ghostT3D);

	// Now do the same thing for the viewer
	viewTG.getTransform(vpGhostT3D);
	Vector3d vpGhostTrans = new Vector3d();
	vpGhostT3D.get(vpGhostTrans);

	Vector3d tmpVec = new Vector3d();
	tmpVec.sub(vpGhostTrans, ghostTrans);
	vpGhostT3D.setTranslation(tmpVec);
	vpGhostT3D.mul(vpGhostT3D_Inverted, vpGhostT3D);

	vpGhostT3D.mul(orbitT3D, vpGhostT3D);
	vpGhostT3D.mul(vpGhostT3D_NonInverted, vpGhostT3D);

	vpGhostT3D.get(tmpVec);
	tmpVec.add(ghostTrans);
	vpGhostT3D.setTranslation(tmpVec);

	// Make it so
	viewTG.setTransform(vpGhostT3D);
    }
    
    /**
     * Zoom the transform.
     * @param e Mouse event from MouseDragged
     */
    public void mouseZoom(MouseEvent e)
    {
	int dy = e.getY() - lastY;
  	double scaled_dy = 
	    (e.isShiftDown() ? 
	     VERNIER_TRANS_SCALE : COARSE_TRANS_SCALE)*(double)dy;

	Transform3D zoom = new Transform3D();
	zoom.set(new Vector3d(0,0, scaled_dy));
	
	Transform3D currXform = new Transform3D();			
	viewTG.getTransform(currXform);
	
	currXform.mul(zoom);
	
	viewTG.setTransform(currXform);
	
	lastX = e.getX();
	lastY = e.getY();			 
    }
    
    /**
     * Translate the transform
     * @param e Mouse event from MouseDragged
     */
    public void mouseTranslate(MouseEvent e)
    {
	int dx = e.getX() - lastX;
	int dy = e.getY() - lastY;
	boolean vernier = e.isShiftDown();
	
	double xMove = dx * -1 *
	    (vernier ? VERNIER_TRANS_SCALE : COARSE_TRANS_SCALE);;
	double yMove = dy * 
	    (vernier ? VERNIER_TRANS_SCALE : COARSE_TRANS_SCALE);;
	
	Transform3D translate = new Transform3D();
	translate.set(new Vector3d(xMove, yMove, 0));
	
	Transform3D currXform = new Transform3D();
	viewTG.getTransform(currXform);
	
	currXform.mul(translate);
	
	viewTG.setTransform(currXform);
	
	lastX = e.getX();
	lastY = e.getY();			 
    }
    
}
