/*
 * Copyright (c) 1998 - 2012. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/**
 * Polyconic Projection.
 * This file was semi-automatically converted from the public-domain USGS PROJ source.
 *
 * Bernhard Jenny, 19 September 2010: fixed spherical inverse.

 * @author ghansham@sac.isro.gov.in 1/8/2012
 */
package ucar.unidata.geoloc.projection.proj4;

import java.util.Formatter;

import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.Earth;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.ProjectionPoint;
import ucar.unidata.geoloc.ProjectionPointImpl;

public class PolyconicProjection extends ProjectionImpl {

  private double ml0;
  private double[] en;
  private final static double TOL = 1e-10;
  private final static double CONV = 1e-10;
  private final static int N_ITER = 10;
  private final static int I_ITER = 20;
  private final static double ITOL = 1.e-12;

  //New variables added
  private boolean spherical = true;
  private double projectionLatitude;
  private double projectionLongitude;
  private Earth ellipsoid;
  private double es;
  private double falseEasting;
  private double falseNorthing;
  private double totalScale;

  public PolyconicProjection() {
    this(23.56, 76.54);
  }

  public PolyconicProjection(double lat0, double lon0) {
    this(lat0, lon0, new Earth());
  }

  public PolyconicProjection(double lat0, double lon0, Earth ellipsoid) {
    this(lat0, lon0, 0.0, 0.0, ellipsoid);

  }

  public PolyconicProjection(double lat0, double lon0, double falseEasting, double falseNorthing, Earth ellipsoid) {
    super("Polyconic", false);

    //Initialization
    this.projectionLatitude = Math.toRadians(lat0);
    this.projectionLongitude = Math.toRadians(lon0);
    this.ellipsoid = ellipsoid;

    this.falseEasting = falseEasting;
    this.falseNorthing = falseNorthing;
    this.es = this.ellipsoid.getEccentricitySquared();
    this.totalScale = this.ellipsoid.getMajor() * .001;

    initialize();

    //Adding parameters
    addParameter(CF.GRID_MAPPING_NAME, name);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, lat0);
    addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, lon0);
    if ((falseEasting != 0.0) || (falseNorthing != 0.0)) {
      addParameter(CF.FALSE_EASTING, falseEasting);
      addParameter(CF.FALSE_NORTHING, falseNorthing);
      addParameter(CDM.UNITS, "km");
    }

    addParameter(CF.SEMI_MAJOR_AXIS, ellipsoid.getMajor());
    addParameter(CF.INVERSE_FLATTENING, 1.0 / ellipsoid.getFlattening());
  }

  private ProjectionPoint project(double lplam, double lpphi, ProjectionPointImpl out) {
    if (spherical) {
      double cot, E;

      if (Math.abs(lpphi) <= TOL) {
        out.setLocation(lplam, ml0);
      } else {
        cot = 1. / Math.tan(lpphi);
        double x = Math.sin(E = lplam * Math.sin(lpphi)) * cot;
        double y = lpphi - projectionLatitude + cot * (1. - Math.cos(E));
        out.setLocation(x, y);
      }
    } else {

      /*
      FORWARD(e_forward); ellipsoid
      	double  ms, sp, cp;

      	if (fabs(lp.phi) <= TOL) {
      	  xy.x = lp.lam; xy.y = -P->ml0; }
      	else {
      		sp = sin(lp.phi);
      		ms = fabs(cp = cos(lp.phi)) > TOL ? pj_msfn(sp, cp, P->es) / sp : 0.;
      		xy.x = ms * sin(lp.lam *= sp); // LOOK
      		xy.y = (pj_mlfn(lp.phi, sp, cp, P->en) - P->ml0) + ms * (1. - cos(lp.lam));
      	}
      	return (xy);
      }
      */
      double ms, sp, cp;

      if (Math.abs(lpphi) <= TOL) {
        out.setLocation(lplam, -ml0);
      } else {
        sp = Math.sin(lpphi);
        ms = Math.abs(cp = Math.cos(lpphi)) > TOL ? MapMath.msfn(sp, cp, es) / sp : 0.;
        lplam *= sp; // LOOK
        double x = ms * Math.sin(lplam);
        double y = (MapMath.mlfn(lpphi, sp, cp, en) - ml0) + ms * (1. - Math.cos(lplam));
        out.setLocation(x, y);
      }
    }
    return out;
  }

  private ProjectionPoint projectInverse(double xyx, double xyy, ProjectionPointImpl out) {
    double lpphi;

    if (spherical) {
      double B, dphi, tp;
      int i;

      if (Math.abs(xyy = projectionLatitude + xyy) <= TOL) {
        out.setLocation(xyx, 0);

      } else {
        lpphi = xyy;
        B = xyx * xyx + xyy * xyy;
        i = N_ITER;
        do {
          tp = Math.tan(lpphi);
          lpphi -= (dphi = (xyy * (lpphi * tp + 1.) - lpphi
                  - .5 * (lpphi * lpphi + B) * tp)
                  / ((lpphi - xyy) / tp - 1.));
        } while (Math.abs(dphi) > CONV && --i > 0);
        if (i == 0) {
          out.setLocation(Double.NaN, Double.NaN);
        }
        double x = Math.asin(xyx * Math.tan(lpphi)) / Math.sin(lpphi);
        double y = lpphi;
        out.setLocation(x, y);
      }
    } else {
      xyy += ml0;
      if (Math.abs(xyy) <= TOL) {
        out.setLocation(xyx, 0);

      } else {
        double r, c, sp, cp, s2ph, ml, mlb, mlp, dPhi;
        int i;

        r = xyy * xyy + xyx * xyx;
        for (lpphi = xyy, i = I_ITER; i > 0; --i) {
          sp = Math.sin(lpphi);
          s2ph = sp * (cp = Math.cos(lpphi));
          if (Math.abs(cp) < ITOL) {
            throw new RuntimeException("I");
          }
          c = sp * (mlp = Math.sqrt(1. - es * sp * sp)) / cp;
          ml = MapMath.mlfn(lpphi, sp, cp, en);
          mlb = ml * ml + r;
          mlp = (1.0 / es) / (mlp * mlp * mlp);
          lpphi += (dPhi =
                  (ml + ml + c * mlb - 2. * xyy * (c * ml + 1.)) / (es * s2ph * (mlb - 2. * xyy * ml) / c
                          + 2. * (xyy - ml) * (c * mlp - 1. / s2ph) - mlp - mlp));
          if (Math.abs(dPhi) <= ITOL) {
            break;
          }
        }
        if (i == 0) {
          out.setLocation(Double.NaN, Double.NaN);
        }
        c = Math.sin(lpphi);
        double x = Math.asin(xyx * Math.tan(lpphi) * Math.sqrt(1. - es * c * c)) / Math.sin(lpphi);
        double y = lpphi;
        out.setLocation(x, y);
      }
    }
    return out;
  }

  private void initialize() {
    if (!spherical) {
      en = MapMath.enfn(es);
      if (en == null) {
        throw new RuntimeException("E");
      }
      ml0 = MapMath.mlfn(projectionLatitude, Math.sin(projectionLatitude), Math.cos(projectionLatitude), en);
    } else {
      ml0 = -projectionLatitude;
    }
  }

  //   public PolyconicProjection(double lat0, double lon0, double falseEasting, double falseNorthing, Earth ellipsoid) {

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    PolyconicProjection that = (PolyconicProjection) o;

    if (Double.compare(that.falseEasting, falseEasting) != 0) return false;
    if (Double.compare(that.falseNorthing, falseNorthing) != 0) return false;
    if (Double.compare(that.projectionLatitude, projectionLatitude) != 0) return false;
    if (Double.compare(that.projectionLongitude, projectionLongitude) != 0) return false;
    if (!ellipsoid.equals(that.ellipsoid)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = Double.doubleToLongBits(projectionLatitude);
    result = (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(projectionLongitude);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    result = 31 * result + ellipsoid.hashCode();
    temp = Double.doubleToLongBits(falseEasting);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(falseNorthing);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

  /*
   * Check for equality with the Object in question
   *
   * @param proj object to check
   * @return true if they are equal
   *
  public boolean equals(Object proj) {
    if (!(proj instanceof PolyconicProjection))
      return false;

    PolyconicProjection oo = (PolyconicProjection) proj;
    if ((this.getDefaultMapArea() == null) != (oo.defaultMapArea == null)) return false; // common case is that these are null
    if (this.getDefaultMapArea() != null && !this.defaultMapArea.equals(oo.defaultMapArea)) return false;

    return ((this.getOriginLatitude() == oo.getOriginLatitude())
          && (this.getOriginLongitude() == oo.getOriginLongitude())
          && this.ellipsoid.equals(oo.getEarth()));
  }  */

  //Beans properties
  public Earth getEarth() {
    return ellipsoid;
  }

  /**
   * Set the origin latitude.
   *
   * @param lat the origin latitude.
   */
  public void setOriginLatitude(double lat) { //suppply in degrees
    projectionLatitude = Math.toRadians(lat);
  }

  /**
   * Get the origin longitude.
   *
   * @return the origin longitude in degrees
   */
  public double getOriginLatitude() {
    return Math.toDegrees(projectionLatitude);
  }

  /**
   * Set the origin longitude.
   *
   * @param lon the origin longitude.
   */
  public void setOriginLongitude(double lon) {//suppply in degrees
    projectionLongitude = Math.toRadians(lon);
  }

  /**
   * Get the origin longitude.
   *
   * @return the origin longitude in degrees
   */
  public double getOriginLongitude() {
    return Math.toDegrees(projectionLongitude);
  }

  /**
   * Get the false easting, in km.
   *
   * @return the false easting in km
   */
  public double getFalseEasting() {
    return falseEasting;
  }

  /**
   * Set the false_easting, in km.
   * natural_x_coordinate + false_easting = x coordinate
   *
   * @param falseEasting x offset
   */
  public void setFalseEasting(double falseEasting) {
    this.falseEasting = falseEasting;
  }

  /**
   * Get the false northing, in km.
   *
   * @return the false northing in km
   */
  public double getFalseNorthing() {
    return falseNorthing;
  }

  /**
   * Set the false northing, in km.
   * natural_y_coordinate + false_northing = y coordinate
   *
   * @param falseNorthing y offset
   */
  public void setFalseNorthing(double falseNorthing) {
    this.falseNorthing = falseNorthing;
  }

  /**
   * Get the label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "Polyconic Projection";
  }

  /**
   * Create a String of the parameters.
   *
   * @return a String of the parameters
   */
  @Override
  public String paramsToString() {
    Formatter f = new Formatter();
    f.format("origin lat=%f, origin lon=%f earth=%s", Math.toDegrees(projectionLatitude), Math.toDegrees(projectionLongitude), ellipsoid);
    return f.toString();
  }

  /**
   * This returns true when the line between pt1 and pt2 crosses the seam.
   * When the cone is flattened, the "seam" is lon0 +- 180.
   *
   * @param pt1 point 1
   * @param pt2 point 2
   * @return true when the line between pt1 and pt2 crosses the seam.
   */
  @Override
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }

    // opposite signed X values, larger then 20,000 kml... similar to LambertConformal Conic
    return (pt1.getX() * pt2.getX() < 0)
            && (Math.abs(pt1.getX() - pt2.getX()) > 20000.0);

  }

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latlon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  @Override
  public ProjectionPoint latLonToProj(LatLonPoint latlon, ProjectionPointImpl result) {
    double fromLat = Math.toRadians(latlon.getLatitude());
    double theta = Math.toRadians(latlon.getLongitude());
    if (projectionLongitude != 0 && !Double.isNaN(theta)) {
      theta = MapMath.normalizeLongitude(theta - projectionLongitude);
    }

    ProjectionPointImpl out = new ProjectionPointImpl();
    project(theta, fromLat, out);
    result.setLocation(totalScale * out.getX() + falseEasting, totalScale * out.getY() + falseNorthing);
    return result;
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param world  convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  @Override
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double fromX = (world.getX() - falseEasting) / totalScale; // assumes cartesian coords in km
    double fromY = (world.getY() - falseNorthing) / totalScale;

    ProjectionPointImpl pp = new ProjectionPointImpl();
    projectInverse(fromX, fromY, pp);
    if (pp.getX() < -Math.PI) {
      pp.setX(-Math.PI);
    } else if (pp.getX() > Math.PI) {
      pp.setX(Math.PI);
    }
    if (projectionLongitude != 0 && !Double.isNaN(pp.getX())) {
      pp.setX(MapMath.normalizeLongitude(pp.getX() + projectionLongitude));
    }

    result.setLatitude( Math.toDegrees(pp.getY()));
    result.setLongitude( Math.toDegrees(pp.getX()));
    return result;
  }

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new PolyconicProjection(getOriginLatitude(), getOriginLongitude(), getFalseEasting(), getFalseNorthing(), getEarth());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }
}


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

