/* ----------------------------------------------------------------------
   SPARTA - Stochastic PArallel Rarefied-gas Time-accurate Analyzer
   http://sparta.github.io
   Steve Plimpton, sjplimp@gmail.com, Michael Gallis, magalli@sandia.gov
   Sandia National Laboratories

   Copyright (2014) Sandia Corporation.  Under the terms of Contract
   DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
   certain rights in this software.  This software is distributed under
   the GNU General Public License.

   See the README file in the top-level SPARTA directory.
------------------------------------------------------------------------- */

/* ----------------------------------------------------------------------
   Contributing author: Nathan Fabian (Sandia)
------------------------------------------------------------------------- */

#include "mpi.h"
#include "math.h"
#include "ctype.h"
#include "stdlib.h"
#include "string.h"
#include "image.h"
#include "math_extra.h"
#include "update.h"
#include "random_mars.h"
#include "random_knuth.h"
#include "math_const.h"
#include "error.h"
#include "memory.h"

#ifdef SPARTA_JPEG
#include "jpeglib.h"
#endif

#ifdef SPARTA_PNG
#include <png.h>
#include <zlib.h>
#include <setjmp.h>
#include "version.h"
#endif

using namespace SPARTA_NS;
using namespace MathConst;

#define NCOLORS 140
#define NELEMENTS 109
#define BIG 1.0e20
#define EPSILON 1.0e-6

enum{NUMERIC,MINVALUE,MAXVALUE};
enum{CONTINUOUS,DISCRETE,SEQUENTIAL};
enum{ABSOLUTE,FRACTIONAL};
enum{NO,YES};

/* ---------------------------------------------------------------------- */

Image::Image(SPARTA *sparta, int nmap_caller) : Pointers(sparta)
{
  MPI_Comm_rank(world,&me);
  MPI_Comm_size(world,&nprocs);

  // defaults for 3d viz

  width = height = 512;
  theta = 60.0 * MY_PI/180.0;
  phi = 30.0 * MY_PI/180.0;
  zoom = 1.0;
  persp = 0.0;
  shiny = 1.0;
  ssao = NO;

  up[0] = 0.0;
  up[1] = 0.0;
  up[2] = 1.0;

  // colors

  ncolors = 0;
  username = NULL;
  userrgb = NULL;

  boxcolor = color2rgb("yellow");
  background[0] = background[1] = background[2] = 0;

  // define nmap colormaps, all with default settings

  nmap = nmap_caller;
  maps = new ColorMap*[nmap];
  for (int i = 0; i < nmap; i++)
    maps[i] = new ColorMap(sparta,this);

  // static parameters

  FOV = MY_PI/6.0;              // 30 degrees
  ambientColor[0] = 0.0;
  ambientColor[1] = 0.0;
  ambientColor[2] = 0.0;

  keyLightPhi = -MY_PI4;        // -45 degrees
  keyLightTheta = MY_PI/6.0;    // 30 degrees
  keyLightColor[0] = 0.9;
  keyLightColor[1] = 0.9;
  keyLightColor[2] = 0.9;

  fillLightPhi = MY_PI/6.0;     // 30 degrees
  fillLightTheta = 0;
  fillLightColor[0] = 0.45;
  fillLightColor[1] = 0.45;
  fillLightColor[2] = 0.45;

  backLightPhi = MY_PI;         // 180 degrees
  backLightTheta = MY_PI/12.0;  // 15 degrees
  backLightColor[0] = 0.9;
  backLightColor[1] = 0.9;
  backLightColor[2] = 0.9;

  random = NULL;

  // MPI_Gatherv vectors

  recvcounts = NULL;
  displs = NULL;
}

/* ---------------------------------------------------------------------- */

Image::~Image()
{
  for (int i = 0; i < nmap; i++) delete maps[i];
  delete [] maps;

  for (int i = 0; i < ncolors; i++) delete [] username[i];
  memory->sfree(username);
  memory->destroy(userrgb);

  memory->destroy(depthBuffer);
  memory->destroy(surfaceBuffer);
  memory->destroy(imageBuffer);
  memory->destroy(depthcopy);
  memory->destroy(surfacecopy);
  memory->destroy(rgbcopy);

  if (random) delete random;

  memory->destroy(recvcounts);
  memory->destroy(displs);
}

/* ----------------------------------------------------------------------
   allocate image and depth buffers
   called after image size is set
------------------------------------------------------------------------- */

void Image::buffers()
{
  npixels = width * height;
  memory->create(depthBuffer,npixels,"image:depthBuffer");
  memory->create(surfaceBuffer,2*npixels,"image:surfaceBuffer");
  memory->create(imageBuffer,3*npixels,"image:imageBuffer");
  memory->create(depthcopy,npixels,"image:depthcopy");
  memory->create(surfacecopy,2*npixels,"image:surfacecopy");
  memory->create(rgbcopy,3*npixels,"image:rgbcopy");
}

/* ----------------------------------------------------------------------
   reset view parameters
   called once if view is STATIC
   called before every render if view is DYNAMIC
------------------------------------------------------------------------- */

void Image::view_params(double boxxlo, double boxxhi, double boxylo,
                        double boxyhi, double boxzlo, double boxzhi)
{
  // camDir points at the camera, view direction = -camDir

  camDir[0] = sin(theta)*cos(phi);
  camDir[1] = sin(theta)*sin(phi);
  camDir[2] = cos(theta);

  // normalize up vector

  if (up[0] == 0.0 && up[1] == 0.0 && up[2] == 0.0)
    error->all(FLERR,"Invalid image up vector");
  MathExtra::norm3(up);

  // adjust camDir by epsilon if camDir and up are parallel
  // do this by tweaking view direction, not up direction
  // try to insure continuous images as changing view passes thru up
  // sufficient to handle common cases where theta = 0 or 180 is degenerate?

  double dot = MathExtra::dot3(up,camDir);
  if (fabs(dot) > 1.0-EPSILON) {
    if (theta == 0.0) {
      camDir[0] = sin(EPSILON)*cos(phi);
      camDir[1] = sin(EPSILON)*sin(phi);
      camDir[2] = cos(EPSILON);
    } else if (theta == MY_PI) {
      camDir[0] = sin(theta-EPSILON)*cos(phi);
      camDir[1] = sin(theta-EPSILON)*sin(phi);
      camDir[2] = cos(theta-EPSILON);
    } else {
      camDir[0] = sin(theta+EPSILON)*cos(phi);
      camDir[1] = sin(theta+EPSILON)*sin(phi);
      camDir[2] = cos(theta+EPSILON);
    }
  }

  // camUp = camDir x (Up x camDir)

  MathExtra::cross3(up,camDir,camRight);
  MathExtra::norm3(camRight);
  MathExtra::cross3(camDir,camRight,camUp);
  if (camUp[0] == 0.0 && camUp[1] == 0.0 && camUp[2] == 0.0)
    error->all(FLERR,"Invalid image up vector");
  MathExtra::norm3(camUp);

  // zdist = camera distance = function of zoom & bounding box
  // camPos = camera position = function of camDir and zdist

  double delx = 2.0*(boxxhi-boxxlo);
  double dely = 2.0*(boxyhi-boxylo);
  double delz = 2.0*(boxzhi-boxzlo);
  double maxdel = MAX(delx,dely);
  maxdel = MAX(maxdel,delz);

  zdist = maxdel;
  zdist /= tan(FOV);
  zdist += 0.5 * (delx*camDir[0] + dely*camDir[1] + delz*camDir[2]);
  zdist /= zoom;

  camPos[0] = camDir[0] * zdist;
  camPos[1] = camDir[1] * zdist;
  camPos[2] = camDir[2] * zdist;

  // light directions in terms of -camDir = z

  keyLightDir[0] = cos(keyLightTheta) * sin(keyLightPhi);
  keyLightDir[1] = sin(keyLightTheta);
  keyLightDir[2] = cos(keyLightTheta) * cos(keyLightPhi);

  fillLightDir[0] = cos(fillLightTheta) * sin(fillLightPhi);
  fillLightDir[1] = sin(fillLightTheta);
  fillLightDir[2] = cos(fillLightTheta) * cos(fillLightPhi);

  backLightDir[0] = cos(backLightTheta) * sin(backLightPhi);
  backLightDir[1] = sin(backLightTheta);
  backLightDir[2] = cos(backLightTheta) * cos(backLightPhi);

  keyHalfDir[0] = 0 + keyLightDir[0];
  keyHalfDir[1] = 0 + keyLightDir[1];
  keyHalfDir[2] = 1 + keyLightDir[2];
  MathExtra::norm3(keyHalfDir);

  // adjust shinyness of the reflection

  specularHardness = 16.0 * shiny;
  specularIntensity = shiny;

  // adjust strength of the SSAO

  if (ssao) {
    if (!random) {
      random = new RanKnuth(update->ranmaster->uniform());
      double seed = update->ranmaster->uniform();
      random->reset(seed,me,100);
    }
    SSAORadius = maxdel * 0.05 * ssaoint;
    SSAOSamples = static_cast<int> (8.0 + 32.0*ssaoint);
    SSAOJitter = MY_PI / 12;
    ambientColor[0] = 0.5;
    ambientColor[1] = 0.5;
    ambientColor[2] = 0.5;
  }

  // param for rasterizing spheres

  tanPerPixel = -(maxdel / (double) height);
}

