import DE.*;
import Geometry.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.media.j3d.*;
import javax.vecmath.*; 
import com.sun.j3d.utils.geometry.*;
import com.sun.j3d.utils.image.*;
import com.sun.j3d.utils.universe.*;


/**
 * A Java3D program for viewing the output of the FindPoints program.
 * Written by Chris Studholme
 * Licenced for redistribution under the GNU General Public Licence
 */
class GraphicalView3D extends Canvas3D implements KeyListener {

    /** Rate of rotation.
     */
    public static final double ROTATE_RATE     = 0.05;
    /** Rate of movement when in rectangular coordinates.
     */
    public static final double RECT_MOVE_RATE  = 0.1;
    /** Rate of movement when in polar coordinates.
     */
    public static final double POLAR_MOVE_RATE = 0.2;

    /** Scale factor.  Make larger to space features out more.
     */
    public static final double SCALE_FACTOR = 1;
    /** Feature size.  Actually, this is the size of the camera box, but all 
     * other features are multiples of this size.
     */
    public static final float FEATURE_SIZE = 0.05f;

    /** Point size for the point labels.
     */
    public static final int FONT_SIZE_POINT = 10;
    /** Point size for the photo labels.
     */ 
    public static final int FONT_SIZE_PHOTO = 10;

    /** Base directory where the photos can be found.
     */
    public static final String PICTURE_DIR = "pictures";

    /** Point and photo location parameters to display.
     */
    PointParameters pp;
    /** Pixel observation data.
     */
    ObservedPoints data;

    /** Array of colors to be used for coloring points and projection lines.
     */
    Color3f[] colors;

    /** The universe.
     */
    SimpleUniverse universe;

    /** Center of coordinate system.
     */
    Point3d center = new Point3d(0,0,0);

    /** View transform.
     */
    TransformGroup viewTrans;

    /** View parameter: location of eye.
     */
    Point3d eye;
    /** View parameter: direction of view.
     */
    Vector3d dir;
    /** View parameter: direction of up.
     */
    Vector3d up;
    /** Are we in polar mode or rectangular mode?
     */
    boolean polarMode=true;

    /** Appearance object: for point labels.
     */
    Appearance appearance_pointlabel;
    /** Appearance object: for photo labels.
     */
    Appearance appearance_photolabel;
    /** Appearance object: for the convex hull.
     */
    Appearance appearance_convexhull;
    /** Appearance object: for the closed hull.
     */
    Appearance appearance_closedhull;

    /** The convex hull.
     */
    Hull convexhull;
    /** The closed hull.
     */
    Hull closedhull;

    /**
     * Default constructor creates universe and initalizes the colors
     * array, amoung other things.
     */
    public GraphicalView3D() {
	super(GraphicsEnvironment.getLocalGraphicsEnvironment().
	      getDefaultScreenDevice().getDefaultConfiguration());

	// colors
	colors = new Color3f[24];
	int pos=0;
	for (int i=0; i<3; ++i)
	    for (int j=0; j<3; ++j)
		for (int k=0; k<3; ++k)
		    if ((i!=j)||(j!=k))
			colors[pos++] = new Color3f(0.5f+0.25f*i,
						    0.5f+0.25f*j,
						    0.5f+0.25f*k);

	addKeyListener(this);

	// create universe
	universe = new SimpleUniverse(this);
        universe.getViewingPlatform().setNominalViewingTransform();

	// initial view
	eye = new Point3d(2,0,0);
	dir = new Vector3d(-2,0,0);
	up = new Vector3d(0,0,1);
    }

    /**
     * Figure out if i is a power of 2.
     * @param i integer to check
     * @return true if i is a power of two
     */
    public static boolean isPowerOf2(int i) {
	while (i>1) {
	    if ((i&1)==1) return false;
	    i>>=1;
	}
	return true;
    }

