import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.lang.Math;
import javax.swing.*;

/**
 * The gravy presure gauge
 * @author <a href=mailto:faber@lunabase.org>Ted Faber</a>
 * @version 2.0
 */
public class Gravy extends JApplet {
    /** To lock this subset of info */
    Object lock;
    /** Previous angle of the pointer */
    double previousPointerAngle;
    /** Angle of the pointer now */
    double pointerAngle;
    /** Thread that updates the pointer */
    Thread t;
    /** The pointer */
    Polygon pointer = new Polygon(new int[] {0, -2, 0, 2, 0}, 
	    new int[] {5, 0, -40, 0, 5}, 5);
    /** X and y coordinates of the center of the applet */
    int cx, cy;
    /** name of the font to load */
    String fontName;
    /** Size of the font to load */
    int fontSize;
    /** font for text */
    Font font;
    /** Info to size text */
    FontMetrics fm;
    /** number of ticks to draw */
    int nticks;
    /** value of the first tick (for the label) */
    int tickZero;
    /** Number of units per tick (for the label) */
    int tickWidth;
    /** Gauge label */
    String label[];
    /** the pointer color */
    Color pcolor;
    /** the dial color */
    Color dcolor;
    /** The label color */
    Color lcolor;
    /** The warning color */
    Color wcolor;
    /** The background color */
    Color background;
    /** Number of ticks to label for warning */
    int nwarn;
    /** Number of degrees in the warning section */
    int warning;
    /** Update frequency in msec */
    int freq;
    /** Have we read the parameters */
    boolean parametersRead;
    /** are we stopped? */
    boolean stopped;
    /** redraw the face? - set to false before calls the animation thread
     * initiates, and reset to true in each paint() call. 
     */
    boolean mustDrawFace;
    /** scale transform - scale the 100 pixel gauge to display size and move
     * the origin to the center of the display. */
    AffineTransform scaleXform;
    /** True if the scale transform needs to be (re)calculated */
    boolean calculateScaleXform;

    /**
     * Read applet parameters, and if not present, set the defaults.
     */
    void readParameters() {
	// Hex ints for colors (foreground, background, labels, warnings, and
	// dials).
	try {
	    background = new Color(Integer.parseInt(getParameter("Background"),
						16));
	}
	catch (Exception e) {
	    background = getBackground();
	}

	try {
	    pcolor = new Color(Integer.parseInt(getParameter("PointerColor"),
						16));
	}
	catch (Exception e) {
	    pcolor = Color.black;
	}
	try {
	    lcolor = new Color(Integer.parseInt(getParameter("LabelColor"), 
						16));
	}
	catch (Exception e) {
	    lcolor = Color.black;
	}
	try {
	    dcolor = new Color(Integer.parseInt(getParameter("DialColor"),
						16));
	}
	catch (Exception e) {
	    dcolor = Color.black;
	}

	try {
	    wcolor = new Color(Integer.parseInt(getParameter("WarningColor"),
						16));
	}
	catch (Exception e) {
	    wcolor = null;
	}

	// Number of ticks
	try {
	    nticks = Integer.parseInt(getParameter("Ticks"));
	}
	catch (Exception e) {
	    nticks = 7;
	}

	// Number of ticks to color with the warning color
	try {
	    nwarn = Integer.parseInt(getParameter("WarningTicks"));
	}
	catch (Exception e) {
	    nwarn = 1;
	}

	// Labels for the dial
	try {
	    int nlabels = Integer.parseInt(getParameter("NumLabels"));
	    label = new String[nlabels];
	    for ( int i = 1; i <= nlabels; i++ ) 
		label[i-1] = getParameter("Label"+i);
	}
	catch (Exception e ) {
	    label = new String [] { "Gravy", "Pressure"};
	}

	// Font for labels
	try {
	    if ( (fontName = getParameter("FontName")) == null ) 
		fontName = "SansSerif";
	} catch (Exception e ) {
	    fontName = "SansSerif";
	}

	try {
	    fontSize = Integer.parseInt(getParameter("FontSize"));
	}
	catch (Exception e ) {
	    fontSize = 8;
	}

	// How often to update the dial (in ms)
	try {
	    freq = Integer.parseInt(getParameter("UpdateFreq"));
	}
	catch (Exception e ) {
	    freq = 33;
	}

	// Lowest tick label
	try {
	    tickZero = Integer.parseInt(getParameter("TickZero"));
	}
	catch (Exception e) {
	    tickZero = 0;
	}

	// Increment of tick labels
	try {
	    tickWidth = Integer.parseInt(getParameter("TickWidth"));
	}
	catch (Exception e) {
	    tickWidth = 10;
	}
	parametersRead = true;
    }