/* ----------------------------------------------------------------------
   initialize image to background color and depth buffer
   no need to init surfaceBuffer, since will be based on depth
------------------------------------------------------------------------- */

void Image::clear()
{
  int red = background[0];
  int green = background[1];
  int blue = background[2];

  int ix,iy;
  for (iy = 0; iy < height; iy ++)
    for (ix = 0; ix < width; ix ++) {
      imageBuffer[iy * width * 3 + ix * 3 + 0] = red;
      imageBuffer[iy * width * 3 + ix * 3 + 1] = green;
      imageBuffer[iy * width * 3 + ix * 3 + 2] = blue;
      depthBuffer[iy * width + ix] = -1;
    }
}

/* ----------------------------------------------------------------------
   merge image from each processor into one composite image
   done pixel by pixel, respecting depth buffer
   hi procs send to lo procs, cascading down logarithmically
------------------------------------------------------------------------- */

void Image::merge()
{
  MPI_Request requests[3];
  MPI_Status statuses[3];

  int nhalf = 1;
  while (nhalf < nprocs) nhalf *= 2;
  nhalf /= 2;

  while (nhalf) {
    if (me < nhalf && me+nhalf < nprocs) {
      MPI_Irecv(rgbcopy,npixels*3,MPI_BYTE,me+nhalf,0,world,&requests[0]);
      MPI_Irecv(depthcopy,npixels,MPI_DOUBLE,me+nhalf,0,world,&requests[1]);
      if (ssao)
        MPI_Irecv(surfacecopy,npixels*2,MPI_DOUBLE,
                  me+nhalf,0,world,&requests[2]);
      if (ssao) MPI_Waitall(3,requests,statuses);
      else MPI_Waitall(2,requests,statuses);

      for (int i = 0; i < npixels; i++) {
        if (depthBuffer[i] < 0 || (depthcopy[i] >= 0 &&
                                   depthcopy[i] < depthBuffer[i])) {
          depthBuffer[i] = depthcopy[i];
          imageBuffer[i*3+0] = rgbcopy[i*3+0];
          imageBuffer[i*3+1] = rgbcopy[i*3+1];
          imageBuffer[i*3+2] = rgbcopy[i*3+2];
          if (ssao) {
            surfaceBuffer[i*2+0] = surfacecopy[i*2+0];
            surfaceBuffer[i*2+1] = surfacecopy[i*2+1];
          }
        }
      }

    } else if (me >= nhalf && me < 2*nhalf) {
      MPI_Send(imageBuffer,npixels*3,MPI_BYTE,me-nhalf,0,world);
      MPI_Send(depthBuffer,npixels,MPI_DOUBLE,me-nhalf,0,world);
      if (ssao) MPI_Send(surfaceBuffer,npixels*2,MPI_DOUBLE,me-nhalf,0,world);
    }

    nhalf /= 2;
  }

  // extra SSAO enhancement
  // bcast full image to all procs
  // each works on subset of pixels
  // MPI_Gather() result back to proc 0
  // use Gatherv() if subset of pixels is not the same size on every proc

  if (ssao) {
    MPI_Bcast(imageBuffer,npixels*3,MPI_BYTE,0,world);
    MPI_Bcast(surfaceBuffer,npixels*2,MPI_DOUBLE,0,world);
    MPI_Bcast(depthBuffer,npixels,MPI_DOUBLE,0,world);
    compute_SSAO();

    int pixelstart = 3 * static_cast<int> (1.0*me/nprocs * npixels);
    int pixelstop = 3 * static_cast<int> (1.0*(me+1)/nprocs * npixels);
    int mypixels = pixelstop - pixelstart;

    if (npixels % nprocs == 0) {
      MPI_Gather(imageBuffer+pixelstart,mypixels,MPI_BYTE,
                 rgbcopy,mypixels,MPI_BYTE,0,world);

    } else {
      if (recvcounts == NULL) {
        memory->create(recvcounts,nprocs,"image:recvcounts");
        memory->create(displs,nprocs,"image:displs");
        MPI_Allgather(&mypixels,1,MPI_INT,recvcounts,1,MPI_INT,world);
        displs[0] = 0;
        for (int i = 1; i < nprocs; i++)
          displs[i] = displs[i-1] + recvcounts[i-1];
      }

      MPI_Gatherv(imageBuffer+pixelstart,mypixels,MPI_BYTE,
                  rgbcopy,recvcounts,displs,MPI_BYTE,0,world);
    }

    writeBuffer = rgbcopy;
  } else {
    writeBuffer = imageBuffer;
  }
}

/* ----------------------------------------------------------------------
   draw a line as a cylinder
------------------------------------------------------------------------- */

void Image::draw_line(double *x1, double *x2, double *color, double diameter)
{
  draw_cylinder(x1,x2,color,diameter,3);
}

/* ----------------------------------------------------------------------
   draw outline of a 3d box as 12 cylinders
------------------------------------------------------------------------- */

void Image::draw_box(double (*corners)[3], double *color, double diameter)
{
  draw_cylinder(corners[0],corners[1],color,diameter,3);
  draw_cylinder(corners[2],corners[3],color,diameter,3);
  draw_cylinder(corners[0],corners[2],color,diameter,3);
  draw_cylinder(corners[1],corners[3],color,diameter,3);
  draw_cylinder(corners[0],corners[4],color,diameter,3);
  draw_cylinder(corners[1],corners[5],color,diameter,3);
  draw_cylinder(corners[2],corners[6],color,diameter,3);
  draw_cylinder(corners[3],corners[7],color,diameter,3);
  draw_cylinder(corners[4],corners[5],color,diameter,3);
  draw_cylinder(corners[6],corners[7],color,diameter,3);
  draw_cylinder(corners[4],corners[6],color,diameter,3);
  draw_cylinder(corners[5],corners[7],color,diameter,3);
}

/* ----------------------------------------------------------------------
   draw outline of a 2d rectangle as 4 cylinders
------------------------------------------------------------------------- */

void Image::draw_box2d(double (*corners)[3], double *color, double diameter)
{
  draw_cylinder(corners[0],corners[1],color,diameter,3);
  draw_cylinder(corners[2],corners[3],color,diameter,3);
  draw_cylinder(corners[0],corners[2],color,diameter,3);
  draw_cylinder(corners[1],corners[3],color,diameter,3);
}

/* ----------------------------------------------------------------------
   draw XYZ axes in red/green/blue
   axes = 4 end points
------------------------------------------------------------------------- */

void Image::draw_axes(double (*axes)[3], double diameter)
{
  draw_cylinder(axes[0],axes[1],color2rgb("red"),diameter,3);
  draw_cylinder(axes[0],axes[2],color2rgb("green"),diameter,3);
  draw_cylinder(axes[0],axes[3],color2rgb("blue"),diameter,3);
}

/* ----------------------------------------------------------------------
   draw sphere at x with surfaceColor and diameter
   render pixel by pixel onto image plane with depth buffering
------------------------------------------------------------------------- */

void Image::draw_sphere(double *x, double *surfaceColor, double diameter)
{
  int ix,iy;
  double projRad;
  double xlocal[3],surface[3];
  double depth;

  xlocal[0] = x[0] - xctr;
  xlocal[1] = x[1] - yctr;
  xlocal[2] = x[2] - zctr;

  double xmap = MathExtra::dot3(camRight,xlocal);
  double ymap = MathExtra::dot3(camUp,xlocal);
  double dist = MathExtra::dot3(camPos,camDir) - MathExtra::dot3(xlocal,camDir);

  double radius = 0.5*diameter;
  double radsq = radius*radius;
  double pixelWidth = (tanPerPixel > 0) ? tanPerPixel * dist :
    -tanPerPixel / zoom;
  double pixelRadiusFull = radius / pixelWidth;
  int pixelRadius = static_cast<int> (pixelRadiusFull + 0.5) + 1;

  double xf = xmap / pixelWidth;
  double yf = ymap / pixelWidth;
  int xc = static_cast<int> (xf);
  int yc = static_cast<int> (yf);
  double width_error = xf - xc;
  double height_error = yf - yc;

  // shift 0,0 to screen center (vs lower left)

  xc += width / 2;
  yc += height / 2;

  for (iy = yc - pixelRadius; iy <= yc + pixelRadius; iy++) {
    for (ix = xc - pixelRadius; ix <= xc + pixelRadius; ix++) {
      if (iy < 0 || iy >= height || ix < 0 || ix >= width) continue;

      surface[1] = ((iy - yc) - height_error) * pixelWidth;
      surface[0] = ((ix - xc) - width_error) * pixelWidth;
      projRad = surface[0]*surface[0] + surface[1]*surface[1];

      // outside the sphere in the projected image

      if (projRad > radsq) continue;
      surface[2] = sqrt(radsq - projRad);
      depth = dist - surface[2];

      surface[0] /= radius;
      surface[1] /= radius;
      surface[2] /= radius;

      draw_pixel (ix, iy, depth, surface, surfaceColor);
    }
  }
}