    /**
     * Create a Shape3D object representing a 2D image loaded from the file
     * with the specified name.
     * @param name filename of image
     * @return Shape3D object
     */
    public Shape3D createImage(String name) {
	// create appearance
	Appearance appear = new Appearance();
        TextureLoader loader = new TextureLoader(PICTURE_DIR+"/"+name,null);
        ImageComponent2D image = loader.getImage();
        if (image==null) {
	    System.err.println("Load failed for image: "+name);
	    return null;
        }
	float aspect=1;
	if (!isPowerOf2(image.getWidth())||!isPowerOf2(image.getHeight())) {
	    aspect = image.getHeight()/(float)image.getWidth();
	    image = loader.getScaledImage(256,256);
	}
        Texture2D texture = new Texture2D(Texture.BASE_LEVEL,Texture.RGBA,
                                          image.getWidth(),image.getHeight());
        texture.setImage(0, image);
        texture.setEnable(true);
        texture.setMagFilter(Texture.NICEST);
        appear.setTexture(texture);
        appear.setTransparencyAttributes
	    (new TransparencyAttributes(TransparencyAttributes.FASTEST,0.1f));

	// location
	float z = FEATURE_SIZE*2;
        float width=FEATURE_SIZE*2;
	float height=width*aspect;
	
	// create geometry
        QuadArray plane = new QuadArray(4,GeometryArray.COORDINATES|
					GeometryArray.TEXTURE_COORDINATE_2);

        plane.setCoordinate(0,new Point3f(-width,height,z));
        plane.setCoordinate(1,new Point3f(-width,-height,z));
        plane.setCoordinate(2,new Point3f(width,-height,z));
        plane.setCoordinate(3,new Point3f(width,height,z));
        plane.setTextureCoordinate(0,0,new TexCoord2f(0,1));
        plane.setTextureCoordinate(0,1,new TexCoord2f(0,0));
        plane.setTextureCoordinate(0,2,new TexCoord2f(1,0));
        plane.setTextureCoordinate(0,3,new TexCoord2f(1,1));

	return new Shape3D(plane,appear);
    }

    /**
     * Create a particular camera object and add it to the specified group.
     * @param i index of camera to create
     * @param group group to add object to
     * @param scale scale factor
     * @return group
     */
    public Group createCamera(int i, Group group, double scale) {
	// create camera
	Transform3D rotate = new Transform3D();
	Point3d dir = new Point3d(pp.photos[i].dir.component[0],
				  pp.photos[i].dir.component[1],
				  pp.photos[i].dir.component[2]);
	Vector3d up = new Vector3d(pp.photos[i].up.component[0],
				   pp.photos[i].up.component[1],
				   pp.photos[i].up.component[2]);
	rotate.lookAt(new Point3d(),dir,up);
	rotate.invert();
	TransformGroup rg = new TransformGroup(rotate);
	Appearance a = new Appearance();
	a.setColoringAttributes(new ColoringAttributes
	    (new Color3f(1,1,1),ColoringAttributes.FASTEST));
	Primitive b = 
	    new com.sun.j3d.utils.geometry.Box(FEATURE_SIZE,
					       FEATURE_SIZE,
					       FEATURE_SIZE,a);
	rg.addChild(b);
	// create text label
	Text2D t = new Text2D(data.photos[i].toString(),
			      new Color3f(1,1,0),
			      "monospaced",FONT_SIZE_PHOTO,Font.PLAIN);
	if (appearance_photolabel!=null)
	    t.setAppearance(appearance_photolabel);
	else {
	    appearance_photolabel=t.getAppearance();
	    appearance_photolabel.setCapability
		(Appearance.ALLOW_RENDERING_ATTRIBUTES_READ);
	    RenderingAttributes ra = new RenderingAttributes();
	    ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
	    ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
	    appearance_photolabel.setRenderingAttributes(ra);
	    PolygonAttributes pa = new PolygonAttributes();
	    pa.setCullFace(PolygonAttributes.CULL_NONE);
	    pa.setBackFaceNormalFlip(true);
	    appearance_photolabel.setPolygonAttributes(pa);
	}
	rg.addChild(t);
	// create image
	rg.addChild(createImage(data.photos[i].toString()));
	group.addChild(rg);
	// create projected lines
	NVector[] pixels=data.getPixelArray(data.photos[i],pp.getWidth());
	for (int j=0; j<pixels.length; ++j)
	    if ((pixels[j]!=null)&&(pp.points[j]!=null)) {
		NVector proj = pp.photos[i].projectPixel(pixels[j]);
		double dist = pp.points[j].subtract(pp.photos[i].pos).
		    innerProduct(proj);
		proj = proj.scaleDirect(dist*scale);
		LineArray la = new LineArray(2,GeometryArray.COORDINATES);
		la.setCoordinate(0,new Point3d(0,0,0));
		la.setCoordinate(1,new Point3d(proj.component[0],
					       proj.component[1],
					       proj.component[2]));
		Appearance a2 = new Appearance();
		Color3f c = new Color3f(colors[j%colors.length]);
		c.scale(0.5f);
		a2.setColoringAttributes(new ColoringAttributes
		    (c,ColoringAttributes.FASTEST));
		Shape3D l = new Shape3D(la,a2);
		group.addChild(l);
	    }
	return group;
    }

