#include <image.h>

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : The only way to instantiate an Image object is to use this
                 constructor.  The image size, color system, and the type of
		 coordinate system must all be provided here. 

   Parameters  :
      int s[2]   : 2D array containing the image dimensions in pixels.  For a
                   360 wide by 180 high (360x180) image, set the array with
	 	   values "int s[2] = {360, 180}"
      color_t c  : the type of image to be created, either a black and white
                   (BW) or a red, green, blue (RGB).
      coord_t cs : specify the coordinate system of the image.  The two
                   coordinate systems supported are RECTANGULAR and SPHERICAL.
*/
/* *******************************************************************/
Image::Image (int s[2], color_t c, coord_t cs) {
  int totWeights, totColors;
  
  size[0] = s[0];
  size[1] = s[1];

  switch (c) {
  case BW:
    numComponents = 1;
    break;
  case RGB:
    numComponents = 3;
    break;
  default:
    numComponents = 3;
  }
  
  coordSystem   = cs;

  /* bounds is only set for the RECTANGULAR system because it is not used for
     SPHERICAL plots
  */
  if (coordSystem == RECTANGULAR) {
    bounds[0] = -1.0;
    bounds[1] = -1.0;
    bounds[2] =  1.0;
    bounds[3] =  1.0;
  }

  totWeights = size[0]*size[1];
  totColors  = numComponents*size[0]*size[1];

  /* allocate the arrays */
  weights = new float[totWeights];
  colors  = new float[totColors];
  bytes   = new unsigned char[totColors];

  /* set everything to zero (black) to start off */
  clear();
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Destructor functions used to deallocate some memory
                 previously allocated.
*/
/* *******************************************************************/
Image::~Image () {
  delete [] weights;
  delete [] colors;
  delete [] bytes;
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Clears the image to black by setting all the values in the
                 arrays to zero.
*/
/* *******************************************************************/
void Image::clear () {
  int totWeights, totColors;
  
  totWeights = size[0]*size[1];
  totColors  = numComponents*size[0]*size[1];
  
  memset(weights, 0, sizeof(float)*totWeights);
  memset(colors,  0, sizeof(float)*totColors);
  memset(bytes,   0, sizeof(unsigned char)*totColors);
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : A wrapper function for the drawSTDot() and drawXYDot
                 functions.  A dot is drawn with radius r and filter f at
		 point p with color c.

   Parameters  :
      float p[3] : center of dot to be drawn, values are stored with format
                   {x, y, z}.  *Note* if coordSystem is set to RECTANGULAR the
		   z component is not used.
      float c[3] : color of dot to be drawn, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
      float r    : radius of the the dot to be drawn.  The radius has the
                   same units as the image domain.
      filter_t f : type of filter to be applied to the dot.  Currently
                   supports BOX, TENT, QUADRATIC, CUBIC, and QUARTIC.
*/
/* *******************************************************************/
void Image::drawDot (float p[3], float c[3], float r, filter_t f) {

  switch (coordSystem) {
  case SPHERICAL:
    drawSTDot(p, c, r, f);
    break;
  case RECTANGULAR:
    drawXYDot(p, c, r, f);
    break;
  default:
    drawXYDot(p, c, r, f);
    break;
  }
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Returns the pixels in the image as an array of unsigned
                 chars. The s and nc arguments are filled in with the
                 dimensions of the image and the number of components in
                 the  image;  the  number of bytes in the array returned
                 will be s[0]*s[1]*nc.

   Parameters  :
      int  *s : pointer to a two dimensional array where the image dimensions
                (in pixels) will be stored.
      int &nc : number of components in image will be stored here.
*/
/* *******************************************************************/
unsigned char* Image::getValue (int *s, int &nc) {
  s[0] = size[0];
  s[1] = size[1];
  nc   = numComponents;

  return bytes;
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Sets the background of the image to color c.

          *NOTE* this has the same affect as calling clear() and then
		 setBackground().  In other words, this will erase whatever
		 changes have been made to the image.

   Parameters  :
      float c[3] : color background will be, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
*/
/* *******************************************************************/
void Image::setBackground (float c[3]) {
  for (int i = 0, j = 0;
       i < size[0]*size[1]*numComponents;
       i += numComponents, j++) {
    weights[j] = 0.0;
    for (int k = 0; k < numComponents; k++) {
      colors[i + k] = c[k];
      bytes[i + k]  = (unsigned char)(255.0 * colors[i + k]);
    }
  }
} 

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Sets a point p with color c on the image.

         *NOTE* this is not like setting an individual pixel.  This could
	        have undesirable effects if the resolution of the image 
		is not high enough and/or the domain of the image is not
		large enough.

   Parameters  :
      float p[3] : coordinates of point to be drawn, values are stored with
                   format {x, y, z}.  *Note* if coordSystem is set to
		   RECTANGULAR the z component is not used. 
      float c[3] : color of point to be set, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
*/
/* *******************************************************************/
void Image::setPoint (float p[3], float c[3]) {
  int imgIndex;
  float s, t;

  switch (coordSystem) {
  case SPHERICAL:
    getSTCoords(p, s, t);
    imgIndex = getSTIndex(s, t);
    break;
  case RECTANGULAR:
    imgIndex = getXYIndex(p[0], p[1]);
    break;
  default:
    imgIndex = 0;
    break;
  }

  setPixel(imgIndex, c, 1.0);
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : public
   Description : Writes the current state of the image to a binary formatted
                 PPM file with name filename.

   Parameters  :
      char *filename : name of file where image is to be written.  If
                       filename is set to NULL, then the file is written to
		       STDOUT.
*/
/* *******************************************************************/
void Image::writePPM (char *filename) {
  int num;
  FILE *fptr;

  if (filename == NULL)
    fptr = stdout;
  else
    fptr = fopen(filename, "w");

  /* header for PPM file */
  fprintf(fptr, "P6\n");
  fprintf(fptr, "%d %d\n", size[0], size[1]);
  fprintf(fptr, "255\n");

  /* writing out actual image bytes */
  if (numComponents == 1) {
    for (int i = size[1] - 1; i >= 0; i--) {
      for (int j = 0; j < size[0]; j++) {
	unsigned char *ptr;
	
	ptr = bytes + i*size[0] + j;
	fwrite((void *)ptr, 1, sizeof(unsigned char), fptr);
	fwrite((void *)ptr, 1, sizeof(unsigned char), fptr);
	fwrite((void *)ptr, 1, sizeof(unsigned char), fptr);
      }
    }
  }
  else {
    num = size[0]*numComponents;
    for (int i = size[1] - 1; i >= 0; i--) {
      unsigned char *ptr;
      int index;

      index = i*size[0]*numComponents;
      ptr   = bytes + index*sizeof(unsigned char);
      fwrite((void *)ptr, num, sizeof(unsigned char), fptr);
    }
  }
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : draws a dot with radius r and filter f at point p with color
                 c on an image with a SPHERICAL coordinate system.

   Parameters  :
      float p[3] : center of dot to be drawn, values are stored with format
                   {x, y, z}.
      float c[3] : color of dot to be drawn, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
      float r    : radius of the the dot to be drawn.  The radius has the
                   same units as the image domain.  If the radius becomes too
		   large undesirable effects may occur due to the nature of
		   the SPHERICAL coordinate system.
      filter_t f : type of filter to be applied to the dot.  Currently
                   supports BOX, TENT, QUADRATIC, CUBIC, and QUARTIC.
*/
/* *******************************************************************/
void Image::drawSTDot (float p[3], float c[3], float r, filter_t f) {
  float sdelta, tdelta, s1, t1, s2, t2;
  float weight, sinVal, sdist, tdist, dist;
  float phi, theta;
  int width, pixelRadius;

  /* amount of image space covered by each pixel in both directions. */
  sdelta = 2.0*M_PI / (size[0]);
  tdelta =     M_PI / (size[1]);

  /* what is the center of the dot in ST coordinates */
  getSTCoords(p, theta, phi);

  /* the radius of the dot in pixels */
  pixelRadius = (int)(r / tdelta);

  /* the s (theta) values become crowded as t (phi) approaches 0 or PI.
     Therefore we must ensure that the dot will look the same everywhere on
     the image when it is wrapped around sphere.  To do this the width of the
     dot in the s (theta) direction is determined by dividing it by the
     sin(phi).
  */
  t1 = phi - tdelta*pixelRadius + tdelta*0.5;
  for (int i = 0; i < pixelRadius*2 + 1; i++) {
    t1 += tdelta;
    
    sinVal = sin(t1);
    if (sinVal > 0.0)
      width = (int)(pixelRadius/sinVal) * 2;
    else if (size[0] % 2 == 1)
      width = size[0] - 1;
    else
      width = size[0];

    if (width > size[0]) {
      if (size[0] % 2 == 1)
        width = size[0] - 1;
      else
	width = size[0];
    }

    if (t1 >= M_PI)
      t2 = M_PI + (M_PI - t1);
    else if (t1 < 0.0)
      t2 = fabs(t1);
    else
      t2 = t1;

    tdist = pow((t1 - phi), 2.0);

    s1 = theta - sdelta*(width/2) + sdelta*0.5;
    for (int j = 0; j < width; j++) {
      s1 += sdelta;
 
      if (s1 < 0.0)
        s2 = s1 + 2.0*M_PI;
      else if (s1 >= 2*M_PI)
        s2 = s1 - 2.0*M_PI;
      else
	s2 = s1;

      sdist = pow((s1 - theta) * sinVal, 2.0);
      dist = sqrt(sdist + tdist);

      weight = filterVal(dist, pixelRadius*tdelta, f);

      if (weight > 0.0)
        setPixel(getSTIndex(s2, t2), c, weight);
    }
  }
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : draws a dot with radius r and filter f at point p with color
                 c on an image with a RECTANGULAR coordinate system.

   Parameters  :
      float p[3] : center of dot to be drawn, values are stored with format
                   {x, y, z}.  For this function the z component is not used.
      float c[3] : color of dot to be drawn, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
      float r    : radius of the the dot to be drawn.  The radius has the
                   same units as the image domain.  If the radius becomes too
		   large and areas of the dot exceed the image domain,
		   portions of the dot will be clamped.
      filter_t f : type of filter to be applied to the dot.  Currently
                   supports BOX, TENT, QUADRATIC, CUBIC, and QUARTIC.
*/
/* *******************************************************************/
void Image::drawXYDot (float p[3], float c[3], float r, filter_t f) {
  int xRadius, yRadius;
  float xdelta, ydelta, x, y;
  float xdist, ydist, dist, weight;

  /* amount of image space covered by each pixel in both directions. */
  xdelta = (bounds[2] - bounds[0]) / (size[0]);
  ydelta = (bounds[3] - bounds[1]) / (size[1]);

  xRadius = (int)(r / xdelta);
  yRadius = (int)(r / ydelta);

  y = p[1] - xdelta*yRadius + ydelta*0.5;
  for (int i = 0; i < yRadius*2 + 1; i++) {
    y += ydelta;
    
    if (y > bounds[1] && y < bounds[3]) {
      ydist = pow((y - p[1]), 2.0);

      x = p[0] - xdelta*xRadius + xdelta*0.5;
      for (int j = 0; j < xRadius*2 + 1; j++) {
	x += xdelta;

	if (x > bounds[0] && x < bounds[2]) {
	  xdist = pow((x - p[0]), 2.0);
	  dist = sqrt(xdist + ydist);

	  if (xRadius > yRadius)
            weight = filterVal(dist, xRadius*xdelta, f);
	  else
	    weight = filterVal(dist, yRadius*ydelta, f);

          if (weight > 0.0)
            setPixel(getXYIndex(x, y), c, weight);
	}
      }
    }
  }
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : based on the value of f, the appropriate filter function is
                 chosen and the value returned by the filter function is
		 returned by this function.
		 
   Parameters  :
      float dist   : distance from the center of the dot.  Has the same
                     units as the image
      float radius : radius of the dot being drawn.  Has the same units as
                     the image.
      filter_t f   : type of filter to be applied to the dot.  Currently
                     supports BOX, TENT, QUADRATIC, CUBIC, and QUARTIC.
*/
/* *******************************************************************/
float Image::filterVal (float dist, float radius, filter_t f) {
  float weight;
  
  switch (f) {
  case BOX:
    weight = box(dist, radius);
    break;
  case TENT:
    weight = tent(dist, radius);
    break;
  case QUADRATIC:
    weight = quadratic(dist, radius);
    break;
   case CUBIC:
    weight = cubic(dist, radius);
    break;
  case QUARTIC:
    weight = quartic(dist, radius);
    break;
  default:
    weight = tent(dist, radius);
  }

  return weight;
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : when a point in {x, y, z} space needs to be mapped to ST
                 space on an image, the values of s and t must be
		 determined.  This function calculates the values of s and t
		 based on following orientation of XYZ axis.  (Imagine
		 positive z-axis is coming out of the screen).  This
		 orientation places every point along the positive z axis in
		 the center of the ST image.  The s values are equivalent to
		 theta values in the xz plane, and the t values are
		 equivalent to phi values in the xy plane.
		                   
                         +y               -------------------------------
		          |   /           |                             |
			  |  /            |                             |
			  | /             |                             |
			  |/              |              The            |
                   ---------------- +x    | ^           Image           |
		         /|               | |                           |
			/ |               |phi                          |
		       /  |               |                             |
		      /	  |               |     theta ->                |
                     +z                   -------------------------------

		         Mapping of theta             Mapping of phi
		               -z                           +y
		              0 | 2*PI                       |PI
				|                            |
				|                            |
                         	|                            |
		      -x --------------- +x        -x --------------- +x
			  	|                            |
				|                            | 
				|                            |
				|PI                          |0
			       +z                           -y

   Parameters  :
      float p[3] : center of dot to be drawn, values are stored with format
                   {x, y, z}.
      float &s   : value of equivalent s (or theta) value in SPHERICAL
                   coordinates.
      float &t   : value of equivalent t (or phi) value in SPHERICAL
                   coordinates.
*/
/* *******************************************************************/
void Image::getSTCoords (float p[3], float &s, float &t) {
  double r, dist;

  r    = sqrt(p[0]*p[0] + p[1]*p[1] + p[2]*p[2]);
  dist = sqrt(p[0]*p[0] + p[2]*p[2]);
  t    = fabs(acos(p[1] / r) - M_PI);

  if (dist == 0.0) {
    t = M_PI - M_PI / (2 * size[1]);
    s = 2.0 * M_PI / (2 * size[0]);
  }
  else if (p[0] < 0.0)
    s = fabs(acos(p[2] / dist) - M_PI);
  else
    s = M_PI + acos(p[2] / dist);
}  

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : based on the values of s and t, an index value into the
                 image array can be calculated.  This index is returned.
		 
   Parameters  :
      float s: the s component of the image and desired index.
      float t: the t component of the image and desired index.
*/
/* *******************************************************************/
int Image::getSTIndex (float s, float t) {
  int sIndex, tIndex, index;

  sIndex = (int)((s/(2.0*M_PI))*size[0]);
  tIndex = (int)((t/M_PI)*size[1]);

  index  = tIndex*numComponents*size[0] + sIndex*numComponents;

  return index;
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : based on the values of x and y, an index value into the
                 image array can be calculated.  This index is returned.
		 
   Parameters  :
      float s: the x component of the image and desired index.
      float t: the y component of the image and desired index.
*/
/* *******************************************************************/
int Image::getXYIndex (float x, float y) {
  int xIndex, yIndex, index;

  xIndex = (int)(((x - bounds[0])/(bounds[2] - bounds[0]))*(size[0]));
  yIndex = (int)(((y - bounds[1])/(bounds[3] - bounds[1]))*(size[1]));

  //cerr << xIndex << ":" << yIndex << " ";

  index  = yIndex*numComponents*size[0] + xIndex*numComponents;

  return index;
}

/* *******************************************************************/
/* Author      : Josh Grant
   Date        : May 6th, 2001
   Version     : 1.0
   Use         : private
   Description : a given pixel can be set by specifying an integer index
                 value.  The function sets the color c with weight w at array
		 value index.
		 
   Parameters  :
      int index  : index into array where pixel is to be modified.
      float c[3] : color of dot to be drawn, values are stored with format
                   {r, g, b}.  Individual components have values in the range
		   [0.0, 1.0]. *Note* if numComponents is set to 1 for B&W
		   then only the first component (the red component) is used.
      float w    : weight of the color being set at index.  This weight will
                   added to the total weights array.
*/
/* *******************************************************************/
void Image::setPixel (int index, float c[3], float w) {
  int wIndex;

  wIndex = (int)(index / numComponents);

  weights[wIndex] += w;
  for (int i = 0; i < numComponents; i++) {
    float val;
    
    colors[index + i] += c[i] * w;
    
    if (weights[wIndex] > 1.0)
      val = 255.0 * (colors[index + i] / weights[wIndex]);
    else
      val = 255.0 * (colors[index + i]);

    if (val > 255.0)
      bytes[index + i] = 255;
    else
      bytes[index + i] = (unsigned char)val;
  }
}

float Image::box (float dist, float rad) {
  if (dist <= rad)
    return 1.0;
  else
    return 0.0;
}

float Image::tent (float dist, float rad) {
  if (dist <= rad)
    return (1.0 - (1.0/rad) * dist);
  else
    return 0.0;
}

float Image::quadratic (float dist, float rad) {
  if (dist <= rad)
    return (1.0 - (1.0 / pow(rad, 2.0) * pow(dist, 2.0)));
  else
    return 0.0;
}

float Image::cubic (float dist, float rad) {
  if (dist <= rad) {
    float a, b, c;
    a = 1.0;
    b = (-3.0 / pow(rad, 2.0)) * pow(dist, 2.0);
    c = ( 2.0 / pow(rad, 3.0)) * pow(dist, 3.0);
    return (a + b + c);
  }
  else
    return 0.0;
}

float Image::quartic (float dist, float rad) {
  if (dist <= rad) {
    float a, b, c;
    a = 1.0;
    b = (-3.0 / pow(rad, 2.0)) * pow(dist, 2.0);
    c = ( 2.0 / pow(rad, 3.0)) * pow(dist, 3.0);
    return (a + b + c);
  }
  else
    return 0.0;
}
