// AstroClock.java
// $Id: AstroClock.java,v 1.4 2004/08/17 09:48:21 matthew Exp $
// (C) 2004 by Matthew Arcus

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

import java.util.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.awt.image.*;
import java.applet.*;
import java.text.*;

public class AstroClock extends Applet implements Runnable {
    private volatile Thread thread;       // The thread that displays clock
    private Font clockFaceFont;           // Font for number display on clock

    private Color darkGreen = new Color(0x6400);
    private Color brass = new Color(0xFF8C00);
    private Color lightRed = new Color(0xFF6A6A);
    private Color dial1Back = lightRed;
    private Color dial2Back = brass.brighter();
    private Color dial3Back = brass;
    private Color centerBack = new Color(0xEEDFCC);
    private Color dial1Text = Color.black;
    private Color dial2Text = Color.black;
    private Color dial3Text = Color.black;
    private Color handColor = Color.darkGray;
    private Color numberColor = Color.darkGray;
    private Color lhandColor = Color.gray;
    private Color sunColor = Color.yellow;
    private Color moonLightColor = Color.white;
    private Color moonDarkColor = Color.black;
    private Color gridColor = Color.lightGray;

    private int radius = 250;
    private int border = 20;
    private int xcenter = radius+border, ycenter = radius+border; // Center position
    private int xs,ys;
    private int bandwidth = 35;
    private int innerdial = radius - 3 * bandwidth;
    private int shand = innerdial-20, mhand = innerdial-40, hhand = innerdial-60;
    private int lhand = radius;
    private int width = 2 * (radius+border);
    private int height = 2 * (radius+border);
    private int sunradius = 10;
    private int moonradius = 10;
    private int lastsecond = -1;
    private int lastminute = -1;
    private BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    private double range(double x, double y) {
	double r = Math.IEEEremainder(x,y);
	while (r < 0) r += y;
	// Don't really need the next line, but what the heck?
	while (r >= y) r -= y;
	return r;
    }

    // Convert to roman numerals, i should be less than 40 or so.
    private String roman(int i) {
	String result = "";
	while (i >= 10) {
	    i -= 10;
	    result += "X";
	}
	if (i == 9) {
	    result += "IX";
	} else {
	    if (i >= 5) {
		result += "V";
		i -= 5;
	    }
	    if (i == 4) {
		result += "IV";
	    } else {
		while (i > 0) {
		    result += "I";
		    i--;
		}
	    }
	}
	return result;
    }
    private String arabic (int i) {
	return Integer.toString(i);
    }
    private String zodiac (int i) {
	String s[] = { "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
		       "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"};
	return s[i-1];
    }

    private void centerString(Graphics2D g2d, String s, int x, int y)
    {
	FontRenderContext frc = g2d.getFontRenderContext();
	Font f = g2d.getFont();
	Rectangle2D r = f.getStringBounds(s,frc);
	g2d.drawString(s,x - (int)(r.getWidth()/2.0),y);
    }

    public void init() {
        clockFaceFont = new Font("Serif", Font.PLAIN, 14);
        resize(width, height); // Set clock window size
    }

    private void drawCircle(Graphics g, int x, int y, int radius) {
	g.drawArc(x-radius,y-radius,2*radius,2*radius,0,360);
    }
    private void fillCircle(Graphics g, int x, int y, int radius) {
	g.fillArc(x-radius,y-radius,2*radius,2*radius,0,360);
    }
    // AstronomyWorld tells us that new moons are at JD:
    // 2449128.59 + 29.53058867n
    // ie. mean moon phase at JD jd is (jd - 2449128.59)/29.53058867 mod 1
    // and jd = j2000 + 2451545.0
    // Returns the position of the moon, relative to the sun.
    // Only 1st order approximation so not very precise, about +-0.25 days.

    private double moonPhase (double j2000) {
	double month = 29.53058867; // Synodic month in days
	//month = 0.001; // Test moon motion with this!
	return range((j2000+2416.41)/month,1.0);
    }

    private void drawMoon(Graphics2D g2d, int x, int y, int r, double p) {
	Color left, right;
	if (p < 0.5) {
	    left = moonDarkColor;
	    right = moonLightColor;
	} else {
	    left = moonLightColor;
	    right = moonDarkColor;
	}
	g2d.setColor(left);
	g2d.fillArc(x-r,y-r,2*r,2*r,90,180);
	g2d.setColor(right);
	g2d.fillArc(x-r,y-r,2*r,2*r,270,180);

	// Now draw the central bit - dark if near to the sun
	double z = (p < 0.5) ? p : p -0.5;
	int w = (int)Math.round(Math.abs(r * Math.sin(2 * Math.PI * (z-0.25))));
	if (p < 0.25 || p >= 0.75) {
	    g2d.setColor(moonDarkColor);
	} else {
	    g2d.setColor(moonLightColor);
	}
	g2d.fillArc(x-w,y-r,2*w,2*r,0,360);
    }

