/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.display2d;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Label;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import javax.swing.JFrame;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.display.DrawingPanel;
import org.opensourcephysics.display.False3D;

/**
 * SurfacePlot draws a 3D surface of a scalar field.
 * Surfaceplot uses code from the Surface Plotter package by Yanto Suryono.
 *
 * @author       Wolfgang Christian and Yanto Suryono
 * @version 1.0
 */
public class ComplexSurfacePlot implements Plot2D, False3D {
  boolean visible = true;
  ComplexColorMapper colorMap = new ComplexColorMapper(1);         // color map with ceiling=1.
  protected DecimalFormat labelFormat = new DecimalFormat("0.00"); //$NON-NLS-1$
  private static final int TOP = 0;
  private static final int CENTER = 1;
  //  // for splitting polygons
  //  private static final int UPPER = 1;
  //  private static final int COINCIDE = 0;
  //  private static final int LOWER = -1;

  /** Field INIT_CALC_DIV */
  public static final int INIT_CALC_DIV = 33;

  /** Field INIT_DISP_DIV */
  public static final int INIT_DISP_DIV = INIT_CALC_DIV;
  private int calc_divisions = INIT_CALC_DIV;                      // number of divisions to calculate
  private int disp_divisions = INIT_DISP_DIV;                      // number of divisions to calculate
  private int plot_mode = ColorMapper.SPECTRUM;
  private boolean isBoxed, isMesh, isScaleBox, isDisplayXY, isDisplayZ, isDisplayGrids;
  private double zmin = -2, zmax = 2;
  private boolean autoscaleZ = true;
  private GridData griddata;
  private double color_factor;
  private Point projection;
  private ComplexSurfaceVertex cop;                                // center of projection
  private ComplexSurfaceVertex[] vertexArray;                      // vertices array
  private final ComplexSurfaceVertex values1[] = new ComplexSurfaceVertex[4];
  // private final ComplexSurfaceVertex values2[] = new ComplexSurfaceVertex[4];
  // private double              color;                               // color of surface

  private Color line_color = Color.black;
  private int factor_x, factor_y;                                  // conversion factors
  private int t_x, t_y, t_z;                                       // determines ticks density
  //private boolean mouseDown = false;
  private int click_x, click_y;                                    // previous mouse cursor position
  private boolean invalidProjection = true;
  private int iwidth = 0, iheight = 0;                             // the width and height of the last drawing operation
  private double xmin, xmax, ymin, ymax;
  private int ampIndex = 0;                                        // amplitude index
  private int reIndex = 1;                                         // real index
  private int imIndex = 2;                                         // imaginary index
  // the following are needed by the SurfaceVertex

  SurfacePlotProjector projector;                                  // the projector
  double zminV, zmaxV, zfactorV;
  int master_project_indexV = 0;                                   // over 4 billion times to reset
  ZExpansion zMap;

  /**
   * Constructs the ComplexSurfacePlot without data.
   */
  public ComplexSurfacePlot() {
    this(null);
  }

  /**
   * ComplexSurfacePlot constructor with the given data model.
   *
   * @param _griddata GridData
   */
  public ComplexSurfacePlot(GridData _griddata) {
    griddata = _griddata;
    defaultVariables();
    autoscaleZ = true;
    projector = new SurfacePlotProjector();
    // projector.setDistance(70);
    projector.setDistance(200);
    // projector.set2DScaling(15);
    projector.set2DScaling(8);
    projector.setRotationAngle(125);
    projector.setElevationAngle(10);
    setGridData(_griddata);
    update();
  }

  /**
   * Gets closest index from the given x  world coordinate.
   *
   * @param x double the coordinate
   * @return int the index
   */
  public int xToIndex(double x) {
    return griddata.xToIndex(x);
  }

  /**
   * Gets closest index from the given y  world coordinate.
   *
   * @param y double the coordinate
   * @return int the index
   */
  public int yToIndex(double y) {
    return griddata.yToIndex(y);
  }

  /**
   * Gets the x coordinate for the given index.
   *
   * @param i int
   * @return double the x coordinate
   */
  public double indexToX(int i) {
    return griddata.indexToX(i);
  }

  /**
   * Gets the y coordinate for the given index.
   *
   * @param i int
   * @return double the y coordinate
   */
  public double indexToY(int i) {
    return griddata.indexToY(i);
  }

  /**
   * Sets the data to new values.
   *
   * The grid is resized to fit the new data if needed.
   *
   * @param obj
   */
  public void setAll(Object obj) {
    double[][][] val = (double[][][]) obj;
    copyComplexData(val);
    update();
  }

  /**
   * Sets the values and the scale.
   *
   * The grid is resized to fit the new data if needed.
   *
   * @param obj array of new values
   * @param xmin double
   * @param xmax double
   * @param ymin double
   * @param ymax double
   */
  public void setAll(Object obj, double xmin, double xmax, double ymin, double ymax) {
    double[][][] val = (double[][][]) obj;
    copyComplexData(val);
    if(griddata.isCellData()) {
      griddata.setCellScale(xmin, xmax, ymin, ymax);
    } else {
      griddata.setScale(xmin, xmax, ymin, ymax);
    }
    update();
  }

