/* sunlight.c
 * 
 * To compile:
 *	cc -o sunlight sunlight.c -lglut -lGLU -lGL -lXmu -lXext -lX11 -lm
 *
 * Usage: sunlight
 *
 * Requires: globe.raw
 *
 * This program demonstrates the sun lighting the earth, displayed
 * either as a globe or a flat map.  The time of day and day of year
 * can be adjusted interactively, and the globe can be viewed from
 * any angle.
 *
 * The lighting is done using OpenGL's lighting.  The flat map is
 * lighted by using the normals of globe.  This is a rather unique
 * use of normals, making a flat surface lit as if it were round.
 *
 * The left mouse adjusts the globe orientation, the time of day, or
 * the time of year.  The right mouse activates a menu.
 *
 * Chris Schoeneman - 9/6/98
 * crs23@bigfoot.com
 * http://www.bigfoot.com/~crs23/
 *        
 */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <GL/glut.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

/* menu constants */
#define MENU_SHOW_GLOBE		1
#define MENU_SHOW_MAP		2
#define MENU_ADJUST_GLOBE	3
#define MENU_ADJUST_DAY		4
#define MENU_ADJUST_TIME	5
#define MENU_QUIT		6

static int		mapMode = 0;
static int		adjustMode = 0;
static GLdouble		aspectRatio = 1.0;
static double		timeOfDay = 0.0;
static double		timeOfYear = 0.0;

/*
 * sun direction functions
 */

static const double	radPerDeg = M_PI / 180.0;
static const double	radPerHour = M_PI / 12.0;
static const double	siderealHoursPerHour = 1.002737908;
static const double	epoch = 2415020.0;

static double		getGreenwichSideral(double julianDay)
{
  double jdMidnight;
  double dayFraction;
  double T;		/* time (fractions of a century) */
  double greenwichMidnight;

  /* true position requires sidereal time of midnight at prime meridian.
     get midnight of given julian day (midnight has decimal of .5). */
  jdMidnight = floor(julianDay);
  if (julianDay - jdMidnight >= 0.5) jdMidnight += 0.5;
  else jdMidnight -= 0.5;

  T = (jdMidnight - epoch) / 36525.0;

  /* get fraction of a day */
  dayFraction = (julianDay - jdMidnight) * 24.0;

  /* get Greenwich midnight sidereal time (in hours) */
  greenwichMidnight =
		fmod((0.00002581 * T + 2400.051262) * T + 6.6460656, 24.0);

  /* return Greenwich sidereal time */
  return radPerHour * (greenwichMidnight + dayFraction * siderealHoursPerHour);
}

static void		getTruePosition(double julianDay,
					float latitude, float longitude,
					double sx, double sy, double sz,
					float pos[3])
{
  double tx, ty, tz;
  double localSidereal;

  /* convert to radians */
  latitude *= radPerDeg;
  longitude *= radPerDeg;

  /* get local sidereal time */
  localSidereal = getGreenwichSideral(julianDay) - longitude;

  /* rotate around polar axis (y-axis) by local sidereal time */
  tx = sx * cos(localSidereal) - sz * sin(localSidereal);
  ty = sy;
  tz = sz * cos(localSidereal) + sx * sin(localSidereal);

  /* rotate by latitude to local position */
  pos[0] = (float)tx;
  pos[1] = (float)(ty * cos(latitude) - tz * sin(latitude));
  pos[2] = (float)(tz * cos(latitude) + ty * sin(latitude));
}

static void		getSunDirection(double julianDay, float latitude,
					float longitude, float pos[3])
{
  double C;
  double T;		/* time (fractions of a century) */
  double sx, sy, sz;	/* sun direction */
  double obliquity;	/* earth's tilt */
  double meanAnomaly;
  double geometricMeanLongitude;
  double trueLongitude;

  T = (julianDay - epoch) / 36525.0;

  obliquity = radPerDeg *
		(23.452294 + (-0.0130125 + (-0.00000164 +
						0.000000503 * T) * T) * T);

  meanAnomaly = radPerDeg * (358.47583 +
		((0.0000033 * T + 0.000150) * T + 35999.04975) * T);

  geometricMeanLongitude = radPerDeg *
		((0.0003025 * T + 36000.76892) * T + 279.69668);

  C = radPerDeg *
		(sin(meanAnomaly) * (1.919460 - (0.004789 + 0.000014 * T) * T) +
		sin(2.0 * meanAnomaly) * (0.020094 - 0.0001 * T) +
		sin(3.0 * meanAnomaly) * 0.000293);

  trueLongitude = geometricMeanLongitude + C;

  /* position of sun if earth didn't rotate: */
  sx = sin(trueLongitude) * cos(obliquity);
  sy = sin(trueLongitude) * sin(obliquity);
  sz = cos(trueLongitude);

  /* get true position */
  getTruePosition(julianDay, latitude, longitude, sx, sy, sz, pos);
}

