

import javax.media.opengl.GL2;
import javax.media.opengl.glu.GLU;

import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

/**
 * A Camera object encapsulates the information needed to define a
 * viewing transform and a projection for an OpenGL context.  The
 * apply method can be called to applied this information to
 * a context.  The default view is from the point (0,0,30),
 * looking at (0,0,0), with (0,1,0) pointing upwards on the screen.
 * The default projection is a perspective projection.  The
 * x and y limits on the screen include at least -5 to 5.  Limits
 * in either the x or y direction will be expanded if necessary
 * to match the aspect ratio of the screen.  And the view volume
 * extends from -10 to 10 along the z-axis.  Only the default
 * constructor exists.  Non-default properties must be set by
 * calling methods.
 *     The camera comes along with a simulated trackball that
 * lets the user rotate the view by dragging on the drawing
 * surface.  See the installTrackball() method.
 */
public class Camera {
   
   private double eyex, eyey, eyez = 30;
   private double refx, refy, refz;
   private double upx, upy = 1, upz;
   
   private double xminRequested = -5, xmaxRequested = 5;
   private double yminRequested = -5, ymaxRequested = 5;
   private double zmin = -10, zmax = 10;
   private boolean orthographic;
   private boolean preserveAspect = true;
   
   private double xminActual, xmaxActual, yminActual, ymaxActual;
   private GLU glu = new GLU();
   
   private TrackballMouser trackball;    // handles the mouse for the simulated trackball
   private Component trackballComponent; // if non-null, component where trackball is installed

   
   public boolean getOrthographic() {
      return orthographic;
   }

   /**
    * Determine whether the projection is orthographic or perspective.
    * The default is perspective.
    * @param orthographic set to true for orthographic projection and
    * to false for perspective projection.
    */
   public void setOrthographic(boolean orthographic) {
      this.orthographic = orthographic;
   }

   public boolean getPreserveAspect() {
      return preserveAspect;
   }

   /**
    * Determine whether the xy-limits should be adjusted to match the
    * aspect ratio of the display area.  The default is true.
    */
   public void setPreserveAspect(boolean preserveAspect) {
      this.preserveAspect = preserveAspect;
   }

   /**
    * Set the limits of the view volume.  The limits are set with respect to the
    * viewing coordinates.  That is, the view center is assumed to be at the point
    * (0,0) in the plane of the screen.  The view up vector (more precisely, its projection
    * onto the screen) points upwards on the screen.  The z-axis is perpendicular to the
    * screen, with the positive direction of the z-axis pointing out of the screen.
    * In this coordinate system, xmin and xmax give the horizontal limits on the screen,
    * ymin and ymax give the vertical limits on the screen, and zmin and zmax give
    * the limits of the view volume along the z-axis.  (Note that this is NOT exactly
    * the same as the parameters in either glOrtho or glFrustum!  Most important to 
    * note is that zmin and zmax are given with reference to the view center, not the
    * eye.)  Note that xmin/xmax or ymin/ymax might be adjusted to match the aspect
    * ratio of the display area.
    */
   public void setLimits(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax) {
      xminRequested = xminActual = xmin;
      xmaxRequested = xmaxActual = xmax;
      yminRequested = yminActual = ymin;
      ymaxRequested = ymaxActual = ymax;
      this.zmin = zmin;
      this.zmax = zmax;
   }
   
   /**
    * A convenience method for calling setLimits(-limit,limit,-limit,limit,-2*limit,-2*limit)
    */
   public void setScale(double limit) {
      setLimits(-limit,limit,-limit,limit,-2*limit,2*limit);
   }
   
   /**
    * Returns the view limits.  The return value is an array that contains the same data as
    * the parameters to setLimits().  Note that the returned values included the
    * originally requested xmin/xmax and ymin/ymax, and NOT values that have been
    * adjusted to reflect the aspect ratio of the display area.
    */
   public double[] getLimits() {
      return new double[] { xminRequested, xmaxRequested, yminRequested, ymaxRequested, zmin, zmax };
   }
   
   /**
    * Returns the actual xmin, xmax, ymin, ymax limits that were used when the apply
    * method was most recently called.  These are the limits after they were, possibly,
    * adjusted to match the aspect ratio of the display.  If apply has not been called
    * since the limits were set, then the return value contains the unadjusted, requested
    * limits.
    */
   public double[] getActualXYLimits() {
      return new double[] { xminActual, xmaxActual, yminActual, ymaxActual };
   }
   