  private void copyComplexData(double vals[][][]) {
    if((griddata!=null)&&!(griddata instanceof ArrayData)) {
      throw new IllegalStateException("SetAll only supports ArrayData for data storage."); //$NON-NLS-1$
    }
    if((griddata==null)||(griddata.getNx()!=vals[0].length)||(griddata.getNy()!=vals[0][0].length)) {
      griddata = new ArrayData(vals[0].length, vals[0][0].length, 3);
      setGridData(griddata);
    }
    double[][] mag = griddata.getData()[0];
    double[][] reData = griddata.getData()[1];
    double[][] imData = griddata.getData()[2];
    // current grid has correct size
    int ny = vals[0][0].length;
    for(int i = 0, nx = vals[0].length; i<nx; i++) {
      System.arraycopy(vals[0][i], 0, reData[i], 0, ny);
      System.arraycopy(vals[1][i], 0, imData[i], 0, ny);
      for(int j = 0; j<ny; j++) {
        mag[i][j] = Math.sqrt(vals[0][i][j]*vals[0][i][j]+vals[1][i][j]*vals[1][i][j]);
      }
    }
  }

  /**
   * Gets the GridData object.
   * @return GridData
   */
  public GridData getGridData() {
    return griddata;
  }

  /**
   * Sets the data storage to the given value.
   *
   * @param _griddata
   */
  public void setGridData(GridData _griddata) throws IllegalArgumentException {
    griddata = _griddata;
    if(griddata==null) {
      return;
    }
    if(griddata.getNx()!=griddata.getNy()) {
      throw new IllegalArgumentException("SurfacePlot requires square grids."); //$NON-NLS-1$
    }
  }

  private void generateVerticesFromArray(ArrayData griddata) {
    double[][] ampdata = griddata.getData()[ampIndex];
    double[][] redata = griddata.getData()[reIndex];
    double[][] imdata = griddata.getData()[imIndex];
    int numRows = ampdata.length;
    int numCols = ampdata[0].length;
    calc_divisions = numRows-1;
    double xfactor = 20/(xmax-xmin);
    double yfactor = 20/(ymax-ymin);
    if((vertexArray==null)||(vertexArray.length!=numRows*numCols)) {
      vertexArray = new ComplexSurfaceVertex[numRows*numCols];
    }
    double dx = Math.abs(griddata.getDx());
    double dy = Math.abs(griddata.getDy());
    double x = xmin; // left;
    for(int ix = 0; ix<numCols; ix++) {
      double y = ymin; // bottom;
      for(int iy = 0; iy<numRows; iy++) {
        int iyd = (griddata.getDy()>0) ? iy : numCols-iy-1;
        int ixd = (griddata.getDx()>0) ? ix : numCols-ix-1;
        double zval = ampdata[ixd][iyd];
        if(zMap!=null) {
          zval = zMap.evaluate(zval);
        }
        vertexArray[ix*numRows+iy] = new ComplexSurfaceVertex(-10+(x-xmin)*xfactor, -10+(y-ymin)*yfactor, zval, redata[ixd][iyd], imdata[ixd][iyd], this);
        y += dy;
      }
      x += dx;
    }
    ampdata = null;
  }

  private void generateVerticesFromPoints(GridPointData griddata) throws IllegalArgumentException {
    double[][][] data = griddata.getData();
    int numRows = data.length;
    int numCols = data[0].length;
    calc_divisions = numRows-1;
    double xfactor = 20/(xmax-xmin);
    double yfactor = 20/(ymax-ymin);
    if((vertexArray==null)||(vertexArray.length!=numRows*numCols)) {
      vertexArray = new ComplexSurfaceVertex[numRows*numCols];
    }
    double dx = Math.abs(griddata.getDx());
    double dy = Math.abs(griddata.getDy());
    int ampIndex = this.ampIndex+2;
    int reIndex = this.reIndex+2;
    int imIndex = this.imIndex+2;
    double x = xmin; // left;
    for(int ix = 0; ix<numCols; ix++) {
      double y = ymin; // bottom;
      for(int iy = 0; iy<numRows; iy++) {
        double zval = data[ix][iy][ampIndex];
        if(zMap!=null) {
          zval = zMap.evaluate(zval);
        }
        vertexArray[ix*numRows+iy] = new ComplexSurfaceVertex(-10+(x-xmin)*xfactor, -10+(y-ymin)*yfactor, zval, data[ix][iy][reIndex], data[ix][iy][imIndex], this);
        y += dy;
      }
      x += dx;
    }
    data = null;
  }

  /**
   * Sets the indexes for the data components that will be plotted.
   *
   * Indexes determine the postion of the amplitude, real-component, and imaginary-component
   * in the data array.
   *
   * @param indexes the sample-component indexes
   */
  public void setIndexes(int[] indexes) {
    ampIndex = indexes[0];
    reIndex = indexes[1];
    imIndex = indexes[2];
  }

  /**
   * Method projectVertexArray
   *
   */
  void projectVertexArray() {
    ComplexSurfaceVertex[] tempArray = vertexArray; // reference to the array so it cannot change.
    if(tempArray==null) {
      return;
    }
    for(int i = 0, num = tempArray.length; i<num; i++) {
      tempArray[i].project();
    }
  }

  /**
   * Sets the colors that will be used between the floor and ceiling values.
   * Not implemented.   Color always maps to phase.
   * @param colors
   */
  public void setColorPalette(Color[] colors) {
    // Not implemented.   Color always maps to phase.
  }

  /**
   * Sets the visibility of the lattice.
   * Drawing will be disabled if visible is false.
   *
   * @param isVisible
   */
  public void setVisible(boolean isVisible) {
    visible = isVisible;
  }

  /**
   * Shows a legend of phase angle and color.
   */
  public JFrame showLegend() {
    return colorMap.showLegend();
  }