    /**
     * Create a particular point object and add it to the group.
     * @param i point to create
     * @param group group to add it to
     * @param scale scale factor
     * @return group
     */
    public Group createPoint(int i, Group group, double scale) {
	Appearance a = new Appearance();
	a.setColoringAttributes(new ColoringAttributes
	    (colors[i%colors.length],ColoringAttributes.FASTEST));
	Sphere s = new Sphere(FEATURE_SIZE/4,a);
	group.addChild(s);
	// text label
	Text2D t = new Text2D(data.points[i].toString(),
			      new Color3f(1,1,0),
			      "monospaced",FONT_SIZE_POINT,Font.PLAIN);
	if (appearance_pointlabel!=null)
	    t.setAppearance(appearance_pointlabel);
	else {
	    appearance_pointlabel=t.getAppearance();
	    appearance_pointlabel.setCapability
		(Appearance.ALLOW_RENDERING_ATTRIBUTES_READ);
	    RenderingAttributes ra = new RenderingAttributes();
	    ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
	    ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
	    appearance_pointlabel.setRenderingAttributes(ra);
	    PolygonAttributes pa = new PolygonAttributes();
	    pa.setCullFace(PolygonAttributes.CULL_NONE);
	    pa.setBackFaceNormalFlip(true);
	    appearance_pointlabel.setPolygonAttributes(pa);
	}
	group.addChild(t);
	return group;
    }

    /**
     * Compute the convex hull of the points and create Shape3D to represent
     * the convex hull.
     * @param offset world offset
     * @param scale scale factor
     * @return Shape3D object
     */
    public Shape3D createConvexHull(NVector offset, double scale) {
	NVector[] points = new NVector[pp.npoints];
	int npoints=0;
	for (int i=0; i<pp.npoints; ++i)
	    if (pp.points[i]!=null)
		points[npoints++] = pp.points[i];
	convexhull = new ConvexHull(points,npoints);
	Facet[] facets = convexhull.getFacets();
	System.out.println("convex hull facets: "+facets.length);
	TriangleArray ta = new TriangleArray(3*facets.length,
					     GeometryArray.COORDINATES);
	int pos=0;
	for (int i=0; i<facets.length; ++i) {
	    NVector[] vertices = facets[i].getVertices();
	    for (int j=0; j<vertices.length; ++j) {
		NVector p1 = vertices[j].add(offset).scale(scale);
		Point3d p2 = new Point3d(p1.component[0],
					 p1.component[1],
					 p1.component[2]);
		ta.setCoordinate(pos++,p2);
	    }
	}
	appearance_convexhull = new Appearance();
	appearance_convexhull.setCapability
	    (Appearance.ALLOW_RENDERING_ATTRIBUTES_READ);
	appearance_convexhull.setCapability
	    (Appearance.ALLOW_POLYGON_ATTRIBUTES_READ);
	RenderingAttributes ra = new RenderingAttributes();
	ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
	ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
	appearance_convexhull.setRenderingAttributes(ra);
	Color3f c = new Color3f(1,1,1);
	appearance_convexhull.setColoringAttributes(new ColoringAttributes
	    (c,ColoringAttributes.FASTEST));
	PolygonAttributes pa = new PolygonAttributes();
	pa.setCapability(PolygonAttributes.ALLOW_MODE_READ);
	pa.setCapability(PolygonAttributes.ALLOW_MODE_WRITE);
	pa.setCapability(PolygonAttributes.ALLOW_CULL_FACE_READ);
	pa.setCapability(PolygonAttributes.ALLOW_CULL_FACE_WRITE);
	pa.setCullFace(PolygonAttributes.CULL_NONE);
	//pa.setBackFaceNormalFlip(true);
	pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
	appearance_convexhull.setPolygonAttributes(pa);
	return new Shape3D(ta,appearance_convexhull);
    }