   /**
    * Set the information for the viewing transformation.  The view will be set
    * in the apply method with a call to
    * gluLookAt(eyeX,eyeY,eyeZ,viewCenterX,viewCenterY,viewCenterZ,viewUpX,viewUpY,viewUpZ)
    */
   public void lookAt(double eyeX, double eyeY, double eyeZ,
                        double viewCenterX, double viewCenterY, double viewCenterZ,
                        double viewUpX, double viewUpY, double viewUpZ) {
      eyex = eyeX;
      eyey = eyeY;
      eyez = eyeZ;
      refx = viewCenterX;
      refy = viewCenterY;
      refz = viewCenterZ;
      upx = viewUpX;
      upy = viewUpY;
      upz = viewUpZ;
   }

   /**
    * Returns the view information -- the 9 parameters of lookAt(), in an array.
    */
   public double[] getViewParameters() {
      return new double[] { eyex, eyey, eyez, refx, refy, refz, upx, upy, upz };
   }
   
   /**
    * Apply the camera to an OpenGL context.  This method completely replaces the
    * projection and the modelview transformation in the context.  It sets these
    * transformations to the identity and then applies the view and projection
    * represented by the camera.  This method is meant to be called at the begining
    * of the display method and should replace any other means of setting the
    * projection and view.
    */
   public void apply(GL2 gl) {
        int[] viewport = new int[4];
        gl.glGetIntegerv(GL2.GL_VIEWPORT, viewport, 0);
        xminActual = xminRequested;
        xmaxActual = xmaxRequested;
        yminActual = yminRequested;
        ymaxActual = ymaxRequested;
        if (preserveAspect) {
           double viewWidth = viewport[2];
           double viewHeight = viewport[3];
           double windowWidth = xmaxActual - xminActual;
           double windowHeight = ymaxActual - yminActual;
           double aspect = viewHeight / viewWidth;
           double desired = windowHeight / windowWidth;
           if (desired > aspect) { //expand width
               double extra = (desired / aspect - 1.0) * (xmaxActual - xminActual) / 2.0;
               xminActual -= extra;
               xmaxActual += extra;
           } else if (aspect > desired) {
               double extra = (aspect / desired - 1.0) * (ymaxActual - yminActual) / 2.0;
               yminActual -= extra; 
               ymaxActual += extra;
           }
        }
        gl.glMatrixMode(GL2.GL_PROJECTION);
        gl.glLoadIdentity();
       double viewDistance = norm(new double[] {refx-eyex, refy-eyey, refz-eyez});
        if (orthographic) {
            gl.glOrtho(xminActual, xmaxActual, yminActual, ymaxActual, viewDistance-zmax, viewDistance-zmin);
        }
        else {
            double near = viewDistance-zmax;
            if (near < 0.1)
               near = 0.1;
            double centerx = (xminActual + xmaxActual) / 2;
            double centery = (yminActual + ymaxActual) / 2;
            double newwidth = (near / viewDistance) * (xmaxActual - xminActual);
            double newheight = (near / viewDistance) * (ymaxActual - yminActual);
            double x1 = centerx - newwidth / 2;
            double x2 = centerx + newwidth / 2;
            double y1 = centery - newheight / 2;
            double y2 = centery + newheight / 2;
            gl.glFrustum(x1, x2, y1, y2, near, viewDistance-zmin);
        }
        gl.glMatrixMode(GL2.GL_MODELVIEW);
        gl.glLoadIdentity();
        glu.gluLookAt(eyex, eyey, eyez, refx, refy, refz, upx, upy, upz);
   }
   
   /**
    * Installs a simulated trackball for an OpenGL component, which must be the
    * same component on which this camera is used.  The user can rotate the
    * view by dragging on the component.  This will only work if the camera's
    * apply() method is called at the start of the component's display function
    * to set up the viewing and projection transformations.  The camera words
    * by modifying the camera's viewing parameters.
    * @param c  The OpenGL drawing surface where the trackball will be installed.
    *    This is probably a GLJPanel or GLCanvas.  Passing a null as the parameter
    *    will remove the trackball from the component (if it has been installed
    *    there).
    */
   public void installTrackball(Component c) {
	   if ( trackballComponent != null && trackballComponent != c ) {
		   trackballComponent.removeMouseListener(trackball);
	   }
	   if (trackballComponent == c) {
		   return;
	   }
	   trackballComponent = c;
	   if ( trackballComponent == null ) {
		   return;
	   }
	   if (trackball == null ) {
		   trackball = new TrackballMouser();
	   }
	   trackballComponent.addMouseListener(trackball);
   }

    private double norm(double[] v) {
        double norm2 = v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
        if (Double.isNaN(norm2) || Double.isInfinite(norm2) || norm2 == 0)
            throw new NumberFormatException("Vector length zero, undefined, or infinite.");
        return Math.sqrt(norm2);
    }

