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

import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;

/**
 * Stereographic projection, spherical earth.
 * Projection plane is a plane tangent to the earth at latt, lont.
 * see John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 153
 *
 * @author John Caron
 */

public class Stereographic extends ProjectionImpl {

  /**
   * Construct a Stereographic Projection using latitude of true scale and calculating scale factor.
   * <p> Since the scale factor at lat = k = 2*k0/(1+sin(lat))  [Snyder,Working Manual p157]
   * then to make scale = 1 at lat, set k0 = (1+sin(lat))/2
   *
   * @param latt    tangent point of projection, also origin of projection coord system
   * @param lont    tangent point of projection, also origin of projection coord system
   * @param latTrue latitude of true scale in degrees north; latitude where scale factor = 1.0
   * @return Stereographic projection
   */
  static public Stereographic factory(double latt, double lont, double latTrue) {
    double scale = (1.0 + Math.sin(Math.toRadians(latTrue))) / 2.0;
    return new Stereographic(latt, lont, scale);
  }

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

  private double falseEasting, falseNorthing;
  private double scale, earthRadius;
  private double latt, lont;
  private double sinlatt, coslatt, latts;
  private boolean isNorth = false;
  private boolean isPolar = false;

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new Stereographic(getTangentLat(), getTangentLon(), getScale(), getFalseEasting(), getFalseNorthing(), getEarthRadius());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters = North Polar
   */
  public Stereographic() {
    this(90.0, -105.0, 1.0);
  }

  /**
   * Construct a Stereographic Projection.
   *
   * @param latt  tangent point of projection, also origin of projection coord system
   * @param lont  tangent point of projection, also origin of projection coord system
   * @param scale scale factor at tangent point, "normally 1.0 but may be reduced"
   */
  public Stereographic(double latt, double lont, double scale) {
    this(latt, lont, scale, 0, 0, EARTH_RADIUS);
  }

  /**
   * Construct a Stereographic Projection.
   *
   * @param latt  tangent point of projection, also origin of projection coord system
   * @param lont  tangent point of projection, also origin of projection coord system
   * @param scale scale factor at tangent point, "normally 1.0 but may be reduced"
   */
  public Stereographic(double latt, double lont, double scale, double false_easting, double false_northing) {
    this(latt, lont, scale, false_easting, false_northing, EARTH_RADIUS);
  }

  /**
   * Construct a Stereographic Projection.
   *
   * @param latt           tangent point of projection, also origin of projection coord system
   * @param lont           tangent point of projection, also origin of projection coord system
   * @param scale          scale factor at tangent point, "normally 1.0 but may be reduced"
   * @param false_easting  false easting in units of x coords
   * @param false_northing false northing in units of y coords
   * @param radius         earth radius in km
   */
  public Stereographic(double latt, double lont, double scale, double false_easting, double false_northing, double radius) {
    super("Stereographic", false);

    this.latt = Math.toRadians(latt);
    this.lont = Math.toRadians(lont);
    this.earthRadius = radius;
    this.scale = scale * earthRadius;
    this.falseEasting = false_easting;
    this.falseNorthing = false_northing;
    precalculate();

    addParameter(CF.GRID_MAPPING_NAME, CF.STEREOGRAPHIC);
    addParameter(CF.LONGITUDE_OF_PROJECTION_ORIGIN, lont);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, latt);
    addParameter(CF.SCALE_FACTOR_AT_PROJECTION_ORIGIN, scale);
    addParameter(CF.EARTH_RADIUS, earthRadius * 1000);