    /**
     * Complete to the closed hull by adding any points that are not already
     * vertices of the hull.
     */
    public void completeClosedHull() {
	NVector[] points = new NVector[pp.npoints];
	int npoints=0;
	for (int i=0; i<pp.npoints; ++i)
	    if (pp.points[i]!=null)
		points[npoints++] = pp.points[i];
	Facet[] facets = closedhull.getFacets();
	for (int i=0; i<facets.length; ++i) {
	    NVector[] vertices = facets[i].getVertices();
	    for (int j=0; j<vertices.length; ++j)
		for (int k=0; k<npoints; ++k)
		    if (points[k].equals(vertices[j])) {
			// remove it
			points[k] = points[--npoints];
			break;
		    }
	}
	System.out.println("ClosedHull: "+npoints+" points to add");
	while (npoints>0) {
	    double mindist=Double.POSITIVE_INFINITY;
	    int minpoint=0;
	    int minfacet=0;
	    // find best point and facet to work with first
	    for (int i=0; i<facets.length; ++i) {
		NVector[] vertices = facets[i].getVertices();
		NVector center = new NVector(vertices[0]);
		for (int j=1; j<vertices.length; ++j)
		    center = center.addDirect(vertices[j]);
		center = center.scaleDirect(1.0/vertices.length);
		for (int j=0; j<npoints; ++j) {
		    double dist = points[j].subtract(center).length();
		    if (dist<mindist) {
			mindist=dist;
			minpoint=j;
			minfacet=i;
		    }
		}
	    }
	    // add minpoint to facet minfacet
	    //System.out.println(points[minpoint]);
	    HalfEdge[] edges = facets[minfacet].getEdges();
	    for (int i=0; i<edges.length; ++i)
		edges[i] = edges[i].twin;
	    facets[minfacet].disconnect();
	    // create new facets
	    Facet[] newfacets = new Facet[edges.length];
	    for (int i=0; i<edges.length; ++i) {
	        newfacets[i] = new Facet(edges[i],points[minpoint]);
		for (int j=0; j<i; ++j)
		    newfacets[i].connectTo(newfacets[j]);
	    }
	    // fetch new set of facets
	    facets = closedhull.getFacets();
	    // remove point
	    points[minpoint] = points[--npoints];
	}
    }

    /**
     * Compute the closed hull of the points and create a Shape3D object to
     * represent the closed hull.
     * @param offset world offset
     * @param scale scale factor
     * @return Shape3D object
     */
    public Shape3D createClosedHull(NVector offset, double scale) {
	closedhull = new Hull(convexhull);
	completeClosedHull();
	Facet[] facets = closedhull.getFacets();
	System.out.println("closed hull facets: "+facets.length);
	TriangleArray ta = new TriangleArray(3*facets.length,
					     GeometryArray.COORDINATES);
	int pos=0;
	for (int i=0; i<facets.length; ++i) {
	    NVector[] vertices = facets[i].getVertices();
	    for (int j=0; j<vertices.length; ++j) {
		NVector p1 = vertices[j].add(offset).scale(scale);
		Point3d p2 = new Point3d(p1.component[0],
					 p1.component[1],
					 p1.component[2]);
		ta.setCoordinate(pos++,p2);
	    }
	}
	appearance_closedhull = new Appearance();
	appearance_closedhull.setCapability
	    (Appearance.ALLOW_RENDERING_ATTRIBUTES_READ);
	appearance_closedhull.setCapability
	    (Appearance.ALLOW_POLYGON_ATTRIBUTES_READ);
	RenderingAttributes ra = new RenderingAttributes();
	ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
	ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
	appearance_closedhull.setRenderingAttributes(ra);
	Color3f c = new Color3f(1,1,1);
	appearance_closedhull.setColoringAttributes(new ColoringAttributes
	    (c,ColoringAttributes.FASTEST));
	PolygonAttributes pa = new PolygonAttributes();
	pa.setCapability(PolygonAttributes.ALLOW_MODE_READ);
	pa.setCapability(PolygonAttributes.ALLOW_MODE_WRITE);
	pa.setCapability(PolygonAttributes.ALLOW_CULL_FACE_READ);
	pa.setCapability(PolygonAttributes.ALLOW_CULL_FACE_WRITE);
	pa.setCullFace(PolygonAttributes.CULL_NONE);
	//pa.setBackFaceNormalFlip(true);
	pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
	appearance_closedhull.setPolygonAttributes(pa);
	return new Shape3D(ta,appearance_closedhull);
    }