import thredds.featurecollection.FeatureCollectionConfig;
import ucar.nc2.grib.collection.GribCollectionMutable;
import ucar.nc2.grib.collection.PartitionCollectionMutable;
import ucar.nc2.time.CalendarDate;

import java.util.*;

/**
 * Create the overall coordinate across the same variable in different partitions
 * The CoordinateBuilders create unique sets of Coordinates.
 * The CoordinateND result is then the cross-product of those Coordinates.
 *
 * Used by GribPartitionBuilder
 * This is a builder helper class, the result is obtained from List<Coordinate> finish().
 *
 * So if theres a lot of missing records in that cross-product, we may have the variable wrong (?),
 *  or our assumption that the data comprises a multidim array may be wrong
 *
 * @author John
 * @since 12/10/13
 */
public class CoordinatePartitionUnionizer {
  private static final boolean debugPartitionErrors = true;

  private final FeatureCollectionConfig.GribIntvFilter intvFilter;
  private final org.slf4j.Logger logger;
  private final GribCollectionMutable.VariableIndex vi;
  private final Map<Long, PartitionCollectionMutable.Partition> timeMap;

  public CoordinatePartitionUnionizer(GribCollectionMutable.VariableIndex vi, FeatureCollectionConfig.GribIntvFilter intvFilter, org.slf4j.Logger logger) {
    this.vi = vi;
    this.intvFilter = intvFilter;
    this.logger = logger;
    timeMap = (debugPartitionErrors) ? new HashMap<>() : null;
  }

  List<Coordinate> unionCoords = new ArrayList<>();

  CoordinateBuilder runtimeBuilder ;
  CoordinateBuilder timeBuilder;
  CoordinateBuilder timeIntvBuilder;
  CoordinateBuilder vertBuilder;
  CoordinateBuilder ensBuilder;
  CoordinateTime2DUnionizer time2DBuilder;

  boolean duplicateRuntimeMessage; // only one message per CoordinatePartitionUnionizer instance

  public void addCoords(List<Coordinate> coords, PartitionCollectionMutable.Partition part) {
    Coordinate runtime = null;
    for (Coordinate coord : coords) {
      switch (coord.getType()) {
        case runtime:
          CoordinateRuntime rtime = (CoordinateRuntime) coord;
          if (runtimeBuilder == null) runtimeBuilder = new CoordinateRuntime.Builder2(rtime.getTimeUnits());
          runtimeBuilder.addAll(coord);
          runtime = coord;
          if (debugPartitionErrors && !duplicateRuntimeMessage && part != null)
            testDuplicateRuntime(rtime, part);
          break;

        case time:
          CoordinateTime time = (CoordinateTime) coord;
          if (timeBuilder == null) timeBuilder = new CoordinateTime.Builder2(coord.getCode(), time.getTimeUnit(), time.getRefDate());
          timeBuilder.addAll(coord);
          break;

        case timeIntv:
          CoordinateTimeIntv timeIntv = (CoordinateTimeIntv) coord;
          if (timeIntvBuilder == null) timeIntvBuilder = new CoordinateTimeIntv.Builder2(null, coord.getCode(), timeIntv.getTimeUnit(), timeIntv.getRefDate());
          timeIntvBuilder.addAll(intervalFilter((CoordinateTimeIntv)coord));
          break;

        case time2D:
          CoordinateTime2D time2D = (CoordinateTime2D) coord;
          if (time2DBuilder == null) time2DBuilder = new CoordinateTime2DUnionizer(time2D.isTimeInterval(), time2D.getTimeUnit(), coord.getCode(), false, logger);
          time2DBuilder.addAll(time2D);
          // debug
          CoordinateRuntime runtimeFrom2D = time2D.getRuntimeCoordinate();
          if (!runtimeFrom2D.equals(runtime))
            logger.warn("HEY CoordinateUnionizer runtimes not equal");
          break;

        case ens:
          if (ensBuilder == null) ensBuilder = new CoordinateEns.Builder2(coord.getCode());
          ensBuilder.addAll(coord);
          break;

        case vert:
          CoordinateVert vertCoord = (CoordinateVert) coord;
          if (vertBuilder == null) vertBuilder = new CoordinateVert.Builder2(coord.getCode(), vertCoord.getVertUnit());
          vertBuilder.addAll(coord);
          break;
      }
    }
  }

  private void testDuplicateRuntime(CoordinateRuntime runtime, PartitionCollectionMutable.Partition part) {
    PartitionCollectionMutable.Partition shownPrevPart = null;
    for (int idx=0; idx<runtime.getNCoords(); idx++) {    // possible duplicate runtimes from different partitions
      long time = runtime.getRuntime(idx);
      PartitionCollectionMutable.Partition prevPart = timeMap.get(time);
      if (prevPart != null && prevPart != part && prevPart != shownPrevPart) {
        logger.warn("Variable {} Runtime {} in part {} and partition {}", vi.id(), CalendarDate.of(time), prevPart.getName(), part.getName());
        shownPrevPart = prevPart; // eliminate extra messages
        duplicateRuntimeMessage = true;
      }
      timeMap.put(time, part);
    }
  }

  private List<TimeCoordIntvValue> intervalFilter(CoordinateTimeIntv coord) {
    if (intvFilter == null) return coord.getTimeIntervals();
    List<TimeCoordIntvValue> result = new ArrayList<>();
    for (TimeCoordIntvValue tinv : coord.getTimeIntervals()) {
      if (!intvFilter.filter(vi.getVarid(), tinv.getBounds1(), tinv.getBounds2(), Integer.MIN_VALUE))
        result.add(tinv);
    }
    return result;
  }

