/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.unidata.geoloc;

import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.util.*;

import java.util.*;

/**
 * Superclass for our implementations of geoloc.Projection.
 *

 *
All subclasses must:

 *
 override equals() and return true when all parameters are equal
 *
 create "atts" list of parameters as string-valued Attribute pairs
 *
 implement abstract methods
 *

 *

 * If possible, set defaultmapArea to some reasonable world coord bounding box
 * otherwise, provide a way for the user to specify it when a specific projection
 * is created.
 *

 *
 Note on "false_easting" and "fale_northing" projection parameters:
 *

false_easting(northing) = The value added to all x (y) values in the rectangular coordinates for a map projection.
 * This value frequently is assigned to eliminate negative numbers.
 * Expressed in the unit of measure identified in Planar Coordinate Units.
 *
We dont currently use, assuming that the x and y are just fine as negetive numbers.
 *

 *
 * @author John Caron
 * @see Projection
 */
public abstract class ProjectionImpl implements Projection, java.io.Serializable {
  /**
   * Earth radius in kilometers
   */
  static public final double EARTH_RADIUS = Earth.getRadius() * 0.001;  // km

  /**
   * Latitude index
   */
  public static final int INDEX_LAT = 0;

  /**
   * Longitude index
   */
  public static final int INDEX_LON = 1;

  /**
   * X index
   */
  public static final int INDEX_X = 0;

  /**
   * Y index
   */
  public static final int INDEX_Y = 1;

  /**
   * tolerence for checks
   */
  protected static final double TOLERANCE = 1.0e-6;

  /**
   * PI
   */
  public static final double PI = Math.PI;

  /**
   * PI/2
   */
  public static final double PI_OVER_2 = Math.PI / 2.0;

  /**
   * PI/4
   */
  public static final double PI_OVER_4 = Math.PI / 4.0;

  ///////////////////////////////////////////////////////////////////////

  /**
   * name of this projection.
   */
  protected String name;  // LOOK should be final, IDV needs setName()

  /**
   * flag for latlon
   */
  protected final boolean isLatLon;

  /**
   * list of attributes
   */
  protected final List<Parameter> atts = new ArrayList<>();

  /**
   * default map area
   */
  protected ProjectionRect defaultMapArea = new ProjectionRect();

  /**
   * copy constructor - avoid clone !!
   *
   * @return a copy of this Projection
   */
  abstract public ProjectionImpl constructCopy();

  protected ProjectionImpl(String name, boolean isLatLon) {
    this.name = name;
    this.isLatLon = isLatLon;
  }

  /**
   * Get the name of the type of the projection.
   *
   * @return the class name
   */
  public String getClassName() {
    String className = getClass().getName();
    int index = className.lastIndexOf(".");
    if (index >= 0) {
      className = className.substring(index + 1);
    }
    return className;
  }

  /**
   * Get a string representation of the projection parameters
   *
   * @return string representation of the projection parameters
   */
  public abstract String paramsToString();

  /**
   * Get the label to be used in the gui for this type of projection.
   * This defaults to call getClassName
   *
   * @return Type label
   */

  public String getProjectionTypeLabel() {
    return getClassName();
  }

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latlon    convert from these lat, lon coordinates
   * @param destPoint the object to write to
   * @return the given destPoint
   */
  public abstract ProjectionPoint latLonToProj(LatLonPoint latlon, ProjectionPointImpl destPoint);

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param ppt       convert from these projection coordinates
   * @param destPoint the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  public abstract LatLonPoint projToLatLon(ProjectionPoint ppt, LatLonPointImpl destPoint);

  /**
   * Convert a LatLonPoint to projection coordinates
   * Note: a new object is now created on each call for the return value, as of 4.0.46
   *
   * @param latLon convert from these lat, lon coordinates
   * @return ProjectionPoint convert to these projection coordinates
   */
  public ProjectionPoint latLonToProj(LatLonPoint latLon) {
    return latLonToProj(latLon, new ProjectionPointImpl());
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is now created on each call for the return value, as of 4.0.46
   *
   * @param ppt convert from these projection coordinates
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  public LatLonPoint projToLatLon(ProjectionPoint ppt) {
    return projToLatLon(ppt, new LatLonPointImpl());
  }

  /**
   * Does the line between these two points cross the projection "seam".
   *
   * @param pt1 the line goes between these two points
   * @param pt2 the line goes between these two points
   * @return false if there is no seam
   */
  public abstract boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2);

  /**
   * Returns true if this represents the same Projection as proj.
   *
   * @param proj projection in question
   * @return true if this represents the same Projection as proj.
   */
  public abstract boolean equals(Object proj);

  /**
   * Get the name of this specific projection (also see getClassName)
   *
   * @return name of the projection
   */
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  /**
   * Get parameters as list of ucar.unidata.util.Parameter
   *
   * @return List of parameters
   */
  public List<Parameter> getProjectionParameters() {
    return atts;
  }

  public Parameter findProjectionParameter(String want) {
    for (Parameter p : atts) {
      if (p.getName().equals(want)) return p;
    }
    return null;
  }

  /**
   * Add an attribute to this projection
   *
   * @param name  name of the attribute
   * @param value attribute value as a string
   */
  protected void addParameter(String name, String value) {
    atts.add(new Parameter(name, value));
  }

  /**
   * Add an attribute to this projection
   *
   * @param name  name of the attribute
   * @param value attribute value as a double
   */
  protected void addParameter(String name, double value) {
    atts.add(new Parameter(name, value));
  }

  /**
   * Add an attribute to this projection
   *
   * @param p specify as a Parameter
   */
  protected void addParameter(Parameter p) {
    atts.add(p);
  }

  /**
   * Is this the lat/lon Projection ?
   *
   * @return true if it is the lat/lon Projection
   */
  public boolean isLatLon() {
    return isLatLon;
  }

  /**
   * Get a header for display.
   *
   * @return human readable header for display
   */
  public static String getHeader() {
    StringBuilder headerB = new StringBuilder(60);
    headerB.append("Name");
    Format.tab(headerB, 20, true);
    headerB.append("Class");
    Format.tab(headerB, 40, true);
    headerB.append("Parameters");
    return headerB.toString();
  }

  /**
   * Get a String representation of this projection.
   *
   * @return the name of the projection.  This is what gets
   *         displayed when you add the projection object to
   *         a UI widget (e.g. label, combobox)
   */
  public String toString() {
    return getName();
  }

  /**
   * Get a reasonable bounding box for this projection.
   *
   * @return reasonable bounding box
   */
  public ProjectionRect getDefaultMapArea() {
    return defaultMapArea;
  }

  /**
   * Get the bounding box in lat/lon.
   *
   * @return the LatLonRectangle for the bounding box
   */
  public LatLonRect getDefaultMapAreaLL() {
    return projToLatLonBB(defaultMapArea);
  }

  /**
   * Set a reasonable bounding box for this specific projection.
   * Projections are typically specific to an area of the world;
   * theres no bounding box that works for all projections.
   *
   * @param bb bounding box
   */
  public void setDefaultMapArea(ProjectionRect bb) {
    if (bb == null) return;
    defaultMapArea = new ProjectionRect(bb);
  }

  //////// convenience routines

  /**
   * Convert a LatLonPoint to projection coordinates
   * Note: a new object is now created on each call for the return value, as of 4.0.46
   *
   * @param lat latitude of point to convert
   * @param lon longitude of point to convert
   * @return ProjectionPointImpl convert to these projection coordinates
   */
  public ProjectionPoint latLonToProj(double lat, double lon) {
    return latLonToProj(new LatLonPointImpl(lat, lon));
  }

  /**
   * Convert a projection coordinate to a LatLonPoint
   * Note: a new object is now created on each call for the return value, as of 4.0.46
   *
   * @param x x value to convert
   * @param y y value to convert
   * @return LatLonPointImpl convert to these lat/lon coordinates
   */
  public LatLonPoint projToLatLon(double x, double y) {
    return projToLatLon(new ProjectionPointImpl(x, y));
  }

  ///////////////////////////////////////////////////////////////////////////////////
  // optimizations for doing double and float arrays