    /**
     * Create the world.
     * @return BranchGroup object
     */
    public BranchGroup createBranchGroup() {
	BranchGroup bg = new BranchGroup();

	TransformGroup objTrans = new TransformGroup();
        objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        bg.addChild(objTrans);

	// compute mean point and variances
	Stats[] spoint = new Stats[3];
	Stats[] sphoto = new Stats[3];
	for (int i=0; i<3; ++i) {
	    spoint[i] = new Stats();
	    sphoto[i] = new Stats();
	}
	for (int j=0; j<pp.npoints; ++j) 
	    if (pp.points[j]!=null)
		for (int i=0; i<3; ++i)
		    spoint[i].add(pp.points[j].component[i]);
	for (int j=0; j<pp.nphotos; ++j)
	    for (int i=0; i<3; ++i)
		sphoto[i].add(pp.photos[j].pos.component[i]);
	    
	NVector offset = new NVector(-spoint[0].getMean(),
				     -spoint[1].getMean(),
				     -spoint[2].getMean());

	double scale = 0;
	for (int i=0; i<3; ++i) {
	    if (scale<spoint[i].getVariance())
		scale=spoint[i].getVariance();
	    if (scale<sphoto[i].getVariance())
		scale=sphoto[i].getVariance();
	}
	scale = SCALE_FACTOR/Math.sqrt(scale);

	Group pr = objTrans;

	// draw points
	for (int i=0; i<pp.npoints; ++i) 
	    if (pp.points[i]!=null) {
		Transform3D translate = new Transform3D();
		NVector p = pp.points[i].add(offset);
		Vector3d point = new Vector3d(p.component[0],
					      p.component[1],
					      p.component[2]);
		point.scale(scale);
		translate.set(point);
		TransformGroup tg = new TransformGroup(translate);
		pr.addChild(createPoint(i,tg,scale));
	    }

	// draw cameras
	for (int i=0; i<pp.nphotos; ++i) {
	    Transform3D translate = new Transform3D();
	    NVector p = pp.photos[i].pos.add(offset);
	    Vector3d point = new Vector3d(p.component[0],
					  p.component[1],
					  p.component[2]);
	    point.scale(scale);
	    translate.set(point);
	    TransformGroup tg = new TransformGroup(translate);
	    pr.addChild(createCamera(i,tg,scale));
	}

	// convex hull
	pr.addChild(createConvexHull(offset, scale));

	// closed hull (not working)
	//pr.addChild(createClosedHull(offset, scale));

	return bg;
    }

    /**
     * Set the parameters to use to create display and render.
     * @param pp parameters to render
     */
    public void renderParameters(PointParameters pp) {
	this.pp = pp;
	viewTrans = universe.getViewingPlatform().getViewPlatformTransform();
	BranchGroup bg = createBranchGroup();
        bg.compile();
	universe.addBranchGraph(bg);
    }

    /**
     * Process movement key by moving in polar coordinates.
     * precondition: dir and up are unit length and orthogonal
     * postcondition: dir and up may violate precondition
     * @param event key pressed
     */
    public void processPolarChange(KeyEvent event) {
	Vector3d right = new Vector3d();
	right.cross(dir,up);
	double radius = eye.distance(center);
	switch (event.getKeyCode()) {
	    // movement keys
	case KeyEvent.VK_RIGHT:
	    right.scale(POLAR_MOVE_RATE);
	    eye.add(right);
	    eye.scale(radius/eye.distance(center));
	    break;
	case KeyEvent.VK_LEFT:
	    right.scale(POLAR_MOVE_RATE);
	    eye.sub(right);
	    eye.scale(radius/eye.distance(center));
	    break;
	case KeyEvent.VK_UP:
	    up.scale(POLAR_MOVE_RATE);
	    eye.add(up);
	    eye.scale(radius/eye.distance(center));
	    break;
	case KeyEvent.VK_DOWN:
	    up.scale(POLAR_MOVE_RATE);
	    eye.sub(up);
	    eye.scale(radius/eye.distance(center));
	    break;
	case KeyEvent.VK_PAGE_UP:
	    dir.scale(RECT_MOVE_RATE);
	    eye.add(dir);
	    break;
	case KeyEvent.VK_PAGE_DOWN:
	    dir.scale(RECT_MOVE_RATE);
	    eye.sub(dir);
	    break;
	    // rotation keys
	case KeyEvent.VK_HOME:
	    right.scale(ROTATE_RATE);
	    up.add(right);
	    break;
	case KeyEvent.VK_END:
	    right.scale(ROTATE_RATE);
	    up.sub(right);
	    break;
	}
	// make sure dir points to center
	dir = new Vector3d(center);
	dir.sub(eye);
    }