    if ((false_easting != 0.0) || (false_northing != 0.0)) {
      addParameter(CF.FALSE_EASTING, false_easting);
      addParameter(CF.FALSE_NORTHING, false_northing);
      addParameter(CDM.UNITS, "km");
    }
  }

  /**
   * Construct a polar Stereographic Projection, from the "natural origin" and the tangent point,
   * calculating the scale factor.
   *
   * @param lat_ts_deg Latitude at natural origin (degrees_north)
   * @param latt_deg   tangent point of projection (degrees_north)
   * @param lont_deg   tangent point of projection, also origin of projection coord system  ((degrees_east)
   * @param north      true if north pole, false is south pole
   */
  public Stereographic(double lat_ts_deg, double latt_deg, double lont_deg, boolean north) {
    super("PolarStereographic", false);

    this.latts = Math.toRadians(lat_ts_deg);
    this.latt = Math.toRadians(latt_deg);
    this.lont = Math.toRadians(lont_deg);
    this.isPolar = true;
    this.isNorth = north;
    this.earthRadius = EARTH_RADIUS;
    this.falseEasting = 0;
    this.falseNorthing = 0;

    precalculate();

    double scaleFactor = (lat_ts_deg == 90 || lat_ts_deg == -90) ? 1.0 : getScaleFactor(latts, north);
    this.scale = scaleFactor * earthRadius;

    addParameter(CF.GRID_MAPPING_NAME, "polar_stereographic");
    addParameter("longitude_of_projection_origin", lont_deg);
    addParameter("latitude_of_projection_origin", latt_deg);
    addParameter("scale_factor_at_projection_origin", scaleFactor);
  }

  /**
   * Calculate polar stereographic scale factor based on the natural latitude and
   * longitude of the original
   * Ref: OGP Surveying and Positioning Guidance Note number 7, part 2 April 2009
   * http://www.epsg.org
   * added by Qun He <qunhe@unc.edu>
   *
   * @param lat_ts Latitude at natural origin
   * @param north  Is it north polar?
   * @return scale factor
   */
  private double getScaleFactor(double lat_ts, boolean north) {
    double e = 0.081819191;
    double tf = 1.0, mf = 1.0, k0 = 1.0;
    double root = (1 + e * Math.sin(lat_ts)) / (1 - e * Math.sin(lat_ts));
    double power = e / 2;

    if (north)
      tf = Math.tan(Math.PI / 4 - lat_ts / 2) * (Math.pow(root, power));
    else
      tf = Math.tan(Math.PI / 4 + lat_ts / 2) / (Math.pow(root, power));

    mf = Math.cos(lat_ts) / Math.sqrt(1 - e * e * Math.pow(Math.sin(lat_ts), 2));
    k0 = mf * Math.sqrt(Math.pow(1 + e, 1 + e) * Math.pow(1 - e, 1 - e)) / (2 * tf);

    return Double.isNaN(k0) ? 1.0 : k0;
  }

  /**
   * precalculate some stuff
   */
  private void precalculate() {
    sinlatt = Math.sin(latt);
    coslatt = Math.cos(latt);
  }

  // bean properties

  /**
   * Get the scale
   *
   * @return the scale
   */
  public double getScale() {
    return scale / earthRadius;
  }

  /**
   * Get the latitude at natural origin
   *
   * @return latitude at natural origin
   */
  public double getNaturalOriginLat() {
    return Math.toDegrees(latts);
  }

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

  /**
   * Get the tangent latitude in degrees
   *
   * @return the origin latitude.
   */
  public double getTangentLat() {
    return Math.toDegrees(latt);
  }

  public double getEarthRadius() {
    return earthRadius;
  }

  public boolean isNorth() {
    return isNorth;
  }

  public boolean isPolar() {
    return isPolar;
  }

   //////////////////////////////////////////////
  // setters for IDV serialization - do not use except for object creating

  /**
   * @deprecated
   */
  public void setScale(double scale) {
    this.scale = earthRadius * scale;
  }

  /**
   * @deprecated
   */
  public void setTangentLat(double latt) {
    this.latt =  Math.toRadians(latt);
    precalculate();
  }

  /**
   * @deprecated
   */
  public void setTangentLon(double lont) {
    this.lont = Math.toRadians(lont);
    precalculate();
  }

  // cruft from IDV bundle

  /**
   * @deprecated
   */
  public void setCentralMeridian(double lont) {
    setTangentLon(lont);
  }

  /**
   * 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;
  }

  /**
   * 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 parameters as a String
   *
   * @return the parameters as a String
   */
  public String paramsToString() {
    return toString();
  }

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

  @Override
  public String toString() {
    return "Stereographic{" +
            "falseEasting=" + falseEasting +
            ", falseNorthing=" + falseNorthing +
            ", scale=" + scale +
            ", earthRadius=" + earthRadius +
            ", latt=" + latt +
            ", lont=" + lont +
            '}';
  }

  /**
   * 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 boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    return false;  // ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2);
  }

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

    Stereographic that = (Stereographic) o;

    if (Double.compare(that.earthRadius, earthRadius) != 0) return false;
    if (Double.compare(that.falseEasting, falseEasting) != 0) return false;
    if (Double.compare(that.falseNorthing, falseNorthing) != 0) return false;
    if (Double.compare(that.latt, latt) != 0) return false;
    if (Double.compare(that.lont, lont) != 0) return false;
    if (Double.compare(that.scale, scale) != 0) return false;
    if ((defaultMapArea == null) != (that.defaultMapArea == null)) return false; // common case is that these are null
    if (defaultMapArea != null && !that.defaultMapArea.equals(defaultMapArea)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = falseEasting != +0.0d ? Double.doubleToLongBits(falseEasting) : 0L;
    result = (int) (temp ^ (temp >>> 32));
    temp = falseNorthing != +0.0d ? Double.doubleToLongBits(falseNorthing) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = scale != +0.0d ? Double.doubleToLongBits(scale) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = earthRadius != +0.0d ? Double.doubleToLongBits(earthRadius) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = latt != +0.0d ? Double.doubleToLongBits(latt) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = lont != +0.0d ? Double.doubleToLongBits(lont) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

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

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

  /*MACROBODY
  projToLatLon {double phi, lam;} {
    double rho = Math.sqrt( fromX*fromX + fromY*fromY);
    double c = 2.0 * Math.atan2( rho, 2.0*scale);
    double sinc = Math.sin(c);
    double cosc = Math.cos(c);

    if (Math.abs(rho) < TOLERANCE)
        phi = latt;
    else
        phi = Math.asin( cosc * sinlatt + fromY * sinc * coslatt / rho);

    toLat = Math.toDegrees(phi);

    if ((Math.abs(fromX) < TOLERANCE) && (Math.abs(fromY) < TOLERANCE))
        lam = lont;
    else if (Math.abs(coslatt) < TOLERANCE)
        lam = lont + Math.atan2( fromX, ((latt > 0) ? -fromY : fromY) );
    else
        lam = lont + Math.atan2( fromX*sinc, rho*coslatt*cosc - fromY*sinc*sinlatt);

    toLon = Math.toDegrees(lam);
  }

  latLonToProj {} {
    double lat = Math.toRadians (fromLat);
    double lon = Math.toRadians(fromLon);
    // keep away from the singular point
    if ((Math.abs(lat + latt) <= TOLERANCE)) {
        lat = -latt * (1.0 - TOLERANCE);
    }

    double sdlon = Math.sin(lon - lont);
    double cdlon = Math.cos(lon - lont);
    double sinlat = Math.sin(lat);
    double coslat = Math.cos(lat);

    double k = 2.0 * scale / (1.0 + sinlatt * sinlat + coslatt * coslat * cdlon);
    toX =  k * coslat * sdlon;
    toY =  k * ( coslatt * sinlat - sinlatt * coslat * cdlon);
}
  MACROBODY*/

  /*BEGINGENERATED*/

  /*
  Note this section has been generated using the convert.tcl script.
  This script, run as:
  tcl convert.tcl Stereographic.java
  takes the actual projection conversion code defined in the MACROBODY
  section above and generates the following 6 methods
  */

  /**
   * 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
   */
  public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();

    double lat = Math.toRadians(fromLat);
    double lon = Math.toRadians(fromLon);
    // keep away from the singular point
    if ((Math.abs(lat + latt) <= TOLERANCE)) {
      lat = -latt * (1.0 - TOLERANCE);
    }

    double sdlon = Math.sin(lon - lont);
    double cdlon = Math.cos(lon - lont);
    double sinlat = Math.sin(lat);
    double coslat = Math.cos(lat);

    double k = 2.0 * scale
            / (1.0 + sinlatt * sinlat + coslatt * coslat * cdlon);
    toX = k * coslat * sdlon;
    toY = k * (coslatt * sinlat - sinlatt * coslat * cdlon);

    result.setLocation(toX + falseEasting, toY + 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
   */
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double toLat, toLon;
    double fromX = world.getX() - falseEasting;
    double fromY = world.getY() - falseNorthing;
    double phi, lam;

    double rho = Math.sqrt(fromX * fromX + fromY * fromY);
    double c = 2.0 * Math.atan2(rho, 2.0 * scale);
    double sinc = Math.sin(c);
    double cosc = Math.cos(c);

    if (Math.abs(rho) < TOLERANCE) {
      phi = latt;
    } else {
      phi = Math.asin(cosc * sinlatt + fromY * sinc * coslatt / rho);
    }

    toLat = Math.toDegrees(phi);

    if ((Math.abs(fromX) < TOLERANCE) && (Math.abs(fromY) < TOLERANCE)) {
      lam = lont;
    } else if (Math.abs(coslatt) < TOLERANCE) {
      lam = lont + Math.atan2(fromX, ((latt > 0)
              ? -fromY
              : fromY));
    } else {
      lam = lont + Math.atan2(fromX * sinc, rho * coslatt * cosc - fromY * sinc * sinlatt);
    }

    toLon = Math.toDegrees(lam);

    result.setLatitude(toLat);
    result.setLongitude(toLon);
    return result;
  }

  /**
   * 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) {
    int cnt = from[0].length;
    float[] fromLatA = from[latIndex];
    float[] fromLonA = from[lonIndex];
    float[] resultXA = to[INDEX_X];
    float[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      double lat = Math.toRadians(fromLat);
      double lon = Math.toRadians(fromLon);
      // keep away from the singular point
      if ((Math.abs(lat + latt) <= TOLERANCE)) {
        lat = -latt * (1.0 - TOLERANCE);
      }

      double sdlon = Math.sin(lon - lont);
      double cdlon = Math.cos(lon - lont);
      double sinlat = Math.sin(lat);
      double coslat = Math.cos(lat);

      double k = 2.0 * scale
              / (1.0 + sinlatt * sinlat + coslatt * coslat * cdlon);
      toX = k * coslat * sdlon;
      toY = k * (coslatt * sinlat - sinlatt * coslat * cdlon);

      resultXA[i] = (float) (toX + falseEasting);
      resultYA[i] = (float) (toY + falseNorthing);
    }
    return to;
  }

  /**
   * 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) {
    int cnt = from[0].length;
    float[] fromXA = from[INDEX_X];
    float[] fromYA = from[INDEX_Y];
    float[] toLatA = to[INDEX_LAT];
    float[] toLonA = to[INDEX_LON];
    double phi, lam;
    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;

      double rho = Math.sqrt(fromX * fromX + fromY * fromY);
      double c = 2.0 * Math.atan2(rho, 2.0 * scale);
      double sinc = Math.sin(c);
      double cosc = Math.cos(c);

      if (Math.abs(rho) < TOLERANCE) {
        phi = latt;
      } else {
        phi = Math.asin(cosc * sinlatt
                + fromY * sinc * coslatt / rho);
      }

      toLat = Math.toDegrees(phi);

      if ((Math.abs(fromX) < TOLERANCE)
              && (Math.abs(fromY) < TOLERANCE)) {
        lam = lont;
      } else if (Math.abs(coslatt) < TOLERANCE) {
        lam = lont + Math.atan2(fromX, ((latt > 0)
                ? -fromY
                : fromY));
      } else {
        lam = lont
                + Math.atan2(fromX * sinc,
                rho * coslatt * cosc
                        - fromY * sinc * sinlatt);
      }

      toLon = Math.toDegrees(lam);

      toLatA[i] = (float) toLat;
      toLonA[i] = (float) toLon;
    }
    return to;
  }

  /**
   * 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) {
    int cnt = from[0].length;
    double[] fromLatA = from[latIndex];
    double[] fromLonA = from[lonIndex];
    double[] resultXA = to[INDEX_X];
    double[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      double lat = Math.toRadians(fromLat);
      double lon = Math.toRadians(fromLon);
      // keep away from the singular point
      if ((Math.abs(lat + latt) <= TOLERANCE)) {
        lat = -latt * (1.0 - TOLERANCE);
      }

      double sdlon = Math.sin(lon - lont);
      double cdlon = Math.cos(lon - lont);
      double sinlat = Math.sin(lat);
      double coslat = Math.cos(lat);

      double k = 2.0 * scale
              / (1.0 + sinlatt * sinlat + coslatt * coslat * cdlon);
      toX = k * coslat * sdlon;
      toY = k * (coslatt * sinlat - sinlatt * coslat * cdlon);

      resultXA[i] = toX + falseEasting;
      resultYA[i] = toY + falseNorthing;
    }
    return to;
  }

  /**
   * 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) {
    int cnt = from[0].length;
    double[] fromXA = from[INDEX_X];
    double[] fromYA = from[INDEX_Y];
    double[] toLatA = to[INDEX_LAT];
    double[] toLonA = to[INDEX_LON];
    double phi, lam;
    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;

      double rho = Math.sqrt(fromX * fromX + fromY * fromY);
      double c = 2.0 * Math.atan2(rho, 2.0 * scale);
      double sinc = Math.sin(c);
      double cosc = Math.cos(c);

      if (Math.abs(rho) < TOLERANCE) {
        phi = latt;
      } else {
        phi = Math.asin(cosc * sinlatt
                + fromY * sinc * coslatt / rho);
      }

      toLat = Math.toDegrees(phi);

      if ((Math.abs(fromX) < TOLERANCE)
              && (Math.abs(fromY) < TOLERANCE)) {
        lam = lont;
      } else if (Math.abs(coslatt) < TOLERANCE) {
        lam = lont + Math.atan2(fromX, ((latt > 0)
                ? -fromY
                : fromY));
      } else {
        lam = lont
                + Math.atan2(fromX * sinc,
                rho * coslatt * cosc
                        - fromY * sinc * sinlatt);
      }

      toLon = Math.toDegrees(lam);

      toLatA[i] = toLat;
      toLonA[i] = toLon;
    }
    return to;
  }

  /*ENDGENERATED*/

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
// $Id: DatasetSourceStructure.java 63 2006-07-12 21:50:51Z edavis $

package thredds.cataloggen.config;

/**
 * Type-safe enumeration of CatalogGen DatasetSource structures.
 *
 * @author Ethan Davis (from John Caron's thredds.catalog.ServiceType)
 * @version $Revision: 63 $
 */
public final class DatasetSourceStructure
{
  private static java.util.HashMap hash = new java.util.HashMap(20);

  public final static DatasetSourceStructure FLAT =
    new DatasetSourceStructure( "Flat");
  public final static DatasetSourceStructure DIRECTORY_TREE =
    new DatasetSourceStructure( "DirTree");

  private String structureName;
  private DatasetSourceStructure( String name)
  {
    this.structureName = name;
    hash.put( name, this);
  }

  /**
   * Find the DatasetSourceStructure that matches this name.
   * @param name
   * @return DatasetSourceStructure or null if no match.
   */
  public static DatasetSourceStructure getStructure( String name)
  {
    if ( name == null) return null;
    return (DatasetSourceStructure) hash.get( name);
  }

  /**
   * Return the string name.
   */
  public String toString()
  {
    return structureName;
  }

}


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

package thredds.server.dap4;

import dap4.core.data.DSPRegistry;
import dap4.core.util.DapContext;
import dap4.core.util.DapException;
import dap4.core.util.DapUtil;
import dap4.dap4lib.DapCodes;
import dap4.dap4lib.DapLog;
import dap4.servlet.DSPFactory;
import dap4.servlet.DapCache;
import dap4.servlet.DapController;
import dap4.servlet.DapRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import thredds.core.TdsRequestedDataset;
import ucar.nc2.NetcdfFile;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

@Controller
@RequestMapping("/dap4")
public class Dap4Controller extends DapController
{

    //////////////////////////////////////////////////
    // Constants

    static final boolean DEBUG = false;

    static final boolean PARSEDEBUG = false;

    // NetcdfDataset enhancement to use: need only coord systems
    //static Set<NetcdfDataset.Enhance> ENHANCEMENT = EnumSet.of(NetcdfDataset.Enhance.CoordSystems);

    //////////////////////////////////////////////////
    // Type Decls

    static class Dap4Factory extends DSPFactory
    {

        public Dap4Factory()
        {
            // For TDS, we only need to register one DSP type: ThreddsDSP.
            // This is because we will always serve only NetcdfFile objects.
            // See D4TSServlet for a multiple registration case.
            DapCache.dspregistry.register(ThreddsDSP.class, DSPRegistry.LAST);
        }

    }

    static {
        DapCache.setFactory(new Dap4Factory());
    }

    //////////////////////////////////////////////////
    // Spring Elements

    @Autowired
    private ServletContext servletContext;

    @RequestMapping("**")
    public void handleRequest(HttpServletRequest req, HttpServletResponse res)
            throws IOException
    {
        super.handleRequest(req, res);
    }

    //////////////////////////////////////////////////
    // Constructor(s)

    public Dap4Controller()
    {
        super();
    }

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

    @Override
    protected void
    doFavicon(String icopath, DapContext cxt)
            throws IOException
    {
        throw new UnsupportedOperationException("Favicon");
    }