/*
 * globe/map calculation
 */

#define LAT_GRID	20
#define LON_GRID	40
static GLfloat		sphere[LAT_GRID + 1][LON_GRID + 1][3];
static GLfloat		map[LAT_GRID + 1][LON_GRID + 1][2];
static GLfloat		uv[LAT_GRID + 1][LON_GRID + 1][2];

static void		initSphere(void)
{
  int lat, lon;

  for (lat = 0; lat <= LAT_GRID; lat++) {
    const float y = (float)-cos(M_PI * (double)lat / (double)LAT_GRID);
    const float r = (float)sin(M_PI * (double)lat / (double)LAT_GRID);
    for (lon = 0; lon <= LON_GRID; lon++) {
      sphere[lat][lon][0] = r * (float)sin(-M_PI + 2.0 * M_PI *
					(double)lon / (double)LON_GRID);
      sphere[lat][lon][1] = y;
      sphere[lat][lon][2] = r * (float)cos(-M_PI + 2.0 * M_PI *
					(double)lon / (double)LON_GRID);
    }
  }
}

static void		initMap(void)
{
  int lat, lon;

  for (lat = 0; lat <= LAT_GRID; lat++) {
    const float y = 1.0f * ((float)lat / (float)LAT_GRID - 0.5f);
    for (lon = 0; lon <= LON_GRID; lon++) {
      map[lat][lon][0] = 2.0f * ((float)lon / (float)LON_GRID - 0.5f);
      map[lat][lon][1] = y;
    }
  }
}

static int		isPowerOfTwo(int x)
{
  if (x <= 0) return 0;
  while ((x & 1) == 0) x >>= 1;
  return x == 1;
}

static void		initTexture(const char* filename)
{
  FILE* imageFile;
  int lat, lon;
  unsigned char buffer[4];
  unsigned char* image;
  int dx, dy;

  /* initialize texture coordinates */
  for (lat = 0; lat <= LAT_GRID; lat++) {
    const float y = (float)lat / (float)LAT_GRID;
    for (lon = 0; lon <= LON_GRID; lon++) {
      uv[lat][lon][0] = (float)lon / (float)LON_GRID;
      uv[lat][lon][1] = y;
    }
  }

  /* open image file */
  imageFile = fopen(filename, "rb");
  if (!imageFile) {
    fprintf(stderr, "cannot open image file %s for reading\n", filename);
    exit(1);
  }

  /* read image size */
  fread(buffer, 1, 4, imageFile);
  dx = (int)((buffer[0] << 8) | buffer[1]);
  dy = (int)((buffer[2] << 8) | buffer[3]);
  if (!isPowerOfTwo(dx) || !isPowerOfTwo(dy)) {
    fprintf(stderr, "image %s has an illegal size: %dx%d\n", filename, dx, dy);
    exit(1);
  }

  /* read image */
  image = (unsigned char*)malloc(3 * dx * dy * sizeof(unsigned char));
  fread(image, 3 * dx, dy, imageFile);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, dx, dy, 0,
		GL_RGB, GL_UNSIGNED_BYTE, image);

  /* tidy up */
  free(image);
  fclose(imageFile);
}

/*
 * misc OpenGL stuff
 */

static const GLfloat	sunColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
static GLfloat		sunDirection[4] = { 0.0f, 0.0f, 1.0f, 0.0f };
static const GLfloat	surfaceColor[] = { 0.8f, 0.8f, 0.8f, 1.0f };