  /**
   * Convert projection coordinates to lat/lon coordinates.
   *
   * @param from array of projection coordinates: from[2][n],
   *             where from[0][i], from[1][i] is the x, y coordinate
   *             of the ith point
   * @return resulting array of lat/lon coordinates, where to[0][i], to[1][i]
   *         is the lat,lon coordinate of the ith point
   */
  public double[][] projToLatLon(double[][] from) {
    return projToLatLon(from, new double[2][from[0].length]);
  }

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) coordinate of
   *             the ith point
   * @return the "to" array
   */
  public double[][] projToLatLon(double[][] from, double[][] to) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "from array not same length as to array");
    }

    for (int i = 0; i < from[0].length; i++) {
      LatLonPoint endL = projToLatLon(from[0][i], from[1][i]);
      to[0][i] = endL.getLatitude();
      to[1][i] = endL.getLongitude();
    }

    return to;
  }

  /**
   * Convert projection coordinates to lat/lon coordinates.
   *
   * @param from array of projection coordinates: from[2][n],
   *             where from[0][i], from[1][i] is the x, y coordinate
   *             of the ith point
   * @return resulting array of lat/lon coordinates, where to[0][i], to[1][i]
   *         is the lat,lon coordinate of the ith point
   */
  public float[][] projToLatLon(float[][] from) {
    return projToLatLon(from, new float[2][from[0].length]);
  }

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) coordinate of
   *             the ith point
   * @return the "to" array
   */
  public float[][] projToLatLon(float[][] from, float[][] to) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:"
              + "from array not same length as to array");
    }

    ProjectionPointImpl ppi = new ProjectionPointImpl();
    LatLonPointImpl llpi = new LatLonPointImpl();

    for (int i = 0; i < from[0].length; i++) {
      ppi.setLocation((double) from[0][i], (double) from[1][i]);
      projToLatLon(ppi, llpi);
      to[0][i] = (float) llpi.getLatitude();
      to[1][i] = (float) llpi.getLongitude();
    }

    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n],
   *             where from[0][i], from[1][i] is the (lat,lon)
   *             coordinate of the ith point
   * @return resulting array of projection coordinates, where to[0][i],
   *         to[1][i] is the (x,y) coordinate of the ith point
   */
  public double[][] latLonToProj(double[][] from) {
    return latLonToProj(from, new double[2][from[0].length]);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public double[][] latLonToProj(double[][] from, double[][] to) {
    return latLonToProj(from, to, INDEX_LAT, INDEX_LON);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return resulting array of projection coordinates: to[2][n] where
   *         (to[0][i], to[1][i]) is the (x,y) coordinate of the ith point
   */
  public double[][] latLonToProj(double[][] from, int latIndex,
                                 int lonIndex) {
    return latLonToProj(from, new double[2][from[0].length], latIndex,
            lonIndex);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates: to[2][n]
   *                 where (to[0][i], to[1][i]) is the (x,y) coordinate of
   *                 the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return the "to" array
   */
  public double[][] latLonToProj(double[][] from, double[][] to,
                                 int latIndex, int lonIndex) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "from array not same length as to array");
    }

    ProjectionPointImpl ppi = new ProjectionPointImpl();
    LatLonPointImpl llpi = new LatLonPointImpl();

    for (int i = 0; i < from[0].length; i++) {
      llpi.setLatitude(from[latIndex][i]);
      llpi.setLongitude(from[lonIndex][i]);
      latLonToProj(llpi, ppi);
      to[0][i] = ppi.getX();
      to[1][i] = ppi.getY();
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n],
   *             where from[0][i], from[1][i] is the (lat,lon)
   *             coordinate of the ith point
   * @return resulting array of projection coordinates, where to[0][i],
   *         to[1][i] is the (x,y) coordinate of the ith point
   */
  public float[][] latLonToProj(float[][] from) {
    return latLonToProj(from, new float[2][from[0].length]);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public float[][] latLonToProj(float[][] from, float[][] to) {
    return latLonToProj(from, to, INDEX_LAT, INDEX_LON);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return resulting array of projection coordinates: to[2][n] where
   *         (to[0][i], to[1][i]) is the (x,y) coordinate of the ith point
   */
  public float[][] latLonToProj(float[][] from, int latIndex,
                                int lonIndex) {
    return latLonToProj(from, new float[2][from[0].length], latIndex,
            lonIndex);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates: to[2][n]
   *                 where (to[0][i], to[1][i]) is the (x,y) coordinate of
   *                 the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return the "to" array
   */
  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex,
                                int lonIndex) {
    //      ucar.unidata.util.Misc.printStack ("latLonToProj-" + this + " size=" + from[0].length, 4, null);

    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:"
              + "from array not same length as to array");
    }

    ProjectionPointImpl ppi = new ProjectionPointImpl();
    LatLonPointImpl llpi = new LatLonPointImpl();

    for (int i = 0; i < from[0].length; i++) {
      llpi.setLatitude(from[latIndex][i]);
      llpi.setLongitude(from[lonIndex][i]);
      latLonToProj(llpi, ppi);
      to[0][i] = (float) ppi.getX();
      to[1][i] = (float) ppi.getY();
    }

    return to;
  }

  // bounding box utilities

  /**
   * Alternate way to calculate latLonToProjBB, originally in GridCoordSys.
   * Difficult to do this in a general way.
   *
   * @param latlonRect desired lat/lon rectangle
   * @return a ProjectionRect
   */
  ProjectionRect latLonToProjBB2(LatLonRect latlonRect) {
    double minx, maxx, miny, maxy;

    LatLonPointImpl llpt = latlonRect.getLowerLeftPoint();
    LatLonPointImpl urpt = latlonRect.getUpperRightPoint();
    LatLonPointImpl lrpt = latlonRect.getLowerRightPoint();
    LatLonPointImpl ulpt = latlonRect.getUpperLeftPoint();

    if (isLatLon()) {
      minx = getMinOrMaxLon(llpt.getLongitude(), ulpt.getLongitude(), true);
      miny = Math.min(llpt.getLatitude(), lrpt.getLatitude());
      maxx = getMinOrMaxLon(urpt.getLongitude(), lrpt.getLongitude(), false);
      maxy = Math.min(ulpt.getLatitude(), urpt.getLatitude());

    } else {
      ProjectionPoint ll = latLonToProj(llpt, new ProjectionPointImpl());
      ProjectionPoint ur = latLonToProj(urpt, new ProjectionPointImpl());
      ProjectionPoint lr = latLonToProj(lrpt, new ProjectionPointImpl());
      ProjectionPoint ul = latLonToProj(ulpt, new ProjectionPointImpl());

      minx = Math.min(ll.getX(), ul.getX());
      miny = Math.min(ll.getY(), lr.getY());
      maxx = Math.max(ur.getX(), lr.getX());
      maxy = Math.max(ul.getY(), ur.getY());
    }

    return new ProjectionRect(minx, miny, maxx, maxy);
  }

  private double getMinOrMaxLon(double lon1, double lon2, boolean wantMin) {
    double midpoint = (lon1 + lon2) / 2;
    lon1 = LatLonPointImpl.lonNormal(lon1, midpoint);
    lon2 = LatLonPointImpl.lonNormal(lon2, midpoint);

    return wantMin ? Math.min(lon1, lon2) : Math.max(lon1, lon2);
  }

  /**
   * Convert a lat/lon bounding box to a world coordinate bounding box,
   * by finding the minimum enclosing box.
   * Handles lat/lon points that do not intersect the projection panel.
   *
   * @param latlonRect input lat,lon bounding box
   * @return minimum enclosing box in world coordinates, or null if no part of the LatLonRect intersects the projection plane
   */
  public ProjectionRect latLonToProjBB(LatLonRect latlonRect) {
    if (isLatLon) {
      LatLonProjection llp = (LatLonProjection) this;
      llp.setCenterLon(latlonRect.getCenterLon()); // LOOK side effect BAD !!
    }

    ProjectionPointImpl w1 = new ProjectionPointImpl();
    ProjectionPointImpl w2 = new ProjectionPointImpl();

    LatLonPoint ll = latlonRect.getLowerLeftPoint();
    LatLonPoint ur = latlonRect.getUpperRightPoint();
    latLonToProj(ll, w1);
    latLonToProj(ur, w2);

    //if (!isLatLon && crossSeam(w1, w2)) {
    //  log.warn("CROSS SEAM failure=" + w1 + " " + w2+" for "+this, new Throwable());
    //}

    // make bounding box out of those two corners
    ProjectionRect world = new ProjectionRect(w1.getX(), w1.getY(), w2.getX(), w2.getY());

    LatLonPointImpl la = new LatLonPointImpl();
    LatLonPointImpl lb = new LatLonPointImpl();

    // now extend if needed to the other two corners
    la.setLatitude(ur.getLatitude());
    la.setLongitude(ll.getLongitude());
    latLonToProj(la, w1);
    world.add(w1);

    lb.setLatitude(ll.getLatitude());
    lb.setLongitude(ur.getLongitude());
    latLonToProj(lb, w2);
    world.add(w2);

    return world;
  }

  /**
   * Convert a world coordinate bounding box to a lat/lon bounding box,
   * by finding the minimum enclosing box.
   *
   * @param world input world coordinate bounding box
   * @return minimum enclosing box in lat,lon coordinates.
   */
  public LatLonRect projToLatLonBBold(ProjectionRect world) {
    //System.out.println("world = " + world);
    ProjectionPoint min = world.getMinPoint();
    ProjectionPoint max = world.getMaxPoint();
    //System.out.println("min = " + min);
    //System.out.println("max = " + max);
    LatLonRect llbb;
    LatLonPointImpl llmin = new LatLonPointImpl();
    LatLonPointImpl llmax = new LatLonPointImpl();

    // make bounding box out of the min, max corners
    projToLatLon(min, llmin);
    projToLatLon(max, llmax);
    llbb = new LatLonRect(llmin, llmax);
    //System.out.println("llbb = " + llbb);

    /*
   double lona = la.getLongitude();
   double lonb = lb.getLongitude();

   if (((lona < lonb) && (lonb - lona <= 180.0))
           || ((lona > lonb) && (lona - lonb >= 180.0))) {
       llbb = new LatLonRect(la, lb);
   } else {
       llbb = new LatLonRect(lb, la);
   } */

    ProjectionPointImpl w1 = new ProjectionPointImpl();
    ProjectionPointImpl w2 = new ProjectionPointImpl();

    // now extend if needed using the other two corners
    w1.setLocation(min.getX(), max.getY());
    projToLatLon(w1, llmin);
    llbb.extend(llmin);

    w2.setLocation(max.getX(), min.getY());
    projToLatLon(w2, llmax);
    llbb.extend(llmax);

    return llbb;
  }

  /**
   * Compute lat/lon bounding box from projection bounding box by finding the minimum enclosing box.
   * @param bb projection bounding box
   * @return lat, lon bounding box.
   */
  public LatLonRect projToLatLonBB(ProjectionRect bb) {
    // look at all 4 corners of the bounding box
    LatLonPoint llpt = projToLatLon(bb.getLowerLeftPoint(), new LatLonPointImpl());
    LatLonPoint lrpt = projToLatLon(bb.getLowerRightPoint(), new LatLonPointImpl());
    LatLonPoint urpt = projToLatLon(bb.getUpperRightPoint(), new LatLonPointImpl());
    LatLonPoint ulpt = projToLatLon(bb.getUpperLeftPoint(), new LatLonPointImpl());

    // Check if grid contains poles.
    boolean includesNorthPole = false;
    /* int[] resultNP;
    findXYindexFromLatLon(90.0, 0, resultNP);
    if (resultNP[0] != -1 && resultNP[1] != -1)
      includesNorthPole = true;      */
    boolean includesSouthPole = false;
    /* int[] resultSP = new int[2];
    findXYindexFromLatLon(-90.0, 0, resultSP);
    if (resultSP[0] != -1 && resultSP[1] != -1)
      includesSouthPole = true; */

    LatLonRect llbb;

    if (includesNorthPole && !includesSouthPole) {
      llbb = new LatLonRect(llpt, new LatLonPointImpl(90.0, 0.0)); // ??? lon=???
      llbb.extend(lrpt);
      llbb.extend(urpt);
      llbb.extend(ulpt);
      // OR
      //llbb.extend( new LatLonRect( llpt, lrpt ));
      //llbb.extend( new LatLonRect( lrpt, urpt ) );
      //llbb.extend( new LatLonRect( urpt, ulpt ) );
      //llbb.extend( new LatLonRect( ulpt, llpt ) );
    } else if (includesSouthPole && !includesNorthPole) {
      llbb = new LatLonRect(llpt, new LatLonPointImpl(-90.0, -180.0)); // ??? lon=???
      llbb.extend(lrpt);
      llbb.extend(urpt);
      llbb.extend(ulpt);

    } else {
      double latMin = Math.min(llpt.getLatitude(), lrpt.getLatitude());
      double latMax = Math.max(ulpt.getLatitude(), urpt.getLatitude());

      // longitude is a bit tricky as usual
      double lonMin = getMinOrMaxLon(llpt.getLongitude(), ulpt.getLongitude(), true);
      double lonMax = getMinOrMaxLon(lrpt.getLongitude(), urpt.getLongitude(), false);

      LatLonPointImpl min = new LatLonPointImpl(latMin, lonMin);
      LatLonPointImpl max = new LatLonPointImpl(latMax, lonMax);
      llbb = new LatLonRect(min, max);
    }

    return llbb;
  }

}


/*
Copyright 2006 Jerry Huxtable

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package ucar.unidata.geoloc.projection.proj4;

import ucar.unidata.geoloc.ProjectionPoint;
import ucar.unidata.geoloc.ProjectionPointImpl;
import ucar.unidata.geoloc.ProjectionRect;

/**
 * Taken from com.jhlabs.map.proj
 *
 * Also see "http://search.cpan.org/src/DSTAHLKE/Cartography-Projection-GCTP-0.03/gctpc/" for C code
 *
 * @see "http://www.jhlabs.com/java/maps/proj/index.html"
 * @see "http://trac.osgeo.org/proj/"
 *
 * @since Oct 8, 2009
 */

public class MapMath {

	public final static double HALFPI = Math.PI/2.0;
	public final static double QUARTERPI = Math.PI/4.0;
	public final static double TWOPI = Math.PI*2.0;

	public final static ProjectionRect WORLD_BOUNDS_RAD = new ProjectionRect(-Math.PI, -Math.PI/2, Math.PI*2, Math.PI);
	public final static ProjectionRect WORLD_BOUNDS = new ProjectionRect(-180, -90, 360, 180);

  public final static double EPS10 = 1e-10;
  public final static double RTD = 180.0 / Math.PI;
  public final static double DTR = Math.PI / 180.0;

	/**
	 * Degree versions of trigonometric functions
	 */
	public static double sind(double v) {
		return Math.sin(v * DTR);
	}

	public static double cosd(double v) {
		return Math.cos(v * DTR);
	}

	public static double tand(double v) {
		return Math.tan(v * DTR);
	}

	public static double asind(double v) {
		return Math.asin(v) * RTD;
	}

	public static double acosd(double v) {
		return Math.acos(v) * RTD;
	}

	public static double atand(double v) {
		return Math.atan(v) * RTD;
	}

	public static double atan2d(double y, double x) {
		return Math.atan2(y, x) * RTD;
	}

	public static double asin(double v) {
		if (Math.abs(v) > 1.)
			return v < 0.0 ? -Math.PI/2 : Math.PI/2;
		return Math.asin(v);
	}

	public static double acos(double v) {
		if (Math.abs(v) > 1.)
			return v < 0.0 ? Math.PI : 0.0;
		return Math.acos(v);
	}

	public static double sqrt(double v) {
		return v < 0.0 ? 0.0 : Math.sqrt(v);
	}

	public static double distance(double dx, double dy) {
		return Math.sqrt(dx*dx+dy*dy);
	}

	public static double distance(ProjectionPoint a, ProjectionPoint b) {
		return distance(a.getX()-b.getX(), a.getY()-b.getY());
	}

	public static double hypot(double x, double y) {
		if (x < 0.0)
			x = -x;
		else if (x == 0.0)
			return y < 0.0 ? -y : y;
		if (y < 0.0)
			y = -y;
		else if (y == 0.0)
			return x;
		if (x < y) {
			x /= y;
			return y * Math.sqrt(1.0 + x * x);
		} else {
			y /= x;
			return x * Math.sqrt(1.0 + y * y);
		}
	}