/* ----------------------------------------------------------------------
   draw axis oriented brick at x with surfaceColor and diameter[3] in extent
   render pixel by pixel onto image plane with depth buffering
------------------------------------------------------------------------- */

void Image::draw_brick(double *x, double *surfaceColor, double *diameter)
{
  double xlocal[3],surface[3],normal[3];
  double t,tdir[3];
  double depth;

  xlocal[0] = x[0] - xctr;
  xlocal[1] = x[1] - yctr;
  xlocal[2] = x[2] - zctr;

  double xmap = MathExtra::dot3(camRight,xlocal);
  double ymap = MathExtra::dot3(camUp,xlocal);
  double dist = MathExtra::dot3(camPos,camDir) - MathExtra::dot3(xlocal,camDir);

  double radius[3];
  radius[0] = 0.5*diameter[0];
  radius[1] = 0.5*diameter[1];
  radius[2] = 0.5*diameter[2];

  double pixelWidth = (tanPerPixel > 0) ? tanPerPixel * dist :
    -tanPerPixel / zoom;

  double halfWidth = MAX(diameter[0],diameter[1]);
  halfWidth = MAX(halfWidth,diameter[2]);
  double pixelHalfWidthFull = halfWidth / pixelWidth;
  int pixelHalfWidth = static_cast<int> (pixelHalfWidthFull + 0.5);

  double xf = xmap / pixelWidth;
  double yf = ymap / pixelWidth;
  int xc = static_cast<int> (xf);
  int yc = static_cast<int> (yf);
  double width_error = xf - xc;
  double height_error = yf - yc;

  // shift 0,0 to screen center (vs lower left)

  xc += width / 2;
  yc += height / 2;

  for (int iy = yc - pixelHalfWidth; iy <= yc + pixelHalfWidth; iy ++) {
    for (int ix = xc - pixelHalfWidth; ix <= xc + pixelHalfWidth; ix ++) {
      if (iy < 0 || iy >= height || ix < 0 || ix >= width) continue;

      double sy = ((iy - yc) - height_error) * pixelWidth;
      double sx = ((ix - xc) - width_error) * pixelWidth;
      surface[0] = camRight[0] * sx + camUp[0] * sy;
      surface[1] = camRight[1] * sx + camUp[1] * sy;
      surface[2] = camRight[2] * sx + camUp[2] * sy;

      // iterate through each of the 6 axis-oriented planes of the box
      // only render up to 3 which are facing the camera
      // these checks short circuit a dot product, testing for > 0

      for (int dim = 0; dim < 3; dim ++) {
        if (camDir[dim] > 0) {          // positive faces camera
          t = (radius[dim] - surface[dim]) / camDir[dim];
          normal[0] = camRight[dim];
          normal[1] = camUp[dim];
          normal[2] = camDir[dim];
        } else if (camDir[dim] < 0) {   // negative faces camera
          t = -(radius[dim] + surface[dim]) / camDir[dim];
          normal[0] = -camRight[dim];
          normal[1] = -camUp[dim];
          normal[2] = -camDir[dim];
        }
        if (camDir[dim] != 0) {
          tdir[0] = camDir[0] * t;
          tdir[1] = camDir[1] * t;
          tdir[2] = camDir[2] * t;

          bool xin = ((surface[0]+tdir[0]) >= -radius[0]) &&
            ((surface[0]+tdir[0]) <= radius[0]);
          bool yin = ((surface[1]+tdir[1]) >= -radius[1]) &&
            ((surface[1]+tdir[1]) <= radius[1]);
          bool zin = ((surface[2]+tdir[2]) >= -radius[2]) &&
            ((surface[2]+tdir[2]) <= radius[2]);

          switch (dim) {
          case 0:
            if (yin & zin) {
              depth = dist - t;
              draw_pixel (ix, iy, depth, normal, surfaceColor);
            }
            break;
          case 1:
            if (xin & zin) {
              depth = dist - t;
              draw_pixel (ix, iy, depth, normal, surfaceColor);
            }
            break;
          case 2:
            if (xin & yin) {
              depth = dist - t;
              draw_pixel (ix, iy, depth, normal, surfaceColor);
            }
            break;
          }
        }
      }
    }
  }
}

/* ----------------------------------------------------------------------
   draw cylinder from x to y with surfaceColor and diameter
   render pixel by pixel onto image plane with depth buffering
   if sflag = 0, draw no end spheres
   if sflag = 1, draw 1st end sphere
   if sflag = 2, draw 2nd end sphere
   if sflag = 3, draw both end spheres
------------------------------------------------------------------------- */

void Image::draw_cylinder(double *x, double *y,
                          double *surfaceColor, double diameter, int sflag)
{
  double surface[3], normal[3];
  double mid[3],xaxis[3],yaxis[3],zaxis[3];
  double camLDir[3], camLRight[3], camLUp[3];
  double zmin, zmax;

  if (sflag % 2) draw_sphere(x,surfaceColor,diameter);
  if (sflag/2) draw_sphere(y,surfaceColor,diameter);

  double radius = 0.5*diameter;
  double radsq = radius*radius;

  zaxis[0] = y[0] - x[0];
  zaxis[1] = y[1] - x[1];
  zaxis[2] = y[2] - x[2];

  double rasterWidth = fabs(MathExtra::dot3(zaxis, camRight)) + diameter;
  double rasterHeight = fabs(MathExtra::dot3(zaxis, camUp)) + diameter;

  mid[0] = (y[0] + x[0]) * 0.5 - xctr;
  mid[1] = (y[1] + x[1]) * 0.5 - yctr;
  mid[2] = (y[2] + x[2]) * 0.5 - zctr;

  double len = MathExtra::len3(zaxis);
  MathExtra::scale3(1.0/len,zaxis);
  len *= 0.5;
  zmax = len;
  zmin = -len;

  double xmap = MathExtra::dot3(camRight,mid);
  double ymap = MathExtra::dot3(camUp,mid);
  double dist = MathExtra::dot3(camPos,camDir) - MathExtra::dot3(mid,camDir);

  double pixelWidth = (tanPerPixel > 0) ? tanPerPixel * dist :
    -tanPerPixel / zoom;

  double xf = xmap / pixelWidth;
  double yf = ymap / pixelWidth;
  int xc = static_cast<int> (xf);
  int yc = static_cast<int> (yf);
  double width_error = xf - xc;
  double height_error = yf - yc;

  // shift 0,0 to screen center (vs lower left)

  xc += width / 2;
  yc += height / 2;

  double pixelHalfWidthFull = (rasterWidth * 0.5) / pixelWidth;
  double pixelHalfHeightFull = (rasterHeight * 0.5) / pixelWidth;
  int pixelHalfWidth = static_cast<int> (pixelHalfWidthFull + 0.5);
  int pixelHalfHeight = static_cast<int> (pixelHalfHeightFull + 0.5);

  if (zaxis[0] == camDir[0] && zaxis[1] == camDir[1] && zaxis[2] == camDir[2])
    return;
  if (zaxis[0] == -camDir[0] && zaxis[1] == -camDir[1] &&
      zaxis[2] == -camDir[2]) return;

  MathExtra::cross3(zaxis,camDir,yaxis);
  MathExtra::norm3(yaxis);
  MathExtra::cross3(yaxis,zaxis,xaxis);
  MathExtra::norm3(xaxis);

  camLDir[0] = MathExtra::dot3(camDir,xaxis);
  camLDir[1] = 0.0;
  camLDir[2] = MathExtra::dot3(camDir,zaxis);

  camLRight[0] = MathExtra::dot3(camRight,xaxis);
  camLRight[1] = MathExtra::dot3(camRight,yaxis);
  camLRight[2] = MathExtra::dot3(camRight,zaxis);
  MathExtra::norm3(camLRight);

  camLUp[0] = MathExtra::dot3(camUp,xaxis);
  camLUp[1] = MathExtra::dot3(camUp,yaxis);
  camLUp[2] = MathExtra::dot3(camUp,zaxis);
  MathExtra::norm3(camLUp);

  double a = camLDir[0] * camLDir[0];

  for (int iy = yc - pixelHalfHeight; iy <= yc + pixelHalfHeight; iy ++) {
    for (int ix = xc - pixelHalfWidth; ix <= xc + pixelHalfWidth; ix ++) {
      if (iy < 0 || iy >= height || ix < 0 || ix >= width) continue;

      double sy = ((iy - yc) - height_error) * pixelWidth;
      double sx = ((ix - xc) - width_error) * pixelWidth;
      surface[0] = camLRight[0] * sx + camLUp[0] * sy;
      surface[1] = camLRight[1] * sx + camLUp[1] * sy;
      surface[2] = camLRight[2] * sx + camLUp[2] * sy;

      double b = 2 * camLDir[0] * surface[0];
      double c = surface[0] * surface[0] + surface[1] * surface[1] - radsq;

      double partial = b*b - 4*a*c;
      if (partial < 0) continue;
      partial = sqrt (partial);

      double t = (-b + partial) / (2*a);
      double t2 = (-b - partial) / (2*a);
      if (t2 > t) { t = t2; }

      surface[0] += t * camLDir[0];
      surface[1] += t * camLDir[1];
      surface[2] += t * camLDir[2];

      if (surface[2] > zmax || surface[2] < zmin) continue;

      // convert surface into the surface normal

      normal[0] = surface[0] / radius;
      normal[1] = surface[1] / radius;
      normal[2] = 0.0;

      // in camera space

      surface[0] = MathExtra::dot3 (normal, camLRight);
      surface[1] = MathExtra::dot3 (normal, camLUp);
      surface[2] = MathExtra::dot3 (normal, camLDir);

      double depth = dist - t;
      draw_pixel (ix, iy, depth, surface, surfaceColor);
    }
  }
}