   public List<Coordinate> finish() {
    if (runtimeBuilder != null)
      unionCoords.add(runtimeBuilder.finish());
    else
      logger.warn("HEY CoordinateUnionizer missing runtime");

    if (timeBuilder != null)
      unionCoords.add(timeBuilder.finish());
    else if (timeIntvBuilder != null)
      unionCoords.add(timeIntvBuilder.finish());
    else if (time2DBuilder != null)
      unionCoords.add(time2DBuilder.finish());
    else
      logger.warn("HEY CoordinateUnionizer missing time");

    if (ensBuilder != null) // ens must come before vert to preserve order
       unionCoords.add(ensBuilder.finish());
    if (vertBuilder != null)
      unionCoords.add(vertBuilder.finish());

    // result = new CoordinateND<>(unionCoords);
    return unionCoords;
  }

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information. */

package dap4.core.dmr.parser;

import java.util.ArrayList;
import java.util.List;

/**
 * Misc. utilities; avoid duplicating dap4.core.dmr.{Escape,Util}
 */

public class ParseUtil
{
    static public final int MAXTEXT = 12;

    /* Common Flag Set */
    static public final int FLAG_NONE = 0;
    static public final int FLAG_ESCAPE = 1; //convert \n,\r, etc to \\ form
    static public final int FLAG_NOCR = 2; // elide \r
    static public final int FLAG_ELIDETEXT = 4; // only print the first 12 characters of text
    static public final int FLAG_TRIMTEXT = 8; //remove leading and trailing whitespace;
    // if result is empty, then ignore
    static public final int FLAG_TRACE = 16;   // Trace the DomLexer tokens

    static public final int DEFAULTFLAGS = (FLAG_ELIDETEXT | FLAG_ESCAPE | FLAG_NOCR | FLAG_TRIMTEXT);

    /* Characters legal as first char of an element or attribute name */
    static public boolean namechar1(char c)
    {
        return (":_?".indexOf(c) >= 0
            || "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(c) >= 0
            || ((int) c) > 127);
    }

    /* Characters legal as greater than first char of an element or attribute name */
    static public boolean namecharn(char c)
    {
        return (namechar1(c)
            || "-.".indexOf(c) >= 0
            || "0123456789".indexOf(c) >= 0);
    }

    /**
     * Split a <Value>...</Value> into its component strings.
     * Generally, type checking is not performed. String quotes
     * are obeyed and backslash escapes are removed.
     */
    static public List<String>
    collectValues(String text)
        throws ParseException
    {
        List<String> values = new ArrayList<String>();
        StringBuffer buf = new StringBuffer();
        text = text.trim() + '\0';
        int i = 0;
        for(; ; ) {
            char c = text.charAt(i++);
            if(c == '\0') break; // eos
            if(c <= ' ' || c == 127) // whitespace
                continue;
            if(c == '\'') {// collect char constant
                c = text.charAt(i++);
                if(c == '\0')
                    throw new ParseException("Malformed char constant: no final '''");
                else if(i >= 128)
                    throw new ParseException("Illegal char constant: " + (int) c);
                buf.append(c);
                values.add(buf.toString());
                buf.setLength(0);
            } else if(c == '"') { // collect quoted string
                for(; ; ) {
                    c = text.charAt(i++);
                    if(c == '\0') {
                        i--;
                        break;
                    }
                    if(c == '\\') {
                        c = text.charAt(i++);
                    } else if(c == '"') break;
                    buf.append(c);
                }
                if(c == '\0')
                    throw new ParseException("Malformed string: no final '\"'");
                values.add(buf.toString());
                buf.setLength(0);
            } else {// collect upto next whitespace or eos
                do {
                    if(c == '\\') {
                        c = text.charAt(i++);
                    }
                    buf.append(c);
                    c = text.charAt(i++);
                } while(c > ' ' && c != 127);
                values.add(buf.toString());
                buf.setLength(0);
                if(c == 0) i--; // So we never move past the trailing eol
            }
        }
        return values;
    }

    static public boolean
    isLegalEnumConstName(String name)
    {
        // Name must consist of non-blank non-control characters
        for(int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if(c <= ' ' || c == 127) return false;
        }
        return true;
    }

} // class Util


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

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.iosp.fysat.FysatHeader.Vinfo;
import ucar.nc2.iosp.fysat.util.EndianByteBuffer;
import ucar.nc2.iosp.AbstractIOServiceProvider;

import java.io.*;
import java.awt.image.*;
import java.util.zip.Inflater;
import java.util.zip.DataFormatException;

/**
 * FY satellite data stored in AWX format include both original observation and the derived dataset
 *
 * @author yuan
 * @see "http://www.nsmc.cma.gov.cn/en/NSMC/UploadFiles/files/AWX%20File%20Format%20Specification%20Version%202.1.pdf"
 */

public class Fysatiosp extends AbstractIOServiceProvider {

  protected FysatHeader headerParser;

  final static int Z_DEFLATED = 8;
  final static int DEF_WBITS = 15;

  public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) throws IOException {
    FysatHeader localHeader = new FysatHeader();
    return (localHeader.isValidFile(raf));
  }

  public String getFileTypeId() {
    return "FYSAT";
  }

  public String getFileTypeDescription() {
    return "Chinese FY-2 satellite image data in AWX format";
  }

  /////////////////////////////////////////////////////////////////////////////
  // reading

  public void open(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile,
                   ucar.nc2.util.CancelTask cancelTask) throws IOException {

    super.open(raf, ncfile, cancelTask);

    headerParser = new FysatHeader();
    headerParser.read(raf, ncfile);

    ncfile.finish();
  }

  public Array readData(ucar.nc2.Variable v2, Section section) throws IOException, InvalidRangeException {
    // subset
    //  Range[] section = Range.toArray( sectionList);
    int[] origin = section.getOrigin();
    int[] shape = section.getShape();
    int[] stride = section.getStride();

    FysatHeader.Vinfo vinfo = (FysatHeader.Vinfo) v2.getSPobject();

    if (headerParser.getCompressType() == 0)
      return readData(v2, vinfo.begin, origin, shape, stride);
    else if (headerParser.getCompressType() == 2)
      return readCompressedData(v2, vinfo.begin, origin, shape, stride);
    else if (headerParser.getCompressType() == 1)
      return readCompressedZlib(v2, vinfo.begin, vinfo.nx, vinfo.ny, origin, shape, stride);
    else
      return null;
  }

  // all the work is here, so can be called recursively
  private Array readData(ucar.nc2.Variable v2, long dataPos, int[] origin, int[] shape, int[] stride) throws IOException, InvalidRangeException {

    // long length = myRaf.length();
    raf.seek(dataPos);
    Vinfo vi = (Vinfo) v2.getSPobject();
    int data_size = vi.vsize;
    byte[] data = new byte[data_size];
    raf.readFully(data);

    Array array;
    if (vi.classType == DataType.BYTE.getPrimitiveClassType()) {

      array = Array.factory(DataType.BYTE, v2.getShape(), data);
    } else if (vi.classType == DataType.SHORT.getPrimitiveClassType()) {
      EndianByteBuffer byteBuff = new EndianByteBuffer(data, vi.byteOrder);
      short[] sdata = byteBuff.getShortArray();
      //for(int i=0; i<sdata.length; i++){
      //	System.out.println(sdata[i]);
      //}
      array = Array.factory(DataType.SHORT, v2.getShape(), sdata);
    } else if (vi.classType == DataType.INT.getPrimitiveClassType()) {
      EndianByteBuffer byteBuff = new EndianByteBuffer(data, vi.byteOrder);
      short[] idata = byteBuff.getShortArray();
      array = Array.factory(DataType.INT, v2.getShape(), idata);

    } else {
      throw new UnsupportedEncodingException();
    }

    return array.sectionNoReduce(origin, shape, stride);

  }

  public Array readDataOld(ucar.nc2.Variable v2, long dataPos, int[] origin, int[] shape, int[] stride) throws IOException, InvalidRangeException {
    int start_l, stride_l, stop_l;
    int start_p, stride_p, stop_p;
    if (origin == null) origin = new int[v2.getRank()];
    if (shape == null) shape = v2.getShape();

    FysatHeader.Vinfo vinfo = (FysatHeader.Vinfo) v2.getSPobject();
    //ucar.ma2.DataType dataType = v2.getDataType();

    int nx = vinfo.nx;
    int ny = vinfo.ny;
    start_l = origin[0];
    stride_l = stride[0];
    stop_l = origin[0] + shape[0] - 1;
    // Get data values from GINI
    // Loop over number of lines (slower dimension) for actual data Array
    start_p = origin[1];
    stride_p = stride[1];
    stop_p = origin[1] + shape[1] - 1;

    if (start_l + stop_l + stride_l == 0) { //default lines
      start_l = 0;
      stride_l = 1;
      stop_l = ny - 1;
    }
    if (start_p + stop_p + stride_p == 0) { //default pixels
      start_p = 0;
      stride_p = 1;
    }

    int Len = shape[1]; // length of pixels read each line
    ArrayByte adata = new ArrayByte(new int[]{shape[0], shape[1]}, false); // LOOK unsigned ??
    Index indx = adata.getIndex();
    long doff = dataPos + start_p;
    // initially no data conversion is needed.
    for (int iline = start_l; iline <= stop_l; iline += stride_l) {
      /* read 1D byte[] */
      byte[] buf = getGiniLine(nx, ny, doff, iline, Len, stride_p);
      /* write into 2D array */
      for (int i = 0; i < Len; i++) {
        adata.setByte(indx.set(iline - start_l, i), buf[i]);
      }
    }
    return adata;
  }
  // for the compressed data read all out into a array and then parse into requested

  // for the compressed data read all out into a array and then parse into requested