	public static double atan2(double y, double x) {
		return Math.atan2(y, x);
	}

	public static double trunc(double v) {
		return v < 0.0 ? Math.ceil(v) : Math.floor(v);
	}

	public static double frac(double v) {
		return v - trunc(v);
	}

	public static double degToRad(double v) {
		return v * Math.PI / 180.0;
	}

	public static double radToDeg(double v) {
		return v * 180.0 / Math.PI;
	}

	// For negative angles, d should be negative, m & s positive.
	public static double dmsToRad(double d, double m, double s) {
		if (d >= 0)
			return (d + m/60 + s/3600) * Math.PI / 180.0;
		return (d - m/60 - s/3600) * Math.PI / 180.0;
	}

	// For negative angles, d should be negative, m & s positive.
	public static double dmsToDeg(double d, double m, double s) {
		if (d >= 0)
			return (d + m/60 + s/3600);
		return (d - m/60 - s/3600);
	}

	public static double normalizeLatitude(double angle) {
		if (Double.isInfinite(angle) || Double.isNaN(angle))
			throw new RuntimeException("Infinite latitude");

		while (angle > MapMath.HALFPI)
			angle -= Math.PI;
		while (angle < -MapMath.HALFPI)
			angle += Math.PI;
		return angle;
//		return Math.IEEEremainder(angle, Math.PI);
	}

	public static double normalizeLongitude(double angle) {
		if (Double.isInfinite(angle) || Double.isNaN(angle))
			throw new RuntimeException("Infinite longitude");

		while (angle > Math.PI)
			angle -= TWOPI;
		while (angle < -Math.PI)
			angle += TWOPI;
		return angle;
//		return Math.IEEEremainder(angle, Math.PI);
	}

	public static double normalizeAngle(double angle) {
		if (Double.isInfinite(angle) || Double.isNaN(angle))
			throw new RuntimeException("Infinite angle");

		while (angle > TWOPI)
			angle -= TWOPI;
		while (angle < 0)
			angle += TWOPI;
		return angle;
	}

/*
	public static void latLongToXYZ(ProjectionPoint ll, Point3D xyz) {
		double c = Math.cos(ll.y);
		xyz.x = c * Math.cos(ll.x);
		xyz.y = c * Math.sin(ll.x);
		xyz.z = Math.sin(ll.y);
	}

	public static void xyzToLatLong(Point3D xyz, ProjectionPoint ll) {
		ll.y = MapMath.asin(xyz.z);
		ll.x = MapMath.atan2(xyz.y, xyz.x);
	}
*/

	public static double greatCircleDistance(double lon1, double lat1, double lon2, double lat2 ) {
		double dlat = Math.sin((lat2-lat1)/2);
		double dlon = Math.sin((lon2-lon1)/2);
		double r = Math.sqrt(dlat*dlat + Math.cos(lat1)*Math.cos(lat2)*dlon*dlon);
		return 2.0 * Math.asin(r);
	}

	public static double sphericalAzimuth(double lat0, double lon0, double lat, double lon) {
		double diff = lon - lon0;
		double coslat = Math.cos(lat);

		return Math.atan2(
			coslat * Math.sin(diff),
			(Math.cos(lat0) * Math.sin(lat) -
			Math.sin(lat0) * coslat * Math.cos(diff))
		);
	}

	public static boolean sameSigns(double a, double b) {
		return a < 0 == b < 0;
	}

	public static boolean sameSigns(int a, int b) {
		return a < 0 == b < 0;
	}

	public static double takeSign(double a, double b) {
		a = Math.abs(a);
		if (b < 0)
			return -a;
		return a;
	}

	public static int takeSign(int a, int b) {
		a = Math.abs(a);
		if (b < 0)
			return -a;
		return a;
	}

	public final static int DONT_INTERSECT = 0;
	public final static int DO_INTERSECT = 1;
	public final static int COLLINEAR = 2;

	public static int intersectSegments(ProjectionPoint aStart, ProjectionPoint aEnd, ProjectionPoint bStart, ProjectionPoint bEnd, ProjectionPointImpl p) {
		double a1, a2, b1, b2, c1, c2;
		double r1, r2, r3, r4;
		double denom, offset, num;

		a1 = aEnd.getY()-aStart.getY();
		b1 = aStart.getX()-aEnd.getX();
		c1 = aEnd.getX()*aStart.getY() - aStart.getX()*aEnd.getY();
		r3 = a1*bStart.getX() + b1*bStart.getY() + c1;
		r4 = a1*bEnd.getX() + b1*bEnd.getY() + c1;

		if (r3 != 0 && r4 != 0 && sameSigns(r3, r4))
			return DONT_INTERSECT;

		a2 = bEnd.getY()-bStart.getY();
		b2 = bStart.getX()-bEnd.getX();
		c2 = bEnd.getX()*bStart.getY()-bStart.getX()*bEnd.getY();
		r1 = a2*aStart.getX() + b2*aStart.getY() + c2;
		r2 = a2*aEnd.getX() + b2*aEnd.getY() + c2;

		if (r1 != 0 && r2 != 0 && sameSigns(r1, r2))
			return DONT_INTERSECT;

		denom = a1*b2 - a2*b1;
		if (denom == 0)
			return COLLINEAR;

		offset = denom < 0 ? -denom/2 : denom/2;

		num = b1*c2 - b2*c1;
		p.setX((num < 0 ? num-offset : num+offset) / denom);

		num = a2*c1 - a1*c2;
		p.setY((num < 0 ? num-offset : num+offset) / denom);

		return DO_INTERSECT;
	}

	public static double dot(ProjectionPoint a, ProjectionPoint b) {
		return a.getX()*b.getX() + a.getY()*b.getY();
	}

	public static ProjectionPoint perpendicular(ProjectionPoint a) {
		return new ProjectionPointImpl(-a.getY(), a.getX());
	}

	public static ProjectionPoint add(ProjectionPoint a, ProjectionPoint b) {
		return new ProjectionPointImpl(a.getX()+b.getX(), a.getY()+b.getY());
	}

	public static ProjectionPoint subtract(ProjectionPoint a, ProjectionPoint b) {
		return new ProjectionPointImpl(a.getX()-b.getX(), a.getY()-b.getY());
	}

	public static ProjectionPoint multiply(ProjectionPoint a, ProjectionPoint b) {
		return new ProjectionPointImpl(a.getX()*b.getX(), a.getY()*b.getY());
	}

	public static double cross(ProjectionPoint a, ProjectionPoint b) {
		return a.getX()*b.getY() - b.getX()*a.getY();
	}

	public static double cross(double x1, double y1, double x2, double y2) {
		return x1*y2 - x2*y1;
	}

	public static void normalize(ProjectionPointImpl a) {
		double d = distance(a.getX(), a.getY());
		a.setLocation( a.getX() / d, a.getY() / d);
	}

	public static void negate(ProjectionPointImpl a) {
    a.setLocation( -a.getX(), -a.getY());
	}

	public static double longitudeDistance(double l1, double l2) {
		return Math.min(
			Math.abs(l1-l2),
			((l1 < 0) ? l1+Math.PI : Math.PI-l1) + ((l2 < 0) ? l2+Math.PI : Math.PI-l2)
		);
	}

	public static double geocentricLatitude(double lat, double flatness) {
		double f = 1.0 - flatness;
		return Math.atan((f*f) * Math.tan(lat));
	}

	public static double geographicLatitude(double lat, double flatness) {
		double f = 1.0 - flatness;
		return Math.atan(Math.tan(lat) / (f*f));
	}

	public static double tsfn(double phi, double sinphi, double e) {
		sinphi *= e;
		return (Math.tan (.5 * (MapMath.HALFPI - phi)) /
		   Math.pow((1. - sinphi) / (1. + sinphi), .5 * e));
	}

	public static double msfn(double sinphi, double cosphi, double es) {
		return cosphi / Math.sqrt(1.0 - es * sinphi * sinphi);
	}

	private final static int N_ITER = 15;

	public static double phi2(double ts, double e) {
		double eccnth, phi, con, dphi;
		int i;

		eccnth = .5 * e;
		phi = MapMath.HALFPI - 2. * Math.atan(ts);
		i = N_ITER;
		do {
			con = e * Math.sin(phi);
			dphi = MapMath.HALFPI - 2. * Math.atan(ts * Math.pow((1. - con) / (1. + con), eccnth)) - phi;
			phi += dphi;
		} while (Math.abs(dphi) > 1e-10 && --i != 0);
		if (i <= 0)
			throw new RuntimeException();
		return phi;
	}

	private final static double C00 = 1.0;
	private final static double C02 = .25;
	private final static double C04 = .046875;
	private final static double C06 = .01953125;
	private final static double C08 = .01068115234375;
	private final static double C22 = .75;
	private final static double C44 = .46875;
	private final static double C46 = .01302083333333333333;
	private final static double C48 = .00712076822916666666;
	private final static double C66 = .36458333333333333333;
	private final static double C68 = .00569661458333333333;
	private final static double C88 = .3076171875;
	private final static int MAX_ITER = 10;

	public static double[] enfn(double es) {
		double t;
		double[] en = new double[5];
		en[0] = C00 - es * (C02 + es * (C04 + es * (C06 + es * C08)));
		en[1] = es * (C22 - es * (C04 + es * (C06 + es * C08)));
		en[2] = (t = es * es) * (C44 - es * (C46 + es * C48));
		en[3] = (t *= es) * (C66 - es * C68);
		en[4] = t * es * C88;
		return en;
	}

	public static double mlfn(double phi, double sphi, double cphi, double[] en) {
		cphi *= sphi;
		sphi *= sphi;
		return en[0] * phi - cphi * (en[1] + sphi*(en[2] + sphi*(en[3] + sphi*en[4])));
	}

	public static double inv_mlfn(double arg, double es, double[] en) {
		double s, t, phi, k = 1./(1.-es);

		phi = arg;
		for (int i = MAX_ITER; i != 0; i--) {
			s = Math.sin(phi);
			t = 1. - es * s * s;
			phi -= t = (mlfn(phi, s, Math.cos(phi), en) - arg) * (t * Math.sqrt(t)) * k;
			if (Math.abs(t) < 1e-11)
				return phi;
		}
		return phi;
	}

	private final static double P00 = .33333333333333333333;
	private final static double P01 = .17222222222222222222;
	private final static double P02 = .10257936507936507936;
	private final static double P10 = .06388888888888888888;
	private final static double P11 = .06640211640211640211;
	private final static double P20 = .01641501294219154443;

	public static double[] authset(double es) {
		double t;
		double[] APA = new double[3];
		APA[0] = es * P00;
		t = es * es;
		APA[0] += t * P01;
		APA[1] = t * P10;
		t *= es;
		APA[0] += t * P02;
		APA[1] += t * P11;
		APA[2] = t * P20;
		return APA;
	}

	public static double authlat(double beta, double []APA) {
		double t = beta+beta;
		return(beta + APA[0] * Math.sin(t) + APA[1] * Math.sin(t+t) + APA[2] * Math.sin(t+t+t));
	}

	public static double qsfn(double sinphi, double e, double one_es) {
		double con;

		if (e >= 1.0e-7) {
			con = e * sinphi;
			return (one_es * (sinphi / (1. - con * con) -
			   (.5 / e) * Math.log ((1. - con) / (1. + con))));
		} else
			return (sinphi + sinphi);
	}