  /**
   * Outlines the data grid's boundaries.
   *
   * @param show
   */
  public void setShowGridLines(boolean show) {
    isMesh = show;
  }

  /**
   *  Sets the color for grid line boundaries
   *
   * @param  c
   */
  public void setGridLineColor(Color c) {
    line_color = c;
  }

  /**
   * Paint the surface.
   * @param panel
   * @param g
   */
  public void draw(DrawingPanel panel, Graphics g) {
    if(!visible||(griddata==null)) {
      return;
    }
    projector.setProjectionArea(new Rectangle(0, 0, panel.getBounds().width, panel.getBounds().height));
    if(invalidProjection||(iwidth!=panel.getWidth())||(iheight!=panel.getHeight())) {
      master_project_indexV++;
      invalidProjection = false;
      projectVertexArray();
      iwidth = panel.getWidth();
      iheight = panel.getHeight();
    }
    plotSurface(g);
  }

  private void defaultVariables() {
    plot_mode = ColorMapper.SPECTRUM;
    isBoxed = true;
    isMesh = true;
    isScaleBox = false;
    isDisplayXY = true;
    isDisplayZ = true;
    isDisplayGrids = false;
  }

  /**
   * Determines whether a plane is plottable, i.e: does not have
   * invalid vertex.
   *
   * @return <code>true</code> if the plane is plottable,
   *         <code>false</code> otherwise
   * @param values vertices array of the plane
   */
  private final boolean plottable(ComplexSurfaceVertex[] values) {
    try {
      return(!values[0].isInvalid()&&!values[1].isInvalid()&&!values[2].isInvalid()&&!values[3].isInvalid());
    } catch(Exception ex) {}
    return false;
  }

  /**
   * Sets the axes scaling factor. Computes the proper axis lengths
   * based on the ratio of variable ranges. The axis lengths will
   * also affect the size of bounding box.
   */
  private final void setAxesScale() {
    double scale_x, scale_y, scale_z, divisor;
    int longest;
    if(!isScaleBox) {
      projector.setScaling(1);
      t_x = t_y = t_z = 4;
      return;
    }
    scale_x = xmax-xmin;
    scale_y = ymax-ymin;
    scale_z = zmax-zmin;
    if(scale_x<scale_y) {
      if(scale_y<scale_z) {
        longest = 3;
        divisor = scale_z;
      } else {
        longest = 2;
        divisor = scale_y;
      }
    } else {
      if(scale_x<scale_z) {
        longest = 3;
        divisor = scale_z;
      } else {
        longest = 1;
        divisor = scale_x;
      }
    }
    scale_x /= divisor;
    scale_y /= divisor;
    scale_z /= divisor;
    if((scale_x<0.2f)||(scale_y<0.2f)&&(scale_z<0.2f)) {
      switch(longest) {
         case 1 :
           if(scale_y<scale_z) {
             scale_y /= scale_z;
             scale_z = 1.0f;
           } else {
             scale_z /= scale_y;
             scale_y = 1.0f;
           }
           break;
         case 2 :
           if(scale_x<scale_z) {
             scale_x /= scale_z;
             scale_z = 1.0f;
           } else {
             scale_z /= scale_x;
             scale_x = 1.0f;
           }
           break;
         case 3 :
           if(scale_y<scale_x) {
             scale_y /= scale_x;
             scale_x = 1.0f;
           } else {
             scale_x /= scale_y;
             scale_y = 1.0f;
           }
           break;
      }
    }
    if(scale_x<0.2f) {
      scale_x = 1.0f;
    }
    projector.setXScaling(scale_x);
    if(scale_y<0.2f) {
      scale_y = 1.0f;
    }
    projector.setYScaling(scale_y);
    if(scale_z<0.2f) {
      scale_z = 1.0f;
    }
    projector.setZScaling(scale_z);
    if(scale_x<0.5f) {
      t_x = 8;
    } else {
      t_x = 4;
    }
    if(scale_y<0.5f) {
      t_y = 8;
    } else {
      t_y = 4;
    }
    if(scale_z<0.5f) {
      t_z = 8;
    } else {
      t_z = 4;
    }
  }

  /**
   * Gets the number of divisions to be displayed.
   * Automatically fixes invalid values.
   *
   * @return valid number of divisions to be displayed
   */
  private int getDispDivisions() {
    int plot_density;
    plot_density = disp_divisions;
    if(plot_density>calc_divisions) {
      plot_density = calc_divisions;
    }
    while((calc_divisions%plot_density)!=0) {
      plot_density++;
    }
    return plot_density;
  }