    /**
     * Read the parameters and set up.
     */
    public void init() {
	parametersRead = false;
	pointerAngle = -3 * Math.PI / 4;
	t = null;
	stopped = true;
	lock = new Object();
	tickZero =0; 
	tickWidth = 10;

	if ( !parametersRead) readParameters();
	font = new Font(fontName, Font.PLAIN, fontSize);
	fm = null;
	setBackground(background);

	scaleXform = AffineTransform.getScaleInstance(1,1);
	calculateScaleXform = true;
	mustDrawFace = true;

	/* If the applet is resized (if started from a command line)
	 * recalculate the scaling matrix */
	addComponentListener(new ComponentAdapter() {
	    public void componentResized(ComponentEvent e) {
		calculateScaleXform = true;
	    }
	});

	if ( t == null ) {
	    t = new Thread(new Animate());
	    t.start();
	}
    };

    /** 
     * Draw the face of the dial.  The arcs, the ticks, the tick labels and the
     * label.  There's a lot of straightforward calculation involved to build
     * the face on a given component.
     * @param g2 the graphics context
     */
    public void drawFace(Graphics2D g2) {
	double angle = - 5 * Math.PI /4;
	int warning = (int) ((180/ Math.PI) * 3 * Math.PI/2 / (nticks -1 )) 
	    * nwarn;

	g2.clearRect(-50, -50, 100, 100);
	g2.setFont(font);
	if ( fm == null ) fm = g2.getFontMetrics();
	// These are the actual widths on a 100 pixel grid
	final int inner_w = 58 ;
	final int inner_h = 58 ;
	final int outer_w = 66;
	final int outer_h = 66;
	// These are the offsets into that grid
	final int inner_xo = -29;
	final int inner_yo = -29;
	final int outer_xo = -33;
	final int outer_yo = -33;

	// Color the warning areas
	if ( wcolor != null ) {
	    g2.setColor(wcolor);
	    g2.fillArc(outer_xo,outer_yo,outer_w,outer_h, -45, warning);
	    g2.setColor(background);
	    g2.fillArc(inner_xo,inner_yo,inner_w,inner_h, -135, -270);
	}

	// Now the dials and ticks
	g2.setColor(dcolor);
	g2.drawArc(outer_xo,outer_yo,outer_w,outer_h,-135, -270);
	g2.drawArc(inner_xo,inner_yo,inner_w,inner_h,-135, -270);
	for ( int i = 0; i < nticks; i ++ ) {
	    final int ntickx = (int) (inner_w/2 * Math.cos(angle));
	    final int nticky = (int) (inner_h/2 * Math.sin(angle));
	    final int ftickx = (int) (outer_w/2 * Math.cos(angle));
	    final int fticky = (int) (outer_h/2 * Math.sin(angle));
	    final int ltickx = (int) ((outer_w/2 + 10 ) * Math.cos(angle));
	    final int lticky = (int) ((outer_h/2 + 10 ) * Math.sin(angle));
	    String tlabel = Integer.toString(tickZero + i * tickWidth);

	    angle += 3 * Math.PI/2 / (nticks -1 );
	    g2.setColor(dcolor);
	    g2.drawLine(ntickx, nticky,ftickx, fticky);
	    g2.setColor(lcolor);
	    g2.drawString(tlabel, ltickx - fm.stringWidth(tlabel)/2, 
		lticky+ (int) fm.getHeight()/4); 
	}

	// And the dial label
	for ( int i = 0 ; i < label.length; i++ ) {
	    g2.drawString(label[i],  - fm.stringWidth(label[i])/2, 
			  - inner_w /4 + i * fm.getHeight()/2);
	}
    }