	/*
	 * Java translation of "Nice Numbers for Graph Labels"
	 * by Paul Heckbert
	 * from "Graphics Gems", Academic Press, 1990
	 */
	public static double niceNumber(double x, boolean round) {
		int expv;				/* exponent of x */
		double f;				/* fractional part of x */
		double nf;				/* nice, rounded fraction */

		expv = (int)Math.floor(Math.log(x) / Math.log(10));
		f = x/Math.pow(10., expv);		/* between 1 and 10 */
		if (round) {
			if (f < 1.5)
				nf = 1.;
			else if (f < 3.)
				nf = 2.;
			else if (f < 7.)
				nf = 5.;
			else
				nf = 10.;
		} else if (f <= 1.)
			nf = 1.;
		else if (f <= 2.)
			nf = 2.;
		else if (f <= 5.)
			nf = 5.;
		else
			nf = 10.;
		return nf*Math.pow(10., expv);
	}
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.grib.grib1.tables;

import javax.annotation.Nullable;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import thredds.featurecollection.TimeUnitConverter;
import ucar.nc2.grib.*;
import ucar.nc2.grib.coord.VertCoordType;
import ucar.nc2.grib.grib1.*;
import ucar.nc2.wmo.CommonCodeTable;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Interprets grib1 info in a way that may be customized.
 * This class handles the default case, using only standard WMO tables.
 * Subclasses override as needed.
 *
 * Bit of a contradiction, since getParamter() allows different center, subcenter, version (the version is for sure needed)
 * But other tables are fixed by center.
 *
 * @author caron
 * @since 1/13/12
 */
public class Grib1Customizer implements GribTables {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Grib1Customizer.class);

  public static Grib1Customizer factory(Grib1Record proto, Grib1ParamTables tables) {
    int center = proto.getPDSsection().getCenter();
    int subcenter = proto.getPDSsection().getSubCenter();
    int version = proto.getPDSsection().getTableVersion();
    return factory(center, subcenter, version, tables);
  }

  public static Grib1Customizer factory(int center, int subcenter, int version, Grib1ParamTables tables) {
    switch (center) {
      case 7: return new NcepTables(tables);
      case 9: return new NcepRfcTables(tables);
      case 34: return new JmaTables(tables);
      case 57: return new AfwaTables(tables);
      case 58: return new FnmocTables(tables);
      case 60: return new NcarTables(tables);
      default: return new Grib1Customizer(center, tables);
    }
  }

  public static String getSubCenterNameStatic(int center, int subcenter) {
    Grib1Customizer cust = Grib1Customizer.factory(center, subcenter, 0, null);
    return cust.getSubCenterName( subcenter);
  }

  ///////////////////////////////////////
  private final int center;
  private final Grib1ParamTables tables;

  protected Grib1Customizer(int center, Grib1ParamTables tables) {
    this.center = center;
    this.tables = (tables == null) ? new Grib1ParamTables() : tables;

    synchronized (Grib1Customizer.class) {
      if (wmoTable3 == null)
        wmoTable3 = readTable3("resources/grib1/wmoTable3.xml");
    }
  }

  public int getCenter() {
    return center;
  }

  public Grib1Parameter getParameter(int center, int subcenter, int tableVersion, int param_number) {
    return tables.getParameter(center, subcenter, tableVersion, param_number);
  }

  @Override
  @Nullable
  public String getGeneratingProcessName(int genProcess) {
    return null;
  }

  @Override
  public String getGeneratingProcessTypeName(int genProcess) {
    return null;
  }

  @Nullable
  public String getSubCenterName(int subcenter) {
    return CommonCodeTable.getSubCenterName(center, subcenter);
  }

  @Override
  @Nullable
  public String getSubCenterName(int center, int subcenter) {
    return CommonCodeTable.getSubCenterName(center, subcenter);
  }

  ///////////////////////////////////////////////////
  // time

  public Grib1ParamTime getParamTime(Grib1SectionProductDefinition pds) {
    return new Grib1ParamTime(this, pds);
  }

  // code table 5
  public String getTimeTypeName(int timeRangeIndicator) {
    return Grib1ParamTime.getTimeTypeName(timeRangeIndicator);
  }

  @Override
  @Nullable
  public GribStatType getStatType(int timeRangeIndicator) {
    return Grib1WmoTimeType.getStatType(timeRangeIndicator);
  }

  /////////////////////////////////////////
  // level

  public Grib1ParamLevel getParamLevel(Grib1SectionProductDefinition pds) {
    return new Grib1ParamLevel(this, pds);
  }

  @Override
  public VertCoordType getVertUnit(int code) {
    return makeVertUnit(code);
  }

  public boolean isVerticalCoordinate(int levelType) {
    return getLevelUnits(levelType) != null;
  }

  // below are the methods a subclass may need to override for levels

  protected VertCoordType makeVertUnit(int code) {
    return getLevelType(code);
  }

  @Override
  public String getLevelNameShort(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    String result = lt.getAbbrev();
    if (result == null) result = "unknownLevel"+levelType;
    return result;
  }

  public String getLevelDescription(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    return lt.getDesc();
  }

  public boolean isLayer(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    return lt.isLayer();
  }

  // only for 3D
  public boolean isPositiveUp(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    return lt.isPositiveUp();
  }

  // only for 3D
  public String getLevelUnits(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    return lt.getUnits();
  }

  // only for 3D
  public String getLevelDatum(int levelType) {
    VertCoordType lt = getLevelType(levelType);
    return lt.getDatum();
  }

  /////////////////////////////////////////////
  private TimeUnitConverter timeUnitConverter;

  public void setTimeUnitConverter(TimeUnitConverter timeUnitConverter) {
    this.timeUnitConverter = timeUnitConverter;
  }

  public int convertTimeUnit(int timeUnit) {
    if (timeUnitConverter == null) return timeUnit;
    return timeUnitConverter.convertTimeUnit(timeUnit);
  }

  ////////////////////////////////////////////////////////////////////////

  private static Map<Integer, VertCoordType> wmoTable3;  // shared by all instances

  protected VertCoordType getLevelType(int code) {
    VertCoordType result = wmoTable3.get(code);
    if (result == null)
      result = new VertCoordType(code, "unknownLayer"+code, null, "unknownLayer"+code, null, false, false);
    return result;
  }

  @Nullable
  protected synchronized Map<Integer, VertCoordType> readTable3(String path) {
    try (InputStream is =  GribResourceReader.getInputStream(path)) {
      SAXBuilder builder = new SAXBuilder();
      org.jdom2.Document doc = builder.build(is);
      Element root = doc.getRootElement();

      Map<Integer, VertCoordType> result = new HashMap<>(200);
      List<Element> params = root.getChildren("parameter");
      for (Element elem1 : params) {
        int code = Integer.parseInt(elem1.getAttributeValue("code"));
        String desc = elem1.getChildText("description");
        String abbrev = elem1.getChildText("abbrev");
        String units = elem1.getChildText("units");
        String datum = elem1.getChildText("datum");
        boolean isLayer = elem1.getChild("isLayer") != null;
        boolean isPositiveUp = elem1.getChild("isPositiveUp") != null;
        VertCoordType lt = new VertCoordType(code, desc, abbrev, units, datum, isPositiveUp, isLayer);
        result.put(code, lt);
      }

      return Collections.unmodifiableMap(result);  // all at once - thread safe
    } catch (IOException | JDOMException e) {
      logger.error("Cant parse NcepLevelTypes = " + path, e);
      return null;
    }
  }