/* ----------------------------------------------------------------------
   draw triangle with 3 corner points x,y,z and surfaceColor
------------------------------------------------------------------------- */

void Image::draw_triangle(double *x, double *y, double *z, double *surfaceColor)
{
  double d1[3], d1len, d2[3], d2len, normal[3], invndotd;
  double xlocal[3], ylocal[3], zlocal[3];
  double surface[3];
  double depth;

  xlocal[0] = x[0] - xctr;
  xlocal[1] = x[1] - yctr;
  xlocal[2] = x[2] - zctr;
  ylocal[0] = y[0] - xctr;
  ylocal[1] = y[1] - yctr;
  ylocal[2] = y[2] - zctr;
  zlocal[0] = z[0] - xctr;
  zlocal[1] = z[1] - yctr;
  zlocal[2] = z[2] - zctr;

  MathExtra::sub3 (xlocal, ylocal, d1);
  d1len = MathExtra::len3 (d1);
  MathExtra::scale3 (1.0 / d1len, d1);
  MathExtra::sub3 (zlocal, ylocal, d2);
  d2len = MathExtra::len3 (d2);
  MathExtra::scale3 (1.0 / d2len, d2);

  MathExtra::cross3 (d1, d2, normal);
  MathExtra::norm3 (normal);

  // ----------------
  // old code

  //invndotd = 1.0 / MathExtra::dot3(normal, camDir);

  // invalid triangle (parallel)

  //if (invndotd == 0) return;

  // ----------------
  // new code

  double ndotd = MathExtra::dot3(normal,camDir);
  if (ndotd >= 0.0) return;
  invndotd = 1.0 / ndotd;

  // ----------------

  double r[3],u[3];

  r[0] = MathExtra::dot3(camRight,xlocal);
  r[1] = MathExtra::dot3(camRight,ylocal);
  r[2] = MathExtra::dot3(camRight,zlocal);

  u[0] = MathExtra::dot3(camUp,xlocal);
  u[1] = MathExtra::dot3(camUp,ylocal);
  u[2] = MathExtra::dot3(camUp,zlocal);

  double rasterLeft = r[0] - MIN(r[0],MIN(r[1],r[2]));
  double rasterRight = MAX(r[0],MAX(r[1],r[2])) - r[0];
  double rasterDown = u[0] - MIN(u[0],MIN(u[1],u[2]));
  double rasterUp = MAX(u[0],MAX(u[1],u[2])) - u[0];

  double xmap = MathExtra::dot3(camRight,xlocal);
  double ymap = MathExtra::dot3(camUp,xlocal);
  double dist = MathExtra::dot3(camPos,camDir) - MathExtra::dot3(xlocal,camDir);

  double pixelWidth = (tanPerPixel > 0) ? tanPerPixel * dist :
    -tanPerPixel / zoom;

  double xf = xmap / pixelWidth;
  double yf = ymap / pixelWidth;
  int xc = static_cast<int> (xf);
  int yc = static_cast<int> (yf);
  double width_error = xf - xc;
  double height_error = yf - yc;

  // shift 0,0 to screen center (vs lower left)

  xc += width / 2;
  yc += height / 2;

  double pixelLeftFull = rasterLeft / pixelWidth;
  double pixelRightFull = rasterRight / pixelWidth;
  double pixelDownFull = rasterDown / pixelWidth;
  double pixelUpFull = rasterUp / pixelWidth;

  //old
  //int pixelLeft = static_cast<int> (pixelLeftFull + 0.5);
  //int pixelRight = static_cast<int> (pixelRightFull + 0.5);
  //int pixelDown = static_cast<int> (pixelDownFull + 0.5);
  //int pixelUp = static_cast<int> (pixelUpFull + 0.5);

  // new
  int pixelLeft = static_cast<int> (pixelLeftFull + 1.0);
  int pixelRight = static_cast<int> (pixelRightFull + 1.0);
  int pixelDown = static_cast<int> (pixelDownFull + 1.0);
  int pixelUp = static_cast<int> (pixelUpFull + 1.0);

  for (int iy = yc - pixelDown; iy <= yc + pixelUp; iy ++) {
    for (int ix = xc - pixelLeft; ix <= xc + pixelRight; ix ++) {
      if (iy < 0 || iy >= height || ix < 0 || ix >= width) continue;

      double sy = ((iy - yc) - height_error) * pixelWidth;
      double sx = ((ix - xc) - width_error) * pixelWidth;
      surface[0] = camRight[0] * sx + camUp[0] * sy;
      surface[1] = camRight[1] * sx + camUp[1] * sy;
      surface[2] = camRight[2] * sx + camUp[2] * sy;

      double t = -MathExtra::dot3(normal,surface) * invndotd;

      // test inside the triangle

      double p[3];
      p[0] = xlocal[0] + surface[0] + camDir[0] * t;
      p[1] = xlocal[1] + surface[1] + camDir[1] * t;
      p[2] = xlocal[2] + surface[2] + camDir[2] * t;

      double s1[3], s2[3], s3[3];
      double c1[3], c2[3];

      MathExtra::sub3 (zlocal, xlocal, s1);
      MathExtra::sub3 (ylocal, xlocal, s2);
      MathExtra::sub3 (p, xlocal, s3);
      MathExtra::cross3 (s1, s2, c1);
      MathExtra::cross3 (s1, s3, c2);
      if (MathExtra::dot3 (c1, c2) <= 0) continue;

      MathExtra::sub3 (xlocal, ylocal, s1);
      MathExtra::sub3 (zlocal, ylocal, s2);
      MathExtra::sub3 (p, ylocal, s3);
      MathExtra::cross3 (s1, s2, c1);
      MathExtra::cross3 (s1, s3, c2);
      if (MathExtra::dot3 (c1, c2) <= 0) continue;

      MathExtra::sub3 (ylocal, zlocal, s1);
      MathExtra::sub3 (xlocal, zlocal, s2);
      MathExtra::sub3 (p, zlocal, s3);
      MathExtra::cross3 (s1, s2, c1);
      MathExtra::cross3 (s1, s3, c2);
      if (MathExtra::dot3 (c1, c2) <= 0) continue;

      double cNormal[3];
      cNormal[0] = MathExtra::dot3(camRight, normal);
      cNormal[1] = MathExtra::dot3(camUp, normal);
      cNormal[2] = MathExtra::dot3(camDir, normal);

      depth = dist - t;
      draw_pixel(ix,iy,depth,cNormal,surfaceColor);
    }
  }
}

/* ---------------------------------------------------------------------- */