    /**
     * Process movement key by moving in rectangular coordinates.
     * precondition: dir and up are unit length and orthogonal
     * postcondition: dir and up may violate precondition
     * @param event key pressed
     */
    public void processRectangularChange(KeyEvent event) {
	Vector3d right = new Vector3d();
	right.cross(dir,up);
	switch (event.getKeyCode()) {
	    // rotation keys
	case KeyEvent.VK_RIGHT:
	    right.scale(ROTATE_RATE);
	    dir.add(right);
	    break;
	case KeyEvent.VK_LEFT:
	    right.scale(ROTATE_RATE);
	    dir.sub(right);
	    break;
	case KeyEvent.VK_DOWN:
	    up.scale(ROTATE_RATE);
	    dir.add(up);
	    break;
	case KeyEvent.VK_UP:
	    up.scale(ROTATE_RATE);
	    dir.sub(up);
	    break;
	case KeyEvent.VK_HOME:
	    right.scale(ROTATE_RATE);
	    up.add(right);
	    break;
	case KeyEvent.VK_END:
	    right.scale(ROTATE_RATE);
	    up.sub(right);
	    break;
	    // movement keys
	case KeyEvent.VK_PAGE_UP:
	    dir.scale(RECT_MOVE_RATE);
	    eye.add(dir);
	    break;
	case KeyEvent.VK_PAGE_DOWN:
	    dir.scale(RECT_MOVE_RATE);
	    eye.sub(dir);
	    break;
	case KeyEvent.VK_A:
	    up.scale(RECT_MOVE_RATE);
	    eye.add(up);
	    break;
	case KeyEvent.VK_Z:
	    up.scale(RECT_MOVE_RATE);
	    eye.sub(up);
	    break;
	case KeyEvent.VK_PERIOD:
	    right.scale(RECT_MOVE_RATE);
	    eye.add(right);
	    break;
	case KeyEvent.VK_COMMA:
	    right.scale(RECT_MOVE_RATE);
	    eye.sub(right);
	    break;
	}
    }

    /**
     * Process a key pressed to change the visibility of objects in the scene.
     * @param event key pressed
     */
    public void processVisibleFeatureChange(KeyEvent event) {
	RenderingAttributes ra;
	PolygonAttributes pa;
	switch (event.getKeyCode()) {
	case KeyEvent.VK_1:
	    ra = appearance_pointlabel.getRenderingAttributes();
	    ra.setVisible(!ra.getVisible());
	    break;
	case KeyEvent.VK_2:
	    ra = appearance_photolabel.getRenderingAttributes();
	    ra.setVisible(!ra.getVisible());
	    break;
	case KeyEvent.VK_3:
	    ra = appearance_convexhull.getRenderingAttributes();
	    pa = appearance_convexhull.getPolygonAttributes();
	    if (!ra.getVisible()) {
		pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
		ra.setVisible(true);
	    }
	    else if (pa.getPolygonMode()==PolygonAttributes.POLYGON_LINE) {
		if (pa.getCullFace()!=PolygonAttributes.CULL_NONE)
		    pa.setCullFace(PolygonAttributes.CULL_NONE);
		else {
		    pa.setCullFace(PolygonAttributes.CULL_BACK);
		    pa.setPolygonMode(PolygonAttributes.POLYGON_FILL);
		}
	    }
	    else if (pa.getPolygonMode()==PolygonAttributes.POLYGON_FILL) {
		ra.setVisible(false);
	    }
	    break;
	case KeyEvent.VK_4:
	    if (appearance_closedhull!=null) {
		ra = appearance_closedhull.getRenderingAttributes();
		pa = appearance_closedhull.getPolygonAttributes();
		if (!ra.getVisible()) {
		    pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
		    ra.setVisible(true);
		}
		else if (pa.getPolygonMode()==PolygonAttributes.POLYGON_LINE) {
		    pa.setPolygonMode(PolygonAttributes.POLYGON_FILL);
		}
		else if (pa.getPolygonMode()==PolygonAttributes.POLYGON_FILL) {
		    ra.setVisible(false);
		}
	    }
	    break;
	}
    }