  public static void main(String[] args) {
    Grib1Customizer cust = new Grib1Customizer(0, null);
    String units = cust.getLevelUnits(110);
    assert units != null;
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.dataset.conv;

import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CF;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.ma2.Array;
import ucar.ma2.IndexIterator;
import ucar.ma2.DataType;

import java.util.*;
import java.io.IOException;

/**
 * CF-1 Convention.
 * see http://www.cgd.ucar.edu/cms/eaton/cf-metadata/index.html
 * <p/>
 * <i>
 * "The CF conventions for climate and forecast metadata are designed to promote the
 * processing and sharing of files created with the netCDF API. The conventions define
 * metadata that provide a definitive description of what the data in each variable
 * represents, and of the spatial and temporal properties of the data.
 * This enables users of data from different sources to decide which quantities are
 * comparable, and facilitates building applications with powerful extraction, regridding,
 * and display capabilities."
 * </i>
 *
 * @author caron
 */

public class CF1Convention extends CSMConvention {
  private static final String convName = "CF-1.";   // start with

  /**
   * Get which CF version this is, ie CF-1.x
   * @param hasConvName extract from convention name or list of names
   * @return version, or -1 if not CF
   */
  public static int getVersion(String hasConvName) {
    int result = extractVersion(hasConvName);
    if (result >= 0) return result;
    List<String> names = breakupConventionNames(hasConvName);
    for (String name : names) {
      result = extractVersion(name);
      if (result >= 0) return result;
    }
    return -1;
  }

  private static int extractVersion(String hasConvName) {
    if (!hasConvName.startsWith(convName)) return -1;
    String versionS = hasConvName.substring(convName.length());
    try {
      return Integer.parseInt(versionS);
    } catch (Exception e) {
      return -1;
    }
  }

  /**
   * Guess the value of ZisPositive based on z axis name and units
   *
   * @param zaxisName      z coordinate axis name
   * @param vertCoordUnits z coordinate axis name
   * @return CF.POSITIVE_UP or CF.POSITIVE_DOWN
   */
  public static String getZisPositive(String zaxisName, String vertCoordUnits) {
    if (vertCoordUnits == null) return CF.POSITIVE_UP;
    if (vertCoordUnits.isEmpty()) return CF.POSITIVE_UP;

    if (SimpleUnit.isCompatible("millibar", vertCoordUnits))
      return CF.POSITIVE_DOWN;

    if (SimpleUnit.isCompatible("m", vertCoordUnits))
      return CF.POSITIVE_UP;

    // dunno - make it up
    return CF.POSITIVE_UP;
  }

  private static final String[] vertical_coords = {
          "atmosphere_ln_pressure_coordinate",
          "atmosphere_sigma_coordinate",
          "atmosphere_hybrid_sigma_pressure_coordinate",
          "atmosphere_hybrid_height_coordinate",
          "atmosphere_sleve_coordinate",
          "ocean_sigma_coordinate",
          "ocean_s_coordinate",
          "ocean_sigma_z_coordinate",
          "ocean_double_sigma_coordinate",
          "ocean_s_coordinate_g1",          // -sachin 03/25/09
          "ocean_s_coordinate_g2"};

  public CF1Convention() {
    this.conventionName = "CF-1.X";
  }

  public void augmentDataset(NetcdfDataset ds, CancelTask cancelTask) throws IOException {
    boolean got_grid_mapping = false;

    // look for transforms
    List<Variable> vars = ds.getVariables();
    for (Variable v : vars) {
      // look for special standard_names
      String sname = ds.findAttValueIgnoreCase(v, CF.STANDARD_NAME, null);
      if (sname != null) {
        sname = sname.trim();

        if (sname.equalsIgnoreCase(CF.atmosphere_ln_pressure_coordinate)) { // LOOK why isnt this with other Transforms?
          makeAtmLnCoordinate(ds, v);
          continue;
        }

        if (sname.equalsIgnoreCase(CF.TIME_REFERENCE)) {
          v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.RunTime.toString()));
          continue;
        }

        if (sname.equalsIgnoreCase(CF.TIME_OFFSET)) {
          v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.TimeOffset.toString()));
          continue;
        }

        if (sname.equalsIgnoreCase(CF.TIME)) {
          v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
        }

        if (sname.equalsIgnoreCase("ensemble") || sname.equalsIgnoreCase("realization")) {
          v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Ensemble.toString()));
          continue;
        }

        for (String vertical_coord : vertical_coords)
          if (sname.equalsIgnoreCase(vertical_coord)) {
            v.addAttribute(new Attribute(_Coordinate.TransformType, TransformType.Vertical.toString()));
            if (v.findAttribute(_Coordinate.Axes) == null)
              v.addAttribute(new Attribute(_Coordinate.Axes, v.getFullName())); // LOOK: may also be time dependent
          }

          // look for time variables and check to see if they have a calendar attribute. if not, add the default
          checkTimeVarForCalendar(v);
      }

      // look for horiz transforms. only ones that are referenced by another variable.
      String grid_mapping = ds.findAttValueIgnoreCase(v, CF.GRID_MAPPING, null);
      if (grid_mapping != null) {
        Variable gridMap = ds.findVariable(grid_mapping);
        if (gridMap == null) {
          Group g = v.getParentGroup(); // might be group relative - CF does not specify
          gridMap = g.findVariable(grid_mapping);
        }
        if (gridMap != null) {
          gridMap.addAttribute(new Attribute(_Coordinate.TransformType, TransformType.Projection.toString()));

          String grid_mapping_name = ds.findAttValueIgnoreCase(gridMap, CF.GRID_MAPPING_NAME, null);
          if (grid_mapping_name != null && grid_mapping_name.equals(CF.LATITUDE_LONGITUDE)) {
            // "grid_mapping_name == latitude_longitude" is special in CF: it's applied to variables that describe
            // properties of lat/lon CRSes.
            gridMap.addAttribute(new Attribute(_Coordinate.AxisTypes, AxisType.Lat + " " + AxisType.Lon));
          } else {
            gridMap.addAttribute(new Attribute(_Coordinate.AxisTypes, AxisType.GeoX + " " + AxisType.GeoY));
          }

          got_grid_mapping = true;
        }
      }

      // simple geometry

      if(ds.findGlobalAttribute(CF.CONVENTIONS) != null)
      if(getVersion(ds.findGlobalAttribute(CF.CONVENTIONS).getStringValue()) >= 8) // only acknowledge simple geometry standard extension if CF-1.8 or higher
      if (v.findAttribute(CF.GEOMETRY) != null) {

    	Attribute container = v.findAttribute(CF.GEOMETRY);
    	Variable coordsvar = ds.findVariable(container.getStringValue());

      	v.addAttribute(new Attribute(CF.GEOMETRY_TYPE, ds.findAttValueIgnoreCase(coordsvar, CF.GEOMETRY_TYPE, "")));

      	// Only add attribute if present, sometimes optional
      	if(ds.findAttValueIgnoreCase(coordsvar, CF.NODES, "").equals("") == false) {
      		v.addAttribute(new Attribute(CF.NODES, ds.findAttValueIgnoreCase(coordsvar, CF.NODES, "")));
      	}

      	// Only add attribute if present, sometimes optional
      	if(ds.findAttValueIgnoreCase(coordsvar, CF.NODE_COUNT, "").equals("") == false) {
      		v.addAttribute(new Attribute(CF.NODE_COUNT, ds.findAttValueIgnoreCase(coordsvar, CF.NODE_COUNT, "")));
      	}

      	v.addAttribute(new Attribute(CF.NODE_COORDINATES, ds.findAttValueIgnoreCase(coordsvar, CF.NODE_COORDINATES, "")));
      	v.addAttribute(new Attribute(CF.PART_NODE_COUNT, ds.findAttValueIgnoreCase(coordsvar, CF.PART_NODE_COUNT, "")));
      	if (CF.POLYGON.equalsIgnoreCase(ds.findAttValueIgnoreCase(coordsvar, CF.GEOMETRY_TYPE, ""))) {

      		// Again, interior ring is not always required, but add it if it is present
      		if(ds.findAttValueIgnoreCase(coordsvar, CF.INTERIOR_RING, "").equals("") == false)
      			v.addAttribute(new Attribute(CF.INTERIOR_RING, ds.findAttValueIgnoreCase(coordsvar, CF.INTERIOR_RING, "")));
      	}

      	if (v.findAttribute(CF.NODE_COORDINATES) != null) {

      		String[] coords = ds.findAttValueIgnoreCase(coordsvar, CF.NODE_COORDINATES, "").split(" ");

  			String cds = "";
      		for (int i = 0; i < coords.length; i++) {
      			Variable temp = ds.findVariable(coords[i]);
      			if (temp != null) {
      				Attribute axis = temp.findAttribute(CF.AXIS);
      				if (axis != null) {
      					if ("x".equalsIgnoreCase(axis.getStringValue())) {
      						temp.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.SimpleGeometryX.toString()));
      					}
      					if ("y".equalsIgnoreCase(axis.getStringValue())) {
      						temp.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.SimpleGeometryY.toString()));
      					}
      					if ("z".equalsIgnoreCase(axis.getStringValue())) {
      						temp.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.SimpleGeometryZ.toString()));
      					}

      					cds += coords[i] + " ";

      				}
      			}
      		}

      		List<Dimension> dims = v.getDimensions();

      		// Append any geometry dimensions as axis
      		String pre = "";

      		for(Dimension di: dims) {

      			if(!di.getShortName().equals("time")) {
      				if(ds.findVariable(di.getFullNameEscaped()) != null)
      					ds.findVariable(di.getFullNameEscaped()).addAttribute(new Attribute(_Coordinate.AxisType, AxisType.SimpleGeometryID.toString()));
      				// handle else case as malformed CF NetCDF
      			}

      			pre = di.getShortName() + " " + pre;
      		}

      		v.addAttribute(new Attribute(_Coordinate.Axes, pre + cds.trim()));
      	}
      }
    }

    if (!got_grid_mapping) { // see if there are any grid mappings anyway
      for (Variable v : ds.getVariables()) {
        String grid_mapping_name = ds.findAttValueIgnoreCase(v, CF.GRID_MAPPING_NAME, null);
        if (grid_mapping_name != null) {
          v.addAttribute(new Attribute(_Coordinate.TransformType, TransformType.Projection.toString()));

          if (grid_mapping_name.equals(CF.LATITUDE_LONGITUDE)) {
            v.addAttribute(new Attribute(_Coordinate.AxisTypes, AxisType.Lat + " " + AxisType.Lon));
          } else {
            v.addAttribute(new Attribute(_Coordinate.AxisTypes, AxisType.GeoX + " " + AxisType.GeoY));
          }
        }
      }
    }

    // make corrections for specific datasets
    String src = ds.findAttValueIgnoreCase(null, "Source", "");
    if (src.equals("NOAA/National Climatic Data Center")) {
      String title = ds.findAttValueIgnoreCase(null, "title", "");
      avhrr_oiv2 = title.indexOf("OI-V2") > 0;
    }
    ds.finish();
  }

  private boolean avhrr_oiv2 = false;

  // this is here because it doesnt fit into the 3D array thing.
  private void makeAtmLnCoordinate(NetcdfDataset ds, Variable v) {
    // get the formula attribute
    String formula = ds.findAttValueIgnoreCase(v, CF.formula_terms, null);
    if (null == formula) {
      String msg = " Need attribute 'formula_terms' on Variable " + v.getFullName() + "\n";
      parseInfo.format(msg);
      userAdvice.format(msg);
      return;
    }

    // parse the formula string
    Variable p0Var = null, levelVar = null;
    StringTokenizer stoke = new StringTokenizer(formula, " :");
    while (stoke.hasMoreTokens()) {
      String toke = stoke.nextToken();
      if (toke.equalsIgnoreCase("p0")) {
        String name = stoke.nextToken();
        p0Var = ds.findVariable(name);
      } else if (toke.equalsIgnoreCase("lev")) {
        String name = stoke.nextToken();
        levelVar = ds.findVariable(name);
      }
    }

    if (null == p0Var) {
      String msg = " Need p0:varName on Variable " + v.getFullName() + " formula_terms\n";
      parseInfo.format(msg);
      userAdvice.format(msg);
      return;
    }

    if (null == levelVar) {
      String msg = " Need lev:varName on Variable " + v.getFullName() + " formula_terms\n";
      parseInfo.format(msg);
      userAdvice.format(msg);
      return;
    }

    String units = ds.findAttValueIgnoreCase(p0Var, CDM.UNITS, "hPa");

    // create the data and the variable
    try { // p(k) = p0 * exp(-lev(k))
      double p0 = p0Var.readScalarDouble();
      Array levelData = levelVar.read();
      Array pressureData = Array.factory(DataType.DOUBLE, levelData.getShape());
      IndexIterator ii = levelData.getIndexIterator();
      IndexIterator iip = pressureData.getIndexIterator();
      while (ii.hasNext()) {
        double val = p0 * Math.exp(-1.0 * ii.getDoubleNext());
        iip.setDoubleNext(val);
      }

      CoordinateAxis1D p = new CoordinateAxis1D(ds, null, v.getShortName() + "_pressure", DataType.DOUBLE,
              levelVar.getDimensionsString(), units,
              "Vertical Pressure coordinate synthesized from atmosphere_ln_pressure_coordinate formula");
      p.setCachedData(pressureData, false);
      p.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Pressure.toString()));
      p.addAttribute(new Attribute(_Coordinate.AliasForDimension, p.getDimensionsString()));
      ds.addVariable(null, p);
      parseInfo.format(" added Vertical Pressure coordinate %s from CF-1 %s%n", p.getFullName(), CF.atmosphere_ln_pressure_coordinate);

    } catch (IOException e) {
      String msg = " Unable to read variables from " + v.getFullName() + " formula_terms\n";
      parseInfo.format(msg);
      userAdvice.format(msg);
    }

  }

  /*  vertical coordinate will be identifiable by:
      1. units of pressure; or
      2. the presence of the positive attribute with a value of up or down (case insensitive).
      3. Optionally, the vertical type may be indicated additionally by providing the standard_name attribute with an appropriate value, and/or the axis attribute with the value Z.
   */

  // we assume that coordinate axes get identified by
  //  1) being coordinate variables or
  //  2) being listed in coordinates attribute.

  /**
   * Augment COARDS axis type identification with Standard names (including dimensionless vertical coordinates) and  CF.AXIS attributes
   */
  protected AxisType getAxisType(NetcdfDataset ncDataset, VariableEnhanced v) {

     // standard names for unitless vertical coords
    String sname = ncDataset.findAttValueIgnoreCase((Variable) v, CF.STANDARD_NAME, null);
    if (sname != null) {
      sname = sname.trim();

      for (String vertical_coord : vertical_coords)
        if (sname.equalsIgnoreCase(vertical_coord))
          return AxisType.GeoZ;
    }

    // COARDS - check units
    AxisType at = super.getAxisType(ncDataset, v);
    if (at != null) return at;

    // standard names for X, Y : bug in CDO putting wrong standard name, so check units first (!)
    if (sname != null) {
      if (sname.equalsIgnoreCase(CF.ENSEMBLE))
        return AxisType.Ensemble;

      if (sname.equalsIgnoreCase(CF.LATITUDE))
        return AxisType.Lat;

      if (sname.equalsIgnoreCase(CF.LONGITUDE))
        return AxisType.Lon;

      if (sname.equalsIgnoreCase(CF.PROJECTION_X_COORDINATE) || sname.equalsIgnoreCase(CF.GRID_LONGITUDE) || sname.equalsIgnoreCase("rotated_longitude"))
        return AxisType.GeoX;

      if (sname.equalsIgnoreCase(CF.PROJECTION_Y_COORDINATE) || sname.equalsIgnoreCase(CF.GRID_LATITUDE) || sname.equalsIgnoreCase("rotated_latitude"))
        return AxisType.GeoY;

      if (sname.equalsIgnoreCase(CF.TIME_REFERENCE))
        return AxisType.RunTime;

      if (sname.equalsIgnoreCase(CF.TIME_OFFSET))
        return AxisType.TimeOffset;
    }

    // check axis attribute - only for X, Y, Z
    String axis = ncDataset.findAttValueIgnoreCase((Variable) v, CF.AXIS, null);
    if (axis != null) {
      axis = axis.trim();
      String unit = v.getUnitsString();

      if (axis.equalsIgnoreCase("X")) {
        if (SimpleUnit.isCompatible("m", unit))
          return AxisType.GeoX;

      } else if (axis.equalsIgnoreCase("Y")) {
        if (SimpleUnit.isCompatible("m", unit))
          return AxisType.GeoY;

      } else if (axis.equalsIgnoreCase("Z")) {
        if (unit == null) return AxisType.GeoZ;
        if (SimpleUnit.isCompatible("m", unit))
          return AxisType.Height;
        else if (SimpleUnit.isCompatible("mbar", unit))
          return AxisType.Pressure;
        else
          return AxisType.GeoZ;
      }
    }

    if (avhrr_oiv2) {
      if (v.getShortName().equals("zlev"))
        return AxisType.Height;
    }

    try {
      String units = v.getUnitsString();
      CalendarDateUnit cd = CalendarDateUnit.of(null, units);
      if (cd != null) return AxisType.Time;
    } catch (Throwable t) {
      // ignore
    }

    return null;
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.dt.image.image;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Read in all images in a dir and subdirs, and randomly iterate.
 *
 * @author caron
 * @since Oct 9, 2008
 */
public class ImageFactoryRandom {
  private java.util.List<File> holdList;
  private java.util.List<File> fileList;
  private Random random = new Random( System.currentTimeMillis());

  public ImageFactoryRandom(File topDir) {
    if (!topDir.exists())
      return;

    fileList = new ArrayList<>(1000);
    addToList(topDir, fileList);
    System.out.println("nfiles= "+ fileList.size());
    holdList = new ArrayList<>( fileList);
  }

  private void addToList(File dir, List<File> list) {
    for (File file : dir.listFiles()) {
      if (file.isDirectory())
        addToList(file, list);
      else if (file.getName().endsWith(".jpg") || file.getName().endsWith(".JPG"))
        list.add(file);
    }
  }

  File nextFile = null;
  public BufferedImage getNextImage() {
    if (holdList.size() == 0)
      holdList = new ArrayList<>( fileList);

    int next = random.nextInt( holdList.size());
    nextFile = holdList.get(next);
    holdList.remove( nextFile); // random draw without replacement

    try {
      System.out.printf("next %d %s %n", next, nextFile);
      return javax.imageio.ImageIO.read(nextFile);
    } catch (IOException e) {
      System.out.println("Failed to open image " + nextFile);
      fileList.remove( nextFile);
      return getNextImage();
    }

  }

  // remove last file
  public boolean delete() {
    if (nextFile == null) return false;
    fileList.remove( nextFile);
    File f = new File("C:/tmp/deleted/"+nextFile.getName());
    return nextFile.renameTo(f);
  }

}


/*
 * Copyright (c) 1998-2019 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.ui.coverage2;

import ucar.ma2.*;
import ucar.nc2.ft2.coverage.GeoReferencedArray;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.ft2.coverage.*;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.ui.grid.ColorScale;
import ucar.nc2.util.Optional;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.util.Format;
import ucar.util.prefs.PreferencesExt;
import ucar.util.prefs.ui.Debug;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.Formatter;

/**
 * ft2.coverage widget for displaying using Java2D API.
 * more or less the view in MVC
 *
 * @author John
 * @since 12/27/12
 */
public class CoverageRenderer {
  // draw state
  private boolean drawGrid = true;
  private boolean drawGridLines = true;
  private boolean drawContours = false;
  private boolean drawContourLabels = false;
  private boolean drawBB = false;
  private boolean isNewField = true;

  private ColorScale colorScale = null;
  private ColorScale.MinMaxType dataMinMaxType = ColorScale.MinMaxType.horiz;
  private ProjectionImpl drawProjection = null;    // current drawing Projection
  private ProjectionImpl dataProjection = null;    // current data Projection

  // data stuff
  private DataState dataState;
  private GeoReferencedArray dataH;
  private Array geodata; // reduced to 2D
  private double useLevel;
  private int wantLevel = -1, wantSlice = -1, wantTime = -1, horizStride = 1;   // for next draw()
  private int wantRunTime = -1, wantEnsemble = -1;
  private int lastLevel = -1, lastTime = -1, lastStride = -1;   // last data read
  private int lastRunTime = -1, lastEnsemble = -1;   // last data read
  private Coverage lastGrid = null;

  // drawing optimization
  private boolean useModeForProjections = false; // use colorMode optimization for different projections
  private boolean sameProjection = true;
  private LatLonProjection projectll;       // special handling for LatLonProjection

  private static final boolean debugHorizDraw = false, debugMiss = false;
  private boolean debugPts = false, debugPathShape = true;

  /**
   * constructor
   */
  public CoverageRenderer(PreferencesExt store) {
    //rects[0] = new ProjectionRect();
  }

  ///// bean properties

  /* get the current ColorScale */
  public ColorScale getColorScale() {
    return colorScale;
  }

  /* set the ColorScale to use */
  public void setColorScale(ColorScale cs) {
    this.colorScale = cs;
  }

  /* set the ColorScale data min/max type */
  public void setDataMinMaxType(ColorScale.MinMaxType type) {
    if (type != dataMinMaxType) {
      dataMinMaxType = type;
    }
  }

  /* set the Grid */
  public DataState setCoverage(CoverageCollection coverageDataset, Coverage grid) {
    this.dataState = new DataState(coverageDataset, grid);
    this.lastGrid = null;
    isNewField = true;
    return this.dataState;
  }

  /* get the current data projection */
  public ProjectionImpl getDataProjection() {
    return dataProjection;
  }

  public void setDataProjection(ProjectionImpl dataProjection) {
    this.dataProjection = dataProjection;
  }

  /* get the current display projection */
  public ProjectionImpl getDisplayProjection() {
    return drawProjection;
  }

  /* set the Projection to use for drawing */
  public void setViewProjection(ProjectionImpl project) {
    drawProjection = project;
  }

  /* set the Projection to use for drawing */
  public void setDrawBB(boolean drawBB) {
    this.drawBB = drawBB;
  }

  /* set whether grid should be drawn */
  public void setDrawGridLines(boolean drawGrid) {
    this.drawGridLines = drawGrid;
  }

  /* set whether countours  should be drawn */
  public void setDrawContours(boolean drawContours) {
    this.drawContours = drawContours;
  }

  /* set whether contour labels should be drawn */
  public void setDrawContourLabels(boolean drawContourLabels) {
    this.drawContourLabels = drawContourLabels;
  }

  /* get what vertical level to draw */
  public int getLevel() {
    return wantLevel;
  }

  /* set what vertical level to draw */
  public void setLevel(int level) {
    wantLevel = level;
  }

  /* get what time slice to draw */
  public int getTime() {
    return wantTime;
  }

  /* set what time slice to draw */
  public void setTime(int time) {
    wantTime = time;
  }

  /* set what runtime slice to draw */
  public void setRunTime(int runtime) {
    wantRunTime = runtime;
  }

  /* set what ensemble slice to draw */
  public void setEnsemble(int ensemble) {
    wantEnsemble = ensemble;
  }

  /* set what y-z slice to draw */
  public void setSlice(int slice) {
    wantSlice = slice;
  }

  public void setHorizStride(int horizStride) {
    this.horizStride = horizStride;
  }

  /*
   * Get the data value at this projection (x,y) point.
   *
   * @param loc : point in display projection coordinates (plan view)
   * @return String representation of value
   */
  public String getXYvalueStr(ProjectionPoint loc) {
    if ((lastGrid == null) || (geodata == null))
      return "";

    // convert to dataProjection, where x and y are orthogonal
    if (!sameProjection) {
      LatLonPoint llpt = drawProjection.projToLatLon(loc);
      loc = dataProjection.latLonToProj(llpt);
    }

    // find the grid indexes
    HorizCoordSys hcs = lastGrid.getCoordSys().getHorizCoordSys();
    Optional<HorizCoordSys.CoordReturn> opt = hcs.findXYindexFromCoord(loc.getX(), loc.getY());

    // get value, construct the string
    if (!opt.isPresent())
      return opt.getErrorMessage();
    else {
      HorizCoordSys.CoordReturn cr = opt.get();
      try {
        Index imaH = geodata.getIndex();
        double dataValue = geodata.getDouble(imaH.set(cr.y, cr.x));
        // int wantz = (geocs.getZAxis() == null) ? -1 : lastLevel;
        return makeXYZvalueStr(dataValue, cr);
      } catch (Exception e) {
        return "error " + cr.x + " " + cr.y;
      }
    }
  }

  private String makeXYZvalueStr(double value, HorizCoordSys.CoordReturn cr) {
    String val = lastGrid.isMissing(value) ? "missing value" : Format.d(value, 6);
    Formatter sbuff = new Formatter();
    sbuff.format("%s %s", val, lastGrid.getUnitsString());
    sbuff.format(" @ (%f,%f)", cr.xcoord, cr.ycoord);
    sbuff.format("  [%d,%d]", cr.x, cr.y);
    return sbuff.toString();
  }

  //////// data routines

  /* get an x,y,z data volume for the given time
 private void makeDataVolume( GridDatatype g, int time) {
   try {
     dataVolume = g.readVolumeData( time);
   } catch (java.io.IOException e) {
     System.out.println("Error reading netcdf file "+e);
     dataVolume = null;
   }
   lastGrid = g;
   lastTime = time;
   lastLevel = -1;  // invalidate
   lastSlice = -1;  // invalidate
   dataVolumeChanged = true;
 } */

  private GeoReferencedArray readHSlice(int level, int time, int ensemble, int runtime) {

    /* make sure x, y exists
    CoverageCS gcs = useG.getCoordinateSystem();
    CoordinateAxis xaxis = gcs.getXHorizAxis();
    CoordinateAxis yaxis = gcs.getYHorizAxis();
    if ((xaxis == null) || (yaxis == null))    // doesnt exist
      return null;
    if ((xaxis.getSize() <= 1) || (yaxis.getSize() <= 1)) // LOOK ??
      return null;   */

    // make sure we need new one
    if (dataState.grid.equals(lastGrid) && (time == lastTime) && (level == lastLevel) && (horizStride == lastStride)
            && (ensemble == lastEnsemble) && (runtime == lastRunTime))
      return dataH; // nothing changed

    // get the data slice
    //dataH = useG.readDataSlice(runtime, ensemble, time, level, -1, -1);
    SubsetParams subset = new SubsetParams();
    if (level >= 0 && dataState.zaxis != null) {
      double levelVal = dataState.zaxis.getCoordMidpoint(level);
      subset.set(SubsetParams.vertCoord, levelVal);
    }
    if (time >= 0 && dataState.taxis != null) {
      double timeVal = dataState.taxis.getCoordMidpoint(time);
      CalendarDate date = dataState.taxis.makeDate(timeVal);
      subset.set(SubsetParams.time, date);
    }
    if (runtime >= 0 && dataState.rtaxis != null) {
      double rtimeVal = dataState.rtaxis.getCoordMidpoint(runtime);
      CalendarDate date = dataState.rtaxis.makeDate(rtimeVal);
      subset.set(SubsetParams.runtime, date);
    }
    if (ensemble >= 0 && dataState.ensaxis != null) {
      double ensVal = dataState.ensaxis.getCoordMidpoint(ensemble);
      subset.set(SubsetParams.ensCoord, ensVal);
    }
    if (horizStride != 1)
      subset.setHorizStride( horizStride);

    try {
      dataH = dataState.grid.readData(subset);
      geodata = dataH.getData().reduce(); // get rid of n=1 dimensions

    } catch (IOException | InvalidRangeException e) {
      e.printStackTrace();
    }

    lastGrid = dataState.grid;
    lastTime = time;
    lastLevel = level;
    lastEnsemble = ensemble;
    lastRunTime = runtime;
    lastStride = horizStride;

    /*
    CoordinateAxis1D zaxis = gcs.getVerticalAxis();
    if ((zaxis == null) || (zaxis.getSize() < 1)) {
      dataH = dataVolume;  // volume is xy plane
    } else {
      dataH = dataVolume.slice(0, level);  // if z exists, always first (logical) dimension
    }

    if (debugArrayShape) {
      System.out.println("Horiz shape = ");
      for (int i = 0; i < dataH.getRank(); i++)
        System.out.println("   shape = " + dataH.getShape()[i]);
    } */

    return dataH;
  }

 /*  private Array makeVSlice(Coverage g, int vSlice, int time, int ensemble, int runtime) {
    // make sure we have x, z
    CoverageCS gcs = g.getCoordinateSystem();
    CoordinateAxis xaxis = gcs.getXHorizAxis();
    CoordinateAxis zaxis = gcs.getVerticalAxis();
    if ((xaxis == null) || (zaxis == null))    // doesnt exist
      return null;
    if ((xaxis.getSize() <= 1) || (zaxis.getSize() <= 1)) // LOOK ??
      return null;

    // make sure we need it
    if (g.equals(lastGrid) && (time == lastTime) && (vSlice == lastSlice)
            && (ensemble == lastEnsemble) && (runtime == lastRunTime))
      return dataV; // nothing changed

    // get the slice
    try {
      dataV = g.readDataSlice(runtime, ensemble, time, -1, -1, vSlice);
    } catch (java.io.IOException e) {
      System.out.println("GridRender.readHSlice Error reading netcdf file= " + e);
      return null;
    }

    lastGrid = g;
    lastTime = time;
    lastSlice = vSlice;
    lastEnsemble = ensemble;
    lastRunTime = runtime;

    if (debugArrayShape) {
      System.out.println("Vert shape = ");
      for (int i = 0; i < dataV.getRank(); i++)
        System.out.println("   shape = " + dataV.getShape()[i]);
    }

    return dataV;
  }   */

  //////////// Renderer stuff

  /**
   * returns null
   * public ucar.unidata.geoloc.LatLonRect getPreferredArea() {
   * /* if (dataProjection != null)
   * return dataProjection.getPreferredArea();
   * else
   * return null;
   * }
   * <p>
   * /* private java.awt.Color color, backColor;
   * public void setBackgroundColor(java.awt.Color backColor) { this.backColor = backColor;}
   * public void setColor(java.awt.Color color) { this.color = color;}
   * public java.awt.Color getColor() { return color; }
   */

  // set colorscale limits, missing data
  private void setColorScaleParams() {
    if (dataMinMaxType == ColorScale.MinMaxType.hold && !isNewField)
      return;
    isNewField = false;

    GeoReferencedArray dataArr = readHSlice(wantLevel, wantTime, wantEnsemble, wantRunTime);

    //else
    //  dataArr = makeVSlice(stridedGrid, wantSlice, wantTime, wantEnsemble, wantRunTime);

    if (dataArr != null) {
      MAMath.MinMax minmax = MAMath.getMinMaxSkipMissingData(dataArr.getData(), dataState.grid);
      colorScale.setMinMax(minmax.min, minmax.max);
      colorScale.setGeoGrid(dataState.grid);
    }
  }

  /*
   * Do the rendering to the given Graphics2D object.
   *
   * @param g      Graphics2D object: has clipRect and AffineTransform set.
   * @param dFromN transforms "Normalized Device" to Device coordinates
   *
  public void renderVertView(java.awt.Graphics2D g, AffineTransform dFromN) {
    if ((stridedGrid == null) || (cs == null) || (drawProjection == null))
      return;

    if (!drawGrid && !drawContours)
      return;

    dataV = makeVSlice(stridedGrid, wantSlice, wantTime, wantEnsemble, wantRunTime);
    if (dataV == null)
      return;

    if (Debug.isSet("GridRenderer/vert"))
      System.out.println("GridRenderer/vert: redraw grid");

    CoverageCS geocs = stridedGrid.getCoordinateSystem();
    CoordinateAxis1D zaxis = geocs.getVerticalAxis();
    CoordinateAxis1D yaxis = (CoordinateAxis1D) geocs.getYHorizAxis();
    if ((yaxis == null) || (zaxis == null))
      return;
    int nz = (int) zaxis.getSize();
    int ny = (int) yaxis.getSize();

    if (drawGrid) {
      int count = 0;
      Index imaV = dataV.getIndex();
      for (int z = 0; z < nz; z++) {
        double zbeg = zaxis.getCoordEdge(z);
        double zend = zaxis.getCoordEdge(z + 1);

        for (int y = 0; y < ny; y++) {
          double ybeg = yaxis.getCoordEdge(y);
          double yend = yaxis.getCoordEdge(y + 1);

          double val = dataV.getDouble(imaV.set(z, y));
          int thisColor = cs.getIndexFromValue(val);
          count += drawRect(g, thisColor, ybeg, zbeg, yend, zend, false);
        }
      }
    }

    if (drawContours) {
      double[] zedges = zaxis.getCoordValues();
      double[] yedges = yaxis.getCoordValues();

      int nlevels = cs.getNumColors();
      ArrayList levels = new ArrayList(nlevels);
      for (int i = 1; i < nlevels - 1; i++)
        levels.add(new Double(cs.getEdge(i)));

      long startTime = System.currentTimeMillis();
      ContourFeatureRenderer contourRendererV;
      try {
        ContourGrid conGrid = new ContourGrid(dataV.transpose(0, 1), levels, yedges, zedges, stridedGrid);
        contourRendererV = new ContourFeatureRenderer(conGrid, null);

        //contourRendererV.setProjection(drawProjection);
        contourRendererV.setColor(Color.black);
        contourRendererV.setShowLabels(drawContourLabels);
        contourRendererV.draw(g, dFromN);
      } catch (Exception e) {
        System.out.println("draw Contours Vert exception = " + e);
        e.printStackTrace(System.err);
      }
      if (Debug.isSet("timing/contourDraw")) {
        long tookTime = System.currentTimeMillis() - startTime;
        System.out.println("timing/contourDraw: Vert:" + tookTime * .001 + " seconds");
      }
    }

    if ((lastLevel >= 0) && (lastLevel < nz))
      drawXORline(g, yaxis.getCoordEdge(0), zaxis.getCoordValue(lastLevel),
              yaxis.getCoordEdge(ny), zaxis.getCoordValue(lastLevel));
  } */

  /**
   * Do the rendering to the given Graphics2D object.
   *
   * @param g      Graphics2D object: has clipRect and AffineTransform set.
   * @param dFromN transforms "Normalized Device" to Device coordinates
   */
  public void renderPlanView(java.awt.Graphics2D g, AffineTransform dFromN) {
    if ((dataState.grid == null) || (colorScale == null) || (drawProjection == null))
      return;

    if (!drawGrid && !drawContours)
      return;

    // no anitaliasing
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

    dataH = readHSlice(wantLevel, wantTime, wantEnsemble, wantRunTime);
    if (dataH == null)
      return;

    setColorScaleParams();

    if (drawGrid)
      drawGridHoriz(g, dataH);
    //if (drawContours)
    //  drawContours(g, dataH.transpose(0, 1), dFromN);
    if (drawGridLines)
      drawGridLines(g, dataH);
    if (drawBB)
      drawGridBB(g, this.dataState.coverageDataset.getLatlonBoundingBox());
  }

  private boolean drawGridBB(Graphics2D g, LatLonRect latLonRect) {
    g.setColor(Color.BLACK);
    rect.setRect(latLonRect.getLonMin(), latLonRect.getLatMin(), latLonRect.getWidth(), latLonRect.getHeight());
    g.draw(rect);
    return true;
  }

  private void drawGridHoriz(java.awt.Graphics2D g, GeoReferencedArray geo) {
    CoverageCoordSys csData = geo.getCoordSysForData();
    HorizCoordSys hcs = csData.getHorizCoordSys();
    if (!hcs.isLatLon2D()) {
      drawGridHorizRegular(g, geo);
      return;
    }

    // 2D case
    Array data = geo.getData();
    data = data.reduce();
    if (data.getRank() != 2)
      throw new IllegalArgumentException("must be 2D");
    Index ima = data.getIndex();

    LatLonAxis2D xaxis2D = hcs.getLonAxis2D();
    LatLonAxis2D yaxis2D = hcs.getLatAxis2D();

    /* String stag = geocs.getHorizStaggerType();
    if (stag != null && stag.equals(CDM.ARAKAWA_E)) {
      drawGridHorizRotated(g, data, xaxis2D, yaxis2D);
      return;
    } */

    ArrayDouble.D2 edgex = (ArrayDouble.D2) xaxis2D.getCoordBoundsAsArray();
    ArrayDouble.D2 edgey = (ArrayDouble.D2) yaxis2D.getCoordBoundsAsArray();

    GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 5);

    int[] shape = xaxis2D.getShape(); // should both be the same
    int ny = shape[0];
    int nx = shape[1];

    for (int y = 0; y < ny; y++) {
      for (int x = 0; x < nx; x++) {
        gp.reset();
        gp.moveTo((float) edgex.get(y, x), (float) edgey.get(y, x));
        gp.lineTo((float) edgex.get(y, x + 1), (float) edgey.get(y, x + 1));
        gp.lineTo((float) edgex.get(y + 1, x + 1), (float) edgey.get(y + 1, x + 1));
        gp.lineTo((float) edgex.get(y + 1, x), (float) edgey.get(y + 1, x));

        double val = data.getDouble(ima.set(y, x));   // ordering LOOK
        int colorIndex = colorScale.getIndexFromValue(val);
        g.setColor(colorScale.getColor(colorIndex));
        g.fill(gp);
      }
    }

  }

  private void drawGridLines(java.awt.Graphics2D g, GeoReferencedArray geo) {
    CoverageCoordSys geocs = geo.getCoordSysForData();
    LatLonAxis2D lataxis = geocs.getHorizCoordSys().getLatAxis2D();
    LatLonAxis2D lonaxis = geocs.getHorizCoordSys().getLonAxis2D();

    if (lataxis == null || lonaxis == null)
      return;

    ArrayDouble.D2 edgex = (ArrayDouble.D2) lonaxis.getCoordBoundsAsArray();
    ArrayDouble.D2 edgey = (ArrayDouble.D2) lataxis.getCoordBoundsAsArray();

    GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 5);
    g.setColor(Color.BLACK);

    int[] shape = lataxis.getShape(); // should both be the same
    int ny = shape[0];
    int nx = shape[1];

    for (int y = 0; y < ny + 1; y += 10) {
      gp.reset();
      for (int x = 0; x < nx + 1; x++) {
        if (x == 0)
          gp.moveTo((float) edgex.get(y, x), (float) edgey.get(y, x));
        else
          gp.lineTo((float) edgex.get(y, x), (float) edgey.get(y, x));
      }
      g.draw(gp);
    }

    for (int x = 0; x < nx + 1; x += 10) {
      gp.reset();
      for (int y = 0; y < ny + 1; y++) {
        if (y == 0)
          gp.moveTo((float) edgex.get(y, x), (float) edgey.get(y, x));
        else
          gp.lineTo((float) edgex.get(y, x), (float) edgey.get(y, x));
      }
      g.draw(gp);
    }

  }

  /* draw using GeneralPath shape
 private GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 5);
 private Shape makeShape(double lon1, double lat1, double lon2, double lat2) {
   gp.reset();
   ProjectionPoint pt = drawProjection.latLonToProj( lat1, lon1);
   gp.moveTo( (float) pt.getX(), (float) pt.getY());

   ptP1.setLocation(pt);
   pt = drawProjection.latLonToProj( lat1, lon2);
   gp.lineTo( (float) pt.getX(), (float) pt.getY());
   if (drawProjection.crossSeam(ptP1, pt))
     return null;

   ptP1.setLocation(pt);
   pt = drawProjection.latLonToProj( lat2, lon2);
   gp.lineTo( (float) pt.getX(), (float) pt.getY());
   if (drawProjection.crossSeam(ptP1, pt))
     return null;

   ptP1.setLocation(pt);
   pt = drawProjection.latLonToProj( lat2, lon1);
   gp.lineTo( (float) pt.getX(), (float) pt.getY());
   if (drawProjection.crossSeam(ptP1, pt))
     return null;

   return gp;
 } */

  private void drawGridHorizRegular(java.awt.Graphics2D g, GeoReferencedArray array) {
    int count = 0;

    CoverageCoordSys gsys = array.getCoordSysForData();
    CoverageCoordAxis1D xaxis = (CoverageCoordAxis1D) gsys.getXAxis();
    CoverageCoordAxis1D yaxis = (CoverageCoordAxis1D) gsys.getYAxis();
    Array data = array.getData().reduce();
    if (data.getRank() != 2) {
      System.out.printf("drawGridHorizRegular Rank equals %d, must be 2%n", data.getRank());
      return;
    }

    int nx = xaxis.getNcoords();
    int ny = yaxis.getNcoords();

    //// drawing optimizations
    sameProjection = drawProjection.equals(dataProjection);
    if (drawProjection.isLatLon()) {
      projectll = (LatLonProjection) drawProjection;
      double centerLon = projectll.getCenterLon();
      if (Debug.isSet("projection/LatLonShift")) System.out.println("projection/LatLonShift: gridDraw = " + centerLon);
    }

    // find the most common color and fill the entire area with it
    colorScale.resetHist();
    IndexIterator iiter = array.getData().getIndexIterator();
    while (iiter.hasNext()) {
      double val = iiter.getDoubleNext();
      colorScale.getIndexFromValue(val);                // accum in histogram
    }
    int modeColor = colorScale.getHistMax();
    if (debugMiss) System.out.println("mode = " + modeColor + " sameProj= " + sameProjection);

    if (sameProjection) {
      double xmin = Math.min(xaxis.getCoordEdge1(0), xaxis.getCoordEdgeLast());
      double xmax = Math.max(xaxis.getCoordEdge1(0), xaxis.getCoordEdgeLast());
      double ymin = Math.min(yaxis.getCoordEdge1(0), yaxis.getCoordEdgeLast());
      double ymax = Math.max(yaxis.getCoordEdge1(0), yaxis.getCoordEdgeLast());

      // pre color the drawing area with the most used color
      count += drawRect(g, modeColor, xmin, ymin, xmax, ymax, drawProjection.isLatLon());

    } else if (useModeForProjections)
      drawPathShape(g, modeColor, xaxis, yaxis);

    debugPts = Debug.isSet("GridRenderer/showPts");

    // draw individual rects with run length
    Index imaH = data.getIndex();
    for (int y = 0; y < ny; y++) {
      double ybeg = yaxis.getCoordEdge1(y);
      double yend = yaxis.getCoordEdge2(y);

      int thisColor, lastColor = 0;
      int run = 0;
      int xbeg = 0;

      for (int x = 0; x < nx; x++) {
        double val = data.getDouble(imaH.set(y, x));
        thisColor = colorScale.getIndexFromValue(val);

        if ((run == 0) || (lastColor == thisColor)) { // same color - keep running
          run++;
        } else {
          if (sameProjection) {
            if (lastColor != modeColor) // dont have to draw these
              count += drawRect(g, lastColor, xaxis.getCoordEdge1(xbeg), ybeg, xaxis.getCoordEdge2(x), yend, drawProjection.isLatLon());
          } else {
            if (!useModeForProjections || (lastColor != modeColor)) // dont have to draw mode
              count += drawPathRun(g, lastColor, ybeg, yend, xaxis, xbeg, x-1, debugPts);
          }
          xbeg = x;
        }
        lastColor = thisColor;
      }

      // get the ones at the end
      if (sameProjection) {
        if (lastColor != modeColor)
          count += drawRect(g, lastColor, xaxis.getCoordEdge1(xbeg), ybeg, xaxis.getCoordEdgeLast(), yend, drawProjection.isLatLon());
      } else {
        if (!useModeForProjections || (lastColor != modeColor))
          count += drawPathRun(g, lastColor, ybeg, yend, xaxis, xbeg, nx - 1, false); // needed ?
      }

      // if (debugPts) break;
    }
    if (debugHorizDraw) System.out.println("debugHorizDraw = " + count);
  }

  //// draw using Rectangle when possible
  private Rectangle2D rect = new Rectangle2D.Double();

  private int drawRectLatLon(Graphics2D g, int color, double lon1, double lat1, double lon2, double lat2) {
    g.setColor(colorScale.getColor(color));

    int count = 0;
    ProjectionRect[] rects = projectll.latLonToProjRect(lat1, lon1, lat2, lon2);
    for (int i = 0; i < 2; i++)
      if (null != rects[i]) {
        ProjectionRect r2 = rects[i];
        Rectangle2D.Double r = new Rectangle2D.Double(r2.getX(), r2.getY(), r2.getWidth(), r2.getHeight());
        g.fill(r);
        count++;
      }
    return count;
  }

  private int drawRect(Graphics2D g, int color, double w1, double h1, double w2, double h2, boolean useLatlon) {
    if (useLatlon)
      return drawRectLatLon(g, color, w1, h1, w2, h2);

    g.setColor(colorScale.getColor(color));
    double wmin = Math.min(w1, w2);
    double hmin = Math.min(h1, h2);
    double width = Math.abs(w1 - w2);
    double height = Math.abs(h1 - h2);
    rect.setRect(wmin, hmin, width, height);
    g.fill(rect);
    return 1;
  }

  private int drawPathShape(Graphics2D g, int color, CoverageCoordAxis1D xaxis, CoverageCoordAxis1D yaxis) {
    int count = 0;
    for (int y = 0; y < yaxis.getNcoords() - 1; y++) {
      double y1 = yaxis.getCoordEdge1(y);
      double y2 = yaxis.getCoordEdge2(y);
      count += drawPathRun(g, color, y1, y2, xaxis, 0, xaxis.getNcoords() - 1, false);
    }

    return count;
  }

  private GeneralPath gpRun = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 25);

  private int drawPathRun(Graphics2D g, int color, double y1, double y2, CoverageCoordAxis1D xaxis, int x1, int x2, boolean debugPts) {
    int nx = xaxis.getNcoords();
    if ((x1 < 0) || (x1 > nx) || (x2 < 0) || (x2 > nx) || (x1 > x2)) // from the recursion
      return 0;

    int count = 0;
    gpRun.reset();

    // first point
    LatLonPoint llp = dataProjection.projToLatLon(xaxis.getCoordEdge1(x1), y1);
    ProjectionPoint pt = drawProjection.latLonToProj(llp);
    if (debugPts) System.out.printf("** moveTo = x1=%d (%f, %f)%n", x1, pt.getX(), pt.getY());
    gpRun.moveTo((float) pt.getX(), (float) pt.getY());

    ProjectionPointImpl ptP1 = new ProjectionPointImpl();
    ptP1.setLocation(pt);

    for (int e = x1; e <= x2; e++) {
      llp = dataProjection.projToLatLon(xaxis.getCoordEdge2(e), y1);
      pt = drawProjection.latLonToProj(llp);
 /*   if (drawProjection.crossSeam(ptP1, pt)) { // break it in two & recurse
        // int x = e - 1;  // which col has to be dropped ?
        if (debugPathShape) System.out.println("split1 at x = " + e + " " + x1 + " " + x2);
        int count = 0;
        count += drawPathRun(g, color, y1, y2, xaxis, x1, e - 1);
        count += drawPathRun(g, color, y1, y2, xaxis, e + 1, x2);
        return count;
      } */
      if (debugPts) System.out.printf("%d x2=%d lineTo = (%f, %f)%n", count++, e, pt.getX(), pt.getY());
      gpRun.lineTo((float) pt.getX(), (float) pt.getY());
      ptP1.setLocation(pt);
    }

    for (int e = x2; e >= x1; e--) {
      llp = dataProjection.projToLatLon(xaxis.getCoordEdge2(e), y2);
      pt = drawProjection.latLonToProj(llp);
/*    if (drawProjection.crossSeam(ptP1, pt)) { // break it in two & recurse
        int x = (e == x2 + 1) ? x2 : e;  // which col has to be dropped ?
        if (debugPathShape) System.out.println("split2 at x = " + x + " " + x1 + " " + x2);
        int count = 0;
        count += drawPathRun(g, color, y1, y2, xaxis, x1, x - 1);
        count += drawPathRun(g, color, y1, y2, xaxis, x + 1, x2);
        return count;
      } */
      if (debugPts) System.out.printf("%d x2=%d lineTo = (%f, %f)%n", count++, e, pt.getX(), pt.getY());
      gpRun.lineTo((float) pt.getX(), (float) pt.getY());
      ptP1.setLocation(pt);
    }

    // finish
    llp = dataProjection.projToLatLon(xaxis.getCoordEdge1(x1), y2);
    pt = drawProjection.latLonToProj(llp);
    if (debugPts) System.out.printf("%d (%d,y2) lineTo = [%f, %f]%n", count, x1, pt.getX(), pt.getY());
    gpRun.lineTo((float) pt.getX(), (float) pt.getY());

    g.setColor( colorScale.getColor(color));
    try {
      g.fill(gpRun);
    } catch (Throwable e) {
      System.out.println("Exception in drawPathRun = " + e);
      return 0;
    }
    return 1;
  }