void Image::draw_pixel(int ix, int iy, double depth,
                           double *surface, double *surfaceColor)
{
  double diffuseKey,diffuseFill,diffuseBack,specularKey;
  if (depth < 0 || (depthBuffer[ix + iy*width] >= 0 &&
                    depth >= depthBuffer[ix + iy*width])) return;
  depthBuffer[ix + iy*width] = depth;

  // store only the tangent relative to the camera normal (0,0,-1)

  surfaceBuffer[0 + ix * 2 + iy*width * 2] = surface[1];
  surfaceBuffer[1 + ix * 2 + iy*width * 2] = -surface[0];

  diffuseKey = saturate(MathExtra::dot3(surface, keyLightDir));
  diffuseFill = saturate(MathExtra::dot3(surface, fillLightDir));
  diffuseBack = saturate(MathExtra::dot3(surface, backLightDir));
  specularKey = pow(saturate(MathExtra::dot3(surface, keyHalfDir)),
                    specularHardness) * specularIntensity;

  double c[3];
  c[0] = surfaceColor[0] * ambientColor[0];
  c[1] = surfaceColor[1] * ambientColor[1];
  c[2] = surfaceColor[2] * ambientColor[2];

  c[0] += surfaceColor[0] * keyLightColor[0] * diffuseKey;
  c[1] += surfaceColor[1] * keyLightColor[1] * diffuseKey;
  c[2] += surfaceColor[2] * keyLightColor[2] * diffuseKey;

  c[0] += keyLightColor[0] * specularKey;
  c[1] += keyLightColor[1] * specularKey;
  c[2] += keyLightColor[2] * specularKey;

  c[0] += surfaceColor[0] * fillLightColor[0] * diffuseFill;
  c[1] += surfaceColor[1] * fillLightColor[1] * diffuseFill;
  c[2] += surfaceColor[2] * fillLightColor[2] * diffuseFill;

  c[0] += surfaceColor[0] * backLightColor[0] * diffuseBack;
  c[1] += surfaceColor[1] * backLightColor[1] * diffuseBack;
  c[2] += surfaceColor[2] * backLightColor[2] * diffuseBack;

  c[0] = saturate(c[0]);
  c[1] = saturate(c[1]);
  c[2] = saturate(c[2]);

  imageBuffer[0 + ix*3 + iy*width*3] = static_cast<int>(c[0] * 255.0);
  imageBuffer[1 + ix*3 + iy*width*3] = static_cast<int>(c[1] * 255.0);
  imageBuffer[2 + ix*3 + iy*width*3] = static_cast<int>(c[2] * 255.0);
}

/* ---------------------------------------------------------------------- */

void Image::compute_SSAO()
{
  // used for rasterizing the spheres

  double delTheta = 2.0*MY_PI / SSAOSamples;

  // typical neighborhood value for shading

  double pixelWidth = (tanPerPixel > 0) ? tanPerPixel :
        -tanPerPixel / zoom;
  int pixelRadius = (int) trunc (SSAORadius / pixelWidth + 0.5);

  // each proc is assigned a subset of contiguous pixels from the full image
  // pixels are contiguous in x (columns within a row), then by row
  // index = pixels from 0 to npixel-1
  // x = column # from 0 to width-1
  // y = row # from 0 to height-1

  int pixelstart = static_cast<int> (1.0*me/nprocs * npixels);
  int pixelstop = static_cast<int> (1.0*(me+1)/nprocs * npixels);

  for (int index = pixelstart; index < pixelstop; index++) {
    int x = index % width;
    int y = index / width;

    double cdepth = depthBuffer[index];
    if (cdepth < 0) { continue; }

    double sx = surfaceBuffer[index * 2 + 0];
    double sy = surfaceBuffer[index * 2 + 1];
    double sin_t = -sqrt(sx*sx + sy*sy);

    // DEBUG - remove randomness so image is same on any proc count
    //double mytheta = 0.5 * SSAOJitter;
    double mytheta = random->uniform() * SSAOJitter;
    double ao = 0.0;

    for (int s = 0; s < SSAOSamples; s ++) {
      double hx = cos(mytheta);
      double hy = sin(mytheta);
      mytheta += delTheta;

      // multiply by z cross surface tangent
      // so that dot (aka cos) works here

      double scaled_sin_t = sin_t * (hx*sy + hy*sx);

      // Bresenham's line algorithm to march over depthBuffer

      int dx = static_cast<int> (hx * pixelRadius);
      int dy = static_cast<int> (hy * pixelRadius);
      int ex = x + dx;
      if (ex < 0) { ex = 0; } if (ex >= width) { ex = width - 1; }
      int ey = y + dy;
      if (ey < 0) { ey = 0; } if (ey >= height) { ey = height - 1; }
      double delta;
      int small, large;
      double lenIncr;
      if (fabs(hx) > fabs(hy)) {
        small = (hx > 0) ? 1 : -1;
        large = (hy > 0) ? width : -width;
        delta = fabs(hy / hx);
      } else {
        small = (hy > 0) ? width : -width;
        large = (hx > 0) ? 1 : -1;
        delta = fabs(hx / hy);
      }
      lenIncr = sqrt (1 + delta * delta) * pixelWidth;

      // initialize with one step
      // because the center point doesn't need testing

      int end = ex + ey * width;
      int ind = index + small;
      double len = lenIncr;
      double err = delta;
      if (err >= 1.0) {
        ind += large;
        err -= 1.0;
      }

      double minPeak = -1;
      double peakLen = 0.0;
      int stepsTaken = 1;
      while ((small > 0 && ind <= end) || (small < 0 && ind >= end)) {
        if (ind < 0 || ind >= (width*height)) {
          break;
        }

        // cdepth - depthBuffer B/C we want it in the negative z direction

        if (minPeak < 0 || (depthBuffer[ind] >= 0 &&
                            depthBuffer[ind] < minPeak)) {
          minPeak = depthBuffer[ind];
          peakLen = len;
        }
        ind += small;
        len += lenIncr;
        err += delta;
        if (err >= 1.0) {
          ind += large;
          err -= 1.0;
        }
        stepsTaken ++;
      }

      if (peakLen > 0) {
        double h = atan ((cdepth - minPeak) / peakLen);
        ao += saturate(sin (h) - scaled_sin_t);
      } else {
        ao += saturate(-scaled_sin_t);
      }
    }
    ao /= (double)SSAOSamples;

    double c[3];
    c[0] = (double) (*(unsigned char *) &imageBuffer[index * 3 + 0]);
    c[1] = (double) (*(unsigned char *) &imageBuffer[index * 3 + 1]);
    c[2] = (double) (*(unsigned char *) &imageBuffer[index * 3 + 2]);
    c[0] *= (1.0 - ao);
    c[1] *= (1.0 - ao);
    c[2] *= (1.0 - ao);
    imageBuffer[index * 3 + 0] = (int) c[0];
    imageBuffer[index * 3 + 1] = (int) c[1];
    imageBuffer[index * 3 + 2] = (int) c[2];
  }
}

/* ---------------------------------------------------------------------- */

#ifdef SPARTA_JPEG
void Image::write_JPG(FILE *fp)
{
  struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  JSAMPROW row_pointer;

  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_compress(&cinfo);
  jpeg_stdio_dest(&cinfo,fp);
  cinfo.image_width = width;
  cinfo.image_height = height;
  cinfo.input_components = 3;
  cinfo.in_color_space = JCS_RGB;

  jpeg_set_defaults(&cinfo);
  jpeg_set_quality(&cinfo,85,TRUE);
  jpeg_start_compress(&cinfo,TRUE);

  while (cinfo.next_scanline < cinfo.image_height) {
    row_pointer = (JSAMPROW)
      &writeBuffer[(cinfo.image_height - 1 - cinfo.next_scanline) * 3 * width];
    jpeg_write_scanlines(&cinfo,&row_pointer,1);
  }

  jpeg_finish_compress(&cinfo);
  jpeg_destroy_compress(&cinfo);
}
#else
void Image::write_JPG(FILE*)
{
}
#endif

/* ---------------------------------------------------------------------- */

#ifdef SPARTA_PNG
void Image::write_PNG(FILE *fp)
{
  png_structp png_ptr;
  png_infop info_ptr;

  png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

  if (!png_ptr) return;

  info_ptr = png_create_info_struct(png_ptr);
  if (!info_ptr) {
    png_destroy_write_struct(&png_ptr, NULL);
    return;
  }

  if (setjmp(png_jmpbuf(png_ptr))) {
    png_destroy_write_struct(&png_ptr, &info_ptr);
    return;
  }

  png_init_io(png_ptr, fp);
  png_set_compression_level(png_ptr,Z_BEST_COMPRESSION);
  png_set_IHDR(png_ptr,info_ptr,width,height,8,PNG_COLOR_TYPE_RGB,
    PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);

  png_text text_ptr[2];
  memset(text_ptr,0,2*sizeof(png_text));

  char key0[]  = "Software";
  char text0[] = "SPARTA " SPARTA_VERSION;
  char key1[]  = "Description";
  char text1[] = "Dump image snapshot";
  text_ptr[0].key = key0;
  text_ptr[0].text = text0;
  text_ptr[1].key = key1;
  text_ptr[1].text = text1;
  text_ptr[0].compression = PNG_TEXT_COMPRESSION_NONE;
  text_ptr[1].compression = PNG_TEXT_COMPRESSION_NONE;

  png_set_text(png_ptr,info_ptr,text_ptr,1);
  png_write_info(png_ptr,info_ptr);

  png_bytep row_pointers[height];
  for (int i=0; i < height; ++i)
    row_pointers[i] = (png_bytep) &writeBuffer[(height-i-1)*3*width];

  png_write_image(png_ptr, row_pointers);
  png_write_end(png_ptr, info_ptr);

  png_destroy_write_struct(&png_ptr, &info_ptr);
}
#else
void Image::write_PNG(FILE *)
{
}
#endif

/* ---------------------------------------------------------------------- */