    /**
     * Process keyboard input.
     * @param event key pressed
     */
    public void keyPressed(KeyEvent event) {
	switch (event.getKeyCode()) {
	case KeyEvent.VK_R:
	    System.out.println("Setting rectangular mode.");
	    polarMode=false;
	    break;
	case KeyEvent.VK_P:
	    System.out.println("Setting polar mode.");
	    polarMode=true;
	    break;
	case KeyEvent.VK_LEFT:
	case KeyEvent.VK_RIGHT:
	case KeyEvent.VK_UP:
	case KeyEvent.VK_DOWN:
	case KeyEvent.VK_PAGE_UP:
	case KeyEvent.VK_PAGE_DOWN:
	case KeyEvent.VK_HOME:
	case KeyEvent.VK_END:
	case KeyEvent.VK_A:
	case KeyEvent.VK_Z:
	case KeyEvent.VK_COMMA:
	case KeyEvent.VK_PERIOD:
	    if (polarMode)
		processPolarChange(event);
	    else
		processRectangularChange(event);
	    // make sure parameters are consistent
	    dir.normalize();
	    Vector3d error = new Vector3d(dir);
	    error.scale(up.dot(dir));
	    up.sub(error);
	    up.normalize();
	    // create new transform
	    Transform3D trans = new Transform3D();
	    Point3d at = new Point3d(eye);
	    at.add(dir);
	    trans.lookAt(eye,at,up);
	    trans.invert();
	    viewTrans.setTransform(trans);
	    break;
	case KeyEvent.VK_1:
	case KeyEvent.VK_2:
	case KeyEvent.VK_3:
	case KeyEvent.VK_4:
	case KeyEvent.VK_5:
	case KeyEvent.VK_6:
	case KeyEvent.VK_7:
	case KeyEvent.VK_8:
	case KeyEvent.VK_9:
	case KeyEvent.VK_0:
	    processVisibleFeatureChange(event);
	    break;
	default:
	    System.out.println("Key: '"+event.getKeyChar()+"' ("+
			       event.getKeyCode()+")");
	}
    }

    /**
     * Process key released event.
     * @param event key event
     */
    public void keyReleased(KeyEvent event) {
    }

    /**
     * Process key typed event.
     * @param event key event
     */
    public void keyTyped(KeyEvent event) {
    }


    /**
     * Attempt to read DE object from stream contained in specified file.
     * @param inputfile file name of file containing object
     * @return DE object if successful, null otherwise
     */
    public static DE readDEFile(String inputfile) 
	throws IOException, FileNotFoundException {
	try {
            ObjectInputStream stream =
                new ObjectInputStream(new FileInputStream(inputfile));
            DE de=(DE)stream.readObject();
	    return de;
        }
        catch (StreamCorruptedException e) {
            return null;
        }
        catch (ClassNotFoundException e) {
            throw new StreamCorruptedException(e.toString());
        }
    }

    /**
     * Read observed pixel data from specified file.
     * @param inputfile file name of file containing ascii pixel data
     * @return ObservedPoints object
     */
    public static ObservedPoints readDataFile(String inputfile) 
	throws IOException, FileNotFoundException {
	BufferedReader reader =
            new BufferedReader(new FileReader(inputfile));
        ObservedPoints data = new ObservedPoints();
        int nobservations = data.readData(reader);
        System.out.println("points: "+data.npoints);
        System.out.println("photos: "+data.nphotos);
        System.out.println("observations: "+nobservations);
        data.sort();
	return data;
    }