static void		initProjection(void)
{
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  if (mapMode) {
    /* orthographic for map */
    if (aspectRatio < 2.0)
      glOrtho(-1.0, 1.0, -1.0 / aspectRatio, 1.0 / aspectRatio, 2.0, 4.0);
    else
      glOrtho(-0.5 * aspectRatio, 0.5 * aspectRatio, -0.5, 0.5, 2.0, 4.0);
  }

  else {
    /* prespective for globe */
    gluPerspective(40.0, aspectRatio, 1.0, 5.0);
  }

  glMatrixMode(GL_MODELVIEW);
}

static void		initSunlight(void)
{
  getSunDirection(epoch + timeOfDay + 365.0 * timeOfYear,
					0.0f, 0.0f, sunDirection);

  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, sunColor);
  glLightfv(GL_LIGHT0, GL_POSITION, sunDirection);
  glEnable(GL_LIGHT0);

  glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, surfaceColor);
}

/*
 * glut callbacks
 */

static GLfloat		angle = 0;	/* in degrees */
static GLfloat		angle2 = 0;	/* in degrees */
static int		moving = 0, startx, starty;

static void		reshapeCB(GLsizei w, GLsizei h)
{
  aspectRatio = (GLdouble)w / (GLdouble)h;
  glViewport(0, 0, w, h);
  initProjection();
  glutPostRedisplay();
}

static void		redrawCB(void)
{
  glClear(GL_COLOR_BUFFER_BIT);

  glPushMatrix();

    /* Perform scene rotations based on user mouse input. */
    if (!mapMode) {
      glRotatef(angle2, 1.0f, 0.0f, 0.0f);
      glRotatef(angle, 0.0f, 1.0f, 0.0f);
    }

    glLightfv(GL_LIGHT0, GL_POSITION, sunDirection);

    if (mapMode) {
      /* use the normals for a sphere on a flat surface to get
       * the lighting as if we unwrapped a lighted sphere. */

      int lat, lon;
      for (lat = 0; lat < LAT_GRID; lat++) {
	glBegin(GL_TRIANGLE_STRIP);

	  for (lon = 0; lon <= LON_GRID; lon++) {
	    glTexCoord2fv(uv[lat + 1][lon]);
	    glNormal3fv(sphere[lat + 1][lon]);
	    glVertex2fv(map[lat + 1][lon]);

	    glTexCoord2fv(uv[lat][lon]);
	    glNormal3fv(sphere[lat][lon]);
	    glVertex2fv(map[lat][lon]);
	  }

	glEnd();
      }
    }

    else {
      /* draw a sphere */
      int lat, lon;
      for (lat = 0; lat < LAT_GRID; lat++) {
	glBegin(GL_TRIANGLE_STRIP);

	  for (lon = 0; lon <= LON_GRID; lon++) {
	    glTexCoord2fv(uv[lat + 1][lon]);
	    glNormal3fv(sphere[lat + 1][lon]);
	    glVertex3fv(sphere[lat + 1][lon]);

	    glTexCoord2fv(uv[lat][lon]);
	    glNormal3fv(sphere[lat][lon]);
	    glVertex3fv(sphere[lat][lon]);
	  }

	glEnd();
      }
    }

  glPopMatrix();

  glutSwapBuffers();
}

static void		mouseCB(int button, int state, int x, int y)
{
  if (button == GLUT_LEFT_BUTTON) {
    if (state == GLUT_DOWN) {
      moving = 1;
      startx = x;
      starty = y;
    }
    else if (state == GLUT_UP) {
      moving = 0;
    }
  }
}

static void		motionCB(int x, int y)
{
  if (moving) {
    switch (adjustMode) {
      case 0:
	/* move globe */
	if (!mapMode) {
	  angle = angle + (x - startx);
	  angle2 = angle2 + (y - starty);
	  glutPostRedisplay();
	}
	break;

      case 1:
	/* change day of year */
	timeOfYear = timeOfYear + (y - starty) / 365.0;
	while (timeOfYear < 0) timeOfYear += 1.0;
	while (timeOfYear >= 1.0) timeOfYear -= 1.0;
	getSunDirection(epoch + timeOfDay + 365.0 * timeOfYear,
					0.0f, 0.0f, sunDirection);
	glutPostRedisplay();
	break;

      case 2:
	/* change time of day (by 4 minute increments (24 hrs / 360)) */
	timeOfDay = timeOfDay + (y - starty) / 360.0;
	while (timeOfDay < 0) timeOfDay += 1.0;
	while (timeOfDay >= 1.0) timeOfDay -= 1.0;
	getSunDirection(epoch + timeOfDay + 365.0 * timeOfYear,
					0.0f, 0.0f, sunDirection);
	glutPostRedisplay();
	break;
    }

    startx = x;
    starty = y;
  }
}