  public Array readCompressedData(ucar.nc2.Variable v2, long dataPos, int[] origin, int[] shape, int[] stride) throws IOException, InvalidRangeException {

    long length = raf.length();

    raf.seek(dataPos);

    int data_size = (int) (length - dataPos);
    byte[] data = new byte[data_size];
    raf.readFully(data);
    ByteArrayInputStream ios = new ByteArrayInputStream(data);

    BufferedImage image = javax.imageio.ImageIO.read(ios);
    Raster raster = image.getData();
    DataBuffer db = raster.getDataBuffer();

    if (db instanceof DataBufferByte) {
      DataBufferByte dbb = (DataBufferByte) db;
      byte[] udata = dbb.getData();

      Array array = Array.factory(DataType.BYTE, v2.getShape(), udata);
      v2.setCachedData(array, false);
      return array.sectionNoReduce(origin, shape, stride);
    }

    return null;
  }

  public Array readCompressedZlib(ucar.nc2.Variable v2, long dataPos, int nx, int ny, int[] origin, int[] shape, int[] stride) throws IOException, InvalidRangeException {

    long length = raf.length();

    raf.seek(dataPos);

    int data_size = (int) (length - dataPos);     //  or 5120 as read buffer size
    byte[] data = new byte[data_size];
    raf.readFully(data);

    // decompress the bytes
    int resultLength;
    int result = 0;
    byte[] tmp;
    int uncompLen;        /* length of decompress space    */
    byte[] uncomp = new byte[nx * (ny + 1) + 4000];
    Inflater inflater = new Inflater(false);

    inflater.setInput(data, 0, data_size);
    int offset = 0;
    int limit = nx * ny + nx;

    while (inflater.getRemaining() > 0) {
      try {
        resultLength = inflater.inflate(uncomp, offset, 4000);

      } catch (DataFormatException ex) {
        ex.printStackTrace();
        throw new IOException(ex.getMessage());
      }

      offset = offset + resultLength;
      result = result + resultLength;
      if ((result) > limit) {
        // when uncomp data larger then limit, the uncomp need to increase size
        tmp = new byte[result];
        System.arraycopy(uncomp, 0, tmp, 0, result);
        uncompLen = result + 4000;
        uncomp = new byte[uncompLen];
        System.arraycopy(tmp, 0, uncomp, 0, result);
      }

      if (resultLength == 0) {
        int tt = inflater.getRemaining();
        byte[] b2 = new byte[2];
        System.arraycopy(data, data_size - tt, b2, 0, 2);
        if (isZlibHed(b2) == 0) {
          System.arraycopy(data, data_size - tt, uncomp, result, tt);
          break;
        }
        inflater.reset();
        inflater.setInput(data, data_size - tt, tt);
      }

    }

    inflater.end();

    byte[] inflateData = new byte[nx * ny];
    System.arraycopy(uncomp, 0, inflateData, 0, nx * ny);
    Array array = Array.factory(DataType.BYTE, v2.getShape(), inflateData);
    if (array.getSize() < Variable.defaultSizeToCache)
      v2.setCachedData(array, false);
    return array.sectionNoReduce(origin, shape, stride);
  }

  /*
  ** Name:       GetGiniLine
  **
  ** Purpose:    Extract a line of data from a GINI image
  **
  ** Parameters:
  **             buf     - buffer containing image data
  **
  ** Returns:
  **             SUCCESS == 1
  **             FAILURE == 0
  **
  **
  */
  private byte[] getGiniLine(int nx, int ny, long doff, int lineNumber, int len, int stride) throws IOException {

    byte[] data = new byte[len];

    /*
    ** checking image file and set location of first line in file
    */
    raf.seek(doff);

    if (lineNumber >= ny)
      throw new IOException("Try to access the file at line number= " + lineNumber + " larger then last line number = " + ny);

    /*
    ** Read in the requested line
    */

    int offset = lineNumber * nx + (int) doff;

    //myRaf.seek ( offset );
    for (int i = 0; i < len; i++) {
      raf.seek(offset);
      data[i] = raf.readByte();
      offset = offset + stride;
      //myRaf.seek(offset);
    }
    //myRaf.read( data, 0, len);

    return data;

  }

  public short convertunsignedByte2Short(byte b) {
    return (short) ((b < 0) ? (short) b + 256 : (short) b);
  }

  int isZlibHed(byte[] buf) {
    short b0 = convertunsignedByte2Short(buf[0]);
    short b1 = convertunsignedByte2Short(buf[1]);

    if ((b0 & 0xf) == Z_DEFLATED) {
      if ((b0 >> 4) + 8 <= DEF_WBITS) {
        if ((((b0 << 8) + b1) % 31) == 0) {
          return 1;
        }
      }
    }

    return 0;
  }

}


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

package ucar.nc2.grib;

import com.google.common.base.MoreObjects;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.util.GaussianLatitudes;
import ucar.unidata.util.StringUtil2;

import javax.annotation.concurrent.Immutable;

/**
 * A Horizontal coordinate system generated from a GRIB-1 or GRIB-2 GDS.
 *
 * @author John
 * @since 9/5/11
 */