    /**
     * Parse a space delimited series of fields into an array of strings.
     * @param line string to parse
     * @param delim delimiter
     * @return line broken into an array of strings
     */
    public static String[] parseFields(String line, char delim) {
	String[] temp = new String[line.length()];
	int count=0;
	line=line.trim();
	while (!line.equals("")) {
	    int i = line.indexOf(delim);
	    if (i<0) {
		temp[count++]=line;
		line="";
	    }
	    else {
		temp[count++]=line.substring(0,i);
		line=line.substring(i+1).trim();
	    }
	}
	String[] result = new String[count];
	System.arraycopy(temp,0,result,0,count);
	return result;
    }
    /**
     * Parse a space delimited series of fields into an array of strings.
     * @param line string to parse
     * @return line broken into an array of strings
     */
    public static String[] parseFields(String line) {
	return parseFields(line,' ');
    }

    /**
     * Read a set of point and photo parameters from the specified file.
     * Actually, the file read from is inputfile with the extension changed
     * to ".out".
     * @param inputfile input file name
     * @param data pixel data to correlate parameters with
     * @return parameters object
     */
    public static PointParameters readParametersFile(String inputfile,
						     ObservedPoints data) 
	throws IOException, FileNotFoundException {
	// figure out file name
	String outfile;
        int d = inputfile.lastIndexOf('.');
        int s = inputfile.lastIndexOf('/');
        if ((d==-1)||(d<s))
            outfile = inputfile+".out";
        else
            outfile = inputfile.substring(0,d)+".out";
	// read data
	BufferedReader reader =
            new BufferedReader(new FileReader(outfile));
	PointParameters pp = new PointParameters(data.npoints,data.nphotos);
	// read in list of points
        for (String line=reader.readLine(); !line.equals("");
             line=reader.readLine()) {
	    String[] fields = parseFields(line);
	    int id = Integer.parseInt(fields[0]);
	    for (int i=0; i<data.npoints; ++i)
		if (id==data.points[i].id) {
		    pp.points[i] = new NVector(Double.parseDouble(fields[1]),
					       Double.parseDouble(fields[2]),
					       Double.parseDouble(fields[3]));
		    break;
		}
        }
	// read in list of photos
	int count=0;
        for (String line=reader.readLine(); !line.equals("");
             line=reader.readLine()) {
	    String[] fields = parseFields(line);
	    int id = Integer.parseInt(fields[0]);
	    for (int i=0; i<data.nphotos; ++i) 
		if (id==data.photos[i].id) {
		    pp.photos[i] = new CameraParameters();
		    pp.photos[i].pos = 
			new NVector(Double.parseDouble(fields[1]),
				    Double.parseDouble(fields[2]),
				    Double.parseDouble(fields[3]));
		    pp.photos[i].dir = 
			new NVector(Double.parseDouble(fields[4]),
				    Double.parseDouble(fields[5]),
				    Double.parseDouble(fields[6]));
		    pp.photos[i].up = 
			new NVector(Double.parseDouble(fields[7]),
				    Double.parseDouble(fields[8]),
				    Double.parseDouble(fields[9]));
		    ++count;
		    break;
		}
        }
	pp.nphotos=count;
	// width
	pp.setWidth(Double.parseDouble(reader.readLine()));

	return pp;
    }

    /**
     * Main program.  Creates an instance of this object, reads in a data
     * file specified on the command line, and displays the data.
     * @param args command line arguments
     */
    public static void main(String[] args) 
	throws IOException {

	// open graphical window
	JFrame frame = new JFrame("Graphical View (3D)");
        frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            });
        GraphicalView3D view = new GraphicalView3D();
        frame.getContentPane().setLayout(new BorderLayout());
	frame.getContentPane().add("Center", view);
        frame.pack();
        frame.setVisible(true);
	frame.setSize(400,300);

	try {
	    DE de = readDEFile(args[0]);
	    if (de!=null) {
		view.data = (ObservedPoints)de.getDataSet();
		view.renderParameters((PointParameters)de.getParameterSet());
	    }
	    else {
		view.data = readDataFile(args[0]);
		view.renderParameters(readParametersFile(args[0],view.data));
	    }
	}	
	catch (FileNotFoundException e) {
	    System.err.println("File '"+args[0]+"' not found!");
	    return;
	}
    }
}