/*  private int drawPathShape(Graphics2D g, int color, CoordinateAxis xaxis, CoordinateAxis yaxis) {
    Point2D pt;
    gpRun.reset();
    int nx = xaxis.getNumElements();
    int ny = yaxis.getNumElements();
    boolean debugPathShape = true;
    int x, y;

    pt = drawProjection.latLonToProj( yaxis.getCoordValue(0), xaxis.getCoordValue(0));
    gpRun.moveTo( (float) pt.getX(), (float) pt.getY());
    ptP1.set(pt);

    y = 0;
    for (x=1; x<nx; x++) {
      pt = drawProjection.latLonToProj( yaxis.getCoordValue(y), xaxis.getCoordValue(x));
      gpRun.lineTo( (float) pt.getX(), (float) pt.getY());
      ptP1.set(pt);
      if (debugPathShape) System.out.println(x+" "+y+" "+pt+" "+drawProjection.crossSeam(ptP1, pt));
    }

    x = nx-1;
    for (y=0; y<ny; y++) {
      pt = drawProjection.latLonToProj( yaxis.getCoordValue(y), xaxis.getCoordValue(x));
      gpRun.lineTo( (float) pt.getX(), (float) pt.getY());
      ptP1.set(pt);
      if (debugPathShape) System.out.println(x+" "+y+" "+pt+" "+drawProjection.crossSeam(ptP1, pt));
    }

    y = ny-1;
    for (x=nx-1; x>=0; x--) {
      pt = drawProjection.latLonToProj( yaxis.getCoordValue(y), xaxis.getCoordValue(x));
      gpRun.lineTo( (float) pt.getX(), (float) pt.getY());
      ptP1.set(pt);
      if (debugPathShape) System.out.println(x+" "+y+" "+pt+" "+drawProjection.crossSeam(ptP1, pt));
    }

    x = 0;
    for (y=ny-1; y>=0; y--) {
      pt = drawProjection.latLonToProj( yaxis.getCoordValue(y), xaxis.getCoordValue(x));
      gpRun.lineTo( (float) pt.getX(), (float) pt.getY());
      ptP1.set(pt);
      if (debugPathShape) System.out.println(x+" "+y+" "+pt+" "+drawProjection.crossSeam(ptP1, pt));
    }

    g.setColor( cs.getColor(color));
    //g.fill(gpRun);
    return 1;
  }  */