static void		keyCB(unsigned char c, int x, int y)
{
  if (c == 27) {
    exit(0);
  }
  glutPostRedisplay();
}


/*
 * menu handling
 */

static void		handleMenu(int value)
{
  switch (value) {
    default:
      break;

    case MENU_SHOW_GLOBE:
      mapMode = 0;
      initProjection();
      glutPostRedisplay();
      glutChangeToMenuEntry(2, "Show Globe *", MENU_SHOW_GLOBE);
      glutChangeToMenuEntry(3, "Show Map", MENU_SHOW_MAP);
      break;

    case MENU_SHOW_MAP:
      mapMode = 1;
      initProjection();
      glutPostRedisplay();
      glutChangeToMenuEntry(2, "Show Globe", MENU_SHOW_GLOBE);
      glutChangeToMenuEntry(3, "Show Map *", MENU_SHOW_MAP);
      break;

    case MENU_ADJUST_GLOBE:
      adjustMode = 0;
      glutChangeToMenuEntry(4, "Adjust Globe *", MENU_ADJUST_GLOBE);
      glutChangeToMenuEntry(5, "Adjust Day", MENU_ADJUST_DAY);
      glutChangeToMenuEntry(6, "Adjust Time", MENU_ADJUST_TIME);
      break;

    case MENU_ADJUST_DAY:
      adjustMode = 1;
      glutChangeToMenuEntry(4, "Adjust Globe", MENU_ADJUST_GLOBE);
      glutChangeToMenuEntry(5, "Adjust Day *", MENU_ADJUST_DAY);
      glutChangeToMenuEntry(6, "Adjust Time", MENU_ADJUST_TIME);
      break;

    case MENU_ADJUST_TIME:
      adjustMode = 2;
      glutChangeToMenuEntry(4, "Adjust Globe", MENU_ADJUST_GLOBE);
      glutChangeToMenuEntry(5, "Adjust Day", MENU_ADJUST_DAY);
      glutChangeToMenuEntry(6, "Adjust Time *", MENU_ADJUST_TIME);
      break;

    case MENU_QUIT:
      exit(0);
  }
}

/*
 * main
 */

int			main(int argc, char** argv)
{
  /*
   * initialize GLUT and open a window
   */
  glutInit(&argc, argv);
  glutInitWindowSize(512, 512);
  glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
  glutCreateWindow("Sunlight");
  glutDisplayFunc(redrawCB);
  glutReshapeFunc(reshapeCB);
  glutMouseFunc(mouseCB);
  glutMotionFunc(motionCB);
  glutKeyboardFunc(keyCB);

  /*
   * make the menu
   */
  glutCreateMenu(handleMenu);
  glutAddMenuEntry("SUNLIGHT", 0);
  glutAddMenuEntry("Show Globe *", MENU_SHOW_GLOBE);
  glutAddMenuEntry("Show Map", MENU_SHOW_MAP);
  glutAddMenuEntry("Adjust Globe *", MENU_ADJUST_GLOBE);
  glutAddMenuEntry("Adjust Day", MENU_ADJUST_DAY);
  glutAddMenuEntry("Adjust Time", MENU_ADJUST_TIME);
  glutAddMenuEntry("Quit", MENU_QUIT);
  glutAttachMenu(GLUT_RIGHT_BUTTON);

  /*
   * initialize GL
   */
  initProjection();
  gluLookAt(0.0, 0.0, 3.0,
	    0.0, 0.0, 0.0,
	    0.0, 1.0, 0.0);
  initSunlight();
  glEnable(GL_LIGHTING);
  glEnable(GL_CULL_FACE);
  glEnable(GL_TEXTURE_2D);

  /*
   * initialize data structures
   */
  initSphere();
  initMap();
  initTexture("globe.raw");

  glutMainLoop();
  return 0;             /* ANSI C requires main to return int. */
}