@Immutable
public class GdsHorizCoordSys {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GdsHorizCoordSys.class);

  private final String name;
  public final int template, gdsNumberPoints, scanMode;
  public final ucar.unidata.geoloc.ProjectionImpl proj;
  public final double startx, dx; // km
  public final double starty, dy; // km
  public final int nx, ny;        // regridded
  public final int nxRaw, nyRaw;  // raw
  public final int[] nptsInLine; // non-null id thin grid

  // hmmmm
  private Array gaussLats;
  private Array gaussw;

  public GdsHorizCoordSys(String name, int template, int gdsNumberPoints, int scanMode, ProjectionImpl proj,
                          double startx, double dx, double starty, double dy, int nxRaw, int nyRaw, int[] nptsInLine) {

    this.name = name;
    this.template = template;
    this.gdsNumberPoints = gdsNumberPoints; // only used by GRIB2
    this.scanMode = scanMode;
    this.proj = proj;
    this.startx = startx;
    this.dx = dx;
    this.starty = starty;
    this.dy = dy;
    this.nxRaw = nxRaw;
    this.nyRaw = nyRaw;
    this.nptsInLine = nptsInLine;

    // thin grids
    if (nptsInLine != null) {
      if (nxRaw > 0) {
        nx = nxRaw;
        ny = QuasiRegular.getMax(nptsInLine);
      } else if (nyRaw > 0) {
        ny = nyRaw;
        nx = QuasiRegular.getMax(nptsInLine);
      } else {
        throw new IllegalArgumentException("Quasi Grids nx,ny="+nxRaw+","+nyRaw);
      }
    } else {
      nx = nxRaw;
      ny = nyRaw;
    }
  }

  public String getName() {
    return name;
  }

  public double getStartX() {
    return startx;
  }

  public double getStartY() {
    if (gaussLats != null) return gaussLats.getDouble(0);
    return starty;
  }

  public double getEndX() {
    return startx + dx * (nx-1);
  }

  public double getEndY() {
    if (gaussLats != null) return gaussLats.getDouble((int) gaussLats.getSize() - 1);
    return starty + dy * (ny-1);
  }

  public int getScanMode() {
    return scanMode;
  }

  public boolean isLatLon() {
    return proj instanceof LatLonProjection;
  }

  public LatLonPoint getCenterLatLon() {
    return proj.projToLatLon(startx + dx * nx / 2, starty + dy * ny / 2);
  }

  public String makeDescription() {
    return name + "_" + ny + "X" + nx+" (Center "+getCenterLatLon()+")";
  }

  public String makeId() {
    LatLonPointImpl center = (LatLonPointImpl) getCenterLatLon();
    StringBuilder result = new StringBuilder(name + "_" + ny + "X" + nx+"-"+center.toString(2));
    StringUtil2.replace(result, ". ","p-");
    return result.toString();
  }

  public ProjectionRect getProjectionBB() {
    return new ProjectionRect(new ProjectionPointImpl(getStartX(), getStartY()), getEndX() - getStartX(), getEndY() - getStartY());
  }

  public LatLonRect getLatLonBB() {
    if (isLatLon()) {
      return new LatLonRect(new LatLonPointImpl(getStartY(), getStartX()), dy * (ny-1), dx * (nx-1));
    } else {
      return proj.projToLatLonBB(getProjectionBB());
    }
  }

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

  // set gaussian weights based on nparallels
  // some weird adjustment for la1 and la2.
  public void setGaussianLats(int nparallels, float la1, float la2) {
    log.debug ("la1 {}, la2 {}", la1, la2);
    if (this.gaussLats != null) throw new RuntimeException("Cant modify GdsHorizCoordSys");

    int nlats = (2 * nparallels);
    GaussianLatitudes gaussLats = GaussianLatitudes.factory(nlats);

    int bestStartIndex = 0, bestEndIndex = 0;
    double bestStartDiff = Double.MAX_VALUE;
    double bestEndDiff   = Double.MAX_VALUE;
    for (int i = 0; i < nlats; i++) {
      double diff = Math.abs(gaussLats.latd[i] - la1);
      if (diff < bestStartDiff) {
        bestStartDiff = diff;
        bestStartIndex = i;
      }
      diff = Math.abs(gaussLats.latd[i] - la2);
      if (diff < bestEndDiff) {
        bestEndDiff = diff;
        bestEndIndex = i;
      }
    }

    log.debug ("first pass: bestStartIndex {}, bestEndIndex {}", bestStartIndex, bestEndIndex);

    if (Math.abs(bestEndIndex - bestStartIndex) + 1 != nyRaw) {
      log.warn("GRIB gaussian lats: NP != NY, use NY");  // see email from Toussaint@dkrz.de datafil:
      nlats = nyRaw;
      gaussLats = GaussianLatitudes.factory(nlats);
      bestStartIndex = 0;
      bestEndIndex = nyRaw - 1;
    }
    boolean goesUp = bestEndIndex > bestStartIndex;

    log.debug ("bestStartIndex {}, bestEndIndex {}, goesUp {}", bestStartIndex, bestEndIndex, goesUp);

    // create the data
    int useIndex = bestStartIndex;
    float[] data = new float[nyRaw];
    float[] gaussw = new float[nyRaw];
    for (int i = 0; i < nyRaw; i++) {
      data[i] = (float) gaussLats.latd[useIndex];
      gaussw[i] = (float) gaussLats.gaussw[useIndex];

        log.trace ("i {}, useIndex {}, data {}, gaussw {}", i, useIndex, data[i], gaussw[i]);
      if (goesUp) {
        useIndex++;
      } else {
        useIndex--;
      }
    }

    this.gaussLats = Array.factory(DataType.FLOAT, new int[]{nyRaw}, data);
    this.gaussw    = Array.factory(DataType.FLOAT, new int[]{nyRaw}, gaussw);
  }

  public Array getGaussianLats() {
    return gaussLats;
  }

  public Array getGaussianWeights() {
    return gaussw;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("name", name)
        .add("template", template)
        .add("gdsNumberPoints", gdsNumberPoints)
        .add("scanMode", scanMode)
        .add("proj", proj)
        .add("startx", startx)
        .add("dx", dx)
        .add("starty", starty)
        .add("dy", dy)
        .add("nx", nx)
        .add("ny", ny)
        .add("nxRaw", nxRaw)
        .add("nyRaw", nyRaw)
        .add("nptsInLine", nptsInLine)
        .add("gaussLats", gaussLats)
        .add("gaussw", gaussw)
        .toString();
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
//
// Filename: Gdc_To_Utm_Converter.java
//
// Author: Dan Toms, SRI International
//
// Package: GeoTransform <http://www.ai.sri.com/geotransformtest/>
//
// Acknowledgements:
// The algorithms used in the package were created by Ralph Toms and
// first appeared as part of the SEDRIS Coordinate Transformation API.
// These were subsequently modified for this package. This package is
// not part of the SEDRIS project, and the Java code written for this
// package has not been certified or tested for correctness by NIMA.
//
// License:
// The contents of this file are subject to GeoTransform License Agreement
// (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.ai.sri.com/geotransformtest/license.html
//
// Software distributed under the License is distributed on an "AS IS"
// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
// the License for the specific language governing rights and limitations
// under the License.
//
// Portions are Copyright (c) SRI International, 1998.
//

package ucar.unidata.geoloc.projection;

import ucar.unidata.geoloc.EarthEllipsoid;
import ucar.unidata.geoloc.LatLonPointImpl;

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

import java.lang.*;

/**
 * Converts GDC coordinate(s) to UTM.
 *

 * This class provides the capability to convert from
 * geodetic (GDC), i.e. lat/long coordinates to
 * Universal Transverse Mercator (UTM).
 * This is a direct conversion.
 *
 * @author Dan Toms, SRI International
 *

 *         modified JCaron 01/2005
 *

 *
 turn static methods into object methods, to make thread-safe
 *
 rename methods to follow upper/lower case conventions
 *
 add convenience methods for ucar.unidata.geoloc.Projection
 *
 longitude must be in range +=180.
 *

 *

 *         random testing shows:
 *         avg error x= 0.4 y=0.06 meters
 *         but sometimes x error can be as high as 15 meters
 *         where err = abs(x - inverse(f(x)))
 *

 *         timing: inverse(f(x)) takes 2 - 3 microseconds.
 */

class Gdc_To_Utm_Converter {
  static final double RADIANS_PER_DEGREE = 0.0174532925199432957692;

  private double A;
  private double F; // flattening
  private double Eps2, Eps25, Epps2, CScale = .9996, poly1b, poly2b, poly3b, poly4b, poly5b;
  private double axlon0, axlon0_deg;

  /**
   * Constructor.
   *
   * @param a                the semi-major axis (meters) for the ellipsoid
   * @param f                the inverse flattening for the ellipsoid
   * @param zone             the UTM zone number (1..60)
   * @param hemisphere_north true if the UTM coordinate is in the northern hemisphere
   */
  public Gdc_To_Utm_Converter(double a, double f, int zone, boolean hemisphere_north) {
    init(a, f, zone, hemisphere_north);
  }

  /**
   * Default contructor uses WGS 84 ellipsoid
   *
   * @param zone             the UTM zone number (1..60)
   * @param hemisphere_north true if the UTM coordinate is in the northern hemisphere
   */
  public Gdc_To_Utm_Converter(int zone, boolean hemisphere_north) {
    this(EarthEllipsoid.WGS84, zone, hemisphere_north);  // default to wgs 84
  }

  /**
  * Constructor with ellipsoid.
  * @param ellipse an EarthEllipsoid, e.g. WE_Ellipsoid
  * @param zone             the UTM zone number (1..60)
  * @param isNorth   true if the UTM coordinate is in the northern hemisphere
  */
  public Gdc_To_Utm_Converter(EarthEllipsoid ellipse, int zone, boolean isNorth) {
    init(ellipse.getMajor(), 1.0 / ellipse.getFlattening(), zone, isNorth);
  }

  /**
   * _more_
   *
   * @param a       major axis
   * @param f       inverse flattening
   * @param zone    _more_
   * @param isNorth _more_
   */
  protected void init(double a, double f, int zone, boolean isNorth) {
    A = a;
    F = 1.0 / f;  // F is flattening
    this.axlon0_deg = (zone * 6 - 183);
    this.axlon0 = axlon0_deg * RADIANS_PER_DEGREE;

    double polx2b, polx3b, polx4b, polx5b;

    //  Create the ERM constants.
    Eps2 = (F) * (2.0 - F);
    Eps25 = .25 * (Eps2);
    Epps2 = (Eps2) / (1.0 - Eps2);
    polx2b = Eps2 + 1.0 / 4.0 * Math.pow(Eps2, 2)
        + 15.0 / 128.0 * Math.pow(Eps2, 3)
        - 455.0 / 4096.0 * Math.pow(Eps2, 4);

    polx2b = 3.0 / 8.0 * polx2b;
    polx3b = Math.pow(Eps2, 2) + 3.0 / 4.0 * Math.pow(Eps2, 3)
        - 77.0 / 128.0 * Math.pow(Eps2, 4);
    polx3b = 15.0 / 256.0 * polx3b;
    polx4b = Math.pow(Eps2, 3) - 41.0 / 32.0 * Math.pow(Eps2, 4);
    polx4b = polx4b * 35.0 / 3072.0;
    polx5b = -315.0 / 131072.0 * Math.pow(Eps2, 4);

    poly1b = 1.0 - (1.0 / 4.0 * Eps2) - (3.0 / 64.0 * Math.pow(Eps2, 2))
        - (5.0 / 256.0 * Math.pow(Eps2, 3))
        - (175.0 / 16384.0 * Math.pow(Eps2, 4));

    poly2b = polx2b * -2.0 + polx3b * 4.0 - polx4b * 6.0 + polx5b * 8.0;

    poly3b = polx3b * -8.0 + polx4b * 32.0 - polx5b * 80.0;

    poly4b = polx4b * -32.0 + polx5b * 192.0;

    poly5b = polx5b * -128.0;
  }

  /**
   * get central meridian in degrees (depends on zone)
   * @return central meridian in degrees
   */
  public double getCentralMeridian() {
    return this.axlon0_deg;
  }

  public ProjectionPoint latLonToProj(double latitude, double longitude, ProjectionPointImpl result) {
    double source_lat, source_lon, s1, c1, tx, s12, rn, al, al2, sm, tn2,
        cee, poly1, poly2;

    longitude = LatLonPointImpl.lonNormal(longitude, axlon0_deg);  // normalize to the central meridian

    source_lat = latitude * RADIANS_PER_DEGREE;
    source_lon = longitude * RADIANS_PER_DEGREE;

    s1 = Math.sin(source_lat);
    c1 = Math.cos(source_lat);
    tx = s1 / c1;
    s12 = s1 * s1;

    rn = A / ((.25 - Eps25 * s12 + .9999944354799 / 4)
        + (.25 - Eps25 * s12)
        / (.25 - Eps25 * s12 + .9999944354799 / 4));

    al = (source_lon - axlon0) * c1;
    sm = s1 * c1
        * (poly2b + s12 * (poly3b + s12 * (poly4b + s12 * poly5b)));
    sm = A * (poly1b * source_lat + sm);

    tn2 = tx * tx;
    cee = Epps2 * c1 * c1;
    al2 = al * al;
    poly1 = 1.0 - tn2 + cee;
    poly2 = 5.0 + tn2 * (tn2 - 18.0) + cee * (14.0 - tn2 * 58.0);

    double x = CScale * rn * al
        * (1.0
        + al2
        * (.166666666666667 * poly1
        + .00833333333333333 * al2 * poly2));
    x += 5.0E5;

    poly1 = 5.0 - tn2 + cee * (cee * 4.0 + 9.0);
    poly2 = 61.0 + tn2 * (tn2 - 58.0) + cee * (270.0 - tn2 * 330.0);
    double y = CScale
        * (sm + rn * tx * al2
        * (0.5
        + al2
        * (.0416666666666667 * poly1
        + .00138888888888888 * al2 * poly2)));

    if (source_lat < 0.0) {
      y += 1.0E7;
    }

    result.setLocation(x * .001, y * .001);  // wants km
    return result;
  }

  public double[][] latLonToProj(double[][] from, double[][] to, int latIndex, int lonIndex) {
    double source_lat, source_lon, s1, c1, tx, s12, rn, al, al2, sm, tn2, cee, poly1, poly2;

    for (int i = 0; i < from[0].length; i++) {
      double longitude = LatLonPointImpl.lonNormal(from[lonIndex][i], axlon0_deg);  // normalize to the central meridian
      source_lat = from[latIndex][i] * RADIANS_PER_DEGREE;
      source_lon = longitude * RADIANS_PER_DEGREE;

      s1 = Math.sin(source_lat);
      c1 = Math.cos(source_lat);
      tx = s1 / c1;
      s12 = s1 * s1;

      rn = A / ((.25 - Eps25 * s12 + .9999944354799 / 4)
          + (.25 - Eps25 * s12)
          / (.25 - Eps25 * s12 + .9999944354799 / 4));

      al = (source_lon - axlon0) * c1;
      sm = s1 * c1
          * (poly2b + s12 * (poly3b + s12 * (poly4b + s12 * poly5b)));
      sm = A * (poly1b * source_lat + sm);

      tn2 = tx * tx;
      cee = Epps2 * c1 * c1;
      al2 = al * al;
      poly1 = 1.0 - tn2 + cee;
      poly2 = 5.0 + tn2 * (tn2 - 18.0) + cee * (14.0 - tn2 * 58.0);

      double x = CScale * rn * al
          * (1.0
          + al2
          * (.166666666666667 * poly1
          + .00833333333333333 * al2 * poly2));
      x += 5.0E5;

      poly1 = 5.0 - tn2 + cee * (cee * 4.0 + 9.0);
      poly2 = 61.0 + tn2 * (tn2 - 58.0) + cee * (270.0 - tn2 * 330.0);
      double y = CScale
          * (sm + rn * tx * al2
          * (0.5
          + al2
          * (.0416666666666667 * poly1
          + .00138888888888888 * al2 * poly2)));

      if (source_lat < 0.0) {
        y += 1.0E7;
      }

      to[0][i] = x * .001;
      to[1][i] = y * .001;
    }

    return to;
  }

  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex, int lonIndex) {
    double source_lat, source_lon, s1, c1, tx, s12, rn, al, al2, sm, tn2, cee, poly1, poly2;

    for (int i = 0; i < from[0].length; i++) {
      double longitude = LatLonPointImpl.lonNormal(from[lonIndex][i], axlon0_deg);  // normalize to the central meridian
      source_lat = from[latIndex][i] * RADIANS_PER_DEGREE;
      source_lon = longitude * RADIANS_PER_DEGREE;

      s1 = Math.sin(source_lat);
      c1 = Math.cos(source_lat);
      tx = s1 / c1;
      s12 = s1 * s1;

      rn = A / ((.25 - Eps25 * s12 + .9999944354799 / 4)
          + (.25 - Eps25 * s12)
          / (.25 - Eps25 * s12 + .9999944354799 / 4));

      al = (source_lon - axlon0) * c1;
      sm = s1 * c1
          * (poly2b + s12 * (poly3b + s12 * (poly4b + s12 * poly5b)));
      sm = A * (poly1b * source_lat + sm);

      tn2 = tx * tx;
      cee = Epps2 * c1 * c1;
      al2 = al * al;
      poly1 = 1.0 - tn2 + cee;
      poly2 = 5.0 + tn2 * (tn2 - 18.0) + cee * (14.0 - tn2 * 58.0);

      double x = CScale * rn * al
          * (1.0
          + al2
          * (.166666666666667 * poly1
          + .00833333333333333 * al2 * poly2));
      x += 5.0E5;

      poly1 = 5.0 - tn2 + cee * (cee * 4.0 + 9.0);
      poly2 = 61.0 + tn2 * (tn2 - 58.0) + cee * (270.0 - tn2 * 330.0);
      double y = CScale
          * (sm + rn * tx * al2
          * (0.5
          + al2
          * (.0416666666666667 * poly1
          + .00138888888888888 * al2 * poly2)));

      if (source_lat < 0.0) {
        y += 1.0E7;
      }

      to[0][i] = (float) (x * .001);
      to[1][i] = (float) (y * .001);
    }

    return to;
  }

}


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

