package psplot;

import java.awt.*;
import java.applet.*;

import real.*;
import psplot.*;

/* There is exactly one abstract method here:
	
	void draw() 
	
  It draws the picture onto the offscreen image. */

/** 

The class PlotCanvas is the basic class in all these floating point graphics
routines.  It is instantiated as the child of an applet, its parent.
There are several differences between the usual canvases availble in
the java AWT library: (1) it uses floating point
numbers as coordinates; the coordinate system on the page is
set with the SetCorners method, which sets the coordinates of the corners of
the canvas.  (2) By default, it uses an offscreen image to draw to
before putting anything on the screen.  Furthermore, it overloads
the default update() and paint() so as to avoid flicker.
The reason for all of this is that is is simple to add
components which involve animation or interactive motion.
(3) Graphics drawing commands imitate PostScript.
The basic class of things drawn is `PlotPath'.  After one is instantiated,
we build it with methods `moveto', `lineto', etc.
and draw it with `stroke', `fill'.  Coordiante systems are
changed with commands `scale', `rotate', `translate'.
There is no true graphics state, however; colours are
used in the call directly. 

The class `PlotCanvas' is abstract.  Extensions which are not abstract
must have a method draw(), which draws paths on the offscreen image.

Examples of usage are in various Applet files in this directory.

Programs using this package must also use
the `real' package, which has RealPoints and 
a few utilities for displaying real numbers.

*/

public abstract class PlotCanvas extends Canvas implements Corners {
	double m[][];
	double t[];
	final static double INFTY = 32783.0;
	
	Image img;
	Graphics gfx;
	public int width = 0, height = 0;
	public Applet parent;

	public PlotCanvas(Applet app) {
		// Java 1.1: this(app, app.getBounds().width, app.getBounds().height);
		this(app, app.bounds().width, app.bounds().height);
	}

	/** The variables w and h determine the canvas size in pixels.
	There is a minor problem in aranging the canvas in the applet,
	that often causes things to be offset vertically
	by five pixels, so an undesired boundary strip appears at the top. */

	public PlotCanvas(Applet app, int w, int h) {
		parent = app;
		setBackground(Color.white);
		width = w;
		height = h;
		
		m = new double[2][2];
		t = new double[2];
		m[0][0] = 1.0; m[0][1] =  0.0;
		m[1][0] = 0.0; m[1][1] = -1.0; 
		t[0] = 0.0;
		t[1] = (double) h;
		
		img = parent.createImage(width, height);
		gfx = img.getGraphics();
		resize(width, height);
		clear();
	}
	
	void setCorners(int w, int h, 
			double llx, double lly, double urx, double ury) {
		width = w; height = h;
		setCorners(llx, lly, urx, ury);
	}
	
	/** The point
	(llx, lly) becomes lower left, (urx, ury) upper right. */
	public void setCorners(double llx, double lly, double urx, double ury) {
		m[0][0] = (double) width/(urx - llx);
		m[0][1] = 0.0;
		m[1][1] = - (double) height/(ury - lly);
		m[1][0] = 0.0;
		t[0] = - m[0][0]*llx;
		t[1] = - m[1][1]*ury;
	}

	/** Allows a margin. */
	public void setCorners(double llx, double lly, double urx, double ury, 
							int xmargin, int ymargin) {
		double dx = (double) xmargin*(urx - llx)/(double) (width - 2*xmargin);
		double dy = (double) ymargin*(ury - lly)/(double) (height - 2*ymargin);
		setCorners(llx-dx, lly-dy, urx+dx, ury+dy);
	}
	
	/** A RealRectangle holds four real points.
	This returns the floating point corners of the pixel at upper left. 
	This can be used to determine true pixel size, often important 
	for knowing how fine-grained to do plots at. */

	public RealPolygon pixel() {
		RealPoint b[] = new RealPoint[4];
		b[0] = toRealPoint(0, 0);
		b[1] = toRealPoint(1, 0);
		b[2] = toRealPoint(1, 1);
		b[3] = toRealPoint(0, 1);
		return(new RealPolygon(b));
	}

	/** Returns a pixel of width n,centered at P */
	public RealPolygon pixel(RealPoint P, int n) {
		Point p = toPoint(P);
		p.x -= (n/2); p.y -= (n/2);
		RealPoint b[] = new RealPoint[4];
		b[0] = toRealPoint(p);
		p.x += n;
		b[1] = toRealPoint(p);
		p.y += n;
		b[2] = toRealPoint(p);
		p.x -= n;
		b[3] = toRealPoint(p);
		return(new RealPolygon(b));
	}