/*  private void drawLine(Graphics2D g, double lat1, double lon1, double lat2, double lon2) {
    ptP1.set(drawProjection.latLonToProj( lat1, lon1));
    Point2D pt2 = drawProjection.latLonToProj( lat2, lon2);
    if (drawProjection.crossSeam(ptP1, pt2))
      if (debugSeam) System.out.println( "crossSeam: "+ ptP1+ " to "+ pt2);
    else {
      line2D.setLine( ptP1, pt2);
      g.draw( line2D);
    }
  } */

  //////// contouring

  /* private void drawContours(java.awt.Graphics2D g, Array hslice, AffineTransform dFromN) {
    // make ContourGrid object
    CoverageCS geocs = stridedGrid.getCoordinateSystem();
    CoordinateAxis1D xaxis = (CoordinateAxis1D) geocs.getXHorizAxis();
    CoordinateAxis1D yaxis = (CoordinateAxis1D) geocs.getYHorizAxis();
    double[] xedges = xaxis.getCoordValues();
    double[] yedges = yaxis.getCoordValues();

    int nlevels = cs.getNumColors();
    ArrayList levels = new ArrayList(nlevels);
    for (int i = 1; i < nlevels - 1; i++)
      levels.add(new Double(cs.getEdge(i)));

    ContourFeatureRenderer contourRenderer;
    long startTime = System.currentTimeMillis();
    try {
      ContourGrid conGrid = new ContourGrid((Array) hslice, levels, xedges, yedges, stridedGrid);
      contourRenderer = new ContourFeatureRenderer(conGrid, dataProjection);
    } catch (Exception e) {
      System.out.println("make Contours exception = " + e);
      e.printStackTrace(System.out);
      return;
    }
    if (Debug.isSet("timing/contourMake")) {
      long tookTime = System.currentTimeMillis() - startTime;
      System.out.println("timing/contourMake: " + tookTime * .001 + " seconds");
    }

    startTime = System.currentTimeMillis();
    try {
      contourRenderer.setProjection(drawProjection);
      contourRenderer.setColor(Color.black);
      contourRenderer.setShowLabels(drawContourLabels);
      contourRenderer.draw(g, dFromN);
    } catch (Exception e) {
      System.out.println("draw Contours exception = " + e);
      e.printStackTrace(System.err);
    }
    if (Debug.isSet("timing/contourDraw")) {
      long tookTime = System.currentTimeMillis() - startTime;
      System.out.println("timing/contourDraw: " + tookTime*.001 + " seconds");
    }
  }  */

}