package ucar.nc2;

import ucar.nc2.util.EscapeStrings;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Range;
import ucar.ma2.Section;

import java.util.List;

/**
 * Parsed Section Expression, allows nesting for subsetting of Structure member variables.
 *
 * @author caron
 * @since May 8, 2008
 * @see SectionSpecification
 */
public class ParsedSectionSpec {
  private static final boolean debugSelector = false;

  /**
   * Parse a section specification String. These have the form:
   *

   *  section specification := selector | selector '.' selector
   *  selector := varName ['(' dims ')']
   *  varName := ESCAPED_STRING
   *

   *   dims := dim | dim, dims
   *   dim := ':' | slice | start ':' end | start ':' end ':' stride
   *   slice := INTEGER
   *   start := INTEGER
   *   stride := INTEGER
   *   end := INTEGER
   *   ESCAPED_STRING : must escape characters = ".("
   *

   *

   * Nonterminals are in lower case, terminals are in upper case, literals are in single quotes.
   * Optional components are enclosed between square braces '[' and ']'.
   *
   * @param ncfile          look for variable in here
   * @param variableSection the string to parse, eg "record(12).wind(1:20,:,3)"
   * @return return ParsedSectionSpec, aprsed representation of the variableSection String
   * @throws IllegalArgumentException       when token is misformed, or variable name doesnt exist in ncfile
   * @throws ucar.ma2.InvalidRangeException if section does not match variable shape
   * @see SectionSpecification
   */
  public static ParsedSectionSpec parseVariableSection(NetcdfFile ncfile, String variableSection) throws InvalidRangeException {
    List<String> tokes = EscapeStrings.tokenizeEscapedName(variableSection);
    if (tokes.size() == 0)
      throw new IllegalArgumentException("empty sectionSpec = " + variableSection);

    String selector = tokes.get(0);
    ParsedSectionSpec outerV = parseVariableSelector(ncfile, selector);

    // parse each selector, find the inner variable
    ParsedSectionSpec current = outerV;
    for (int i=1; i<tokes.size(); i++) {
      selector = tokes.get(i);
      current.child = parseVariableSelector( current.v,  selector);
      current = current.child;
    }

    return outerV;
  }

  // selector := varFullNameEsc(indexSelect) or memberNameEsc(indexSelect)
  // parse variable name and index selector out of the selector String. variable name must be escaped
  private static ParsedSectionSpec parseVariableSelector(Object parent, String selector) throws InvalidRangeException {
    String varNameEsc, indexSelect = null;

    int pos1 = EscapeStrings.indexOf(selector, '(');
    if (pos1 < 0) { // no index
      varNameEsc = selector;
    } else {
      varNameEsc = selector.substring(0, pos1);
      int pos2 = selector.indexOf(')', pos1+1);
      indexSelect = selector.substring(pos1, pos2);
    }
    if (debugSelector)
      System.out.println(" parseVariableSection <" + selector + "> = <" + varNameEsc + ">, <" + indexSelect + ">");

    Variable v = null;
    if (parent instanceof NetcdfFile) { // then varNameEsc = varFullNameEsc (i.e. includes groups)
      NetcdfFile ncfile = (NetcdfFile) parent;
      v = ncfile.findVariable(varNameEsc);

    } else if (parent instanceof Structure) { // then varNameEsc = memberNameEsc (i.e. includes groups)
      Structure s = (Structure) parent;
      v = s.findVariable( NetcdfFile.makeNameUnescaped(varNameEsc) ); // s.findVariable wants unescaped version
    }
    if (v == null)
      throw new IllegalArgumentException(" cant find variable: " + varNameEsc + " in selector=" + selector);

    if (v.getDataType() == DataType.SEQUENCE)
      indexSelect = null; // ignore whatever was sent

    // get the selected Ranges, or all, and add to the list
    Section section;
    if (indexSelect != null) {
      section = new Section(indexSelect);
      section = Section.fill(section, v.getShape()); // Check section has no nulls, set from shape array.
    } else {
      section = v.getShapeAsSection(); // all
    }

    return new ParsedSectionSpec(v, section);
  }

  /**
   * Make section specification String from a range list for a Variable.
   * @param v for this Variable.
   * @param ranges list of Range. Must includes all parent structures. The list be null, meaning use all.
   *   Individual ranges may be null, meaning all for that dimension.
   * @return section specification String.
   * @throws InvalidRangeException is specified section doesnt match variable shape
   */
  public static String makeSectionSpecString(Variable v, List<Range> ranges) throws InvalidRangeException {
    StringBuilder sb = new StringBuilder();
    makeSpec(sb, v, ranges);
    return sb.toString();
  }