void Image::write_PPM(FILE *fp)
{
  fprintf(fp,"P6\n%d %d\n255\n",width,height);

  int y;
  for (y = height-1; y >= 0; y--)
    fwrite(&writeBuffer[y*width*3],3,width,fp);
}

/* ----------------------------------------------------------------------
   return static/dynamic status of color map index
------------------------------------------------------------------------- */

int Image::map_dynamic(int index)
{
  return maps[index]->dynamic;
}

/* ----------------------------------------------------------------------
   redefine properties of the color map index
   return 1 if any error in args, else return 0
------------------------------------------------------------------------- */

int Image::map_reset(int index, int narg, char **arg)
{
  return maps[index]->reset(narg,arg);
}

/* ----------------------------------------------------------------------
   set min/max bounds of dynamic color map index
------------------------------------------------------------------------- */

int Image::map_minmax(int index, double mindynamic, double maxdynamic)
{
  return maps[index]->minmax(mindynamic,maxdynamic);
}

/* ----------------------------------------------------------------------
   return 3-vector color corresponding to value from color map index
------------------------------------------------------------------------- */

double *Image::map_value2color(int index, double value)
{
  return maps[index]->value2color(value);
}

/* ----------------------------------------------------------------------
   add a new color to username and userrgb
   redefine RGB values in userrgb if name already exists
   return 1 if RGB values are invalid, else return 0
------------------------------------------------------------------------- */

int Image::addcolor(char *name, double r, double g, double b)
{
  int icolor;
  for (icolor = 0; icolor < ncolors; icolor++)
    if (strcmp(name,username[icolor]) == 0) break;

  if (icolor == ncolors) {
    username = (char **)
      memory->srealloc(username,(ncolors+1)*sizeof(char *),"image:username");
    memory->grow(userrgb,ncolors+1,3,"image:userrgb");
    ncolors++;
  }

  int n = strlen(name) + 1;
  username[icolor] = new char[n];
  strcpy(username[icolor],name);

  if (r < 0.0 || r > 1.0 || g < 0.0 || g > 1.0 || b < 0.0 || b > 1.0)
    return 1;

  userrgb[icolor][0] = r;
  userrgb[icolor][1] = g;
  userrgb[icolor][2] = b;

  return 0;
}

/* ----------------------------------------------------------------------
   if index > 0, return ptr to index-1 color from rgb
   if index < 0, return ptr to -index-1 color from userrgb
   if index = 0, search the 2 lists of color names for the string color
   search user-defined color names first, then the list of NCOLORS names
   return a pointer to the 3 floating point RGB values or NULL if didn't find
------------------------------------------------------------------------- */

double *Image::color2rgb(const char *color, int index)
{
  static const char *name[NCOLORS] = {
    "aliceblue",
    "antiquewhite",
    "aqua",
    "aquamarine",
    "azure",
    "beige",
    "bisque",
    "black",
    "blanchedalmond",
    "blue",
    "blueviolet",
    "brown",
    "burlywood",
    "cadetblue",
    "chartreuse",
    "chocolate",
    "coral",
    "cornflowerblue",
    "cornsilk",
    "crimson",
    "cyan",
    "darkblue",
    "darkcyan",
    "darkgoldenrod",
    "darkgray",
    "darkgreen",
    "darkkhaki",
    "darkmagenta",
    "darkolivegreen",
    "darkorange",
    "darkorchid",
    "darkred",
    "darksalmon",
    "darkseagreen",
    "darkslateblue",
    "darkslategray",
    "darkturquoise",
    "darkviolet",
    "deeppink",
    "deepskyblue",
    "dimgray",
    "dodgerblue",
    "firebrick",
    "floralwhite",
    "forestgreen",
    "fuchsia",
    "gainsboro",
    "ghostwhite",
    "gold",
    "goldenrod",
    "gray",
    "green",
    "greenyellow",
    "honeydew",
    "hotpink",
    "indianred",
    "indigo",
    "ivory",
    "khaki",
    "lavender",
    "lavenderblush",
    "lawngreen",
    "lemonchiffon",
    "lightblue",
    "lightcoral",
    "lightcyan",
    "lightgoldenrodyellow",
    "lightgreen",
    "lightgrey",
    "lightpink",
    "lightsalmon",
    "lightseagreen",
    "lightskyblue",
    "lightslategray",
    "lightsteelblue",
    "lightyellow",
    "lime",
    "limegreen",
    "linen",
    "magenta",
    "maroon",
    "mediumaquamarine",
    "mediumblue",
    "mediumorchid",
    "mediumpurple",
    "mediumseagreen",
    "mediumslateblue",
    "mediumspringgreen",
    "mediumturquoise",
    "mediumvioletred",
    "midnightblue",
    "mintcream",
    "mistyrose",
    "moccasin",
    "navajowhite",
    "navy",
    "oldlace",
    "olive",
    "olivedrab",
    "orange",
    "orangered",
    "orchid",
    "palegoldenrod",
    "palegreen",
    "paleturquoise",
    "palevioletred",
    "papayawhip",
    "peachpuff",
    "peru",
    "pink",
    "plum",
    "powderblue",
    "purple",
    "red",
    "rosybrown",
    "royalblue",
    "saddlebrown",
    "salmon",
    "sandybrown",
    "seagreen",
    "seashell",
    "sienna",
    "silver",
    "skyblue",
    "slateblue",
    "slategray",
    "snow",
    "springgreen",
    "steelblue",
    "tan",
    "teal",
    "thistle",
    "tomato",
    "turquoise",
    "violet",
    "wheat",
    "white",
    "whitesmoke",
    "yellow",
    "yellowgreen"
  };

  static double rgb[NCOLORS][3] = {
    {240/255.0, 248/255.0, 255/255.0},
    {250/255.0, 235/255.0, 215/255.0},
    {0/255.0, 255/255.0, 255/255.0},
    {127/255.0, 255/255.0, 212/255.0},
    {240/255.0, 255/255.0, 255/255.0},
    {245/255.0, 245/255.0, 220/255.0},
    {255/255.0, 228/255.0, 196/255.0},
    {0/255.0, 0/255.0, 0/255.0},
    {255/255.0, 255/255.0, 205/255.0},
    {0/255.0, 0/255.0, 255/255.0},
    {138/255.0, 43/255.0, 226/255.0},
    {165/255.0, 42/255.0, 42/255.0},
    {222/255.0, 184/255.0, 135/255.0},
    {95/255.0, 158/255.0, 160/255.0},
    {127/255.0, 255/255.0, 0/255.0},
    {210/255.0, 105/255.0, 30/255.0},
    {255/255.0, 127/255.0, 80/255.0},
    {100/255.0, 149/255.0, 237/255.0},
    {255/255.0, 248/255.0, 220/255.0},
    {220/255.0, 20/255.0, 60/255.0},
    {0/255.0, 255/255.0, 255/255.0},
    {0/255.0, 0/255.0, 139/255.0},
    {0/255.0, 139/255.0, 139/255.0},
    {184/255.0, 134/255.0, 11/255.0},
    {169/255.0, 169/255.0, 169/255.0},
    {0/255.0, 100/255.0, 0/255.0},
    {189/255.0, 183/255.0, 107/255.0},
    {139/255.0, 0/255.0, 139/255.0},
    {85/255.0, 107/255.0, 47/255.0},
    {255/255.0, 140/255.0, 0/255.0},
    {153/255.0, 50/255.0, 204/255.0},
    {139/255.0, 0/255.0, 0/255.0},
    {233/255.0, 150/255.0, 122/255.0},
    {143/255.0, 188/255.0, 143/255.0},
    {72/255.0, 61/255.0, 139/255.0},
    {47/255.0, 79/255.0, 79/255.0},
    {0/255.0, 206/255.0, 209/255.0},
    {148/255.0, 0/255.0, 211/255.0},
    {255/255.0, 20/255.0, 147/255.0},
    {0/255.0, 191/255.0, 255/255.0},
    {105/255.0, 105/255.0, 105/255.0},
    {30/255.0, 144/255.0, 255/255.0},
    {178/255.0, 34/255.0, 34/255.0},
    {255/255.0, 250/255.0, 240/255.0},
    {34/255.0, 139/255.0, 34/255.0},
    {255/255.0, 0/255.0, 255/255.0},
    {220/255.0, 220/255.0, 220/255.0},
    {248/255.0, 248/255.0, 255/255.0},
    {255/255.0, 215/255.0, 0/255.0},
    {218/255.0, 165/255.0, 32/255.0},
    {128/255.0, 128/255.0, 128/255.0},
    {0/255.0, 128/255.0, 0/255.0},
    {173/255.0, 255/255.0, 47/255.0},
    {240/255.0, 255/255.0, 240/255.0},
    {255/255.0, 105/255.0, 180/255.0},
    {205/255.0, 92/255.0, 92/255.0},
    {75/255.0, 0/255.0, 130/255.0},
    {255/255.0, 240/255.0, 240/255.0},
    {240/255.0, 230/255.0, 140/255.0},
    {230/255.0, 230/255.0, 250/255.0},
    {255/255.0, 240/255.0, 245/255.0},
    {124/255.0, 252/255.0, 0/255.0},
    {255/255.0, 250/255.0, 205/255.0},
    {173/255.0, 216/255.0, 230/255.0},
    {240/255.0, 128/255.0, 128/255.0},
    {224/255.0, 255/255.0, 255/255.0},
    {250/255.0, 250/255.0, 210/255.0},
    {144/255.0, 238/255.0, 144/255.0},
    {211/255.0, 211/255.0, 211/255.0},
    {255/255.0, 182/255.0, 193/255.0},
    {255/255.0, 160/255.0, 122/255.0},
    {32/255.0, 178/255.0, 170/255.0},
    {135/255.0, 206/255.0, 250/255.0},
    {119/255.0, 136/255.0, 153/255.0},
    {176/255.0, 196/255.0, 222/255.0},
    {255/255.0, 255/255.0, 224/255.0},
    {0/255.0, 255/255.0, 0/255.0},
    {50/255.0, 205/255.0, 50/255.0},
    {250/255.0, 240/255.0, 230/255.0},
    {255/255.0, 0/255.0, 255/255.0},
    {128/255.0, 0/255.0, 0/255.0},
    {102/255.0, 205/255.0, 170/255.0},
    {0/255.0, 0/255.0, 205/255.0},
    {186/255.0, 85/255.0, 211/255.0},
    {147/255.0, 112/255.0, 219/255.0},
    {60/255.0, 179/255.0, 113/255.0},
    {123/255.0, 104/255.0, 238/255.0},
    {0/255.0, 250/255.0, 154/255.0},
    {72/255.0, 209/255.0, 204/255.0},
    {199/255.0, 21/255.0, 133/255.0},
    {25/255.0, 25/255.0, 112/255.0},
    {245/255.0, 255/255.0, 250/255.0},
    {255/255.0, 228/255.0, 225/255.0},
    {255/255.0, 228/255.0, 181/255.0},
    {255/255.0, 222/255.0, 173/255.0},
    {0/255.0, 0/255.0, 128/255.0},
    {253/255.0, 245/255.0, 230/255.0},
    {128/255.0, 128/255.0, 0/255.0},
    {107/255.0, 142/255.0, 35/255.0},
    {255/255.0, 165/255.0, 0/255.0},
    {255/255.0, 69/255.0, 0/255.0},
    {218/255.0, 112/255.0, 214/255.0},
    {238/255.0, 232/255.0, 170/255.0},
    {152/255.0, 251/255.0, 152/255.0},
    {175/255.0, 238/255.0, 238/255.0},
    {219/255.0, 112/255.0, 147/255.0},
    {255/255.0, 239/255.0, 213/255.0},
    {255/255.0, 239/255.0, 213/255.0},
    {205/255.0, 133/255.0, 63/255.0},
    {255/255.0, 192/255.0, 203/255.0},
    {221/255.0, 160/255.0, 221/255.0},
    {176/255.0, 224/255.0, 230/255.0},
    {128/255.0, 0/255.0, 128/255.0},
    {255/255.0, 0/255.0, 0/255.0},
    {188/255.0, 143/255.0, 143/255.0},
    {65/255.0, 105/255.0, 225/255.0},
    {139/255.0, 69/255.0, 19/255.0},
    {250/255.0, 128/255.0, 114/255.0},
    {244/255.0, 164/255.0, 96/255.0},
    {46/255.0, 139/255.0, 87/255.0},
    {255/255.0, 245/255.0, 238/255.0},
    {160/255.0, 82/255.0, 45/255.0},
    {192/255.0, 192/255.0, 192/255.0},
    {135/255.0, 206/255.0, 235/255.0},
    {106/255.0, 90/255.0, 205/255.0},
    {112/255.0, 128/255.0, 144/255.0},
    {255/255.0, 250/255.0, 250/255.0},
    {0/255.0, 255/255.0, 127/255.0},
    {70/255.0, 130/255.0, 180/255.0},
    {210/255.0, 180/255.0, 140/255.0},
    {0/255.0, 128/255.0, 128/255.0},
    {216/255.0, 191/255.0, 216/255.0},
    {253/255.0, 99/255.0, 71/255.0},
    {64/255.0, 224/255.0, 208/255.0},
    {238/255.0, 130/255.0, 238/255.0},
    {245/255.0, 222/255.0, 179/255.0},
    {255/255.0, 255/255.0, 255/255.0},
    {245/255.0, 245/255.0, 245/255.0},
    {255/255.0, 255/255.0, 0/255.0},
    {154/255.0, 205/255.0, 50/255.0}
  };

  if (index > 0) {
    if (index > NCOLORS) return NULL;
    return rgb[index-1];
  }
  if (index < 0) {
    if (-index > ncolors) return NULL;
    return userrgb[-index-1];
  }

  for (int i = 0; i < ncolors; i++)
    if (strcmp(color,username[i]) == 0) return userrgb[i];
  for (int i = 0; i < NCOLORS; i++)
    if (strcmp(color,name[i]) == 0) return rgb[i];
  return NULL;
}