    @Override
    protected void
    doCapabilities(DapRequest drq, DapContext cxt)
            throws IOException
    {
        addCommonHeaders(drq);
        OutputStream out = drq.getOutputStream();
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, DapUtil.UTF8));
        pw.println("Capabilities page not supported");
        pw.flush();
    }

    @Override
    public long
    getBinaryWriteLimit()
    {
        return DEFAULTBINARYWRITELIMIT;
    }

    @Override
    public String
    getServletID()
    {
        return "dap4";
    }

    // There is a problem Spring under intellij when using mocking.
    // See TestServlet for more info.  In any case, if autowiring does
    // not work, then TdsRequestedDataset.getLocationFromRequestPath
    // will fail because it internal DatasetManager value will be null.
    // Autowiring would have set it to non-null. So, check to see if
    // the autowiring worked and if so use
    // TdsRequestedDataset.getLocationFromRequestPath.
    // Otherwise, compute the proper path from the drq.getResourceRoot.
    // This is completely a hack until such time as we can get things
    // to work under Intellij.
    @Override
    public String
    getResourcePath(DapRequest drq, String location)
            throws DapException
    {
        String realpath;
        if(TdsRequestedDataset.getDatasetManager() != null) {
            realpath = TdsRequestedDataset.getLocationFromRequestPath(location);
        } else {
            assert TdsRequestedDataset.getDatasetManager() == null;
            String prefix = drq.getResourceRoot();
            assert (prefix != null);
            realpath = DapUtil.canonjoin(prefix, location);
        }

        if (!TESTING) {
            if (!TdsRequestedDataset.resourceControlOk(drq.getRequest(), drq.getResponse(), realpath))
                throw new DapException("Not authorized: " + location)
                        .setCode(DapCodes.SC_FORBIDDEN);
        }
        File f = new File(realpath);
        if (!f.exists() || !f.canRead())
            throw new DapException("Not found: " + location)
                    .setCode(DapCodes.SC_NOT_FOUND);
        //ncfile = TdsRequestedDataset.getNetcdfFile(this.request, this.response, path);
        return realpath;
    }

}


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

import ucar.nc2.*;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.FeatureType;
//import ucar.nc2.dt.fmr.FmrcCoordSys;
import ucar.nc2.units.DateFormatter;
import ucar.nc2.util.CancelTask;

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

/**
 * Create a Netcdf File from a GridIndex
 *
 * @author caron
 */
public class GridIndexToNC {
  static private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(GridIndexToNC.class);

  /**
   * map of horizontal coordinate systems
   */
  private Map<String,GridHorizCoordSys> hcsHash = new HashMap<>(10);  // GridHorizCoordSys

  private DateFormatter formatter = new DateFormatter();
  private String indexFilename;
  private ucar.unidata.io.RandomAccessFile raf;

  public GridIndexToNC(String filename) {
    this.indexFilename = filename;
  }

  public GridIndexToNC(ucar.unidata.io.RandomAccessFile raf) {
    this.indexFilename = raf.getLocation();
    this.raf = raf;
  }

  /**
   * Fill in the netCDF file
   *
   * @param index        grid index
   * @param lookup       lookup table
   * @param version      version of data
   * @param ncfile       netCDF file to fill in
   * @param cancelTask   cancel task
   * @throws IOException Problem reading from the file
   */
  public void open(GridIndex index, GridTableLookup lookup, int version,
                   NetcdfFile ncfile, CancelTask cancelTask) throws IOException {

    // create the HorizCoord Systems : one for each gds
    List<GridDefRecord> hcsList = index.getHorizCoordSys();
    boolean needGroups = (hcsList.size() > 1);
    for (GridDefRecord gds : hcsList) {
      Group g = null;
      if (needGroups) {
        g = new Group(ncfile, null, gds.getGroupName());
        ncfile.addGroup(null, g);
      }
      // (GridDefRecord gdsIndex, String grid_name, String shape_name, Group g)
      GridHorizCoordSys hcs = makeGridHorizCoordSys(gds, lookup, g);
      hcsHash.put(gds.getParam(GridDefRecord.GDS_KEY), hcs);
    }

    // run through each record
    GridRecord firstRecord = null;
    List<GridRecord> records = index.getGridRecords();

    for (GridRecord gridRecord : records) {
      if (firstRecord == null) {
        firstRecord = gridRecord;
      }

      GridHorizCoordSys hcs =  hcsHash.get(gridRecord.getGridDefRecordId());
      int cdmHash = gridRecord.cdmVariableHash();
      GridVariable pv = hcs.varHash.get(cdmHash);
      if (null == pv) {
        String name = gridRecord.cdmVariableName(lookup, true, true);
        pv = makeGridVariable(indexFilename, name, hcs, lookup);
        hcs.varHash.put(cdmHash, pv);

        // keep track of all products with same parameter name == "simple name"
        String simpleName = gridRecord.getParameterDescription();
        List<GridVariable> plist = hcs.productHash.get(simpleName);
        if (null == plist) {
          plist = new ArrayList<>();
          hcs.productHash.put(simpleName, plist);
        }
        plist.add(pv);

      } /* else if ( lookup instanceof Grib2GridTableLookup ) {
        Grib2GridTableLookup g2lookup = (Grib2GridTableLookup) lookup;
        // check for non interval pv and interval record which needs a interval pv
        if( ! pv.isInterval() && g2lookup.isInterval(gridRecord) ) {
          // make an interval variable
          String interval = name +"_interval";
          pv = (GridVariable) hcs.varHash.get(interval);
          if (null == pv) {
            pv = new GridVariable(interval, hcs, lookup);
            hcs.varHash.put(cdmHash, pv);
            String simpleName = makeVariableName(gridRecord, lookup, false, true); // LOOK may not be a good idea
            List<GridVariable> plist = hcs.productHash.get(simpleName);
            if (null == plist) {
              plist = new ArrayList<GridVariable>();
              hcs.productHash.put(simpleName, plist);
            }
            plist.add(pv);
          }

        } else if ( pv.isInterval() && !g2lookup.isInterval(gridRecord)  ) {
          // make a non-interval variable
          // logger.info( "Non-Interval records for %s%n", pv.getName());  LOOK
            continue;
        }
      } // grid2 */

      pv.addProduct(gridRecord);
    }

    // global CF Conventions
    // Conventions attribute change must be in sync with CDM code
    ncfile.addAttribute(null, new Attribute("Conventions", "CF-1.4"));

    addExtraAttributes(firstRecord, lookup, ncfile);

    // CF Global attributes
    ncfile.addAttribute(null, new Attribute("title", lookup.getTitle()));
    if (lookup.getInstitution() != null)
      ncfile.addAttribute(null, new Attribute("institution", lookup.getInstitution()));
    String source = lookup.getSource();
    if ( source != null && ! source.startsWith( "Unknown"))
      ncfile.addAttribute(null, new Attribute("source", source));
    // String now = formatter.toDateTimeStringISO( Calendar.getInstance().getTime());
    ncfile.addAttribute(null, new Attribute("history", "Direct read of "+ lookup.getGridType() +" into NetCDF-Java 4 API"));
    if ( lookup.getComment() != null)
      ncfile.addAttribute(null, new Attribute("comment", lookup.getComment()));

    // dataset discovery
    //if ( center != null)
    //  ncfile.addAttribute(null, new Attribute("center_name", center));

    // CDM attributes
    ncfile.addAttribute(null, new Attribute(CF.FEATURE_TYPE, FeatureType.GRID.toString()));
    ncfile.addAttribute(null, new Attribute("file_format", lookup.getGridType()));
    // ncfile.addAttribute(null, new Attribute("location", ncfile.getLocation()));
    ncfile.addAttribute(null, new Attribute(_Coordinate.ModelRunDate,
            formatter.toDateTimeStringISO(lookup.getFirstBaseTime())));

    /* if (fmrcCoordSys != null) {
      makeDefinedCoordSys(ncfile, lookup, fmrcCoordSys);
    } else {
      makeDenseCoordSys(ncfile, lookup, cancelTask);
    } */
    makeDenseCoordSys(ncfile, lookup, cancelTask);

    if (GridServiceProvider.debugMissing) {
      try (Formatter f = new Formatter(System.out)) {
        int count = 0;
        Collection<GridHorizCoordSys> hcset = hcsHash.values();
        for (GridHorizCoordSys hcs : hcset) {
          List<GridVariable> gribvars = new ArrayList<>(hcs.varHash.values());
          for (GridVariable gv : gribvars) {
            count += gv.showMissingSummary(f);
          }
        }
        System.out.println(" total missing= " + count);
      }
    }

    if (GridServiceProvider.debugMissingDetails) {
      Formatter f = new Formatter();
      Collection<GridHorizCoordSys> hcset = hcsHash.values();
      for (GridHorizCoordSys hcs : hcset) {
        f.format("******** Horiz Coordinate= %s%n", hcs.getGridName());

        String lastVertDesc = null;
        List<GridVariable> gribvars = new ArrayList<>(hcs.varHash.values());
        Collections.sort(gribvars, new CompareGridVariableByVertName());

        for (GridVariable gv : gribvars) {
          String vertDesc = gv.getVertName();
          if (!vertDesc.equals(lastVertDesc)) {
            f.format("---Vertical Coordinate= %s%n", vertDesc);
            lastVertDesc = vertDesc;
          }
          gv.showMissing(f);
        }
      }
    }

    // clean out stuff we dont need anymore
    //for (GridHorizCoordSys ghcs : hcsHash.values()) {
    //  ghcs.empty();
    //}
  }

  protected void addExtraAttributes(GridRecord firstRecord, GridTableLookup lookup, NetcdfFile ncfile) {
  }

  protected GridEnsembleCoord addEnsembles(List<GridEnsembleCoord> ensembleCoords, List<GridRecord> recordList) {
    return null;
  }

  protected GridHorizCoordSys makeGridHorizCoordSys(GridDefRecord gds, GridTableLookup lookup, Group g) {
    return new GridHorizCoordSys(gds, lookup, g);
  }

  protected GridVariable makeGridVariable(String indexFilename, String name, GridHorizCoordSys hcs, GridTableLookup lookup) {
    return new GridVariable(indexFilename, name, hcs, lookup);
  }

  protected GridTimeCoord makeGridTimeCoord(List<GridRecord> recordList, String location){
    return new GridTimeCoord(recordList, location);
  }

  protected GridVertCoord makeGridVertCoord(List<GridRecord> recordList, String vname, GridTableLookup lookup, GridHorizCoordSys hcs) {
    return new GridVertCoord(recordList, vname, lookup, hcs);
  }

  // debugging
  public GridHorizCoordSys getHorizCoordSys(GridRecord gribRecord) {
    return hcsHash.get(gribRecord.getGridDefRecordId());
  }

  public Map<String,GridHorizCoordSys> getHorizCoordSystems() {
    return hcsHash;
  }