	/** Locates corners: ll, lr, ur, ul. */
	public RealPolygon vertexBoundary() {
		RealPoint b[] = new RealPoint[4];
		b[0] = toRealPoint(0, height);
		b[1] = toRealPoint(width, height);
		b[2] = toRealPoint(width, 0);
		b[3] = toRealPoint(0, 0);
		return(new RealPolygon(b));
	}
	
	/** m = margin */
	public RealPolygon vertexBoundary(int m) {
		RealPoint b[] = new RealPoint[4];
		b[0] = toRealPoint(m, height-m);
		b[1] = toRealPoint(width-m, height-m);
		b[2] = toRealPoint(width-m, m);
		b[3] = toRealPoint(m, m);
		return(new RealPolygon(b));
	}
	
	/** m = margins: left, bottom, right, top */
	public RealPolygon vertexBoundary(int m0, int m1, int m2, int m3) {
		RealPoint b[] = new RealPoint[4];
		b[0] = toRealPoint(m0, height-m1);
		b[1] = toRealPoint(width-m2, height-m1);
		b[2] = toRealPoint(width-m2, m3);
		b[3] = toRealPoint(m0, m3);
		return(new RealPolygon(b));
	}
	
		
	/** Paints a blank canvas onto the offscreen image. */
	public void clear() {
		gfx.setColor(getBackground());
		gfx.fillRect(0, 0, width, height);
		gfx.setColor(getForeground());
	}
	
	/** Reverses the normal ordering of paint() and 
	update() for flicker-free performance. */
	public void paint(Graphics g) {
		update(g);
	}

	/** Clears the offscreen image, 
	draws into it, and blips it onto the screen. 
	If there are extras involved in drawing 
	this must be overridden.  For example, in animations it often pays not to
	draw here, because diferent backgrounds should be saved. */

	public void update(Graphics g) {
		// clear the image
		clear();
		// draw the figure into the image
		draw();
		// map the image onto the screen:
		g.drawImage(img, 0, 0, null);
	}
	
	/** Draws paths onto the offscreen image. */
	public abstract void draw();
	
	/** A utility for animation.  Interpolates linearly,
	gives (1-t)x + ty. */
	public static double interp(double t, double x, double y) {
		return((1.0-t)*x + t*y);
	}
	
	/** Returns pixel with coordinates P. 
	Coordinates can be be infinity but not NaN. */
	public Point toPoint(RealPoint p) {
		return(toPoint(p.x, p.y));
	}
	
	/** Returns pixel with coordinates (x, y). 
	Coordinates can be be infinity but not NaN. */
	public Point toPoint(double x, double y) {
		double x0 = m[0][0]*x + m[0][1]*y + t[0];
		double y0 = m[1][0]*x + m[1][1]*y + t[1];
		int x1, y1;
		// never let it wrap around
		if (-INFTY < x0 && x0 < INFTY) {
			x1 = (int) Math.round(x0);
		}
		else if (x0 <= -INFTY || x0 == Double.NEGATIVE_INFINITY) {
			x1 = (int) -INFTY;
		}
		else {
			x1 = (int) INFTY;
		}
		if (-INFTY < y0 && y0 < INFTY) {
			y1 = (int) Math.round(y0);
		}
		else if (y0 <= -INFTY || y0 == Double.NEGATIVE_INFINITY) {
			y1 = (int) -INFTY;
		}
		else {
			y1 = (int) INFTY;
		}
		Point q = new Point(x1, y1);
		return(q);
	}

	/** Pixels to floating point coordinates. */
	public RealPoint toRealPoint(Point p) {
		return(toRealPoint(p.x, p.y));
	}

	/** Pixels to floating point coordinates. */
	// pixels to coords
	// p = m*q + t, q = m^{-1}(p - t)
	public RealPoint toRealPoint(int x, int y) {
		double x0 = (double) x - t[0];
		double y0 = (double) y - t[1];
		double d = m[0][0]*m[1][1] - m[0][1]*m[1][0];
		double x1 =  m[1][1]*x0 - m[0][1]*y0;
		double y1 = -m[1][0]*x0 + m[0][0]*y0;
		x1 /= d;
		y1 /= d;
		RealPoint q = new RealPoint(x1, y1);
		return(q);
	}