  /**
   * Creates a surface plot
   */
  private final void plotSurface(Graphics g) {
    double zi, zx;
    int sx, sy;
    int start_lx, end_lx;
    int start_ly, end_ly;
    zi = zmin;
    zx = zmax;
    int plot_density = getDispDivisions();
    int multiple_factor = calc_divisions/plot_density;
    disp_divisions = plot_density;
    zmin = zi;
    zmax = zx;
    color_factor = 0.8/(zmax-zmin);
    if((plot_mode==ColorMapper.DUALSHADE)||(plot_mode==ColorMapper.RED)||(plot_mode==ColorMapper.GREEN)||(plot_mode==ColorMapper.BLUE)) {
      color_factor *= 0.6/0.8;
    }
    if(vertexArray==null) {
      drawBoxGridsTicksLabels(g, false);
      drawBoundingBox(g);
      return;
    }
    if(plot_mode==ColorMapper.NORENDER) {
      drawBoxGridsTicksLabels(g, true);
      drawBoundingBox(g);
      return;
    }
    drawBoxGridsTicksLabels(g, false);
    // SurfaceVertex.setZRange(zmin,zmax);
    zmaxV = zmax;
    zminV = zmin;
    zfactorV = 20/(zmaxV-zminV);
    // direction test
    double distance = projector.getDistance()*projector.getCosElevationAngle();
    // cop : center of projection
    cop = new ComplexSurfaceVertex(distance*projector.getSinRotationAngle(), distance*projector.getCosRotationAngle(), projector.getDistance()*projector.getSinElevationAngle(), 1, 0, this);
    cop.transform();
    boolean inc_x = cop.x>0;
    boolean inc_y = cop.y>0;
    // critical = false;
    if(inc_x) {
      start_lx = 0;
      end_lx = calc_divisions;
      sx = multiple_factor;
    } else {
      start_lx = calc_divisions;
      end_lx = 0;
      sx = -multiple_factor;
    }
    if(inc_y) {
      start_ly = 0;
      end_ly = calc_divisions;
      sy = multiple_factor;
    } else {
      start_ly = calc_divisions;
      end_ly = 0;
      sy = -multiple_factor;
    }
    if((cop.x>10)||(cop.x<-10)) {
      if((cop.y>10)||(cop.y<-10)) {
        plotArea(g, start_lx, start_ly, end_lx, end_ly, sx, sy);
      } else {                      // split in y direction
        int split_y = (int) ((cop.y+10)*plot_density/20)*multiple_factor;
        plotArea(g, start_lx, 0, end_lx, split_y, sx, multiple_factor);
        plotArea(g, start_lx, calc_divisions, end_lx, split_y, sx, -multiple_factor);
      }
    } else {
      if((cop.y>10)||(cop.y<-10)) { // split in x direction
        int split_x = (int) ((cop.x+10)*plot_density/20)*multiple_factor;
        plotArea(g, 0, start_ly, split_x, end_ly, multiple_factor, sy);
        plotArea(g, calc_divisions, start_ly, split_x, end_ly, -multiple_factor, sy);
      } else {                      // split in both x and y directions
        int split_x = (int) ((cop.x+10)*plot_density/20)*multiple_factor;
        int split_y = (int) ((cop.y+10)*plot_density/20)*multiple_factor;
        // critical = true;
        plotArea(g, 0, 0, split_x, split_y, multiple_factor, multiple_factor);
        plotArea(g, 0, calc_divisions, split_x, split_y, multiple_factor, -multiple_factor);
        plotArea(g, calc_divisions, 0, split_x, split_y, -multiple_factor, multiple_factor);
        plotArea(g, calc_divisions, calc_divisions, split_x, split_y, -multiple_factor, -multiple_factor);
      }
    }
    if(isBoxed) {
      drawBoundingBox(g);
    }
  }

  private final int poly_x[] = new int[9];
  private final int poly_y[] = new int[9];

  /**
   * Plots a single plane
   *
   * @param vertex vertices array of the plane
   * @param verticescount number of vertices to process
   */
  private final void plotPlane(Graphics g, ComplexSurfaceVertex[] vertex, int verticescount) {
    double[] samples = new double[3];
    int count, loop, index;
    double z, re, im, result;
    boolean low1, low2;
    boolean valid1, valid2;
    if(verticescount<3) {
      return;
    }
    count = 0;
    z = 0.0f;
    re = 0.0f;
    im = 0.0f;
    line_color = Color.black;
    low1 = (vertex[0].z<zmin);
    valid1 = !low1&&(vertex[0].z<=zmax);
    index = 1;
    for(loop = 0; loop<verticescount; loop++) {
      low2 = (vertex[index].z<zmin);
      valid2 = !low2&&(vertex[index].z<=zmax);
      if((valid1||valid2)||(low1^low2)) {
        if(!valid1) {
          if(low1) {
            result = zmin;
          } else {
            result = zmax;
          }
          double ratio = (result-vertex[index].z)/(vertex[loop].z-vertex[index].z);
          double new_x = ratio*(vertex[loop].x-vertex[index].x)+vertex[index].x;
          double new_y = ratio*(vertex[loop].y-vertex[index].y)+vertex[index].y;
          if(low1) {
            projection = projector.project(new_x, new_y, -10);
          } else {
            projection = projector.project(new_x, new_y, 10);
          }
          poly_x[count] = projection.x;
          poly_y[count] = projection.y;
          count++;
          z += result;
        }
        if(valid2) {
          projection = vertex[index].projection();
          poly_x[count] = projection.x;
          poly_y[count] = projection.y;
          count++;
          z += vertex[index].z;
          re += vertex[index].re;
          im += vertex[index].im;
        } else {
          if(low2) {
            result = zmin;
          } else {
            result = zmax;
          }
          double ratio = (result-vertex[loop].z)/(vertex[index].z-vertex[loop].z);
          double new_x = ratio*(vertex[index].x-vertex[loop].x)+vertex[loop].x;
          double new_y = ratio*(vertex[index].y-vertex[loop].y)+vertex[loop].y;
          if(low2) {
            projection = projector.project(new_x, new_y, -10);
          } else {
            projection = projector.project(new_x, new_y, 10);
          }
          poly_x[count] = projection.x;
          poly_y[count] = projection.y;
          count++;
          z += result;
        }
      }
      if(++index==verticescount) {
        index = 0;
      }
      valid1 = valid2;
      low1 = low2;
    }
    if(count>0) {
      switch(plot_mode) {
         case ColorMapper.NORENDER :
           g.setColor(Color.lightGray);
           break;
         default :
           samples[0] = 0.99;
           samples[1] = re;
           samples[2] = im;
           g.setColor(colorMap.samplesToColor(samples));
      }
      g.fillPolygon(poly_x, poly_y, count);
      g.setColor(line_color);
      if(isMesh) {
        poly_x[count] = poly_x[0];
        poly_y[count] = poly_y[0];
        count++;
        g.drawPolygon(poly_x, poly_y, count);
      }
    }
  }