  private static List<Range> makeSpec(StringBuilder sb, Variable v, List<Range> orgRanges) throws InvalidRangeException {
    if (v.isMemberOfStructure()) {
      orgRanges = makeSpec(sb, v.getParentStructure(), orgRanges);
      sb.append('.');
    }

    List<Range> ranges = (orgRanges == null) ? v.getRanges() : orgRanges;

    sb.append(v.isMemberOfStructure() ? NetcdfFile.makeValidSectionSpecName(v.getShortName())
                                      : NetcdfFile.makeFullNameSectionSpec(v));

    if (!v.isVariableLength() && !v.isScalar()) { // sequences cant be sectioned
      sb.append('(');
      for (int count=0; count<v.getRank(); count++) {
        Range r = ranges.get(count);
        if (r == null)
          r = new Range( v.getDimension(count).getLength());
        if (count>0) sb.append(", ");
        sb.append(r);
      }
      sb.append(')');
    }

    return (orgRanges == null) ? null : ranges.subList(v.getRank(), ranges.size());
  }

  ///////////////////////////////////////////////////////////////////////////
  // Modify to allow setting after creation
  public Variable v = null;        // the variable
  public Section section = null;   // section for this variable, filled in from variable if needed
  public ParsedSectionSpec child = null; // if not null, v is a Structure, and this is one of its members

  public ParsedSectionSpec(Variable v, Section section)
  {
    this.v = v;
    this.section = section;
  }

    /**
     * Public simple constructor
     */
    public ParsedSectionSpec()
    {
	this.v=null;
	this.section=null;
	this.child=null;
    }

    @Override
    public String toString() {
      return "ParsedSectionSpec{" +
            "v=" + v.getFullName() +
            ", section=" + section +
            ", child=" + child +
            '}';
  }
}


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

import ucar.nc2.dataset.CoordinateAxis2D;
import ucar.ma2.ArrayDouble;
import ucar.ma2.MAMath;

/**
 * fork ucar.nc2.dt.grid.GridCoordinate2D for adaption of GridCoverage
 *
 * 2D Coordinate System has lat(x,y) and lon(x,y).
 * This class implements finding the index (i,j) from (lat, lon) coord.
 * This is for "one-off" computation, not a systematic lookup table for all points in a pixel array.
 * Hueristic search of the 2D space for the cell that contains the point.
 *
 * @author caron
 * @since Jul 10, 2009
 */
public class GeoGridCoordinate2D {
  static private boolean debug = false;
  static private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GeoGridCoordinate2D.class);

  private final CoordinateAxis2D latCoord, lonCoord;
  private final int nrows, ncols;
  private ArrayDouble.D2 latEdge, lonEdge;
  private MAMath.MinMax latMinMax, lonMinMax;

  GeoGridCoordinate2D(CoordinateAxis2D latCoord, CoordinateAxis2D lonCoord) {
    this.latCoord = latCoord;
    this.lonCoord = lonCoord;

    assert latCoord.getRank() == 2;
    assert lonCoord.getRank() == 2;
    int[] shape = latCoord.getShape();

    nrows = shape[0];
    ncols = shape[1];
  }

  private void findBounds() {
    if (lonMinMax != null) return;

    lonEdge = lonCoord.getEdges();
    latEdge = latCoord.getEdges();

    // assume missing values have been converted to NaNs
    latMinMax = MAMath.getMinMax(latEdge);
    lonMinMax = MAMath.getMinMax(lonEdge);

    if (debug)
      System.out.printf("Bounds (%d %d): lat= (%f,%f) lon = (%f,%f) %n", nrows, ncols, latMinMax.min, latMinMax.max, lonMinMax.min, lonMinMax.max);
  }

  // brute force
  public boolean findCoordElementForce(double wantLat, double wantLon, int[] rectIndex) {
    findBounds();
    if (wantLat < latMinMax.min) return false;
    if (wantLat > latMinMax.max) return false;
    if (wantLon < lonMinMax.min) return false;
    if (wantLon > lonMinMax.max) return false;

    boolean saveDebug = debug;
    debug = false;
    for (int row=0; row<nrows; row++) {
      for (int col=0; col<ncols; col++) {

        rectIndex[0] = row;
        rectIndex[1] = col;

        if (contains(wantLat, wantLon, rectIndex)) {
          debug = saveDebug;
          return true;
        }
      }
    }
    //debug = saveDebug;
    return false;
  }

  public boolean findCoordElement(double wantLat, double wantLon, int[] rectIndex) {
    return findCoordElementNoForce(wantLat, wantLon,rectIndex);
  }

  /**
   * Find the best index for the given lat,lon point.
   * @param wantLat   lat of point
   * @param wantLon   lon of point
   * @param rectIndex return (row,col) index, or best guess here. may not be null
   *
   * @return false if not in the grid.
   */
  public boolean findCoordElementNoForce(double wantLat, double wantLon, int[] rectIndex) {
    findBounds();
    if (wantLat < latMinMax.min) return false;
    if (wantLat > latMinMax.max) return false;
    if (wantLon < lonMinMax.min) return false;
    if (wantLon > lonMinMax.max) return false;

    double gradientLat = (latMinMax.max - latMinMax.min) / nrows;
    double gradientLon = (lonMinMax.max - lonMinMax.min) / ncols;

    double diffLat = wantLat - latMinMax.min;
    double diffLon = wantLon - lonMinMax.min;

    // initial guess
    rectIndex[0] = (int) Math.round(diffLat / gradientLat);  // row
    rectIndex[1] =(int) Math.round(diffLon / gradientLon);  // col

    int count = 0;
    while (true) {
      count++;
      if (debug) System.out.printf("%nIteration %d %n", count);
      if (contains(wantLat, wantLon, rectIndex))
        return true;

      if (!jump2(wantLat, wantLon, rectIndex)) return false;

      // bouncing around
      if (count > 10) {
        // last ditch attempt
        return incr(wantLat, wantLon, rectIndex);
        //if (!ok)
        //  log.error("findCoordElement didnt converge lat,lon = "+wantLat+" "+ wantLon);
        //return ok;
      }
    }
  }

  /**
   * Is the point (lat,lon) contained in the (row, col) rectangle ?
   *
   * @param wantLat   lat of point
   * @param wantLon   lon of point
   * @param rectIndex rectangle row, col, will be clipped to [0, nrows), [0, ncols)
   * @return true if contained
   */
  private boolean containsOld(double wantLat, double wantLon, int[] rectIndex) {
    rectIndex[0] = Math.max( Math.min(rectIndex[0], nrows-1), 0);
    rectIndex[1] = Math.max( Math.min(rectIndex[1], ncols-1), 0);

    int row = rectIndex[0];
    int col = rectIndex[1];

    if (debug) System.out.printf(" (%d,%d) contains (%f,%f) in (lat=%f %f) (lon=%f %f) ?%n",
            rectIndex[0], rectIndex[1], wantLat, wantLon,
            latEdge.get(row, col), latEdge.get(row + 1, col), lonEdge.get(row, col), lonEdge.get(row, col + 1));

    if (wantLat < latEdge.get(row, col)) return false;
    if (wantLat > latEdge.get(row + 1, col)) return false;
    if (wantLon < lonEdge.get(row, col)) return false;
    if (wantLon > lonEdge.get(row, col + 1)) return false;
    return true;
  }

  /**
   * Is the point (lat,lon) contained in the (row, col) rectangle ?
   *
   * @param wantLat   lat of point
   * @param wantLon   lon of point
   * @param rectIndex rectangle row, col, will be clipped to [0, nrows), [0, ncols)
   * @return true if contained
   */