/* ----------------------------------------------------------------------
   return number of default colors
------------------------------------------------------------------------- */

int Image::default_colors()
{
  return NCOLORS;
}

/* ----------------------------------------------------------------------
   search the list of element names for the string element
   return a pointer to the 3 floating point RGB values
   this list is used by AtomEye and is taken from its Mendeleyev.c file
------------------------------------------------------------------------- */

double *Image::element2color(char *element)
{
  static const char *name[NELEMENTS] = {
    "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne",
    "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca",
    "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
    "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr",
    "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn",
    "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd",
    "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb",
    "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg",
    "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th",
    "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm",
    "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt"
  };

  static double rgb[NELEMENTS][3] = {
    {0.8, 0.8, 0.8},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.7, 0.7, 0.7},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.9, 0.4, 0},
    {0.35, 0.35, 0.35},
    {0.2, 0.2, 0.8},
    {0.8, 0.2, 0.2},
    {0.7, 0.85, 0.45},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6, 0.6, 0.6},
    {0.6, 0.6, 0.7},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6901960784, 0.768627451, 0.8705882353},
    {0.1, 0.7, 0.3},
    {0.95, 0.9, 0.2},
    {0.15, 0.5, 0.1},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.5, 0.5, 0.5},
    {0.8, 0.8, 0.7},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0, 0.8, 0},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.5176470588, 0.5764705882, 0.6529411765},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.257254902, 0.2666666667, 0.271372549},
    {0.95, 0.7900735294, 0.01385869565},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.9, 0, 1},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {1, 1, 0.3},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.5, 0.08, 0.12},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.5, 0.1, 0.5},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.8, 0.8, 0},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {1, 0.8431372549, 0},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.9, 0.8, 0},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.8, 0.2, 0.2},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.1, 0.7, 0.3},
    {0.1, 0.3, 0.7},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.9, 0.8, 0},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725},
    {0.6431372549, 0.6666666667, 0.6784313725}
  };

  for (int i = 0; i < NELEMENTS; i++)
    if (strcmp(element,name[i]) == 0) return rgb[i];
  return NULL;
}

/* ----------------------------------------------------------------------
   search the list of element names for the string element
   return a pointer to the 3 floating point RGB values
   this list is used by AtomEye and is taken from its Mendeleyev.c file
------------------------------------------------------------------------- */

double Image::element2diam(char *element)
{
  static const char *name[NELEMENTS] = {
    "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne",
    "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca",
    "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
    "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr",
    "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn",
    "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd",
    "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb",
    "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg",
    "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th",
    "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm",
    "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt"
  };

  static double diameter[NELEMENTS] = {
    0.35, 1.785, 1.45, 1.05, 0.85, 0.72, 0.65, 0.6, 0.5, 1.5662,
    1.8, 1.5, 1.4255, 1.07, 1, 1, 1, 1.8597, 2.2, 1.8,
    1.6, 1.4, 1.51995, 1.44225, 1.4, 1.43325, 1.35, 1.35, 1.278, 1.35,
    1.3, 1.25, 1.15, 1.15, 1.15, 2.0223, 2.35, 2, 1.8, 1.55,
    1.6504, 1.3872, 1.35, 1.3, 1.35, 1.4, 1.6, 1.55, 1.55, 1.45,
    1.45, 1.4, 1.4, 2.192, 2.6, 2.15, 1.95, 1.85, 1.85, 1.85,
    1.85, 1.85, 1.85, 1.8, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75,
    1.75, 1.55, 1.6529, 1.5826, 1.35, 1.3, 1.35, 1.35, 1.35, 1.5,
    1.9, 1.8, 1.6, 1.9, 1.6, 1.0, 1.0, 2.15, 1.95, 1.8,
    1.8, 1.75, 1.75, 1.75, 1.75, 1.0, 1.0, 1.6, 1.6, 1.0,
    1.0, 1.0, 1.0, 1.0, 1.6, 1.0, 1.0, 1.0, 1.0
  };

  for (int i = 0; i < NELEMENTS; i++)
    if (strcmp(element,name[i]) == 0) return diameter[i];
  return 0.0;
}