  /**
   * Make coordinate system without missing data - means that we
   * have to make a coordinate axis for each unique set
   * of time or vertical levels.
   *
   * @param ncfile     netCDF file
   * @param lookup     lookup  table
   * @param cancelTask cancel task
   * @throws IOException problem reading file
   */
  private void makeDenseCoordSys(NetcdfFile ncfile, GridTableLookup lookup, CancelTask cancelTask) throws IOException {
    List<GridTimeCoord> timeCoords = new ArrayList<>();
    List<GridVertCoord> vertCoords = new ArrayList<>();
    List<GridEnsembleCoord> ensembleCoords = new ArrayList<>();

    Calendar calendar = Calendar.getInstance();
    calendar.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));

    // loop over HorizCoordSys
    Collection<GridHorizCoordSys> hcset = hcsHash.values();
    for (GridHorizCoordSys hcs : hcset) {
      if ((cancelTask != null) && cancelTask.isCancel()) break;

      // loop over GridVariables in the HorizCoordSys
      // create the time and vertical coordinates
      List<GridVariable> gribvars = new ArrayList<>(hcs.varHash.values());
      for (GridVariable gv : gribvars) {
        if ((cancelTask != null) && cancelTask.isCancel()) break;

        List<GridRecord> recordList = gv.getRecords();
        GridRecord record = recordList.get(0);
        String vname = gv.makeLevelName(record, lookup);

        // look to see if vertical already exists
        GridVertCoord useVertCoord = null;
        for (GridVertCoord gvcs : vertCoords) {
          if (vname.equals(gvcs.getLevelName())) {
            if (gvcs.matchLevels(recordList)) {  // must have the same levels
              useVertCoord = gvcs;
            }
          }
        }
        if (useVertCoord == null) {  // nope, got to create it
          useVertCoord = makeGridVertCoord(recordList, vname, lookup, hcs);
          vertCoords.add(useVertCoord);
        }
        gv.setVertCoord(useVertCoord);
        // look to see if time coord already exists
        GridTimeCoord useTimeCoord = null;
        for (GridTimeCoord gtc : timeCoords) {
          if (gtc.matchTimes(recordList)) {  // must have the same time coords
            useTimeCoord = gtc;
            break;
          }
        }
        if (useTimeCoord == null) {  // nope, got to create it
          useTimeCoord = makeGridTimeCoord(recordList, ncfile.getLocation());
          timeCoords.add(useTimeCoord);
        }
        gv.setTimeCoord(useTimeCoord);

        if (gv.isEnsemble()) {
          GridEnsembleCoord useEnsembleCoord = addEnsembles(ensembleCoords, recordList);
          if (useEnsembleCoord != null)
            gv.setEnsembleCoord(useEnsembleCoord);
        }
      }

      // assign time coordinate names, add dimensions to file
      // reverse sort by length - give time dimensions unique names
      Collections.sort(timeCoords);
      int count = 0;
      for (GridTimeCoord tcs : timeCoords) {
        tcs.setSequence(count++);
        tcs.addDimensionsToNetcdfFile(ncfile, hcs.getGroup());
      }

      // add Ensemble dimensions, give Ensemble dimensions unique names
      int seqno = 0;
      for (GridEnsembleCoord gec : ensembleCoords) {
        gec.setSequence(seqno++);
        gec.addDimensionsToNetcdfFile(ncfile, hcs.getGroup());
      }

      // add x, y dimensions
      hcs.addDimensionsToNetcdfFile(ncfile);

      // add vertical dimensions, give them unique names
      Collections.sort(vertCoords);
      int vcIndex = 0;
      String listName = null;
      int start = 0;
      for (vcIndex = 0; vcIndex < vertCoords.size(); vcIndex++) {
        GridVertCoord gvcs = vertCoords.get(vcIndex);
        String vname = gvcs.getLevelName();
        if (listName == null) {
          listName = vname;  // initial
        }

        if (!vname.equals(listName)) {
          makeVerticalDimensions(vertCoords.subList(start, vcIndex), ncfile, hcs.getGroup());
          listName = vname;
          start = vcIndex;
        }
      }
      makeVerticalDimensions(vertCoords.subList(start, vcIndex), ncfile, hcs.getGroup());

      // create a variable for each entry, but check for other products with same simple name to disambiguate
      List<List<GridVariable>> products = new ArrayList<>(hcs.productHash.values());
      for (List<GridVariable> plist : products) {
        if ((cancelTask != null) && cancelTask.isCancel()) break;

        if (plist.size() == 1) {
          GridVariable pv = plist.get(0);
          String name = pv.getFirstRecord().cdmVariableName(lookup, false, false); // plain ole name
          Variable v = pv.makeVariable(ncfile, hcs.getGroup(), name, raf );
          ncfile.addVariable(hcs.getGroup(), v);

        } else {

          // collect them grouped by vertical coord
          Map<GridVertCoord,VertCollection> vcMap = new HashMap<>();
          for (GridVariable gv : plist) {
            VertCollection vc = vcMap.get(gv.getVertCoord());
            if (vc == null) {
              vc = new VertCollection(gv);
              vcMap.put(gv.getVertCoord(), vc);
            }
            vc.list.add(gv);
          }

          // sort by larger # vert levels
          List<VertCollection> vclist = new ArrayList<>(vcMap.values());
          Collections.sort(vclist);

          boolean firstVertCoord = true;
          for (VertCollection vc : vclist) {
            boolean hasMultipleLevels = vc.vc.getNLevels() > 1;
            boolean noLevelOk = firstVertCoord; //  && hasMultipleLevels;  LOOK turned off for now 9/15/10
            List<GridVariable> list = vc.list;
            if (list.size() == 1) {
              GridVariable gv = list.get(0);
              String name = gv.getFirstRecord().cdmVariableName(lookup, !noLevelOk, false);
              ncfile.addVariable(hcs.getGroup(), gv.makeVariable(ncfile, hcs.getGroup(), name, raf));

            } else {
              for (GridVariable gv : list) { // more than one - disambiguate by stat name
                String name = gv.getFirstRecord().cdmVariableName(lookup, !noLevelOk, true);
                ncfile.addVariable(hcs.getGroup(), gv.makeVariable(ncfile, hcs.getGroup(), name, raf));
              }
            }
            firstVertCoord = false;
          }

        } // multiple vertical levels

      } // create variable

      // add coordinate variables at the end
      for (GridTimeCoord tcs : timeCoords) {
        tcs.addToNetcdfFile(ncfile, hcs.getGroup());
      }

      for (GridEnsembleCoord ens : ensembleCoords) {
        ens.addToNetcdfFile(ncfile, hcs.getGroup());
      }

      hcs.addToNetcdfFile(ncfile);

      for (GridVertCoord gvcs : vertCoords) {
        gvcs.addToNetcdfFile(ncfile, hcs.getGroup());
      }

    } // loop over hcs
    // TODO: check this,  in ToolsUI it caused problems
    //for (GridVertCoord gvcs : vertCoords) {
    //  gvcs.empty();
    //}

  }

  private static class VertCollection implements Comparable<VertCollection> {
    GridVertCoord vc;
    List<GridVariable> list = new ArrayList<>(3);

    VertCollection(GridVariable gv) {
      this.vc = gv.getVertCoord();
    }

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

    @Override
    public int hashCode() {
      return vc.hashCode();
    }

    @Override
    public int compareTo(VertCollection o) {
      int ret = o.vc.getNLevels() - vc.getNLevels();

      if (ret == 0) {  // break ties for consistency
        ret = vc.getLevelName().compareTo(o.vc.getLevelName());
      }

      return ret;
    }
  }

  /**
   * Make a vertical dimensions
   *
   * @param vertCoordList vertCoords all with the same name
   * @param ncfile        netCDF file to add to
   * @param group         group in ncfile
   */
  private void makeVerticalDimensions(List<GridVertCoord> vertCoordList, NetcdfFile ncfile, Group group) {
    // find biggest vert coord
    GridVertCoord gvcs0 = null;
    int maxLevels = 0;
    for (GridVertCoord gvcs : vertCoordList) {
      if (gvcs.getNLevels() > maxLevels) {
        gvcs0 = gvcs;
        maxLevels = gvcs.getNLevels();
      }
    }

    int seqno = 1;
    for (GridVertCoord gvcs : vertCoordList) {
      if (gvcs != gvcs0) {
        gvcs.setSequence(seqno++);
      }
      gvcs.addDimensionsToNetcdfFile(ncfile, group);
    }
  }

  /**
   * Comparator for grids by vertical variable name
   *
   * @author IDV Development Team
   * @version $Revision: 1.3 $
   */
  static private class CompareGridVariableByVertName implements Comparator {

    /**
     * Compare the two lists of names
     *
     * @param o1 first list
     * @param o2 second list
     * @return comparison
     */
    public int compare(Object o1, Object o2) {
      GridVariable gv1 = (GridVariable) o1;
      GridVariable gv2 = (GridVariable) o2;

      return gv1.getVertName().compareToIgnoreCase(gv2.getVertName());
    }
  }
}