    private void normalize(double[] v) {
        double norm = norm(v);
        v[0] /= norm;
        v[1] /= norm;
        v[2] /= norm;
    }

    private void applyTransvection(double[] e1, double[] e2) {
        // rotate vector e1 onto e2; must be 3D *UNIT* vectors.
        double[] zDirection = new double[] {eyex - refx, eyey - refy, eyez - refz};
        double viewDistance = norm(zDirection);
        normalize(zDirection);
        double[] yDirection = new double[] {upx, upy, upz};
        double upLength = norm(yDirection);
        double proj = yDirection[0]*zDirection[0] + yDirection[1]*zDirection[1] + yDirection[2]*zDirection[2];
        yDirection[0] = yDirection[0] - proj*zDirection[0];
        yDirection[1] = yDirection[1] - proj*zDirection[1];
        yDirection[2] = yDirection[2] - proj*zDirection[2];
        normalize(yDirection);
        double[] xDirection = new double[] {yDirection[1]*zDirection[2] - yDirection[2]*zDirection[1],
                yDirection[2]*zDirection[0] - yDirection[0]*zDirection[2],
                yDirection[0]*zDirection[1] - yDirection[1]*zDirection[0] };
       e1 = transformToViewCoords(e1, xDirection, yDirection, zDirection);
       e2 = transformToViewCoords(e2, xDirection, yDirection, zDirection);
        double[] e = new double[]{e1[0] + e2[0], e1[1] + e2[1], e1[2] + e2[2]};
        normalize(e);
        double[] temp = new double[3];
        reflectInAxis(e, zDirection, temp);
        reflectInAxis(e1, temp, zDirection);
        reflectInAxis(e, xDirection, temp);
        reflectInAxis(e1, temp, xDirection);
        reflectInAxis(e, yDirection, temp);
        reflectInAxis(e1, temp, yDirection);
        eyex = refx + viewDistance * zDirection[0];
        eyey = refy + viewDistance * zDirection[1];
        eyez = refz + viewDistance * zDirection[2];
        upx = upLength * yDirection[0];
        upy = upLength * yDirection[1];
        upz = upLength * yDirection[2];
    }

    private void reflectInAxis(double[] axis, double[] source, double[] destination) {
        double s = 2 * (axis[0] * source[0] + axis[1] * source[1] + axis[2] * source[2]);
        destination[0] = s * axis[0] - source[0];
        destination[1] = s * axis[1] - source[1];
        destination[2] = s * axis[2] - source[2];
    }
    
    private double[] transformToViewCoords(double[] v, double[] x, double[] y, double[] z) {
       double[] w = new double[3];
       w[0] = v[0]*x[0] + v[1]*y[0] + v[2]*z[0];
       w[1] = v[0]*x[1] + v[1]*y[1] + v[2]*z[1];
       w[2] = v[0]*x[2] + v[1]*y[2] + v[2]*z[2];
       return w;
    }
    
    private class TrackballMouser implements MouseListener, MouseMotionListener {

        private boolean dragging;
        private double[] prevRay;
        
        public void mousePressed(MouseEvent e) {
           if (dragging)
              return;
           dragging = true;
           prevRay = mousePointToRay(e.getX(), e.getY());
           trackballComponent.addMouseMotionListener(this);
        }

        public void mouseReleased(MouseEvent e) {
           if (!dragging)
              return;
           dragging = false;
           trackballComponent.removeMouseMotionListener(this);
        }

        public void mouseDragged(MouseEvent e) {
           if (!dragging)
              return;
           double[] thisRay = mousePointToRay(e.getX(), e.getY());
           applyTransvection(prevRay, thisRay);
           prevRay = thisRay;
           trackballComponent.repaint();
        }
        
        private double[] mousePointToRay(int x, int y) {
           double dx, dy, dz, norm;
           int centerX = trackballComponent.getWidth()/2;
           int centerY = trackballComponent.getHeight()/2;
           double scale = 0.8*Math.min(centerX, centerY);
           dx = (x - centerX);
           dy = (centerY - y);
           norm = Math.sqrt(dx*dx + dy*dy);
           if (norm >= scale)
              dz = 0;
           else
              dz = Math.sqrt( scale*scale - dx*dx -dy*dy );
           double length = Math.sqrt(dx*dx + dy*dy + dz*dz);
           return new double[] { dx/length, dy/length, dz/length };
        }

        public void mouseClicked(MouseEvent e) { }

        public void mouseEntered(MouseEvent e) { }

        public void mouseExited(MouseEvent e) { }

        public void mouseMoved(MouseEvent e) { }

     }


}