// ----------------------------------------------------------------------
// ----------------------------------------------------------------------
// ColorMap class
// ----------------------------------------------------------------------
// ----------------------------------------------------------------------

ColorMap::ColorMap(SPARTA *sparta, Image *caller) : Pointers(sparta)
{
  image = caller;

  // default color map

  dynamic = 1;

  mlo = MINVALUE;
  mhi = MAXVALUE;
  mstyle = CONTINUOUS;
  mrange = FRACTIONAL;

  nentry = 2;
  mentry = new MapEntry[nentry];
  mentry[0].single = MINVALUE;
  mentry[0].color = image->color2rgb("blue");
  mentry[1].single = MAXVALUE;
  mentry[1].color = image->color2rgb("red");
}

/* ---------------------------------------------------------------------- */

ColorMap::~ColorMap()
{
  delete [] mentry;
}

/* ----------------------------------------------------------------------
   redefine color map
   args = lo hi style delta N entry1 entry2 ... entryN as defined by caller
   return 1 if any error in args, else return 0
------------------------------------------------------------------------- */

int ColorMap::reset(int narg, char **arg)
{
  if (!islower(arg[0][0])) {
    mlo = NUMERIC;
    mlovalue = atof(arg[0]);
  } else if (strcmp(arg[0],"min") == 0) mlo = MINVALUE;
  else return 1;

  if (!islower(arg[1][0])) {
    mhi = NUMERIC;
    mhivalue = atof(arg[1]);
  } else if (strcmp(arg[1],"max") == 0) mhi = MAXVALUE;
  else return 1;

  if (mlo == NUMERIC && mhi == NUMERIC && mlovalue >= mhivalue) return 1;

  if (mlo == MINVALUE || mhi == MAXVALUE) dynamic = 1;
  else dynamic = 0;

  if (strlen(arg[2]) != 2) return 1;
  if (arg[2][0] == 'c') mstyle = CONTINUOUS;
  else if (arg[2][0] == 'd') mstyle = DISCRETE;
  else if (arg[2][0] == 's') mstyle = SEQUENTIAL;
  else return 1;
  if (arg[2][1] == 'a') mrange = ABSOLUTE;
  else if (arg[2][1] == 'f') mrange = FRACTIONAL;
  else return 1;

  if (mstyle == SEQUENTIAL) {
    mbinsize = atof(arg[3]);
    if (mbinsize <= 0.0) return 1;
    mbinsizeinv = 1.0/mbinsize;
  }

  nentry = atoi(arg[4]);
  if (nentry < 1) return 1;
  delete [] mentry;
  mentry = new MapEntry[nentry];

  int n = 5;
  for (int i = 0; i < nentry; i++) {
    if (mstyle == CONTINUOUS) {
      if (n+2 > narg) return 1;
      if (!islower(arg[n][0])) {
        mentry[i].single = NUMERIC;
        mentry[i].svalue = atof(arg[n]);
      } else if (strcmp(arg[n],"min") == 0) mentry[i].single = MINVALUE;
      else if (strcmp(arg[n],"max") == 0) mentry[i].single = MAXVALUE;
      else return 1;
      mentry[i].color = image->color2rgb(arg[n+1]);
      n += 2;
    } else if (mstyle == DISCRETE) {
      if (n+3 > narg) return 1;
      if (!islower(arg[n][0])) {
        mentry[i].lo = NUMERIC;
        mentry[i].lvalue = atof(arg[n]);
      } else if (strcmp(arg[n],"min") == 0) mentry[i].single = MINVALUE;
      else if (strcmp(arg[n],"max") == 0) mentry[i].single = MAXVALUE;
      else return 1;
      if (!islower(arg[n+1][0])) {
        mentry[i].hi = NUMERIC;
        mentry[i].hvalue = atof(arg[n+1]);
      } else if (strcmp(arg[n+1],"min") == 0) mentry[i].single = MINVALUE;
      else if (strcmp(arg[n+1],"max") == 0) mentry[i].single = MAXVALUE;
      else return 1;
      mentry[i].color = image->color2rgb(arg[n+2]);
      n += 3;
    } else if (mstyle == SEQUENTIAL) {
      if (n+1 > narg) return 1;
      mentry[i].color = image->color2rgb(arg[n]);
      n += 1;
    }
    if (mentry[i].color == NULL) return 1;
  }

  if (mstyle == CONTINUOUS) {
    if (nentry < 2) return 1;
    if (mentry[0].single != MINVALUE || mentry[nentry-1].single != MAXVALUE)
      return 1;
    for (int i = 2; i < nentry-1; i++) {
      if (mentry[i].svalue <= mentry[i-1].svalue) return 1;
    }
  } else if (mstyle == DISCRETE) {
    if (nentry < 1) return 1;
    if (mentry[nentry-1].lo != MINVALUE || mentry[nentry-1].hi != MAXVALUE)
      return 1;
  } else if (mstyle == SEQUENTIAL) {
    if (nentry < 1) return 1;
  }

  // one-time call to minmax if color map is static

  if (!dynamic) return minmax(mlovalue,mhivalue);

  return 0;
}

/* ----------------------------------------------------------------------
   set explicit values for all min/max settings in color map
     from min/max dynamic values
   set lo/hi current and lvalue/hvalue entries that are MIN/MAX VALUE
   called only once if mlo/mhi != MIN/MAX VALUE, else called repeatedly
   return 1 = error if any values now overlap incorrectly with dynamic bounds
   else return 0
------------------------------------------------------------------------- */

int ColorMap::minmax(double mindynamic, double maxdynamic)
{
  if (mlo == MINVALUE) locurrent = mindynamic;
  else locurrent = mlovalue;
  if (mhi == MAXVALUE) hicurrent = maxdynamic;
  else hicurrent = mhivalue;
  if (locurrent > hicurrent) return 1;

  if (mstyle == CONTINUOUS) {
    if (mrange == ABSOLUTE) mentry[0].svalue = locurrent;
    else mentry[0].svalue = 0.0;
    if (mrange == ABSOLUTE) mentry[nentry-1].svalue = hicurrent;
    else mentry[nentry-1].svalue = 1.0;

    // error in ABSOLUTE mode if new lo/hi current cause
    // first/last entry to become lo > hi with adjacent entry

    if (mrange == ABSOLUTE) {
      if (mentry[0].svalue > mentry[1].svalue) return 1;
      if (mentry[nentry-2].svalue > mentry[nentry-1].svalue) return 1;
    }

  // OK if new lo/hi current cause an entry to have lo > hi,
  // since last entry will always be a match

  } else if (mstyle == DISCRETE) {
    for (int i = 0; i < nentry; i++) {
      if (mentry[i].lo == MINVALUE) {
        if (mrange == ABSOLUTE) mentry[i].lvalue = locurrent;
        else mentry[i].lvalue = 0.0;
      }
      if (mentry[i].hi == MAXVALUE) {
        if (mrange == ABSOLUTE) mentry[i].hvalue = hicurrent;
        else mentry[i].hvalue = 1.0;
      }
    }
  }

  return 0;
}

/* ----------------------------------------------------------------------
   convert value into an RGB color via color map
   return pointer to 3-vector
------------------------------------------------------------------------- */

double *ColorMap::value2color(double value)
{
  double lo;

  value = MAX(value,locurrent);
  value = MIN(value,hicurrent);

  if (mrange == FRACTIONAL) {
    if (locurrent == hicurrent) value = 0.0;
    else value = (value-locurrent) / (hicurrent-locurrent);
    lo = 0.0;
  } else {
    lo = locurrent;
  }

  if (mstyle == CONTINUOUS) {
    for (int i = 0; i < nentry-1; i++)
      if (value >= mentry[i].svalue && value <= mentry[i+1].svalue) {
        double fraction = (value-mentry[i].svalue) /
          (mentry[i+1].svalue-mentry[i].svalue);
        interpolate[0] = mentry[i].color[0] +
          fraction*(mentry[i+1].color[0]-mentry[i].color[0]);
        interpolate[1] = mentry[i].color[1] +
          fraction*(mentry[i+1].color[1]-mentry[i].color[1]);
        interpolate[2] = mentry[i].color[2] +
          fraction*(mentry[i+1].color[2]-mentry[i].color[2]);
        return interpolate;
      }
  } else if (mstyle == DISCRETE) {
    for (int i = 0; i < nentry; i++)
      if (value >= mentry[i].lvalue && value <= mentry[i].hvalue)
        return mentry[i].color;
  } else {
    int ibin = static_cast<int> ((value-lo) * mbinsizeinv);
    return mentry[ibin%nentry].color;
  }

  return NULL;
}