/*
   * Make coordinate system from a Definition object
   *
   * @param ncfile netCDF file to add to
   * @param lookup lookup table
   * @param fmr    FmrcCoordSys
   * @throws IOException problem reading from file
   * @deprecated dont use definition files as of 4.2
   *
  private void makeDefinedCoordSys(NetcdfFile ncfile, GridTableLookup lookup, FmrcCoordSys fmr) throws IOException {

    List<GridTimeCoord> timeCoords = new ArrayList<GridTimeCoord>();
    List<GridVertCoord> vertCoords = new ArrayList<GridVertCoord>();
    List<GridEnsembleCoord> ensembleCoords = new ArrayList<GridEnsembleCoord>();

    List<Integer> removeVariables = new ArrayList<Integer>();

    // loop over HorizCoordSys
    Collection<GridHorizCoordSys> hcset = hcsHash.values();
    for (GridHorizCoordSys hcs : hcset) {

      // loop over GridVariables in the HorizCoordSys
      // create the time and vertical coordinates
      Set<Integer> keys = hcs.varHash.keySet();
      for (Integer key : keys) {
        GridVariable pv = hcs.varHash.get(key);
        GridRecord record = pv.getFirstRecord();

        // we dont know the name for sure yet, so have to try several
        String searchName = findVariableName(ncfile, record, lookup, fmr);
        if (searchName == null) { // cant find - just remove
          removeVariables.add(key); // cant remove (concurrentModException) so save for later
          continue;
        }
        pv.setVarName( searchName);

        // get the vertical coordinate for this variable, if it exists
        FmrcCoordSys.VertCoord vc_def = fmr.findVertCoordForVariable( searchName);
        if (vc_def != null) {
          String vc_name = vc_def.getName();

          // look to see if GridVertCoord already made
          GridVertCoord useVertCoord = null;
          for (GridVertCoord gvcs : vertCoords) {
            if (vc_name.equals(gvcs.getLevelName()))
              useVertCoord = gvcs;
          }
          if (useVertCoord == null) { // nope, got to create it
            useVertCoord = new GridVertCoord(record, vc_name, lookup, vc_def.getValues1(), vc_def.getValues2());
            useVertCoord.addDimensionsToNetcdfFile( ncfile, hcs.getGroup());
            vertCoords.add( useVertCoord);
          }
          pv.setVertCoord( useVertCoord);

        } else {
          pv.setVertCoord( new GridVertCoord(searchName)); // fake
        }

        // get the time coordinate for this variable
        FmrcCoordSys.TimeCoord tc_def = fmr.findTimeCoordForVariable( searchName, lookup.getFirstBaseTime());
        String tc_name = tc_def.getName();

        // look to see if GridTimeCoord already made
        GridTimeCoord useTimeCoord = null;
        for (GridTimeCoord gtc : timeCoords) {
          if (tc_name.equals(gtc.getName()))
            useTimeCoord = gtc;
        }
        if (useTimeCoord == null) { // nope, got to create it
          useTimeCoord = new GridTimeCoord(tc_name, tc_def.getOffsetHours(), lookup);
          useTimeCoord.addDimensionsToNetcdfFile( ncfile, hcs.getGroup());
          timeCoords.add( useTimeCoord);
        }
        pv.setTimeCoord( useTimeCoord);

        // check for ensemble members
        //System.out.println( pv.getName() +"  "+ pv.getParamName() );
        GridEnsembleCoord useEnsembleCoord = null;
        GridEnsembleCoord  ensembleCoord = new GridEnsembleCoord(record, lookup);
        for (GridEnsembleCoord gec : ensembleCoords) {
          if (ensembleCoord.getNEnsembles() == gec.getNEnsembles()) {
            useEnsembleCoord = gec;
            break;
          }
        }

        if (useEnsembleCoord == null) {
          useEnsembleCoord = ensembleCoord;
          ensembleCoords.add(useEnsembleCoord);
        }
        // only add ensemble dimensions
        if (useEnsembleCoord.getNEnsembles() > 1)
          pv.setEnsembleCoord(useEnsembleCoord);

      }

      // any need to be removed?
      for (Integer key : removeVariables) {
        hcs.varHash.remove(key);
      }

      // add x, y dimensions
      hcs.addDimensionsToNetcdfFile( ncfile);

      // create a variable for each entry
      Collection<GridVariable> vars = hcs.varHash.values();
      for (GridVariable pv : vars) {
        Group g = hcs.getGroup() == null ? ncfile.getRootGroup() : hcs.getGroup();
        Variable v = pv.makeVariable(ncfile, g, null); // name is already set
        if (g.findVariable( v.getShortName()) != null) { // already got. can happen when a new vert level is added
          logger.warn("GribServiceProvider.GridIndexToNC: FmrcCoordSys has 2 variables mapped to ="+v.getShortName()+
                  " for file "+ncfile.getLocation());
        } else
          g.addVariable( v);
      }

      // add coordinate variables at the end
      for (GridTimeCoord tcs : timeCoords) {
        tcs.addToNetcdfFile(ncfile, hcs.getGroup());
      }

      for (GridEnsembleCoord gec : ensembleCoords) {
        if (gec.getNEnsembles() > 1)
          gec.addToNetcdfFile(ncfile, hcs.getGroup());
      }

      hcs.addToNetcdfFile( ncfile);
      for (GridVertCoord gvcs : vertCoords) {
        gvcs.addToNetcdfFile(ncfile, hcs.getGroup());
      }

    } // loop over hcs

    if (debug) System.out.println("GridIndexToNC.makeDefinedCoordSys for "+ncfile.getLocation());
  }

   * Find the variable name for the grid
   *
   * @param ncfile netCDF file
   * @param gr     grid record
   * @param lookup lookup table
   * @param fmr    FmrcCoordSys
   * @return name for the grid
   *
  private String findVariableName(NetcdfFile ncfile, GridRecord gr, GridTableLookup lookup, FmrcCoordSys fmr) {
    // first lookup with name & vert name
    String name = AbstractIOServiceProvider.createValidNetcdfObjectName(makeVariableName(gr, lookup));
    if (fmr.hasVariable(name)) {
      return name;
    }

    // now try just the name
    String pname = AbstractIOServiceProvider.createValidNetcdfObjectName( lookup.getParameter(gr).getDescription());
    if (fmr.hasVariable(pname)) {
      return pname;
    }

    logger.warn( "GridIndexToNC: FmrcCoordSys does not have the variable named ="
            + name + " or " + pname + " for file " + ncfile.getLocation());

    return null;
  }

  private String findVariableName(NetcdfFile ncfile, GridRecord gr, GridTableLookup lookup, FmrcCoordSys fmr) {

    // first lookup with name & vert name
    String name = makeVariableName(gr, lookup, true, true);
    if (debug)
      System.out.println( "name ="+ name );
    if (fmr.hasVariable( name))
      return name;

    // now try just the name
    String pname = lookup.getParameter(gr).getDescription();
    if (debug)
      System.out.println( "pname ="+ pname );
    if (fmr.hasVariable( pname))
      return pname;

    // try replacing the blanks
    String nameWunder = StringUtil.replace(name, ' ', "_");
    if (debug)
      System.out.println( "nameWunder ="+ nameWunder );
    if (fmr.hasVariable( nameWunder))
      return nameWunder;

    String pnameWunder = StringUtil.replace(pname, ' ', "_");
    if (debug)
      System.out.println( "pnameWunder ="+ pnameWunder );
    if (fmr.hasVariable( pnameWunder))
      return pnameWunder;

    logger.warn("GridServiceProvider.GridIndexToNC: FmrcCoordSys does not have the variable named ="+name+" or "+pname+" or "+
            nameWunder+" or "+pnameWunder+" for file "+ncfile.getLocation());

    return null;
  }

   * Make the variable name with suffix and level if present
   *
   * @param gr     grid record
   * @param lookup lookup table
   * @param addLevel add level name if there is one
   * @param addEnsemble  add ensemble name if there is one
   * @return variable name

  public static String makeVariableName(GridRecord gr, GridTableLookup lookup, boolean addLevel, boolean addEnsemble) {
    GridParameter param = lookup.getParameter(gr);
    String paramName = param.getDescription();

    if (addEnsemble) {
      String suffixName = makeSuffixName(gr, lookup);
      paramName = (suffixName.length() == 0) ? paramName : paramName + "_" + suffixName;
    }

    if (addLevel) {
      String levelName = makeLevelName(gr, lookup);
      if (levelName.length() != 0) {
        paramName += "_" + levelName;
      }
    }

    return paramName;
  }

  static private class CompareGridVariableByNumberVertLevels implements Comparator {

     * Compare the two lists of names
     *
     * @param o1 first list
     * @param o2 second list
     * @return comparison
     *
    public int compare(Object o1, Object o2) {
      GridVariable gv1 = (GridVariable) o1;
      GridVariable gv2 = (GridVariable) o2;

      int n1 = gv1.getVertCoord().getNLevels();
      int n2 = gv2.getVertCoord().getNLevels();

      if (n1 == n2) {  // break ties for consistency
        return gv1.getVertCoord().getLevelName().compareTo(
            gv2.getVertCoord().getLevelName());
      } else {
        return n2 - n1;  // highest number first
      }
    }
  }
 */


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

import java.io.*;
import java.util.*;
import java.text.*;

import ucar.nc2.*;
import ucar.nc2.constants.*;
import ucar.nc2.units.DateFormatter;
import ucar.nc2.iosp.fysat.util.EndianByteBuffer;

import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.geoloc.projection.Stereographic;
import ucar.unidata.geoloc.projection.Mercator;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.io.RandomAccessFile;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.unidata.util.Parameter;

/**
 * Netcdf header reading and writing for version 3 file format.
 * This is used by Fysatiosp.
 */

public final class FysatHeader {