    private void drawit(Graphics2D g, boolean force) {
        Calendar now = Calendar.getInstance();
        int s = now.get(Calendar.SECOND);

	// Only update if the second has changed
	if (!force && s == lastsecond) {
	    return;
	}
	lastsecond = s;

        int m = now.get(Calendar.MINUTE);
	int h = now.get(Calendar.HOUR_OF_DAY);
	// Only update backing image if minute has changed.
	if (m != lastminute || s % 10 == 0) {
	    lastminute = m;
	    int y = now.get(Calendar.YEAR);
	    int d = now.get(Calendar.DAY_OF_YEAR);
	    int off = now.get(Calendar.DST_OFFSET) + now.get(Calendar.ZONE_OFFSET);

	    // gmt in hours
	    double gmt = h + (m + (s + -off/1000.0)/60.0)/60.0;
	    // Compute days since J2000
	    // Day number is Jan 1st = 1
	    // Need to check this!
	    double j2000 = (y - 2000) * 365 + (y - 2000 + 3)/4 + d - 1.5 + gmt/24.0;
	    // gmst in hours
	    double gmst = range(18.697374558 + 24.06570982441908 * j2000,24.0);
	    // difference, in hours
	    double dmst = gmst - gmt;
	    double phase = moonPhase(j2000);
	    //System.out.printf("%d %d %d %f %f %f\n", off, y, d, j2000 + 51544.5, gmst,gmt);
	    Graphics2D g2d = image.createGraphics();
	    g2d.setColor(this.getBackground());
	    g2d.fillRect(0,0,width,height);

	    g2d.setFont(clockFaceFont);
	    g2d.setStroke(new BasicStroke(2));
	    // Draw the circle and numbers
	    g2d.setColor(dial1Back);
	    fillCircle(g2d,xcenter,ycenter,radius);
	    g2d.setColor(dial2Back);
	    fillCircle(g2d,xcenter,ycenter,radius-bandwidth);
	    g2d.setColor(dial3Back);
	    fillCircle(g2d,xcenter,ycenter,radius-2*bandwidth);
	    g2d.setColor(centerBack);
	    fillCircle(g2d,xcenter,ycenter,radius-3*bandwidth);

	    g2d.setColor(gridColor);
	    drawCircle(g2d,xcenter,ycenter,radius);
	    drawCircle(g2d,xcenter,ycenter,radius-bandwidth);
	    drawCircle(g2d,xcenter,ycenter,radius-2*bandwidth);
	    drawCircle(g2d,xcenter,ycenter,radius-3*bandwidth);
	    // Draw the numbers on the dials
	    AffineTransform old = g2d.getTransform();
	    // This should be the current sidereal offset
	    g2d.rotate(-Math.PI * dmst/12.0, xcenter, ycenter);
	    AffineTransform t1 = g2d.getTransform();
	    for (int i = 1; i <= 24; i++) {
		g2d.setTransform(t1);
		g2d.rotate(i*Math.PI/12.0, xcenter, ycenter);
		g2d.setColor(gridColor);
		g2d.drawLine(xcenter,ycenter-radius,xcenter,ycenter-radius+bandwidth);
		g2d.setColor(dial1Text);
		int hour = i - 12;
		if (hour < 0) hour += 24;
		g2d.drawString(arabic(hour), xcenter+5, ycenter-radius+2*bandwidth/3);
	    }
	    for (int i = 1; i <= 24; i++) {
		g2d.setTransform(old);
		g2d.rotate(i*Math.PI/12.0, xcenter, ycenter);
		g2d.setColor(gridColor);
		g2d.drawLine(xcenter,
			     ycenter-radius+bandwidth,
			     xcenter,
			     ycenter-radius+2*bandwidth);
		g2d.setColor(dial2Text);
		centerString(g2d,roman((i<=12)?i:i-12), xcenter, ycenter-radius+7*bandwidth/4);
	    }
	    // Now the zodiac, rotated by the sidereal time
	    // When sidereal time == solar time we are at the spring
	    // equinox, ie. at the beginning of Aries.
	    g2d.setTransform(old);
	    g2d.rotate(Math.PI * gmst/12.0, xcenter, ycenter);
	    AffineTransform t2 = g2d.getTransform();
	    for (int i = 1; i <= 12; i++) {
		g2d.setTransform(t2);
		g2d.rotate(-i*Math.PI/6.0, xcenter, ycenter);
		g2d.setColor(gridColor);
		g2d.drawLine(xcenter,
			     ycenter-radius+2*bandwidth,
			     xcenter,
			     ycenter-radius+3*bandwidth);
		g2d.setColor(dial3Text);
		g2d.setTransform(t2);
		g2d.rotate(-(i-0.5)*Math.PI/6.0, xcenter, ycenter);
		centerString(g2d, zodiac(i), xcenter, ycenter-radius+11*bandwidth/4);
	    }

	    for (int i = 1; i <= 12; i++) {
		g2d.setTransform(old);
		g2d.rotate(i*Math.PI/6.0, xcenter, ycenter);
		g2d.setColor(numberColor);
		fillCircle(g2d,xcenter, ycenter-radius+15*bandwidth/4, 5);
	    }
	    g2d.setTransform(old);

	    //g2d.setStroke(new BasicStroke(25));
	    //g2d.setColor(Color.red);
	    //drawCircle(g2d,xcenter + 50, ycenter + 50, 150);

	    int xsun, ysun, xmoon, ymoon;

	    // Set position of the ends of the hands
	    xsun = (int) (Math.cos(((gmt+12)*15) * Math.PI / 180 - Math.PI / 2) * radius
			  + xcenter);
	    ysun = (int) (Math.sin(((gmt+12)*15) * Math.PI / 180 - Math.PI / 2) * radius
			  + ycenter);
	    xmoon = (int) (Math.cos(((gmt+12-phase*24)*15) * Math.PI / 180 - Math.PI / 2) * radius
			  + xcenter);
	    ymoon = (int) (Math.sin(((gmt+12-phase*24)*15) * Math.PI / 180 - Math.PI / 2) * radius
			  + ycenter);
    
	    // Draw date and hands
	    g2d.setStroke(new BasicStroke(1));
	    g2d.setColor(lhandColor);
	    g2d.drawLine(xcenter, ycenter, xsun, ysun);
	    g2d.drawLine(xcenter, ycenter, xmoon, ymoon);
	    g2d.setColor(sunColor);
	    fillCircle(g2d,xsun,ysun,sunradius);

	    // Draw with a rotation to get the direction of the crescent right
	    g2d.rotate(Math.PI * (gmt+12-phase*24) / 12.0, xcenter, ycenter);
	    drawMoon(g2d,xcenter,ycenter-radius,moonradius,phase);
	    g2d.setTransform(old);

	    // Minutes and hours as radian angles
	    double mins = Math.PI * (m + s/60.0) / 30.0;
	    double hours = Math.PI * (h + (m + s/60.0)/60.0) / 6.0;
	    int xm = (int)(Math.sin(mins) * mhand + xcenter);
	    int ym = (int)(-Math.cos(mins) * mhand + ycenter);
	    int xh = (int)(Math.sin(hours) * hhand + xcenter);
	    int yh = (int)(-Math.cos(hours) * hhand + ycenter);
	    g2d.setColor(handColor);
	    g2d.drawLine(xcenter, ycenter-1, xm, ym);
	    g2d.drawLine(xcenter-1, ycenter, xm, ym);
	    g2d.drawLine(xcenter, ycenter-1, xh, yh);
	    g2d.drawLine(xcenter-1, ycenter, xh, yh);
	}
	// And update the screen
	g.drawImage(image,0,0,this);

        g.setColor(Color.gray);
	double secs = Math.PI * s / 30;
	int xs = (int)(Math.sin(secs) * shand + xcenter);
        int ys = (int)(-Math.cos(secs) * shand + ycenter);
        g.drawLine(xcenter, ycenter, xs, ys);
    }

    public void update(Graphics g) {
	drawit((Graphics2D)g, false);
    }

    public void paint(Graphics g) {
	drawit((Graphics2D)g, true);
    }

    public void start() {
        thread = new Thread(this);
        thread.start();
    }

    public void stop() {
        thread = null;
    }

    public void run() {
        Thread me = Thread.currentThread();
        while (thread == me) {
            try {
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
            }
            repaint();
        }
    }

    public String getAppletInfo() {
        return "Astronomical Clock\n";
    }
  
}