/*
  http://mathforum.org/library/drmath/view/54386.html

      Given any three points on the plane (x0,y0), (x1,y1), and
  (x2,y2), the area of the triangle determined by them is
  given by the following formula:

        1 | x0 y0 1 |
    A = - | x1 y1 1 |,
        2 | x2 y2 1 |

  where the vertical bars represent the determinant.
  the value of the expression above is:

       (.5)(x1*y2 - y1*x2 -x0*y2 + y0*x2 + x0*y1 - y0*x1)

  The amazing thing is that A is positive if the three points are
  taken in a counter-clockwise orientation, and negative otherwise.

  To be inside a rectangle (or any convex body), as you trace
  around in a clockwise direction from p1 to p2 to p3 to p4 and
  back to p1, the "areas" of triangles p1 p2 p, p2 p3 p, p3 p4 p,
  and p4 p1 p must all be positive.  If you don't know the
  orientation of your rectangle, then they must either all be
  positive or all be negative.

  this method works for arbitrary convex regions oo the plane.
*/
  private boolean contains(double wantLat, double wantLon, int[] rectIndex) {
    rectIndex[0] = Math.max( Math.min(rectIndex[0], nrows-1), 0);
    rectIndex[1] = Math.max( Math.min(rectIndex[1], ncols-1), 0);

    int row = rectIndex[0];
    int col = rectIndex[1];

    double x1 = lonEdge.get(row, col);
    double y1 = latEdge.get(row, col);

    double x2 = lonEdge.get(row, col+1);
    double y2 = latEdge.get(row, col+1);

    double x3 = lonEdge.get(row+1, col+1);
    double y3 = latEdge.get(row + 1, col+1);

    double x4 = lonEdge.get(row + 1, col);
    double y4 = latEdge.get(row+1, col);

    // must all have same determinate sign
    boolean sign = detIsPositive(x1, y1, x2, y2, wantLon, wantLat);
    if (sign != detIsPositive(x2, y2, x3, y3, wantLon, wantLat)) return false;
    if (sign != detIsPositive(x3, y3, x4, y4, wantLon, wantLat)) return false;
    if (sign != detIsPositive(x4, y4, x1, y1, wantLon, wantLat)) return false;

    return true;
  }

  private boolean detIsPositive(double x0, double y0, double x1, double y1, double x2, double y2) {
    double det = (x1*y2 - y1*x2 -x0*y2 + y0*x2 + x0*y1 - y0*x1);
    if (det == 0)
      log.warn("determinate = 0 on lat/lon=" + latCoord.getFullName() + ", " + latCoord.getFullName()); // LOOK needed?
    return det > 0;
  }

  private boolean jumpOld(double wantLat, double wantLon, int[] rectIndex) {
    int row = Math.max( Math.min(rectIndex[0], nrows-1), 0);
    int col = Math.max(Math.min(rectIndex[1], ncols - 1), 0);
    double gradientLat = latEdge.get(row + 1, col) - latEdge.get(row, col);
    double gradientLon = lonEdge.get(row, col + 1) - lonEdge.get(row, col);

    double diffLat = wantLat - latEdge.get(row, col);
    double diffLon = wantLon - lonEdge.get(row, col);

    int drow = (int) Math.round(diffLat / gradientLat);
    int dcol = (int) Math.round(diffLon / gradientLon);

    if (debug) System.out.printf("   jump from %d %d (grad=%f %f) (diff=%f %f) (delta=%d %d)",
            row, col, gradientLat, gradientLon,
            diffLat, diffLon, drow, dcol);

    if ((drow == 0) && (dcol == 0)) {
      if (debug) System.out.printf("%n   incr:");
      return incr(wantLat, wantLon, rectIndex);
    } else {
      rectIndex[0] = Math.max( Math.min(row + drow, nrows-1), 0);
      rectIndex[1] = Math.max( Math.min(col + dcol, ncols-1), 0);
      if (debug) System.out.printf(" to (%d %d)%n", rectIndex[0], rectIndex[1]);
      if ((row == rectIndex[0]) && (col == rectIndex[1])) return false; // nothing has changed
    }

    return true;
  }

  /**
   * jump to a new row,col
   * @param wantLat want this lat
   * @param wantLon want this lon
   * @param rectIndex starting row, col and replaced by new guess
   * @return true if new guess, false if failure
   */
  /*
    choose x, y such that (matrix multiply) :

    (wantx) = (fxx fxy)  (x)
    (wanty)   (fyx fyy)  (y)

     where fxx = d(fx)/dx  ~= delta lon in lon direction
     where fxy = d(fx)/dy  ~= delta lon in lat direction
     where fyx = d(fy)/dx  ~= delta lat in lon direction
     where fyy = d(fy)/dy  ~= delta lat in lat direction

    2 linear equations in 2 unknowns, solve in usual way
   */
  private boolean jump2(double wantLat, double wantLon, int[] rectIndex) {
    int row = Math.max( Math.min(rectIndex[0], nrows-1), 0);
    int col = Math.max(Math.min(rectIndex[1], ncols - 1), 0);
    double lat = latEdge.get(row, col);
    double lon =  lonEdge.get(row, col);
    double diffLat = wantLat - lat;
    double diffLon = wantLon - lon;

    double dlatdy = latEdge.get(row + 1, col) - lat;
    double dlatdx = latEdge.get(row, col+1) - lat;
    double dlondx = lonEdge.get(row, col + 1) - lon;
    double dlondy = lonEdge.get(row + 1, col) - lon;

    // solve for dlon

    double dx =  (diffLon - dlondy * diffLat / dlatdy) / (dlondx - dlatdx * dlondy / dlatdy);
    // double dy =  (diffLat - dlatdx * diffLon / dlondx) / (dlatdy - dlatdx * dlondy / dlondx);
    double dy =  (diffLat - dlatdx * dx) / dlatdy;

    if (debug) System.out.printf("   jump from %d %d (dlondx=%f dlondy=%f dlatdx=%f dlatdy=%f) (diffLat,Lon=%f %f) (deltalat,Lon=%f %f)",
            row, col, dlondx, dlondy, dlatdx, dlatdy,
            diffLat, diffLon, dy, dx);

    int drow = (int) Math.round(dy);
    int dcol = (int) Math.round(dx);

    if ((drow == 0) && (dcol == 0)) {
      if (debug) System.out.printf("%n   incr:");
      return incr(wantLat, wantLon, rectIndex);
    } else {
      rectIndex[0] = Math.max( Math.min(row + drow, nrows-1), 0);
      rectIndex[1] = Math.max( Math.min(col + dcol, ncols-1), 0);
      if (debug) System.out.printf(" to (%d %d)%n", rectIndex[0], rectIndex[1]);
      if ((row == rectIndex[0]) && (col == rectIndex[1])) return false; // nothing has changed
    }

    return true;
  }

  private boolean incr(double wantLat, double wantLon, int[] rectIndex) {
    int row = rectIndex[0];
    int col = rectIndex[1];
    double diffLat = wantLat - latEdge.get(row, col);
    double diffLon = wantLon - lonEdge.get(row, col);

    if (Math.abs(diffLat) > Math.abs(diffLon)) { // try lat first
      rectIndex[0] = row + ((diffLat > 0) ? 1 : -1);
      if (contains(wantLat, wantLon,  rectIndex)) return true;
      rectIndex[1] = col + ((diffLon > 0) ? 1 : -1);
      if (contains(wantLat, wantLon,  rectIndex)) return true;
    } else {
      rectIndex[1] = col + ((diffLon > 0) ? 1 : -1);
      if (contains(wantLat, wantLon,  rectIndex)) return true;
      rectIndex[0] = row + ((diffLat > 0) ? 1 : -1);
      if (contains(wantLat, wantLon,  rectIndex)) return true;
    }

    // back to original, do box search
    rectIndex[0] = row;
    rectIndex[1] = col;
    return box9(wantLat, wantLon, rectIndex);
  }

  // we think its got to be in one of the 9 boxes around rectIndex
  private boolean box9(double wantLat, double wantLon, int[] rectIndex) {
    int row = rectIndex[0];
    int minrow = Math.max(row-1, 0);
    int maxrow = Math.min(row+1, nrows);

    int col = rectIndex[1];
    int mincol= Math.max(col-1, 0);
    int maxcol = Math.min(col+1, ncols);

    if (debug) System.out.printf("%n   box9:");
    for (int i=minrow; i<=maxrow; i++)
      for (int j=mincol; j<=maxcol; j++) {
        rectIndex[0] = i;
        rectIndex[1] = j;
        if (contains(wantLat, wantLon,  rectIndex)) return true;
      }

    return false;
  }

}


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

import ucar.nc2.AttributeContainer;
import ucar.nc2.Dimension;
import ucar.nc2.Variable;

import ucar.nc2.dataset.*;

import ucar.unidata.geoloc.vertical.HybridHeight;
import ucar.unidata.util.Parameter;

/**
 * Create a atmosphere_hybrid_height_coordinate Vertical Transform from
 * the information in the Coordinate Transform Variable.
 *
 * @author murray
 */
public class CFHybridHeight extends AbstractTransformBuilder implements VertTransformBuilderIF {

  /**
   * The name of the a term
   */
  private String a;

  /**
   * The name of the a term
   */
  private String b;

  /**
   * The name of the orog term
   */
  private String orog;

  /**
   * Get the standard name of this transform
   *
   * @return the name
   */
  public String getTransformName() {
    return VerticalCT.Type.HybridHeight.name();
  }

  /**
   * Get the type of the transform
   *
   * @return the type
   */
  public TransformType getTransformType() {
    return TransformType.Vertical;
  }

  /**
   * Make the CoordinateTransform from the dataset
   *
   * @param ds  the dataset
   * @param ctv the variable with the formula
   * @return the CoordinateTransform
   */
  public VerticalCT makeCoordinateTransform(NetcdfDataset ds, AttributeContainer ctv) {
    String formula_terms = getFormula(ctv);
    if (null == formula_terms)
      return null;

    // parse the formula string
    String[] values = parseFormula(formula_terms, "a b orog");
    if (values == null) return null;

    a = values[0];
    b = values[1];
    orog = values[2];

    VerticalCT rs = new VerticalCT("AtmHybridHeight_Transform_" + ctv.getName(), getTransformName(), VerticalCT.Type.HybridHeight, this);
    rs.addParameter(new Parameter("standard_name", getTransformName()));
    rs.addParameter(new Parameter("formula_terms", formula_terms));

    rs.addParameter(new Parameter("formula", "height(x,y,z) = a(z) + b(z)*orog(x,y)"));
    if (!addParameter(rs, HybridHeight.A, ds, a)) {
      return null;
    }
    if (!addParameter(rs, HybridHeight.B, ds, b)) {
      return null;
    }
    if (!addParameter(rs, HybridHeight.OROG, ds, orog)) {
      return null;
    }
    return rs;
  }

  /**
   * Get a String representation of this object
   *
   * @return a String representation of this object
   */
  public String toString() {
    return "HybridHeight:" + "orog:" + orog + " a:" + a + " b:" + b;
  }

  /**
   * Make the vertical transform transform
   *
   * @param ds      the dataset
   * @param timeDim the time dimention
   * @param vCT     the vertical coordinate transform
   * @return the VerticalTransform
   */
  public ucar.unidata.geoloc.vertical.VerticalTransform makeMathTransform(NetcdfDataset ds, Dimension timeDim, VerticalCT vCT) {
    return new HybridHeight(ds, timeDim, vCT.getParameters());
  }
}