  private static final boolean debug = false;
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FysatHeader.class);

  int FY_AWX_PIB_LEN = 40;   // FY Satellite AWX product indentification block

  double DEG_TO_RAD = 0.017453292;
  private int Z_type = 0;

  private AwxFileFirstHeader firstHeader;
  private AwxFileSecondHeader secondHeader;

  public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) throws IOException {

    //  second check the file name
    if (!((raf.getLocation().endsWith(".AWX") || raf.getLocation().endsWith(".awx")))) {
      return false;
    }
    // more serious checking
    return this.readPIB(raf);// not FY Satellite AWX product file
  }

  /**
   * Read the header and populate the ncfile
   *
   * @param raf
   * @throws IOException
   */
  boolean readPIB(RandomAccessFile raf) throws IOException {

    this.firstHeader = new AwxFileFirstHeader();

    int pos = 0;
    raf.seek(pos);

    // gini header process
    byte[] buf = new byte[FY_AWX_PIB_LEN];
    int count = raf.read(buf);
    EndianByteBuffer byteBuffer;
    if (count == FY_AWX_PIB_LEN) {
      byteBuffer = new EndianByteBuffer(buf, this.firstHeader.byteOrder);
      this.firstHeader.fillHeader(byteBuffer);

    } else {
      return false;
    }

    if (!((this.firstHeader.fileName.endsWith(".AWX") || this.firstHeader.fileName.endsWith(".awx"))
            && this.firstHeader.firstHeaderLength == FY_AWX_PIB_LEN)) {
      return false;
    }

    // skip the fills of the first record
    //  raf.seek(FY_AWX_PIB_LEN + this.firstHeader.fillSectionLength);
    buf = new byte[this.firstHeader.secondHeaderLength];
    raf.readFully(buf);
    byteBuffer = new EndianByteBuffer(buf, this.firstHeader.byteOrder);
    switch (this.firstHeader.typeOfProduct) {
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_UNDEFINED:
        throw new UnsupportedDatasetException();
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GEOSAT_IMAGE:
        secondHeader = new AwxFileGeoSatelliteSecondHeader();
        secondHeader.fillHeader(byteBuffer);
        break;
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_POLARSAT_IMAGE:
        throw new UnsupportedDatasetException();

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GRID:
        secondHeader = new AwxFileGridProductSecondHeader();
        secondHeader.fillHeader(byteBuffer);

        break;
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_DISCREET:
        throw new UnsupportedDatasetException();

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GRAPH_ANALIYSIS:
        throw new UnsupportedDatasetException();

    }

    return true;

  }

  void read(RandomAccessFile raf, NetcdfFile ncfile) throws IOException {

    if ((this.firstHeader == null) && (this.secondHeader == null)) {
      readPIB(raf);
    }
    assert this.firstHeader != null;
    assert this.secondHeader != null;

    Attribute att;
//    Attribute att = new Attribute( "Conventions", "AWX");
//    ncfile.addAttribute(null, att);

    att = new Attribute("version", this.firstHeader.version);
    ncfile.addAttribute(null, att);

    String vname;

    switch (this.firstHeader.typeOfProduct) {

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_UNDEFINED:
        throw new UnsupportedDatasetException();

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GEOSAT_IMAGE: {
        AwxFileGeoSatelliteSecondHeader geoSatelliteSecondHeader = (AwxFileGeoSatelliteSecondHeader) this.secondHeader;

        att = new Attribute("satellite_name", geoSatelliteSecondHeader.satelliteName);
        ncfile.addAttribute(null, att);

        DateFormat dformat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        dformat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
        Calendar cal = Calendar.getInstance();
        cal.set(geoSatelliteSecondHeader.year,
                geoSatelliteSecondHeader.month - 1,
                geoSatelliteSecondHeader.day,
                geoSatelliteSecondHeader.hour,
                geoSatelliteSecondHeader.minute);
        cal.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
        String dstring = dformat.format(cal.getTime());
        ncfile.addAttribute(null, new Attribute("time_coverage", dstring));
        int nz = 1;
        Dimension dimT = new Dimension("time", nz, true, false, false);
        ncfile.addDimension(null, dimT);

        String timeCoordName = "time";
        Variable taxis = new Variable(ncfile, null, null, timeCoordName);
        taxis.setDataType(DataType.DOUBLE);
        taxis.setDimensions("time");
        taxis.addAttribute(new Attribute(CDM.LONG_NAME, "time since base date"));
        taxis.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
        double[] tdata = new double[1];
        tdata[0] = cal.getTimeInMillis();
        Array dataA = Array.factory(DataType.DOUBLE, new int[]{1}, tdata);
        taxis.setCachedData(dataA, false);
        DateFormatter formatter = new DateFormatter();
        taxis.addAttribute(new Attribute(CDM.UNITS, "msecs since " + formatter.toDateTimeStringISO(new Date(0))));
        ncfile.addVariable(null, taxis);
        //  Get dimensions
        Integer ni = (int) geoSatelliteSecondHeader.widthOfImage;
        att = new Attribute("NX", ni);
        ncfile.addAttribute(null, att);

        ni = (int) geoSatelliteSecondHeader.heightOfImage;
        att = new Attribute("NY", ni);
        ncfile.addAttribute(null, att);

        vname = getGeoSatelliteProductName(geoSatelliteSecondHeader.channel);
        if (vname == null)
          throw new UnsupportedDatasetException("Unsupported GeoSatellite Procuct Dataset");

        // set projection attribute
        // ? which projection
        ProjectionImpl projection = null;
        double dxKm = 0.0, dyKm = 0.0;
        short nv = geoSatelliteSecondHeader.flagOfProjection;
        att = new Attribute("ProjIndex", nv);
        ncfile.addAttribute(null, att);
        int proj = nv;
        if (proj == 2) {
          att = new Attribute("ProjName", "MERCATOR");
          double lat0 = geoSatelliteSecondHeader.centerLatitudeOfProjection;
          double lon0 = geoSatelliteSecondHeader.centerLongitudeOfProjection;
          double par = geoSatelliteSecondHeader.standardLatitude1;
          dxKm = geoSatelliteSecondHeader.horizontalResolution;
          dyKm = geoSatelliteSecondHeader.verticalResolution;
          projection = new Mercator(lon0, par);

        } else if (proj == 1) {
          att = new Attribute("ProjName", "LAMBERT_CONFORNAL");
          double lat0 = geoSatelliteSecondHeader.centerLatitudeOfProjection;
          double lon0 = geoSatelliteSecondHeader.centerLongitudeOfProjection;
          double par1 = geoSatelliteSecondHeader.standardLatitude1;
          double par2 = geoSatelliteSecondHeader.standardLatitude2;
          dxKm = geoSatelliteSecondHeader.horizontalResolution / 100;
          dyKm = geoSatelliteSecondHeader.verticalResolution / 100;
          projection = new LambertConformal(lat0, lon0, par1, par2);

        } else if (proj == 3) {
          att = new Attribute("ProjName", "POLARSTEREOGRAPHIC");
          double latt = geoSatelliteSecondHeader.centerLatitudeOfProjection;
          double lont = geoSatelliteSecondHeader.centerLongitudeOfProjection;
          double scale = (1. + Math.sin(DEG_TO_RAD * latt)) / 2.;
          dxKm = geoSatelliteSecondHeader.horizontalResolution;
          dyKm = geoSatelliteSecondHeader.verticalResolution;
          projection = new Stereographic(90.0, lont, scale);

        } else if (proj == 4) {
          att = new Attribute("ProjName", "LatLonProjection");
          projection = new LatLonProjection();
        }
        ncfile.addAttribute(null, att);

        // coordinate transform variable
        if (proj != 4) {

        }
        //double dxKm = 0.0, dyKm = 0.0, latin, lonProjectionOrigin ;

        // deal with projection

        ncfile.addAttribute(null, new Attribute("channel", geoSatelliteSecondHeader.channel));
        ncfile.addAttribute(null, new Attribute("geospatial_lat_min", geoSatelliteSecondHeader.latitudeOfSouth));
        ncfile.addAttribute(null, new Attribute("geospatial_lat_max", geoSatelliteSecondHeader.latitudeOfNorth));
        ncfile.addAttribute(null, new Attribute("geospatial_lon_min", geoSatelliteSecondHeader.longitudeOfWest));
        ncfile.addAttribute(null, new Attribute("geospatial_lon_max", geoSatelliteSecondHeader.longitudeOfEast));
        ncfile.addAttribute(null, new Attribute("geospatial_vertical_min", new Float(0.0)));
        ncfile.addAttribute(null, new Attribute("geospatial_vertical_max", new Float(0.0)));
        ncfile.addAttribute(null, new Attribute("sample_ratio", geoSatelliteSecondHeader.sampleRatio));
        ncfile.addAttribute(null, new Attribute("horizontal_resolution", geoSatelliteSecondHeader.horizontalResolution));
        ncfile.addAttribute(null, new Attribute("vertical_resolution", geoSatelliteSecondHeader.verticalResolution));

        // only one data variable per awx file

        //  set vname and units according to grid feature

        Variable var = new Variable(ncfile, ncfile.getRootGroup(), null, vname);

        var.addAttribute(new Attribute(CDM.LONG_NAME, vname));

        // get dimensions
        int velems;

        int nx = geoSatelliteSecondHeader.widthOfImage;
        int ny = geoSatelliteSecondHeader.heightOfImage;

        Dimension dimX;
        Dimension dimY;

        if (proj != 4) {
          dimX = new Dimension("x", nx, true, false, false);
          dimY = new Dimension("y", ny, true, false, false);
        } else {
          dimX = new Dimension("lon", nx, true, false, false);
          dimY = new Dimension("lat", ny, true, false, false);
        }

        ncfile.addDimension(null, dimY);
        ncfile.addDimension(null, dimX);

        int byteAmountofData = 1;
        var.setDataType(DataType.UBYTE);
        Class dataType = DataType.BYTE.getPrimitiveClassType();
        velems = dimX.getLength() * dimY.getLength() * byteAmountofData;
        List<Dimension> dims = new ArrayList<>();
        dims.add(dimT);
        dims.add(dimY);
        dims.add(dimX);

        var.setDimensions(dims);

        var.addAttribute(new Attribute(CF.COORDINATES, "Lon Lat"));

        //var.addAttribute(new Attribute(CDM.UNSIGNED, "true"));
        var.addAttribute(new Attribute(CDM.UNITS, "percent"));
        // if(var.getDataType() == DataType.BYTE) {
        //     var.addAttribute(new Attribute("_missing_value", new Byte((byte)-1)));
        //     var.addAttribute( new Attribute("scale_factor", new Byte((byte)(1))));
        //     var.addAttribute( new Attribute("add_offset", new Byte((byte)(0))));
        //  } else {
        var.addAttribute(new Attribute("_missing_value", (short) -1));
        var.addAttribute(new Attribute(CDM.SCALE_FACTOR, (short) (1)));
        var.addAttribute(new Attribute(CDM.ADD_OFFSET, (short) (0)));
        //  }

        // size and beginning data position in file
        int vsize = velems;
        long begin = this.firstHeader.recordsOfHeader * this.firstHeader.recoderLength;
        if (debug)
          log.warn(" name= " + vname + " vsize=" + vsize + " velems=" + velems + " begin= " + begin + "\n");
        var.setSPobject(new Vinfo(vsize, begin, nx, ny, dataType, this.firstHeader.byteOrder));
        String coordinates;
        if (proj != 4) {
          coordinates = "x y time";
        } else {
          coordinates = "Lon Lat time";
        }

        var.addAttribute(new Attribute(_Coordinate.Axes, coordinates));

        ncfile.addVariable(ncfile.getRootGroup(), var);

//    	    if (debug) System.out.println("start at proj coord "+start);

        LatLonPointImpl startPnt = new LatLonPointImpl(geoSatelliteSecondHeader.latitudeOfNorth, geoSatelliteSecondHeader.longitudeOfWest);
        LatLonPointImpl endPnt = new LatLonPointImpl(geoSatelliteSecondHeader.latitudeOfSouth, geoSatelliteSecondHeader.longitudeOfEast);
        if (debug) System.out.println("start at geo coord :" + startPnt);

        if (projection != null && proj != 4) {
          // we have to project in order to find the origin
          ProjectionPointImpl start = (ProjectionPointImpl) projection.latLonToProj(
                  new LatLonPointImpl(geoSatelliteSecondHeader.latitudeOfSouth, geoSatelliteSecondHeader.longitudeOfWest));
          double startx = start.getX();
          double starty = start.getY();

          Variable xaxis = new Variable(ncfile, null, null, "x");
          xaxis.setDataType(DataType.DOUBLE);
          xaxis.setDimensions("x");
          xaxis.addAttribute(new Attribute(CDM.LONG_NAME, "projection x coordinate"));
          xaxis.addAttribute(new Attribute(CDM.UNITS, "km"));
          xaxis.addAttribute(new Attribute(_Coordinate.AxisType, "GeoX"));
          double[] data = new double[nx];
          if (proj == 2) {
            double lon_1 = geoSatelliteSecondHeader.longitudeOfEast;
            double lon_2 = geoSatelliteSecondHeader.longitudeOfWest;
            if (lon_1 < 0) lon_1 += 360.0;
            if (lon_2 < 0) lon_2 += 360.0;
            double dx = (lon_1 - lon_2) / (nx - 1);

            for (int i = 0; i < data.length; i++) {
              double ln = lon_2 + i * dx;
              ProjectionPointImpl pt = (ProjectionPointImpl) projection.latLonToProj(
                      new LatLonPointImpl(geoSatelliteSecondHeader.latitudeOfSouth, ln));
              data[i] = pt.getX();  // startx + i*dx;
            }
          } else {
            for (int i = 0; i < data.length; i++)
              data[i] = startx + i * dxKm;
          }

          dataA = Array.factory(DataType.DOUBLE, new int[]{nx}, data);
          xaxis.setCachedData(dataA, false);
          ncfile.addVariable(null, xaxis);

          Variable yaxis = new Variable(ncfile, null, null, "y");
          yaxis.setDataType(DataType.DOUBLE);
          yaxis.setDimensions("y");
          yaxis.addAttribute(new Attribute(CDM.LONG_NAME, "projection y coordinate"));
          yaxis.addAttribute(new Attribute(CDM.UNITS, "km"));
          yaxis.addAttribute(new Attribute(_Coordinate.AxisType, "GeoY"));
          data = new double[ny];
          double endy = starty + dyKm * (data.length - 1); // apparently lat1,lon1 is always the lower ledt, but data is upper left
          double lat2 = geoSatelliteSecondHeader.latitudeOfNorth;
          double lat1 = geoSatelliteSecondHeader.latitudeOfSouth;
          if (proj == 2) {
            double dy = (lat2 - lat1) / (ny - 1);
            for (int i = 0; i < data.length; i++) {
              double la = lat2 - i * dy;
              ProjectionPointImpl pt = (ProjectionPointImpl) projection.latLonToProj(
                      new LatLonPointImpl(la, geoSatelliteSecondHeader.longitudeOfWest));
              data[i] = pt.getY();  //endyy - i*dy;
            }
          } else {
            for (int i = 0; i < data.length; i++)
              data[i] = endy - i * dyKm;
          }
          dataA = Array.factory(DataType.DOUBLE, new int[]{ny}, data);
          yaxis.setCachedData(dataA, false);
          ncfile.addVariable(null, yaxis);

          // coordinate transform variable
          Variable ct = new Variable(ncfile, null, null, projection.getClassName());
          ct.setDataType(DataType.CHAR);
          ct.setDimensions("");
          for (Parameter p : projection.getProjectionParameters()) {
            ct.addAttribute(new Attribute(p));
          }
          ct.addAttribute(new Attribute(_Coordinate.TransformType, "Projection"));
          ct.addAttribute(new Attribute(_Coordinate.Axes, "x, y"));
          // fake data
          dataA = Array.factory(DataType.CHAR, new int[]{});
          dataA.setChar(dataA.getIndex(), ' ');
          ct.setCachedData(dataA, false);

          ncfile.addVariable(null, ct);
          ncfile.addAttribute(null, new Attribute(CDM.CONVENTIONS, _Coordinate.Convention));
        } else {
          Variable yaxis = new Variable(ncfile, null, null, "lat");
          yaxis.setDataType(DataType.DOUBLE);
          yaxis.setDimensions("lat");
          yaxis.addAttribute(new Attribute(CDM.LONG_NAME, "latitude"));
          yaxis.addAttribute(new Attribute(CDM.UNITS, "degree"));
          yaxis.addAttribute(new Attribute(_Coordinate.AxisType, "Lat"));
          double[] data = new double[ny];

          double dy = (endPnt.getLatitude() - startPnt.getLatitude()) / (ny - 1);
          for (int i = 0; i < data.length; i++) {
            data[i] = startPnt.getLatitude() + i * dy; // starty + i*dy;
          }

          dataA = Array.factory(DataType.DOUBLE, new int[]{ny}, data);
          yaxis.setCachedData(dataA, false);
          ncfile.addVariable(null, yaxis);

          // create coordinate variables
          Variable xaxis = new Variable(ncfile, null, null, "lon");
          xaxis.setDataType(DataType.DOUBLE);
          xaxis.setDimensions("lon");
          xaxis.addAttribute(new Attribute(CDM.LONG_NAME, "longitude"));
          xaxis.addAttribute(new Attribute(CDM.UNITS, "degree"));
          xaxis.addAttribute(new Attribute(_Coordinate.AxisType, "Lon"));
          data = new double[nx];

          double dx = (endPnt.getLongitude() - startPnt.getLongitude()) / (nx - 1);
          for (int i = 0; i < data.length; i++) {
            data[i] = startPnt.getLongitude() + i * dx; // startx + i*dx;

          }

          dataA = Array.factory(DataType.DOUBLE, new int[]{nx}, data);
          xaxis.setCachedData(dataA, false);
          ncfile.addVariable(null, xaxis);
        }
        break;
      }
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_POLARSAT_IMAGE:
        throw new UnsupportedDatasetException();

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GRID: {
        AwxFileGridProductSecondHeader gridprocuctSecondHeader = (AwxFileGridProductSecondHeader) this.secondHeader;

        att = new Attribute("satellite_name", gridprocuctSecondHeader.satelliteName);
        ncfile.addAttribute(null, att);

        att = new Attribute("grid_feature", gridprocuctSecondHeader.gridFeature);
        ncfile.addAttribute(null, att);

        att = new Attribute("byte_amount_of_data", gridprocuctSecondHeader.byteAmountofData);
        ncfile.addAttribute(null, att);

        att = new Attribute("data_scale", gridprocuctSecondHeader.dataScale);
        ncfile.addAttribute(null, att);
        ncfile.addAttribute(null, new Attribute(CF.FEATURE_TYPE, FeatureType.GRID.toString()));

        DateFormat dformat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        dformat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
        Calendar cal = Calendar.getInstance();
        cal.set(gridprocuctSecondHeader.startYear,
                gridprocuctSecondHeader.startMonth - 1,
                gridprocuctSecondHeader.startDay,
                gridprocuctSecondHeader.startHour,
                gridprocuctSecondHeader.startMinute, 0);
        cal.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
        String dstring = dformat.format(cal.getTime());
        ncfile.addAttribute(null, new Attribute("time_coverage_start", dstring));
        int nz = 1;
        Dimension dimT = new Dimension("time", nz, true, false, false);
        ncfile.addDimension(null, dimT);

        // set time variable with time_coverage_start
        String timeCoordName = "time";
        Variable taxis = new Variable(ncfile, null, null, timeCoordName);
        taxis.setDataType(DataType.DOUBLE);
        taxis.setDimensions("time");
        taxis.addAttribute(new Attribute(CDM.LONG_NAME, "time since base date"));
        taxis.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
        double[] tdata = new double[1];
        tdata[0] = cal.getTimeInMillis();
        Array dataA = Array.factory(DataType.DOUBLE, new int[]{1}, tdata);
        taxis.setCachedData(dataA, false);
        DateFormatter formatter = new DateFormatter();
        taxis.addAttribute(new Attribute(CDM.UNITS, "msecs since " + formatter.toDateTimeStringISO(new Date(0))));
        ncfile.addVariable(null, taxis);

        cal.set(gridprocuctSecondHeader.endYear,
                gridprocuctSecondHeader.endMonth - 1,
                gridprocuctSecondHeader.endDay,
                gridprocuctSecondHeader.endHour,
                gridprocuctSecondHeader.endMinute, 0);
        dstring = dformat.format(cal.getTime());
        ncfile.addAttribute(null, new Attribute("time_coverage_end", dstring));

        //  Get dimensions
        Integer ni = (int) gridprocuctSecondHeader.amountofHorizontalSpacing;
        att = new Attribute("NX", ni);
        ncfile.addAttribute(null, att);

        ni = (int) gridprocuctSecondHeader.amountofVerticalSpacing;
        att = new Attribute("NY", ni);
        ncfile.addAttribute(null, att);

        // set projection attribute
        // ? which projection
        Byte nv = 0;
        att = new Attribute("ProjIndex", nv);
        ncfile.addAttribute(null, att);
        int proj = nv.intValue();
        if (proj == 1) {
          att = new Attribute("ProjName", "MERCATOR");
        } else if (proj == 3) {
          att = new Attribute("ProjName", "LAMBERT_CONFORNAL");
        } else if (proj == 5) {
          att = new Attribute("ProjName", "POLARSTEREOGRAPGIC");
        }
        ncfile.addAttribute(null, att);

        ProjectionImpl projection = null;
        double dxKm = 0.0, dyKm = 0.0, latin, lonProjectionOrigin;

        // deal with projection
        // System.out.println("unimplemented projection");

        ncfile.addAttribute(null, new Attribute("geospatial_lat_min", gridprocuctSecondHeader.rightBottomLat));
        ncfile.addAttribute(null, new Attribute("geospatial_lat_max", gridprocuctSecondHeader.leftTopLat));
        ncfile.addAttribute(null, new Attribute("geospatial_lon_min", gridprocuctSecondHeader.leftTopLon));
        ncfile.addAttribute(null, new Attribute("geospatial_lon_max", gridprocuctSecondHeader.rightBottomLon));
        ncfile.addAttribute(null, new Attribute("geospatial_vertical_min", new Float(0.0)));
        ncfile.addAttribute(null, new Attribute("geospatial_vertical_max", new Float(0.0)));

        ncfile.addAttribute(null, new Attribute("spacing_unit", gridprocuctSecondHeader.getSpacingUnit()));
        ncfile.addAttribute(null, new Attribute("horizontal_spacing", gridprocuctSecondHeader.horizontalSpacing));
        ncfile.addAttribute(null, new Attribute("vertical_spacing", gridprocuctSecondHeader.verticalSpacing));
        ncfile.addAttribute(null, new Attribute("amount_of_horizontal_spacing", gridprocuctSecondHeader.amountofHorizontalSpacing));
        ncfile.addAttribute(null, new Attribute("amount_of_vertical_spacing", gridprocuctSecondHeader.amountofVerticalSpacing));

        //		    att = new Attribute( "imageResolution", nv);
        //		    ncfile.addAttribute(null, att);

        // only one data variable per awx file
        vname = getGridProductName(gridprocuctSecondHeader.gridFeature);
        if (vname == null)
          throw new UnsupportedDatasetException("Unsupported Grid Procuct Dataset");
        //  set vname and units according to grid feature
        //String vname= this.firstHeader.fileName.substring(0, this.firstHeader.fileName.length() -4);
        Variable var = new Variable(ncfile, ncfile.getRootGroup(), null, vname);

        var.addAttribute(new Attribute(CDM.LONG_NAME, vname)); // getPhysElemLongName(phys_elem, ent_id)));
        var.addAttribute(new Attribute(CDM.UNITS, getPhysElemUnits(gridprocuctSecondHeader.gridFeature)));
        // 		    var.addAttribute( new Attribute(CDM.MISSING_VALUE, new Byte((byte) 0))); // ??

        // get dimensions
        int velems;

        int nx = gridprocuctSecondHeader.amountofHorizontalSpacing;
        int ny = gridprocuctSecondHeader.amountofVerticalSpacing;
        //int nz = 1;

        Dimension dimX = new Dimension("lon", nx, true, false, false);
        Dimension dimY = new Dimension("lat", ny, true, false, false);

        ncfile.addDimension(null, dimY);
        ncfile.addDimension(null, dimX);

        velems = dimX.getLength() * dimY.getLength() * gridprocuctSecondHeader.byteAmountofData;
        List<Dimension> dims = new ArrayList<>();
        dims.add(dimT);
        dims.add(dimY);
        dims.add(dimX);

        var.setDimensions(dims);

        // data type
        Class dataType;
        switch (gridprocuctSecondHeader.byteAmountofData) {
          case 1:
            var.setDataType(DataType.UBYTE);
            dataType = DataType.BYTE.getPrimitiveClassType();
            break;
          case 2:
            var.setDataType(DataType.USHORT);
            dataType = DataType.SHORT.getPrimitiveClassType();
            break;
          case 4:
            var.setDataType(DataType.UINT);
            dataType = DataType.INT.getPrimitiveClassType();
            break;
          default:
            System.out.println("Unsupported Grid Procuct Dataset!");
            throw new UnsupportedDatasetException("Unsupported Grid Procuct Dataset");
        }

        var.addAttribute(new Attribute(CF.COORDINATES, "lon lat"));

        //var.addAttribute(new Attribute(CDM.UNSIGNED, "true"));
        //var.addAttribute(new Attribute(CDM.UNITS, "percent"));

        if (var.getDataType() == DataType.UBYTE) {
          var.addAttribute(new Attribute("_missing_value", (byte) -1));
          var.addAttribute(new Attribute(CDM.ADD_OFFSET, gridprocuctSecondHeader.dataBaseValue));
          var.addAttribute(new Attribute(CDM.SCALE_FACTOR, gridprocuctSecondHeader.dataBaseValue));

        } else {
          var.addAttribute(new Attribute("_missing_value", (short) -1));
          var.addAttribute(new Attribute(CDM.ADD_OFFSET, gridprocuctSecondHeader.dataBaseValue));
          var.addAttribute(new Attribute(CDM.SCALE_FACTOR, gridprocuctSecondHeader.dataScale));
        }

        // size and beginning data position in file
        int vsize = velems;
        long begin = this.firstHeader.recordsOfHeader * this.firstHeader.recoderLength;
        if (debug)
          log.warn(" name= " + vname + " vsize=" + vsize + " velems=" + velems + " begin= " + begin + "\n");
        var.setSPobject(new Vinfo(vsize, begin, nx, ny, dataType, this.firstHeader.byteOrder));
        String coordinates = "lon lat time";
        var.addAttribute(new Attribute(_Coordinate.Axes, coordinates));

        ncfile.addVariable(ncfile.getRootGroup(), var);

        // we have to project in order to find the origin
        //    	    ProjectionPointImpl start = (ProjectionPointImpl) projection.latLonToProj( new LatLonPointImpl( lat1, lon1));
        //    	    if (debug) System.out.println("start at proj coord "+start);

        LatLonPointImpl startPnt = new LatLonPointImpl(gridprocuctSecondHeader.leftTopLat, gridprocuctSecondHeader.leftTopLon);
        LatLonPointImpl endPnt = new LatLonPointImpl(gridprocuctSecondHeader.rightBottomLat, gridprocuctSecondHeader.rightBottomLon);
        if (debug) System.out.println("start at geo coord :" + startPnt);

        Variable yaxis = new Variable(ncfile, null, null, "lat");
        yaxis.setDataType(DataType.DOUBLE);
        yaxis.setDimensions("lat");
        yaxis.addAttribute(new Attribute(CDM.LONG_NAME, "latitude"));
        yaxis.addAttribute(new Attribute(CDM.UNITS, "degree_north"));
        yaxis.addAttribute(new Attribute(_Coordinate.AxisType, "latitude"));
        double[] data = new double[ny];

        double dy = (endPnt.getLatitude() - startPnt.getLatitude()) / (ny - 1);
        for (int i = 0; i < data.length; i++) {
          data[i] = startPnt.getLatitude() + i * dy; // starty + i*dy;
        }

        dataA = Array.factory(DataType.DOUBLE, new int[]{ny}, data);
        yaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, yaxis);

        // create coordinate variables
        Variable xaxis = new Variable(ncfile, null, null, "lon");
        xaxis.setDataType(DataType.DOUBLE);
        xaxis.setDimensions("lon");
        xaxis.addAttribute(new Attribute(CDM.LONG_NAME, "longitude"));
        xaxis.addAttribute(new Attribute(CDM.UNITS, "degree_east"));
        xaxis.addAttribute(new Attribute(_Coordinate.AxisType, "longitude"));
        data = new double[nx];

        double dx = (endPnt.getLongitude() - startPnt.getLongitude()) / (nx - 1);
        for (int i = 0; i < data.length; i++) {
          data[i] = startPnt.getLongitude() + i * dx; // startx + i*dx;

        }

        dataA = Array.factory(DataType.DOUBLE, new int[]{nx}, data);
        xaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, xaxis);

        //    	    // coordinate transform variable
        //    	    Variable ct = new Variable( ncfile, null, null, projection.getClassName());
        //    	    ct.setDataType( DataType.CHAR);
        //    	    ct.setDimensions( "");
        //    	    List params = projection.getProjectionParameters();
        //    	    for (int i = 0; i < params.size(); i++) {
        //    	      Parameter p = (Parameter) params.get(i);
        //    	      ct.addAttribute( new Attribute(p));
        //    	    }
        //    	    ct.addAttribute( new Attribute(_Coordinate.TransformType, "Projection"));
        //    	    ct.addAttribute( new Attribute(_Coordinate.Axes, "x y "));
        //    	    // fake data
        //    	    dataA = Array.factory(DataType.CHAR, new int[] {});
        //    	    dataA.setChar(dataA.getIndex(), ' ');
        //    	    ct.setCachedData(dataA, false);
        //
        //    	    ncfile.addVariable(null, ct);
        //  	    ncfile.addAttribute( null, new Attribute("Conventions", _Coordinate.Convention));

        // add more addAttributes

        //		    String timeCoordName = "time";
        //		    Variable taxis = new Variable(ncfile, null, null, timeCoordName);
        //		    taxis.setDataType(DataType.DOUBLE);
        //		    taxis.setDimensions("time");
        //		    taxis.addAttribute( new Attribute(CDM.LONG_NAME, "time since base date"));
        //		    taxis.addAttribute( new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
        //		    double [] tdata = new double[1];
        //		    tdata[0] = cal.getTimeInMillis();
        //		    Array dataA = Array.factory(DataType.DOUBLE, new int[] {1}, tdata);
        //		    taxis.setCachedData( dataA, false);
        //		    DateFormatter formatter = new DateFormatter();
        //		    taxis.addAttribute( new Attribute(CDM.UNITS, "msecs since "+formatter.toDateTimeStringISO(new Date(0))));
        //		    ncfile.addVariable(null, taxis);
        //
        break;
      }
      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_DISCREET:
        throw new UnsupportedDatasetException();

      case AwxFileFirstHeader.AWX_PRODUCT_TYPE_GRAPH_ANALIYSIS:
        throw new UnsupportedDatasetException();
    }

    // finish
    ncfile.finish();
  }

  String getGeoSatelliteProductName(int channel) {
    String vname;
    switch (channel) {
      case 1:
        vname = "IR";
        break;
      case 2:
        vname = "WV";
        break;
      case 3:
        vname = "IR_WV";
        break;
      case 4:
        vname = "VIS";
        break;
      case 34:
        vname = "DST";
        break;
      default: {
        System.out.println("Unsupported GeoSatellite Procuct Dataset!");
        return null;
      }
    }
    return vname;
  }

  String getGridProductName(int feature) {
    String vname;
    switch (feature) {
      case 1:
        vname = "SST";
        break;
      case 2:
        vname = "SeaICE";
        break;
      case 3:
        vname = "SeaICEDensity";
        break;
      case 4:
        vname = "LongWaveRadiation";
        break;
      case 5:
        vname = "plantIdx";
        break;
      case 6:
        vname = "plantIdxRatio";
        break;
      case 7:
        vname = "snow";
        break;
      case 8:
        vname = "soilHumidity";
        break;
      case 9:
        vname = "sunshine";
        break;
      case 10:
        vname = "cloudTopHeight";
        break;
      case 11:
        vname = "cloudTopTemp";
        break;
      case 12:
        vname = "lowCloudVolume";
        break;
      case 13:
        vname = "highCloudVolume";
        break;
      case 14:
        vname = "precipIdx1hour";
        break;
      case 15:
        vname = "precipIdx6hour";
        break;
      case 16:
        vname = "precipIdx12hour";
        break;
      case 17:
        vname = "precipIdx24hour";
        break;
      case 18:
        vname = "waterVapor";
        break;
      case 19:
        vname = "cloudTemp";
        break;
      case 501:
        vname = "TOVS";
        break;
      case 502:
        vname = "TOVS";
        break;
      case 503:
        vname = "TOVS";
        break;
      case 504:
        vname = "TOVS";
        break;
      case 505:
        vname = "TOVS";
        break;
      case 506:
        vname = "TOVS";
        break;
      case 507:
        vname = "TOVS";
        break;
      default: {
        System.out.println("Unsupported Satellite Grid Procuct Dataset!");
        return null;
      }
    }
    return vname;
  }

  String getPhysElemUnits(int feature) {
    String unit;

    switch (feature) {
      case 1:
        unit = "K";
        break;
      case 2:
        unit = "";
        break;
      case 3:
        unit = "";
        break;
      case 4:
        unit = "W/m2";
        break;
      case 5:
        unit = "";
        break;
      case 6:
        unit = "";
        break;
      case 7:
        unit = "";
        break;
      case 8:
        unit = "kg/m3";
        break;
      case 9:
        unit = "hour";
        break;
      case 10:
        unit = "hPa";
        break;
      case 11:
        unit = "K";
        break;
      case 12:
        unit = "";
        break;
      case 13:
        unit = "";
        break;
      case 14:
        unit = "mm/hour";
        break;
      case 15:
        unit = "mm/(6 hour)";
        break;
      case 16:
        unit = "mm/(12 hour)";
        break;
      case 17:
        unit = "mm/(24 hour)";
        break;
      case 18:
        unit = "";
        break;
      case 19:
        unit = "K";
        break;
      case 501:
        unit = "";
        break;
      case 502:
        unit = "mm";
        break;
      case 503:
        unit = "Db";
        break;
      case 504:
        unit = "W/m2";
        break;
      case 505:
        unit = "hPa";
        break;
      case 506:
        unit = "K";
        break;
      case 507:
        unit = "";
        break;
      default: {
        System.out.println("Unsupported Satellite Grid Procuct Dataset!");
        return null;
      }

    }
    return unit;
  }

  int getCompressType() {
    return Z_type;
  }

  // Return the string of entity ID for the GINI image file

//  ////////////////////////////////////////////////////////////////////////////////////////////////////////
//
  // variable info for reading/writing

  static class Vinfo {
    int vsize; // size of array in bytes. if isRecord, size per record.
    long begin; // offset of start of data from start of file
    int nx;
    int ny;
    Class classType;
    short byteOrder;

    Vinfo(int vsize, long begin, int x, int y, Class dt, short byteOrder) {
      this.vsize = vsize;
      this.begin = begin;
      this.nx = x;
      this.ny = y;
      this.classType = dt;
      this.byteOrder = byteOrder;
    }
  }

}