	/** Real coordinates to pixels, but in real numbers. */
	RealPoint toFinePoint(RealPoint p) {
		return(toFinePoint(p.x, p.y));
	}
	
	/** Real coordinates to pixels, but in real numbers. */
	RealPoint toFinePoint(double x, double y) {
		double x0 = m[0][0]*x + m[0][1]*y + t[0];
		double y0 = m[1][0]*x + m[1][1]*y + t[1];
		RealPoint q = new RealPoint(x0, y0);
		return(q);
	}

	/** Returns true if visible on the screen.
	Returns false if x or y is NaN. */ 
	// since all comparisons with NaN are false
	public boolean isVisible(double x, double y) {
		Point p = toPoint(x, y);
		if (0 <= p.x && p.x < width 
			&& 0 <= p.y && p.y < height) {
			return(true);
		}
		else {
			return(false);
		}
	}

	/** Returns true if visible on the screen.
	Returns false if x or y is NaN. */ 
	public boolean isVisible(RealPoint p) {
		return(isVisible(p.x, p.y));
	}

	/** Returns true if visible on the screen. */ 
	public boolean isVisible(Point p) {
		// doesn't use xmin etc.
		if (0 <= p.x && p.x < width 
			&& 0 <= p.y && p.y < height) {
			return(true);
		}
		else {
			return(false);
		}
	}

	/** Follows PostScript model. */
	// but using column vectors
	// all of these postmultiply the CTM by the relevant matrix 

	public void scale(double a, double b) {
		m[0][0] *= a;
		m[1][0] *= a;
		m[0][1] *= b;
		m[1][1] *= b;
	}

	/** Follows PostScript model. */
	public void translate(double a, double b) {
		t[0] += m[0][0]*a + m[0][1]*b;
		t[1] += m[1][0]*a + m[1][1]*b;
	}
	
	/** Follows PostScript model. */
	// multiply on the right by rotation matrix
	public void rotate(double theta) {
		double c = Math.cos(theta);
		double s = Math.sin(theta);
		
		double x00 =  m[0][0]*c + m[0][1]*s;
		double x01 = -m[0][0]*s + m[0][1]*c;
		double x10 =  m[1][0]*c + m[1][1]*s;
		double x11 = -m[1][0]*s + m[1][1]*c;
		m[0][0] = x00;
		m[0][1] = x01;
		m[1][0] = x10;
		m[1][1] = x11;
	}

	// z = magnification, t = new origin
	public void zoom(double z, RealPoint t) {
		Point s;
		RealPoint r;

		// calculate new matrix
		scale(z, z);
		s = new Point(width/2, height/2);
		r = toRealPoint(s);
		translate(r.x - t.x, r.y - t.y);
	}

	public void zoom(double mx, double my, RealPoint t) {
		Point s;
		RealPoint r;

		// calculate new matrix
		scale(mx, my);
		s = new Point(width/2, height/2);
		r = toRealPoint(s);
		translate(r.x - t.x, r.y - t.y);
	}
	
	public void setFont(Font f) {
		gfx.setFont(f);
	}
	
	public void drawString(String s, RealPoint Q, Color c) {
		Color tmp = gfx.getColor();
		gfx.setColor(c);
		Point P = toPoint(Q);
		gfx.drawString(s, P.x, P.y);
		gfx.setColor(tmp);
	}
	
	public void setColor(Color c) {
		gfx.setColor(c);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, double x, double y, Color c) {
		Color tmp = gfx.getColor();
		gfx.setColor(c);
		Point P = toPoint(x, y);
		gfx.drawString(s, P.x, P.y);
		gfx.setColor(tmp);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, RealPoint Q) {
		Point P = toPoint(Q);
		gfx.drawString(s, P.x, P.y);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, double x, double y) {
		Point P = toPoint(x, y);
		gfx.drawString(s, P.x, P.y);
	}
	
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(Real.toString(m[0][0], 6, 8));
		sb.append(" ");
		sb.append(Real.toString(m[0][1], 6, 8));
		sb.append(" \n");
		sb.append(Real.toString(m[1][0], 6, 8));
		sb.append(" ");
		sb.append(Real.toString(m[1][1], 6, 8));
		sb.append(" \n");
		sb.append(Real.toString(t[0], 6, 8));
		sb.append(" ");
		sb.append(Real.toString(t[1], 6, 8));
		sb.append(" \n");
		return(new String(sb));
	}
	

}