    /** 
     * Redraw the component.  Best case, just rotate the pointer.  If the JVM
     * has called paint, redo the dial, too.  All the drawing is of a 100 pixel
     * gauge, scaled appropriately.  All the rotation of the pointer also uses
     * transforms.
     * @param g the context
     */
    public void paint(Graphics g) {
	Graphics2D g2 = (Graphics2D) g;

	/* If the base scaling/translation transform hasn't been established
	 * (or has been cleared) establish it.  This must be done here, because
	 * the applet will not have a Dimension until it is drawn. */
	if ( calculateScaleXform ) {
	    final Dimension d = getSize();
	    scaleXform = 
		AffineTransform.getScaleInstance((double) d.width/100.0, 
						 (double) d.height/100.0);
	    scaleXform.translate(50, 50);
	    calculateScaleXform = false;
	}

	g2.setTransform(scaleXform);

	if (mustDrawFace) drawFace(g2);

	g2.setColor(background);
	g2.setXORMode(pcolor);

	if ( !mustDrawFace ) {
	    g2.rotate(previousPointerAngle);
	    g2.fillPolygon(pointer);
	    g2.rotate(-previousPointerAngle);
	}
	g2.rotate(pointerAngle);
	g2.fillPolygon(pointer);
	g2.rotate(-pointerAngle);
	g2.setPaintMode();
	previousPointerAngle = pointerAngle;
	mustDrawFace = true;
    }

    /**
     * Change the pointer angle by a small random amount.  Only change it once
     * per update, so if the old and current positions differ, just return.
     * @return true if the angle has changed
     */
    boolean updateAngle() {
	final double amax = 3 * Math.PI/4;
	final double amin = -amax;

	if (pointerAngle != previousPointerAngle ) return false;

	if ( Math.random() < 0.5 ) 
	    pointerAngle += Math.PI/25 * Math.random();
	else
	    pointerAngle -= Math.PI/25 * Math.random();
	if ( pointerAngle > amax ) pointerAngle = amax;
	if ( pointerAngle < amin ) pointerAngle = amin;
	return true;
    }

    /**
     * A thread will be created to run this class's run method.  The method
     * updates the pointer every freq ms unless the stopped variable is true.
     * Access to stopped is synchronized by the lock that is used for the
     * timing.
     */
    class Animate implements Runnable {
	public void run () {
	    Thread me = Thread.currentThread();

	    synchronized (lock) {
		while (t == me) {
		    try {
			lock.wait( stopped ? 0 : freq);
			if (updateAngle()) { 
			    mustDrawFace = false; 
			    repaint();
			}
		    } catch (InterruptedException e) { }
		}
	    }
	    return;
	}
    };

    /**
     * Start the thread updating the pointer.
     */
    public void start() {
	synchronized (lock ) {
	    stopped = false;
	    mustDrawFace = true;
	    lock.notifyAll();
	}
    }

    /**
     * Stop the thread updating the pointer
     */
    public void stop() {
	synchronized (lock) {
	    stopped = true;
	    lock.notifyAll();
	}
    }

    /**
     * Force the thread updating the pointer to exit.
     */
    public void destroy() {
	synchronized (lock ) {
	    if ( t != null ) t = null;
	    stopped = true;
	    lock.notifyAll();
	}
    }
   
    /**
     * Simple command line invocation, for debugging.
     */
    static public void main(String argv[]) {
	JFrame f = new JFrame("Gravy");
	Gravy g = new Gravy();
	int x = 100, y=100;

	if ( argv.length > 0 ) x = Integer.parseInt(argv[0]);
	if ( argv.length > 1 ) y = Integer.parseInt(argv[1]);

	if ( x < 0 ) x = 100;
	if ( y < 0 ) y = 100;

	f.setVisible(true);
	// get the insets (f needs to be visible for that) and resize the frame
	// so that the dial has the requested area.
	Insets in = f.getInsets();
	f.setSize(x + in.left + in.right, y + in.top + in.bottom);
	f.setLocation(10,10);
	g.background = Color.white;
	g.init();
	f.setContentPane(g);
	f.addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {
		System.exit(0);
	    }
	});
	f.validate();
	g.start();
    }
}