  /**
   * Plots an area of group of planes
   *
   * @param start_lx start index in x direction
   * @param start_ly start index in y direction
   * @param end_lx   end index in x direction
   * @param end_ly   end index in y direction
   * @param sx       step in x direction
   * @param sy       step in y direction
   */
  private final void plotArea(Graphics g, int start_lx, int start_ly, int end_lx, int end_ly, int sx, int sy) {
    start_lx *= calc_divisions+1;
    sx *= calc_divisions+1;
    end_lx *= calc_divisions+1;
    int lx = start_lx;
    int ly = start_ly;
    while(ly!=end_ly) {
      values1[1] = vertexArray[lx+ly];
      values1[2] = vertexArray[lx+ly+sy];
      while(lx!=end_lx) {
        values1[0] = values1[1];
        values1[1] = vertexArray[lx+sx+ly];
        values1[3] = values1[2];
        values1[2] = vertexArray[lx+sx+ly+sy];
        if(plottable(values1)) {
          plotPlane(g, values1, 4);
        }
        lx += sx;
      }
      ly += sy;
      lx = start_lx;
    }
  }

  /**
   * Draws non-surface parts, i.e: bounding box, axis grids, axis ticks,
   * axis labels, base plane.
   *
   * @param g         the graphics context to draw
   * @param draw_axes if <code>true</code>, only draws base plane and z axis
   */
  private final void drawBoxGridsTicksLabels(Graphics g, boolean draw_axes) {
    Point projection, tickpos;
    boolean x_left = false, y_left = false;
    int x[], y[], i;
    x = new int[5];
    y = new int[5];
    if(projector==null) {
      return;
    }
    if(draw_axes) {
      drawBase(g, x, y);
      projection = projector.project(0, 0, -10);
      x[0] = projection.x;
      y[0] = projection.y;
      projection = projector.project(10.5f, 0, -10);
      g.drawLine(x[0], y[0], projection.x, projection.y);
      if(projection.x<x[0]) {
        outString(g, (int) (1.05*(projection.x-x[0]))+x[0], (int) (1.05*(projection.y-y[0]))+y[0], "x", Label.RIGHT, TOP); //$NON-NLS-1$
      } else {
        outString(g, (int) (1.05*(projection.x-x[0]))+x[0], (int) (1.05*(projection.y-y[0]))+y[0], "x", Label.LEFT, TOP); //$NON-NLS-1$
      }
      projection = projector.project(0, 11.5f, -10);
      g.drawLine(x[0], y[0], projection.x, projection.y);
      if(projection.x<x[0]) {
        outString(g, (int) (1.05*(projection.x-x[0]))+x[0], (int) (1.05*(projection.y-y[0]))+y[0], "y", Label.RIGHT, TOP); //$NON-NLS-1$
      } else {
        outString(g, (int) (1.05*(projection.x-x[0]))+x[0], (int) (1.05*(projection.y-y[0]))+y[0], "y", Label.LEFT, TOP); //$NON-NLS-1$
      }
      projection = projector.project(0, 0, 10.5f);
      g.drawLine(x[0], y[0], projection.x, projection.y);
      outString(g, (int) (1.05*(projection.x-x[0]))+x[0], (int) (1.05*(projection.y-y[0]))+y[0], "z", Label.CENTER, CENTER); //$NON-NLS-1$
    } else {
      factor_x = factor_y = 1;
      projection = projector.project(0, 0, -10);
      x[0] = projection.x;
      projection = projector.project(10.5f, 0, -10);
      y_left = projection.x>x[0];
      i = projection.y;
      projection = projector.project(-10.5f, 0, -10);
      if(projection.y>i) {
        factor_x = -1;
        y_left = projection.x>x[0];
      }
      projection = projector.project(0, 10.5f, -10);
      x_left = projection.x>x[0];
      i = projection.y;
      projection = projector.project(0, -10.5f, -10);
      if(projection.y>i) {
        factor_y = -1;
        x_left = projection.x>x[0];
      }
      setAxesScale();
      drawBase(g, x, y);
      if(isBoxed) {
        projection = projector.project(-factor_x*10, -factor_y*10, -10);
        x[0] = projection.x;
        y[0] = projection.y;
        projection = projector.project(-factor_x*10, -factor_y*10, 10);
        x[1] = projection.x;
        y[1] = projection.y;
        projection = projector.project(factor_x*10, -factor_y*10, 10);
        x[2] = projection.x;
        y[2] = projection.y;
        projection = projector.project(factor_x*10, -factor_y*10, -10);
        x[3] = projection.x;
        y[3] = projection.y;
        x[4] = x[0];
        y[4] = y[0];
        if(plot_mode!=ColorMapper.WIREFRAME) {
          if(plot_mode==ColorMapper.NORENDER) {
            g.setColor(Color.lightGray);
          } else {
            g.setColor(new Color(192, 220, 192));
          }
          g.fillPolygon(x, y, 4);
        }
        g.setColor(Color.black);
        g.drawPolygon(x, y, 5);
        projection = projector.project(-factor_x*10, factor_y*10, 10);
        x[2] = projection.x;
        y[2] = projection.y;
        projection = projector.project(-factor_x*10, factor_y*10, -10);
        x[3] = projection.x;
        y[3] = projection.y;
        x[4] = x[0];
        y[4] = y[0];
        if(plot_mode!=ColorMapper.WIREFRAME) {
          if(plot_mode==ColorMapper.NORENDER) {
            g.setColor(Color.lightGray);
          } else {
            g.setColor(new Color(192, 220, 192));
          }
          g.fillPolygon(x, y, 4);
        }
        g.setColor(Color.black);
        g.drawPolygon(x, y, 5);
      } else if(isDisplayZ) {
        projection = projector.project(factor_x*10, -factor_y*10, -10);
        x[0] = projection.x;
        y[0] = projection.y;
        projection = projector.project(factor_x*10, -factor_y*10, 10);
        g.drawLine(x[0], y[0], projection.x, projection.y);
        projection = projector.project(-factor_x*10, factor_y*10, -10);
        x[0] = projection.x;
        y[0] = projection.y;
        projection = projector.project(-factor_x*10, factor_y*10, 10);
        g.drawLine(x[0], y[0], projection.x, projection.y);
      }
      for(i = -9; i<=9; i++) {
        if(isDisplayXY||isDisplayGrids) {
          if(!isDisplayGrids||(i%(t_y/2)==0)||isDisplayXY) {
            if(isDisplayGrids&&(i%t_y==0)) {
              projection = projector.project(-factor_x*10, i, -10);
            } else {
              if(i%t_y!=0) {
                projection = projector.project(factor_x*9.8f, i, -10);
              } else {
                projection = projector.project(factor_x*9.5f, i, -10);
              }
            }
            tickpos = projector.project(factor_x*10, i, -10);
            g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
            if((i%t_y==0)&&isDisplayXY) {
              tickpos = projector.project(factor_x*10.5f, i, -10);
              if(y_left) {
                outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(ymax-ymin)+ymin, Label.LEFT, TOP);
              } else {
                outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(ymax-ymin)+ymin, Label.RIGHT, TOP);
              }
            }
          }
          if(!isDisplayGrids||(i%(t_x/2)==0)||isDisplayXY) {
            if(isDisplayGrids&&(i%t_x==0)) {
              projection = projector.project(i, -factor_y*10, -10);
            } else {
              if(i%t_x!=0) {
                projection = projector.project(i, factor_y*9.8f, -10);
              } else {
                projection = projector.project(i, factor_y*9.5f, -10);
              }
            }
            tickpos = projector.project(i, factor_y*10, -10);
            g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
            if((i%t_x==0)&&isDisplayXY) {
              tickpos = projector.project(i, factor_y*10.5f, -10);
              if(x_left) {
                outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(xmax-xmin)+xmin, Label.LEFT, TOP);
              } else {
                outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(xmax-xmin)+xmin, Label.RIGHT, TOP);
              }
            }
          }
        }
        if(isDisplayXY) {
          tickpos = projector.project(0, factor_y*14, -10);
          outString(g, tickpos.x, tickpos.y, "X", Label.CENTER, TOP); //$NON-NLS-1$
          tickpos = projector.project(factor_x*14, 0, -10);
          outString(g, tickpos.x, tickpos.y, "Y", Label.CENTER, TOP); //$NON-NLS-1$
        }
        // z grids and ticks
        if(isDisplayZ||(isDisplayGrids&&isBoxed)) {
          if(!isDisplayGrids||(i%(t_z/2)==0)||isDisplayZ) {
            if(isBoxed&&isDisplayGrids&&(i%t_z==0)) {
              projection = projector.project(-factor_x*10, -factor_y*10, i);
              tickpos = projector.project(-factor_x*10, factor_y*10, i);
            } else {
              if(i%t_z==0) {
                projection = projector.project(-factor_x*10, factor_y*9.5f, i);
              } else {
                projection = projector.project(-factor_x*10, factor_y*9.8f, i);
              }
              tickpos = projector.project(-factor_x*10, factor_y*10, i);
            }
            g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
            if(isDisplayZ) {
              tickpos = projector.project(-factor_x*10, factor_y*10.5f, i);
              if(i%t_z==0) {
                if(x_left) {
                  outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(zmax-zmin)+zmin, Label.LEFT, CENTER);
                } else {
                  outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(zmax-zmin)+zmin, Label.RIGHT, CENTER);
                }
              }
            }
            if(isDisplayGrids&&isBoxed&&(i%t_z==0)) {
              projection = projector.project(-factor_x*10, -factor_y*10, i);
              tickpos = projector.project(factor_x*10, -factor_y*10, i);
            } else {
              if(i%t_z==0) {
                projection = projector.project(factor_x*9.5f, -factor_y*10, i);
              } else {
                projection = projector.project(factor_x*9.8f, -factor_y*10, i);
              }
              tickpos = projector.project(factor_x*10, -factor_y*10, i);
            }
            g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
            if(isDisplayZ) {
              tickpos = projector.project(factor_x*10.5f, -factor_y*10, i);
              if(i%t_z==0) {
                if(y_left) {
                  outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(zmax-zmin)+zmin, Label.LEFT, CENTER);
                } else {
                  outFloat(g, tickpos.x, tickpos.y, (double) (i+10)/20*(zmax-zmin)+zmin, Label.RIGHT, CENTER);
                }
              }
            }
            if(isDisplayGrids&&isBoxed) {
              if(i%t_y==0) {
                projection = projector.project(-factor_x*10, i, -10);
                tickpos = projector.project(-factor_x*10, i, 10);
                g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
              }
              if(i%t_x==0) {
                projection = projector.project(i, -factor_y*10, -10);
                tickpos = projector.project(i, -factor_y*10, 10);
                g.drawLine(projection.x, projection.y, tickpos.x, tickpos.y);
              }
            }
          }
        }
      }
    }
  }

  /**
   * Draws the base plane. The base plane is the x-y plane.
   *
   * @param g the graphics context to draw.
   * @param x used to retrieve x coordinates of drawn plane from this method.
   * @param y used to retrieve y coordinates of drawn plane from this method.
   */
  private final void drawBase(Graphics g, int[] x, int[] y) {
    Point projection = projector.project(-10, -10, -10);
    x[0] = projection.x;
    y[0] = projection.y;
    projection = projector.project(-10, 10, -10);
    x[1] = projection.x;
    y[1] = projection.y;
    projection = projector.project(10, 10, -10);
    x[2] = projection.x;
    y[2] = projection.y;
    projection = projector.project(10, -10, -10);
    x[3] = projection.x;
    y[3] = projection.y;
    x[4] = x[0];
    y[4] = y[0];
    if(plot_mode!=ColorMapper.WIREFRAME) {
      if(plot_mode==ColorMapper.NORENDER) {
        g.setColor(Color.lightGray);
      } else {
        g.setColor(new Color(192, 220, 192));
      }
      g.fillPolygon(x, y, 4);
    }
    g.setColor(Color.black);
    g.drawPolygon(x, y, 5);
  }

  /**
   * Draws the bounding box of surface.
   */
  private final void drawBoundingBox(Graphics g) {
    Point startingpoint, projection;
    startingpoint = projector.project(factor_x*10, factor_y*10, 10);
    g.setColor(Color.black);
    projection = projector.project(-factor_x*10, factor_y*10, 10);
    g.drawLine(startingpoint.x, startingpoint.y, projection.x, projection.y);
    projection = projector.project(factor_x*10, -factor_y*10, 10);
    g.drawLine(startingpoint.x, startingpoint.y, projection.x, projection.y);
    projection = projector.project(factor_x*10, factor_y*10, -10);
    g.drawLine(startingpoint.x, startingpoint.y, projection.x, projection.y);
  }

  /**
   * Draws string at the specified coordinates with the specified alignment.
   *
   * @param g       graphics context to draw
   * @param x       the x coordinate
   * @param y       the y coordinate
   * @param s       the string to draw
   * @param x_align the alignment in x direction
   * @param y_align the alignment in y direction
   */
  private final void outString(Graphics g, int x, int y, String s, int x_align, int y_align) {
    switch(y_align) {
       case TOP :
         y += g.getFontMetrics(g.getFont()).getAscent();
         break;
       case CENTER :
         y += g.getFontMetrics(g.getFont()).getAscent()/2;
         break;
    }
    switch(x_align) {
       case Label.LEFT :
         g.drawString(s, x, y);
         break;
       case Label.RIGHT :
         g.drawString(s, x-g.getFontMetrics(g.getFont()).stringWidth(s), y);
         break;
       case Label.CENTER :
         g.drawString(s, x-g.getFontMetrics(g.getFont()).stringWidth(s)/2, y);
         break;
    }
  }

  /**
   * Draws double at the specified coordinates with the specified alignment.
   *
   * @param g       graphics context to draw
   * @param x       the x coordinate
   * @param y       the y coordinate
   * @param f       the double to draw
   * @param x_align the alignment in x direction
   * @param y_align the alignment in y direction
   */
  private final void outFloat(Graphics g, int x, int y, double f, int x_align, int y_align) {
    // String s = Double.toString(f);
    String s = labelFormat.format(f);
    outString(g, x, y, s, x_align, y_align);
  }

  /**
   * Determines the palette type that will be used.
   * @param type
   */
  public void setPaletteType(int type) {
    plot_mode = type;
  }

  /**
   * Sets the format for the axis labels.
   *
   * For example, _format=0.000 will produce three digits to the right of decimal point
   *
   * @param _format the format string
   */
  public void setLabelFormat(String _format) {
    labelFormat = new DecimalFormat(_format);
  }

  /**
   * Sets the autoscale flag and the floor and ceiling values.
   *
   * If autoscaling is true, then the min and max values of z are set using the data.
   * If autoscaling is false, then floor and ceiling values become the max and min.
   * Values below min map to the first color; values above max map to the last color.
   *
   * @param isAutoscale
   * @param floor
   * @param ceil
   */
  public void setAutoscaleZ(boolean isAutoscale, double floor, double ceil) {
    autoscaleZ = isAutoscale;
    if(autoscaleZ) {
      update();
    } else {
      zmax = ceil;
      zmin = floor;
      if(zMap!=null) {
        zMap.setMinMax(zmin, zmax);
      }
    }
  }

  /**
   * Gets the autoscale flag for z.
   *
   * @return boolean
   */
  public boolean isAutoscaleZ() {
    return autoscaleZ;
  }

  /**
   * Gets the floor for scaling the z data.
   * @return double
   */
  public double getFloor() {
    return zmin;
  }

  /**
   * Gets the ceiling for scaling the z data.
   * @return double
   */
  public double getCeiling() {
    return zmax;
  }

  /**
   * Sets the floor and ceiling colors.
   *
   * @param floorColor
   * @param ceilColor
   */
  public void setFloorCeilColor(Color floorColor, Color ceilColor) {
    colorMap.setCeilColor(ceilColor);
  }

  /**
   * Expands the z scale so as to enhance values close to zero.
   *
   * @param expanded boolean
   * @param expansionFactor double
   */
  public void setExpandedZ(boolean expanded, double expansionFactor) {
    if(expanded&&(expansionFactor>0)) {
      zMap = new ZExpansion(expansionFactor);
      zMap.setMinMax(zmin, zmax);
    } else {
      zMap = null;
    }
  }

  /**
   * Updates the surface plot using the current data.
   */
  public synchronized void update() {
    if(griddata==null) {
      return;
    }
    if(autoscaleZ) {
      double[] minmax = griddata.getZRange(ampIndex);
      zmax = minmax[1];
      zmin = minmax[0];
      if(zMap!=null) {
        zMap.setMinMax(zmin, zmax);
      }
    }
    double left = griddata.getLeft(), right = griddata.getRight(), top = griddata.getTop(), bottom = griddata.getBottom();
    xmin = Math.min(left, right);
    xmax = Math.max(left, right);
    ymin = Math.min(bottom, top);
    ymax = Math.max(bottom, top);
    if(griddata instanceof ArrayData) {
      generateVerticesFromArray((ArrayData) griddata);
    } else if(griddata instanceof GridPointData) {
      generateVerticesFromPoints((GridPointData) griddata);
    }
  }

  /**
   * Translates the view by the specified number of pixels.
   *
   * @param xpix the x translation in pixels
   * @param ypix the y translation in pixels
   */
  public void setTranslation(int xpix, int ypix) {
    projector.set2DTranslation(xpix, ypix);
  }

  /**
   * Sets the viewing rotation angle.
   *
   * @param angle the rotation angle in degrees
   */
  public void setRotationAngle(double angle) {
    projector.setRotationAngle(angle);
  }

  /**
   * Sets the viewing elevation angle.
   *
   * @param angle the elevation angle in degrees
   */
  public void setElevationAngle(double angle) {
    projector.setElevationAngle(angle);
  }

  /**
   * Sets the viewing distance.
   *
   * @param distance the distance
   */
  public void setDistance(double distance) {
    projector.setDistance(distance);
  }

  /**
   * Sets the 2D scaling factor.
   *
   * @param scale the scaling factor
   */
  public void set2DScaling(double scale) {
    projector.set2DScaling(scale);
  }

  /**
   * <code>mouseDown</code> event handler. Sets internal tracking variables
   * for dragging operations.
   *
   * @param e the event
   * @param drawingPanel
   *
   * @return mouse pressed flag
   */
  public boolean mousePressed(MouseEvent e, DrawingPanel drawingPanel) {
    click_x = e.getX();
    click_y = e.getY();
    //mouseDown = true;
    return true;
  }

  /**
   * Method mouseReleased
   *
   * @param e
   * @param drawingPanel
   */
  public void mouseReleased(MouseEvent e, DrawingPanel drawingPanel) {
    //mouseDown = false;
  }

  /**
   * <code>mouseDrag<code> event handler. Tracks dragging operations.
   * Checks the delay regeneration flag and does proper actions.
   *
   * @param e the event
   * @param drawingPanel
   */
  public void mouseDragged(MouseEvent e, DrawingPanel drawingPanel) {
    double new_value = 0.0;
    int x = e.getX();
    int y = e.getY();
    if(e.isControlDown()) {
      projector.set2D_xTranslation(projector.get2D_xTranslation()+(x-click_x));
      projector.set2D_yTranslation(projector.get2D_yTranslation()+(y-click_y));
    } else if(e.isShiftDown()) {
      new_value = projector.get2DScaling()+(y-click_y)*0.5;
      if(new_value>60.0f) {
        new_value = 60.0f;
      }
      if(new_value<2.0f) {
        new_value = 2.0f;
      }
      projector.set2DScaling(new_value);
    } else {
      new_value = projector.getRotationAngle()+(x-click_x);
      while(new_value>360) {
        new_value -= 360;
      }
      while(new_value<0) {
        new_value += 360;
      }
      projector.setRotationAngle(new_value);
      new_value = projector.getElevationAngle()+(y-click_y);
      if(new_value>90) {
        new_value = 90;
      } else if(new_value<0) {
        new_value = 0;
      }
      projector.setElevationAngle(new_value);
    }
    click_x = x;
    click_y = y;
    invalidProjection = true;
    drawingPanel.render();
  }

  /**
   * Gets the minimum x needed to draw this object.
   * @return minimum
   */
  public double getXMin() {
    return 0;
  }

  /**
   * Gets the maximum x needed to draw this object.
   * @return maximum
   */
  public double getXMax() {
    return 0;
  }

  /**
   * Gets the minimum y needed to draw this object.
   * @return minimum
   */
  public double getYMin() {
    return 0;
  }

  /**
   * Gets the maximum y needed to draw this object.
   * @return minimum
   */
  public double getYMax() {
    return 0;
  }

  /**
   * Determines if information is available to set min/max values.
   * X y values have no meaning for this plot.
   *
   * @return false
   */
  public boolean isMeasured() {
    return false;
  }

  /**
   * Gets an XML.ObjectLoader to save and load data for this program.
   *
   * @return the object loader
   */
  public static XML.ObjectLoader getLoader() {
    return new Plot2DLoader() {
      public Object createObject(XMLControl control) {
        return new ComplexSurfacePlot(null);
      }

    };
  }

}

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

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
