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

package ucar.nc2.iosp.gempak;

import ucar.nc2.iosp.grid.GridDefRecord;

/**
 * Class to hold the grid navigation information.
 *

 *     PROJ is the map projection, projection angles, and margins separated
 *     by slashes and an optional image drop flag separated from the rest by
 *     a bar:
 *
 *       map proj / ang1;ang2;ang3 / l;b;r;t (margins) | image drop flag
 *
 *     For all map projections, the lower left and upper right corners of
 *     the graphics area should be specified in GAREA.
 *
 *     The following simple map projections may be specified:
 *
 *       MER     Mercator
 *       NPS     North Polar Stereographic
 *       SPS     South Polar Stereographic
 *       LCC     Northern Hemisphere Lambert Conic Conformal
 *       SCC     Southern Hemisphere Lambert Conic Conformal
 *       CED     Cylindrical Equidistant
 *       MCD     Modified Cylindrical Equidistant
 *       NOR     North Orthographic
 *       SOR     South Orthographic
 *
 *
 *     The following full map projections may also be specified:
 *
 *       MER     (CYL)   Mercator
 *       CED     (CYL)   Cylindrical Equidistant
 *       MCD     (CYL)   Modified Cylindrical Equidistant
 *       STR     (AZM)   Polar Stereographic
 *       AED     (AZM)   Azimuthal Equidistant
 *       ORT     (AZM)   Orthographic
 *       LEA     (AZM)   Lambert equal area
 *       GNO     (AZM)   Gnomonic
 *       LCC     (CON)   Northern Hemisphere Lambert Conic Conformal
 *       SCC     (CON)   Southern Hemisphere Lambert Conic Conformal
 *
 *
 *     In addition for full map projections, three angles MUST be specified
 *     in PROJ.  The angles have the following meanings for the different
 *     projection classes:
 *
 *       CYL     angle1 -- latitude of origin on the projection cylinder
 *                         0 = Equator
 *               angle2 -- longitude of origin on the projection cylinder
 *                         0 = Greenwich meridian
 *               angle3 -- angle to skew the projection by rotation of
 *                         the cylindrical surface of projection about
 *                         the line from the Earth's center passing
 *                         through the origin point. This results in
 *                         curved latitude and longitude lines.
 *
 *                         If angle3 is greater than 360 or less than -360
 *                         degrees, then the rectangular Cartesian coordinate
 *                         system on the projection plane is rotated
 *                         +/- |angle3|-360 degrees. This results in
 *                         latitude and longitude lines that are skewed
 *                         with respect to the edge of the map.  This option
 *                          is only valid when specifying a map projection and
 *                          is not available for grid projections.
 *
 *                         The difference between |angle3| < 360 and
 *                         |angle3| > 360 is that, in the former case,
 *                         the rotation is applied to the developable
 *                         cylindrical surface before projection and
 *                         subsequent development; while, in the latter
 *                         case, the rotation is applied to the Cartesian
 *                         coordinate system in the plane after development.
 *                         Development here refers to the mathematical
 *                         flattening of the surface of projection into a
 *                         planar surface.
 *
 *     Exception:
 *
 *     MCD     angle1 -- scaling factor for latitude
 *                       0 = default scaling (1/cos(avglat))
 *             angle2 -- longitude of origin (center longitude)
 *             angle3 -- not used
 *
 *
 *     AZM     angle1 -- latitude of the projection's point of tangency
 *             angle2 -- longitude of the projection's point of tangency
 *             angle3 -- angle to skew the projection by rotation about
 *                       the line from the Earth's center passing
 *                       through the point of tangency
 *
 *     CON     angle1 -- standard latitude 1
 *             angle2 -- polon is the central longitude
 *             angle3 -- standard latitude 2
 *
 *     The angles for the full map projection types are given as three numbers
 *     separated with semicolons.  Note that THREE angles must be entered even
 *     if some angles are not used.
 *
 *     Note that transverse projections may be obtained using a cylindrical
 *     projection with the first angle set to either 90 or -90.  The second
 *     angle is the longitude at which the cylinder axis intersects the
 *     equator.  This will be the transformed location of the "south" pole
 *     when the first angle is 90 or the "north" pole when the first angle
 *     is -90.  For example, if angle1 = 90 and angle2 = 0, the axis of the
 *     cylinder of projection is perpendicular to the earth's axis and enters
 *     the earth at 0N 0E and emerges at 0N 180E.  The great circle formed
 *     by 90E and 90W becomes the "equator" on the cylinder.  This cylinder
 *     is ideal for a transverse cylindrical projection of locations on the
 *     continent of North America.
 *

 *
 *
 * @author IDV Development Team
 */
public class NavigationBlock extends GridDefRecord {

    /** raw values */
    float[] vals = null;

    /** projection type */
    private String proj;

    /**
     * Create a new grid nav block
     */
    public NavigationBlock() {}

    /**
     * Create a new grid nav block with the values
     *
     * @param words   analysis block values
     */
    public NavigationBlock(float[] words) {
        setValues(words);
    }

    /**
     * Set the grid nav block values
     *
     * @param values   the raw values
     */
    public void setValues(float[] values) {
        vals = values;
        proj = GempakUtil.ST_ITOC(Float.floatToIntBits(vals[1])).trim();
        addParam(PROJ, proj);
        addParam(GDS_KEY, this.toString());
        setParams();
    }

    /**
     * Print out the navibation block so it looks something like this:
     *

     *        PROJECTION:          LCC
     *        ANGLES:                25.0   -95.0    25.0
     *        GRID SIZE:           93  65
     *        LL CORNER:              12.19   -133.46
     *        UR CORNER:              57.29    -49.38
     *

     *
     * @return  a String representation of this.
     */
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append("\n    PROJECTION:         ");
        buf.append(proj);
        buf.append("\n    ANGLES:             ");
        buf.append(vals[10]);
        buf.append("  ");
        buf.append(vals[11]);
        buf.append("  ");
        buf.append(vals[12]);
        buf.append("\n    GRID SIZE:          ");
        buf.append(vals[4]);
        buf.append("  ");
        buf.append(vals[5]);
        buf.append("\n    LL CORNER:          ");
        buf.append(vals[6]);
        buf.append("  ");
        buf.append(vals[7]);
        buf.append("\n    UR CORNER:          ");
        buf.append(vals[8]);
        buf.append("  ");
        buf.append(vals[9]);
        return buf.toString();
    }

    /**
     * Set the parameters for the GDS.
     * TODO Add the following:
     *     The following simple map projections may be specified:
     *
     *       NOR     North Orthographic
     *       SOR     South Orthographic
     *       MER     (CYL)   Mercator
     *       CED     (CYL)   Cylindrical Equidistant
     *       MCD     (CYL)   Modified Cylindrical Equidistant
     *       STR     (AZM)   Polar Stereographic
     *       AED     (AZM)   Azimuthal Equidistant
     *       ORT     (AZM)   Orthographic
     *       LEA     (AZM)   Lambert equal area
     *       GNO     (AZM)   Gnomonic
     * </TODO>
     *
     *     In addition for full map projections, three angles MUST be specified
     *     in PROJ.  The angles have the following meanings for the different
     *     projection classes:
     *
     *       CYL     angle1 -- latitude of origin on the projection cylinder
     *                         0 = Equator
     *               angle2 -- longitude of origin on the projection cylinder
     *                         0 = Greenwich meridian
     *               angle3 -- angle to skew the projection by rotation of
     *                         the cylindrical surface of projection about
     *                         the line from the Earth's center passing
     *                         through the origin point. This results in
     *                         curved latitude and longitude lines.
     *
     *                         If angle3 is greater than 360 or less than -360
     *                         degrees, then the rectangular Cartesian coordinate
     *                         system on the projection plane is rotated
     *                         +/- |angle3|-360 degrees. This results in
     *                         latitude and longitude lines that are skewed
     *                         with respect to the edge of the map.  This option
     *                          is only valid when specifying a map projection and
     *                          is not available for grid projections.
     *
     *                         The difference between |angle3| < 360 and
     *                         |angle3| > 360 is that, in the former case,
     *                         the rotation is applied to the developable
     *                         cylindrical surface before projection and
     *                         subsequent development; while, in the latter
     *                         case, the rotation is applied to the Cartesian
     *                         coordinate system in the plane after development.
     *                         Development here refers to the mathematical
     *                         flattening of the surface of projection into a
     *                         planar surface.
     *
     *     Exception:
     *
     *     MCD     angle1 -- scaling factor for latitude
     *                       0 = default scaling (1/cos(avglat))
     *             angle2 -- longitude of origin (center longitude)
     *             angle3 -- not used
     *
     *
     *     AZM     angle1 -- latitude of the projection's point of tangency
     *             angle2 -- longitude of the projection's point of tangency
     *             angle3 -- angle to skew the projection by rotation about
     *                       the line from the Earth's center passing
     *                       through the point of tangency
     *
     *     CON     angle1 -- standard latitude 1
     *             angle2 -- polon is the central longitude
     *             angle3 -- standard latitude 2
     *
     */
    private void setParams() {
        String angle1 = String.valueOf(vals[10]);
        String angle2 = String.valueOf(vals[11]);
        String angle3 = String.valueOf(vals[12]);
        String lllat  = String.valueOf(vals[6]);
        String lllon  = String.valueOf(vals[7]);
        String urlat  = String.valueOf(vals[8]);
        String urlon  = String.valueOf(vals[9]);
        addParam(NX, String.valueOf(vals[4]));
        addParam(NY, String.valueOf(vals[5]));
        addParam(LA1, lllat);
        addParam(LO1, lllon);
        addParam(LA2, urlat);
        addParam(LO2, urlon);
      switch (proj) {
        case "STR":
        case "NPS":
        case "SPS":
          addParam(LOV, angle2);
          // TODO:  better to just set pole?
          if (proj.equals("SPS")) {
            addParam("NpProj", "false");
          }
          break;
        case "LCC":
        case "SCC":
          addParam(LATIN1, angle1);
          addParam(LOV, angle2);
          addParam(LATIN2, angle3);
          // TODO: test this
          break;
        case "MER":
        case "MCD":
          String standardLat;
          if (vals[10] == 0) {  // use average latitude
            float lat = (vals[8] + vals[6]) / 2;
            standardLat = String.valueOf(lat);
          } else {
            standardLat = angle1;
          }
          addParam("Latin", standardLat);
          addParam(LOV, angle2);
          break;
        case "CED":
          double lllatv = vals[6];
          double lllonv = vals[7];
          double urlatv = vals[8];
          double urlonv = vals[9];
          if (urlonv <= lllonv) {
            urlonv += 360.;
          }
          double dx = Math.abs((urlonv - lllonv) / (vals[4] - 1));
          double dy = Math.abs((urlatv - lllatv) / (vals[5] - 1));
          addParam(DX, String.valueOf(dx));
          addParam(DY, String.valueOf(dy));
          addParam(LO2, String.valueOf(urlonv));

          break;
      }
    }

    /**
     * Get a short name for this GDSKey for the netCDF group.
     * Subclasses should implement as a short description
     * @return short name
     */
    public String getGroupName() {
      return proj + "_" + getParam(NX) + "x" + getParam(NY);
    }

}


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

package ucar.nc2.geotiff;

import ucar.ma2.*;
import ucar.nc2.Attribute;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis2D;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.dt.GridDatatype;
import ucar.unidata.geoloc.*;

import java.io.IOException;

/**
 * This allows writing non-regular data, eg swath data.
 * @author caron
 * @since 3/15/13
 */
public class GeoTiffWriter2 extends GeotiffWriter {

  public GeoTiffWriter2(String fileOut) {
    super(fileOut);
  }

  // variant on writing a subsetted grid as a geotiff file
  // seems to handle swath as well ??
  public void writeGrid(String gridDataset_filename, String gridName, int time, int level, boolean greyScale, LatLonRect pt) throws IOException {
    try (GridDataset dataset = ucar.nc2.dt.grid.GridDataset.open(gridDataset_filename)) {
      GridDatatype grid = dataset.findGridDatatype(gridName);
      if (grid == null) {
        throw new IllegalArgumentException("No grid named " + gridName + " in fileName");
      }

      GridCoordSystem gcs = grid.getCoordinateSystem();
      ProjectionImpl proj = grid.getProjection();

      if (!gcs.isRegularSpatial()) {
        Attribute att = dataset.findGlobalAttributeIgnoreCase("datasetId");
        if (att != null && att.getStringValue().contains("DMSP")) {  // LOOK!!
          writeSwathGrid(gridDataset_filename, gridName, time, level, greyScale, pt);
          return;
        } else {
          throw new IllegalArgumentException("Must have 1D x and y axes for " + grid.getFullName());
        }

      }

      CoordinateAxis1D xaxis = (CoordinateAxis1D) gcs.getXHorizAxis();
      CoordinateAxis1D yaxis = (CoordinateAxis1D) gcs.getYHorizAxis();
      if (!xaxis.isRegular() || !yaxis.isRegular()) {
        throw new IllegalArgumentException("Must be evenly spaced grid = " + grid.getFullName());
      }

      // read in data
      Array data = grid.readDataSlice(time, level, -1, -1);
      Array lon = xaxis.read();
      Array lat = yaxis.read();

      // units may need to be scaled to meters
      double scaler = (xaxis.getUnitsString().equalsIgnoreCase("km")) ? 1000.0 : 1.0;

      if (yaxis.getCoordValue(0) < yaxis.getCoordValue(1)) {
        data = data.flip(0);
        lat = lat.flip(0);
      }

      if (gcs.isLatLon()) {
        data = geoShiftDataAtLon(data, lon);
        lon = geoShiftLon(lon);
      }
      // now it is time to subset the data out of latlonrect
      // it is assumed that latlonrect pt is in +-180
      LatLonPointImpl llp0 = pt.getLowerLeftPoint();
      LatLonPointImpl llpn = pt.getUpperRightPoint();
      double minLon = llp0.getLongitude();
      double minLat = llp0.getLatitude();
      double maxLon = llpn.getLongitude();
      double maxLat = llpn.getLatitude();

      // (x1, y1) is upper left point and (x2, y2) is lower right point
      int x1;
      int x2;
      int y1;
      int y2;
      double xStart;
      double yStart;
      if (!gcs.isLatLon()) {

        ProjectionPoint pjp0 = proj.latLonToProj(maxLat, minLon);
        x1 = getXIndex(lon, pjp0.getX(), 0);
        y1 = getYIndex(lat, pjp0.getY(), 0);
        yStart = pjp0.getY() * 1000.0;  //latArray[y1];
        xStart = pjp0.getX() * 1000.0;  //lonArray[x1];
        ProjectionPoint pjpn = proj.latLonToProj(minLat, maxLon);
        x2 = getXIndex(lon, pjpn.getX(), 1);
        y2 = getYIndex(lat, pjpn.getY(), 1);

      } else {
        xStart = minLon;
        yStart = maxLat;
        x1 = getLonIndex(lon, minLon, 0);
        y1 = getLatIndex(lat, maxLat, 0);
        x2 = getLonIndex(lon, maxLon, 1);
        y2 = getLatIndex(lat, minLat, 1);
      }

      // data must go from top to bottom
      double xInc = xaxis.getIncrement() * scaler;
      double yInc = Math.abs(yaxis.getIncrement()) * scaler;

      // subsetting data inside the box
      Array data1 = getYXDataInBox(data, x1, x2, y1, y2);

      if (pageNumber > 1) {
        geotiff.initTags();
      }

      // write it out
      writeGrid(grid, data1, greyScale, xStart, yStart, xInc, yInc, pageNumber);
      pageNumber++;
    }
  }

  /**
   * Write Swath Grid data to the geotiff file.
   *
   * @param fileName  _more_
   * @param gridName  _more_
   * @param time      _more_
   * @param level     _more_
   * @param greyScale _more_
   * @param llr       _more_
   * @throws IOException _more_
   */
  private void writeSwathGrid(String fileName, String gridName, int time, int level, boolean greyScale, LatLonRect llr) throws IOException {

    GridDataset dataset = ucar.nc2.dt.grid.GridDataset.open(fileName);
    GridDatatype grid = dataset.findGridDatatype(gridName);
    GridCoordSystem gcs = grid.getCoordinateSystem();
    ProjectionImpl proj = grid.getProjection();

    CoordinateAxis2D xaxis = (CoordinateAxis2D) gcs.getXHorizAxis();
    CoordinateAxis2D yaxis = (CoordinateAxis2D) gcs.getYHorizAxis();

    // read in data
    Array data = grid.readDataSlice(time, level, -1, -1);
    Array lon = xaxis.read();
    Array lat = yaxis.read();

    double[] swathInfo = getSwathLatLonInformation(lat, lon);

    // units may need to be scaled to meters
    double scaler = (xaxis.getUnitsString().equalsIgnoreCase("km")) ? 1000.0 : 1.0;

    //if (yaxis.getCoordValue(0, 0) < yaxis.getCoordValue(0, 1)) {//???
    data = data.flip(0);
    //lat = lat.flip(0);
    //}

    if (gcs.isLatLon()) {
      data = geoShiftDataAtLon(data, lon);
      lon = geoShiftLon(lon);
    }

    double minLon;
    double minLat;
    double maxLon;
    double maxLat;
    double xStart;
    double yStart;  //upper right point

    double xInc = swathInfo[0] * scaler;
    double yInc = swathInfo[1] * scaler;

    // (x1, y1) is upper left point and (x2, y2) is lower right point
    int x1;
    int x2;
    int y1;
    int y2;

    if (llr == null)  { //get the whole area
      minLon = swathInfo[4];
      minLat = swathInfo[2];
      maxLon = swathInfo[5];
      maxLat = swathInfo[3];
      xStart = minLon;
      yStart = maxLat;
      x1 = 0;
      y1 = 0;
      x2 = (int) ((maxLon - minLon) / xInc + 0.5);
      y2 = (int) ((maxLat - minLat) / yInc + 0.5);

    } else {           //assign the special area  surrounded by the llr
      LatLonPointImpl llp0 = llr.getLowerLeftPoint();
      LatLonPointImpl llpn = llr.getUpperRightPoint();
      minLon = (llp0.getLongitude() < swathInfo[4])
              ? swathInfo[4]
              : llp0.getLongitude();
      minLat = (llp0.getLatitude() < swathInfo[2])
              ? swathInfo[2]
              : llp0.getLatitude();
      maxLon = (llpn.getLongitude() > swathInfo[5])
              ? swathInfo[5]
              : llpn.getLongitude();
      maxLat = (llpn.getLatitude() > swathInfo[3])
              ? swathInfo[3]
              : llpn.getLatitude();

      //construct the swath  LatLonRect
      LatLonPointImpl pUpLeft = new LatLonPointImpl(swathInfo[3], swathInfo[4]);
      LatLonPointImpl pDownRight = new LatLonPointImpl(swathInfo[2], swathInfo[5]);
      LatLonRect swathLLR = new LatLonRect(pUpLeft, pDownRight);
      LatLonRect bIntersect = swathLLR.intersect(llr);
      if (bIntersect == null) {
        throw new IllegalArgumentException("The assigned extent of latitude and longitude is unvalid. No intersection with the swath extent");
      }

      xStart = minLon;
      yStart = maxLat;
      x1 = (int) ((minLon - swathInfo[4]) / xInc + 0.5);
      y1 = (int) Math.abs((maxLat - swathInfo[3]) / yInc + 0.5);
      x2 = (int) ((maxLon - swathInfo[4]) / xInc + 0.5);
      y2 = (int) Math.abs((minLat - swathInfo[3]) / yInc + 0.5);
    }

    if (!gcs.isLatLon()) {
      ProjectionPoint pjp0 = proj.latLonToProj(maxLat, minLon);
      x1 = getXIndex(lon, pjp0.getX(), 0);
      y1 = getYIndex(lat, pjp0.getY(), 0);
      yStart = pjp0.getY() * scaler;  //latArray[y1];
      xStart = pjp0.getX() * scaler;  //lonArray[x1];
      ProjectionPoint pjpn = proj.latLonToProj(minLat, maxLon);
      x2 = getXIndex(lon, pjpn.getX(), 1);
      y2 = getYIndex(lat, pjpn.getY(), 1);
    } else {
      //calculate the x1, x2, y1, y2, xstart, ystart.
    }

    Array targetImage = getTargetImagerFromSwath(lat, lon, data, swathInfo);
    Array interpolatedImage = interpolation(targetImage);
    Array clippedImage = getClippedImageFromInterpolation(interpolatedImage, x1, x2, y1, y2);
    //Array clippedImage = getYXDataInBox(interpolatedImage, x1, x2, y1, y2);

    if (pageNumber > 1) {
      geotiff.initTags();
    }

    writeGrid(grid, clippedImage, greyScale, xStart, yStart, xInc, yInc, pageNumber);
    pageNumber++;

  }

  private int getXIndex(Array aAxis, double value, int side) {

    IndexIterator aIter = aAxis.getIndexIterator();
    int count = 0;
    int isInd = 0;

    double aValue = aIter.getFloatNext();
    if ((aValue == value) || (aValue > value)) {
      return 0;
    }

    while (aIter.hasNext() && (aValue < value)) {
      count++;
      aValue = aIter.getFloatNext();
      if (aValue == value) {
        isInd = 1;
      }
    }
    if (isInd == 1) {
      count += side;
    }
    count -= side;

    return count;
  }

  private int getYIndex(Array aAxis, double value, int side) {

    IndexIterator aIter = aAxis.getIndexIterator();
    int count = 0;
    int isInd = 0;

    double aValue = aIter.getFloatNext();
    if ((aValue == value) || (aValue < value)) {
      return 0;
    }

    while (aIter.hasNext() && (aValue > value)) {
      count++;
      aValue = aIter.getFloatNext();
      if (aValue == value) {
        isInd = 1;
      }
    }

    if (isInd == 1) {
      count += side;
    }
    count -= side;
    return count;
  }

  private int getLatIndex(Array lat, double value, int side) {
    IndexIterator latIter = lat.getIndexIterator();
    int count = 0;
    int isInd = 0;

    //LatLonPoint p0 = new LatLonPointImpl(lat.getFloat(ind.set(0)), 0);

    double xlat = latIter.getFloatNext();
    if (xlat == value) {
      return 0;
    }

    while (latIter.hasNext() && (xlat > value)) {
      count++;
      xlat = latIter.getFloatNext();
      if (xlat == value) {
        isInd = 1;
      }
    }

    if (isInd == 1) {
      count += side;
    }
    count -= side;
    return count;
  }

  private int getLonIndex(Array lon, double value, int side) {
    IndexIterator lonIter = lon.getIndexIterator();
    int count = 0;
    int isInd = 0;

    // double xlon = lon.getFloat(ind.set(0));
    float xlon = lonIter.getFloatNext();
    if (xlon > 180) {
      xlon = xlon - 360;
    }
    if (xlon == value) {
      return 0;
    }

    while (lonIter.hasNext() && (xlon < value)) {
      count++;
      xlon = lonIter.getFloatNext();
      if (xlon > 180) {
        xlon = xlon - 360;
      }
      if (xlon == value) {
        isInd = 1;
      }
    }

    if (isInd == 1) {
      count += side;
    }
    count -= side;
    return count;
  }

  private Array getYXDataInBox(Array data, int x1, int x2, int y1, int y2) throws java.io.IOException {
    int rank = data.getRank();
    int[] start = new int[rank];
    int[] shape = new int[rank];
    for (int i = 0; i < rank; i++) {
      start[i] = 0;
      shape[i] = 1;
    }

    if ((y1 >= 0) && (y2 >= 0)) {
      start[0] = y1;
      shape[0] = y2 - y1;
    }
    if ((x1 >= 0) && (x2 >= 0)) {
      start[1] = x1;
      shape[1] = x2 - x1;
    }

    // read it
    Array dataVolume;
    try {
      dataVolume = data.section(start, shape);
    } catch (Exception e) {
      throw new java.io.IOException(e.getMessage());
    }

    return dataVolume;
  }

  private Array getClippedImageFromInterpolation(Array arr, int x1, int x2, int y1, int y2) {
    int[] srcShape = arr.getShape();
    int rank = arr.getRank();
    int[] start = new int[rank];
    int[] shape = new int[rank];
    for (int i = 0; i < rank; i++) {
      start[i] = 0;
      shape[i] = 1;
    }

    if ((y1 >= 0) && (y2 >= 0)) {
      start[0] = y1;
      shape[0] = y2 - y1;
    }
    if ((x1 >= 0) && (x2 >= 0)) {
      start[1] = x1;
      shape[1] = x2 - x1;
    }

    Array dataVolume = Array.factory(DataType.FLOAT, shape);
    int count = 0;
    for (int i = y1; i < y2; i++) {
      for (int j = x1; j < x2; j++) {
        int index = i * srcShape[1] + j;
        if (index >= srcShape[0] * srcShape[1]) {
          index = srcShape[0] * srcShape[1] - 1;
        }
        float curValue = arr.getFloat(index);
        if (count >= shape[0] * shape[1]) {
          count = shape[0] * shape[1] - 1;
        }
        dataVolume.setFloat(count, curValue);
        count++;
      }
    }
    return dataVolume;
  }

  private Array getTargetImagerFromSwath(Array lat, Array lon, Array data, double[] swathInfo) {
    int srcDataHeight = data.getShape()[0];
    int srcDataWidth = data.getShape()[1];
    int BBoxHeight = (int) ((swathInfo[3] - swathInfo[2]) / swathInfo[1] + 0.5);
    int BBoxWidth = (int) ((swathInfo[5] - swathInfo[4]) / swathInfo[0] + 0.5);
    int BBoxShape[] = {BBoxHeight, BBoxWidth};  //[height, width]
    Array bBoxArray = Array.factory(DataType.FLOAT, BBoxShape);
    double startLon, startLat;  //upper left and lower right
    startLon = swathInfo[4];
    startLat = swathInfo[3];

    IndexIterator dataIter = data.getIndexIterator();
    IndexIterator latIter = lat.getIndexIterator();
    IndexIterator lonIter = lon.getIndexIterator();
    for (int i = 0; i < srcDataHeight; i++) {
      for (int j = 0; j < srcDataWidth; j++) {
        while (latIter.hasNext() && lonIter.hasNext()
                && dataIter.hasNext()) {
          float curLat = latIter.getFloatNext();
          float curLon = lonIter.getFloatNext();
          float curPix = dataIter.getFloatNext();
          float alreadyValue;
          int curPixelInBBoxIndex =
                  getIndexOfBBFromLatlonOfOri(startLat, startLon,
                          swathInfo[1], swathInfo[0], curLat, curLon,
                          BBoxHeight, BBoxWidth);
          try {
            alreadyValue = bBoxArray.getFloat(curPixelInBBoxIndex);
          } catch (Exception e) {
            alreadyValue = 0;
          }

          if (alreadyValue > 0) {  //This pixel had been filled. So calculate the average
            bBoxArray.setFloat(curPixelInBBoxIndex, (curPix + alreadyValue) / 2);
          } else {
            bBoxArray.setFloat(curPixelInBBoxIndex, curPix);
          }
        }
      }
    }
    return bBoxArray;
  }

  private int getIndexOfBBFromLatlonOfOri(double sLat, double sLon,  //LatLon of the start point
                                  double latInc, double lonInc,  //The increment in Lat/Lon direction
                                  double curLat, double curLon,  //The current Lat/Lon read from the swath image
                                  int bbHeight, int bbWidth)  //The width and height of target image
  {
    double lonDelta = Math.abs((curLon - sLon) / lonInc);
    double latDelta = Math.abs((curLat - sLat) / latInc);

    int row = (int) (lonDelta + 0.5);
    if (row >= bbWidth - 1) {
      row = bbWidth - 2;
    }
    if (row == 0) {
      row = 1;
    }

    int col = (int) (latDelta + 0.5);

    int index = col * bbWidth + row;

    if (index >= bbHeight * bbWidth) {
      index = (col - 1) * bbWidth + row;
    }

    return index;
  }

  /**
   * interpolate the swath data to regular grid
   *
   * @param arr swath array
   * @return regular grid
   */
  private Array interpolation(Array arr) {
    int[] orishape = arr.getShape();
    int width = orishape[1];
    int height = orishape[0];
    int pixelNum = width * height;
    Array interpolatedArray = Array.factory(DataType.FLOAT, orishape);
    for (int i = 0; i < height; i++) {
      for (int j = 0; j < width; j++) {
        int curIndex = i * width + j;
        float curValue = arr.getFloat(curIndex);
        if (curValue == 0)  //Black hole. Need to fill.
        {
          float tempPixelSum = 0;
          int numNeighborHasValue = 0;
          //Get the values of eight neighborhood
          if ((curIndex - 1 >= 0) && (curIndex - 1 < pixelNum)) {
            float left = arr.getFloat(curIndex - 1);
            if (left > 0) {
              tempPixelSum += left;
              numNeighborHasValue++;
            }
          }
          if ((curIndex + 1 >= 0) && (curIndex + 1 < pixelNum)) {
            float right = arr.getFloat(curIndex + 1);
            if (right > 0) {
              tempPixelSum += right;
              numNeighborHasValue++;
            }
          }
          if ((curIndex - width >= 0) && (curIndex - width < pixelNum)) {
            float up = arr.getFloat(curIndex - width);
            if (up > 0) {
              tempPixelSum += up;
              numNeighborHasValue++;
            }
          }
          if ((curIndex + width >= 0) && (curIndex + width < pixelNum)) {
            float down = arr.getFloat(curIndex + width);
            if (down > 0) {
              tempPixelSum += down;
              numNeighborHasValue++;
            }
          }
          if ((curIndex - width - 1 >= 0) && (curIndex - width - 1 < pixelNum)) {
            float upleft = arr.getFloat(curIndex - width - 1);
            if (upleft > 0) {
              tempPixelSum += upleft;
              numNeighborHasValue++;
            }
          }
          if ((curIndex - width + 1 >= 0) && (curIndex - width + 1 < pixelNum)) {
            float upright = arr.getFloat(curIndex - width + 1);
            if (upright > 0) {
              tempPixelSum += upright;
              numNeighborHasValue++;
            }
          }
          if ((curIndex + width - 1 >= 0) && (curIndex + width - 1 < pixelNum)) {
            float downleft = arr.getFloat(curIndex + width - 1);
            if (downleft > 0) {
              tempPixelSum += downleft;
              numNeighborHasValue++;
            }
          }
          if ((curIndex + width + 1 >= 0) && (curIndex + width + 1 < pixelNum)) {
            float downright = arr.getFloat(curIndex + width + 1);
            if (downright > 0) {
              tempPixelSum += downright;
              numNeighborHasValue++;
            }
          }
          if (tempPixelSum > 0) {
            float val =  numNeighborHasValue == 0 ? 0 : tempPixelSum / numNeighborHasValue;
            interpolatedArray.setFloat(curIndex, val);
          }
        } else {
          interpolatedArray.setFloat(curIndex, curValue);
        }
      }
    }

    return interpolatedArray;
  }

  /**
   * get lat lon information from the swath
   *
   * @param lat 2D lat array
   * @param lon 2D lon array
   * @return double array for geotiff
   */
  private double[] getSwathLatLonInformation(Array lat, Array lon) {
    // Calculate the increment of latitude and longitude of original swath data
    // Calculate the size of the boundingBox
    // element0: Longitude increment
    // element1: Latitude increment
    // element2: minLat
    // element3: maxLat
    // element4: minLon
    // element5: maxLon
    // element6: width of the boundingBox
    // element7: height of the boundingBox
    double increment[] = {0, 0, 0, 0, 0, 0, 0, 0};
    IndexIterator latIter = lat.getIndexIterator();
    IndexIterator lonIter = lon.getIndexIterator();

    int numScan = (lat.getShape())[0];
    int numSample = (lat.getShape())[1];

    double maxLat = -91,
            minLat = 91,
            maxLon = -181,
            minLon = 181;

    float firstLineStartLat = 0;
    float firstLineStartLon = 0;
    float firstLineEndLat = 0;
    float firstLineEndLon = 0;
    float lastLineStartLat = 0;
    float lastLineStartLon = 0;
    float lastLineEndLat = 0;
    float lastLineEndLon = 0;

    for (int i = 0; i < numScan; i++) {
      for (int j = 0; j < numSample; j++) {
        if (latIter.hasNext() && lonIter.hasNext()) {
          float curLat = latIter.getFloatNext();
          float curLon = lonIter.getFloatNext();
          if ((i == 0) && (j == 0)) {
            firstLineStartLat = curLat;
            firstLineStartLon = curLon;
          } else if ((i == 0) && (j == numSample - 1)) {
            firstLineEndLat = curLat;
            firstLineEndLon = curLon;
          } else if ((i == numScan - 1) && (j == 0)) {
            lastLineStartLat = curLat;
            lastLineStartLon = curLon;
          } else if ((i == numScan - 1) && (j == numSample - 1)) {
            lastLineEndLat = curLat;
            lastLineEndLon = curLon;
          }
        }
      }
    }
    double[] edgeLat = {firstLineStartLat, firstLineEndLat, lastLineStartLat, lastLineEndLat};
    double[] edgeLon = {firstLineStartLon, firstLineEndLon, lastLineStartLon, lastLineEndLon};
    for (int i = 0; i < edgeLat.length; i++) {
      maxLat = ((maxLat > edgeLat[i])
              ? maxLat
              : edgeLat[i]);
      minLat = ((minLat < edgeLat[i])
              ? minLat
              : edgeLat[i]);
      maxLon = ((maxLon > edgeLon[i])
              ? maxLon
              : edgeLon[i]);
      minLon = ((minLon < edgeLon[i])
              ? minLon
              : edgeLon[i]);
    }

    double xInc1 = Math.abs((firstLineEndLon - firstLineStartLon) / numSample);
    //double xInc2 = Math.abs((lastLineEndLon - lastLineStartLon)/numSample);
    double yInc1 = Math.abs((lastLineStartLat - firstLineStartLat) / numScan);
    //double yInc2 = Math.abs((lastLineEndLat - firstLineEndLat)/numScan);
    increment[0] = xInc1;  // > xInc2 ? xInc1 : xInc2;
    increment[1] = yInc1;  // > yInc2 ? yInc1 : yInc2;
    increment[2] = minLat;
    increment[3] = maxLat;
    increment[4] = minLon;
    increment[5] = maxLon;
    increment[6] = (maxLon - minLon) / xInc1;
    increment[7] = (maxLat - minLat) / yInc1;
    return increment;
  }

  private Array geoShiftLon(Array lon) {
    int count = 0;
    Index lonIndex = lon.getIndex();
    int[] lonShape = lon.getShape();
    ArrayFloat slon = new ArrayFloat(new int[]{lonShape[0]});
    Index slonIndex = slon.getIndex();
    IndexIterator lonIter = lon.getIndexIterator();
    LatLonPoint p0 = new LatLonPointImpl(0, lon.getFloat(lonIndex.set(lonShape[0] - 1)));
    LatLonPoint pN = new LatLonPointImpl(0, lon.getFloat(lonIndex.set(0)));

    while (lonIter.hasNext()) {
      float l = lonIter.getFloatNext();
      if (l > 180.0) {
        count++;
      }
    }

    //checking if the 0 point and the N point are the same point
    int spoint;
    if (p0.getLongitude() == pN.getLongitude()) {
      spoint = lonShape[0] - count - 1;
    } else {
      spoint = lonShape[0] - count;
    }

    if ((count > 0) && (lonShape[0] > count)) {
      for (int j = 1; j < lonShape[0]; j++) {
        int jj;
        if (j >= count) {
          jj = j - count;
        } else {
          jj = j + spoint;
        }

        float dd = lon.getFloat(lonIndex.set(jj));
        slon.setFloat(slonIndex.set(j), (float) LatLonPointImpl.lonNormal(dd));
      }

      if (p0.getLongitude() == pN.getLongitude()) {
        float dd = slon.getFloat(slonIndex.set(lonShape[0] - 1));
        slon.setFloat(slonIndex.set(0), -(float) LatLonPointImpl.lonNormal(dd));
      }
      return slon;

    } else {
      return lon;
    }
  }

  private Array geoShiftDataAtLon(Array data, Array lon) {
    int count = 0;
    int[] shape = data.getShape();
    Index ima = data.getIndex();
    Index ilon = lon.getIndex();
    int[] lonShape = lon.getShape();
    ArrayFloat adata = new ArrayFloat(new int[]{shape[0], shape[1]});
    Index imaa = adata.getIndex();
    IndexIterator lonIter = lon.getIndexIterator();

    LatLonPoint p0 = new LatLonPointImpl(0, lon.getFloat(ilon.set(lonShape[0] - 1)));
    LatLonPoint pN = new LatLonPointImpl(0, lon.getFloat(ilon.set(0)));

    while (lonIter.hasNext()) {
      float l = lonIter.getFloatNext();
      if (l > 180.0) {
        count++;
      }
    }

    //checking if the 0 point and the N point are the same point
    int spoint = 0;
    if (p0.getLongitude() == pN.getLongitude()) {
      spoint = shape[1] - count - 1;
    } else {
      spoint = shape[1] - count;
    }

    if ((count > 0) && (shape[1] > count)) {
      for (int j = 1; j < shape[1]; j++) {
        int jj = 0;

        if (j >= count) {
          jj = j - count;
        } else {
          jj = j + spoint;
        }

        for (int i = 0; i < shape[0]; i++) {
          float dd = data.getFloat(ima.set(i, jj));
          adata.setFloat(imaa.set(i, j), dd);
        }
      }

      if (p0.getLongitude() == pN.getLongitude()) {
        for (int i = 0; i < shape[0]; i++) {
          float dd = adata.getFloat(imaa.set(i, shape[1] - 1));
          adata.setFloat(imaa.set(i, 0), dd);
        }
      }
      return adata;

    } else {
      return data;
    }
  }
}


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

package ucar.nc2.iosp.hdf5;

import ucar.ma2.Section;
import ucar.nc2.Variable;
import ucar.nc2.iosp.LayoutTiled;
import ucar.nc2.util.Misc;
import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * This holds the chunked data storage.
 * level 1A
 * A B-tree, version 1, used for data (node type 1)
 *
 * Version 1 B-trees in HDF5 files an implementation of the B-link tree, in which the sibling nodes at a particular level
 * in the tree are stored in a doubly-linked list
 * The B-link trees implemented by the file format contain one more key than the number of children.
 * In other words, each child pointer out of a B-tree node has a left key and a right key.
 * The pointers out of internal nodes point to sub-trees while the pointers out of leaf nodes point to symbol nodes and
 * raw data chunks. Aside from that difference, internal nodes and leaf nodes are identical.
 *
 * @see "http://www.hdfgroup.org/HDF5/doc/H5.format.html#Btrees"
 * @author caron
 * @since 6/27/12
 */
public class DataBTree {
  private static final boolean debugDataBtree = false;
  private static final boolean debugDataChunk = false;
  private static final boolean debugChunkOrder = false;
  private static java.io.PrintStream debugOut = System.out;

  private final H5header h5;
  private final MemTracker memTracker;

  private final long rootNodeAddress;
  private final Tiling tiling;
  private final int ndimStorage, wantType;

  private Variable owner;

  DataBTree(H5header h5, long rootNodeAddress, int[] varShape, int[] storageSize, MemTracker memTracker) throws IOException {
    this.h5 = h5;
    this.rootNodeAddress = rootNodeAddress;
    this.tiling = new Tiling(varShape, storageSize);
    this.ndimStorage = storageSize.length;
    this.memTracker = memTracker;

    wantType = 1;
  }

  void setOwner(Variable owner) {
    this.owner = owner;
  }

  // used by H5tiledLayoutBB
  DataChunkIterator getDataChunkIteratorFilter(Section want) throws IOException {
    return new DataChunkIterator(want);
  }

  // used by H5tiledLayout
  LayoutTiled.DataChunkIterator getDataChunkIteratorNoFilter(Section want, int nChunkDim) throws IOException {
    /*
    if (if (debugChunkOrder) ) {
    DataChunkIteratorNoFilter iter = new DataChunkIteratorNoFilter(null, nChunkDim);
    int count = 0;
    int last = -1;
    while (iter.hasNext()) {
      LayoutTiled.DataChunk chunk = iter.next();
      System.out.printf("%d : %d%n", count++, tiling.order(chunk.offset));
      if (tiling.order(chunk.offset) <= last)
        System.out.println("HEY");
      last = tiling.order(chunk.offset);
    }
    }*/

    return new DataChunkIteratorNoFilter(want, nChunkDim);
  }

  // An Iterator over the DataChunks in the btree.
  // returns the actual data from the btree leaf (level 0) nodes.
  // used by H5tiledLayout, when there are no filters
  class DataChunkIteratorNoFilter implements LayoutTiled.DataChunkIterator {
    private Node root;
    private int nChunkDim;

    /**
     * Constructor
     *
     * @param want      skip any nodes that are before this section
     * @param nChunkDim number of chunk dimensions - may be less than the offset[] length
     * @throws IOException on error
     */
    DataChunkIteratorNoFilter(Section want, int nChunkDim) throws IOException {
      this.nChunkDim = nChunkDim;
      root = new Node(rootNodeAddress, -1); // should we cache the nodes ???
      int[] wantOrigin = (want != null) ? want.getOrigin() : null;
      root.first(wantOrigin);
    }

    public boolean hasNext() {
      return root.hasNext(); //  && !node.greaterThan(wantOrigin);
    }

    public LayoutTiled.DataChunk next() throws IOException {
      DataChunk dc = root.next();
      int[] offset = dc.offset;
      if (offset.length > nChunkDim) { // may have to eliminate last offset
        offset = new int[nChunkDim];
        System.arraycopy(dc.offset, 0, offset, 0, nChunkDim);
      }
      if (debugChunkOrder) System.out.printf("LayoutTiled.DataChunk next order %d%n", tiling.order(dc.offset));

      return new LayoutTiled.DataChunk(offset, dc.filePos);
    }
  }

  // An Iterator over the DataChunks in the btree.
  // returns the data chunck info from the btree leaf (level 0) nodes
  // used by H5tiledLayoutBB, when there are filters
  class DataChunkIterator {
    private Node root;
    private int[] wantOrigin;

    /**
     * Constructor
     *
     * @param want skip any nodes that are before this section
     * @throws IOException on error
     */
    DataChunkIterator(Section want) throws IOException {
      root = new Node(rootNodeAddress, -1); // should we cache the nodes ???
      wantOrigin = (want != null) ? want.getOrigin() : null;
      root.first(wantOrigin);
    }

    public boolean hasNext() {
      return root.hasNext(); //  && !node.greaterThan(wantOrigin);
    }

    public DataChunk next() throws IOException {
      return root.next();
    }
  }

  // Btree nodes
  class Node {
    private long address;
    private int level, nentries;
    private Node currentNode;

    // level 0 only
    private List<DataChunk> myEntries;
    // level > 0 only
    private int[][] offset; // int[nentries][ndim]; // other levels

    // "For raw data chunk nodes, the child pointer is the address of a single raw data chunk"
    private long[] childPointer; // long[nentries];

    private int currentEntry; // track iteration; LOOK this seems fishy - why not an iterator ??

    Node(long address, long parent) throws IOException {
      if (debugDataBtree) debugOut.println("\n--> DataBTree read tree at address=" + address + " parent= " + parent +
              " owner= " + owner.getNameAndDimensions());

      h5.raf.order(RandomAccessFile.LITTLE_ENDIAN); // header information is in le byte order
      h5.raf.seek( h5.getFileOffset(address));
      this.address = address;

      String magic = h5.raf.readString(4);
      if (!magic.equals("TREE"))
        throw new IllegalStateException("DataBTree doesnt start with TREE");

      int type = h5.raf.readByte();
      level = h5.raf.readByte();
      nentries = h5.raf.readShort();
      if (type != wantType)
        throw new IllegalStateException("DataBTree must be type " + wantType);

      long size = 8 + 2 * h5.getSizeOffsets() + ((long)nentries) * (8 + h5.getSizeOffsets() + 8 + ndimStorage);
      if (memTracker != null) memTracker.addByLen("Data BTree (" + owner + ")", address, size);
      if (debugDataBtree) debugOut.println("    type=" + type + " level=" + level + " nentries=" + nentries + " size = " + size);

      long leftAddress = h5.readOffset();
      long rightAddress = h5.readOffset();
      if (debugDataBtree) debugOut.println("    leftAddress=" + leftAddress + " =0x" + Long.toHexString(leftAddress) +
                " rightAddress=" + rightAddress + " =0x" + Long.toHexString(rightAddress));

      if (level == 0) {
        // read all entries as a DataChunk
        myEntries = new ArrayList<DataChunk>();
        for (int i = 0; i <= nentries; i++) {
          DataChunk dc = new DataChunk(ndimStorage, (i == nentries));
          myEntries.add(dc);
          if (debugDataChunk) debugOut.println(dc);
        }
      } else { // just track the offsets and node addresses
        offset = new int[nentries + 1][ndimStorage];
        childPointer = new long[nentries + 1];
        for (int i = 0; i <= nentries; i++) {
          h5.raf.skipBytes(8); // skip size, filterMask
          for (int j = 0; j < ndimStorage; j++) {
            long loffset = h5.raf.readLong();
            assert loffset < Integer.MAX_VALUE;
            offset[i][j] = (int) loffset;
          }
          this.childPointer[i] = (i == nentries) ? -1 : h5.readOffset();
          if (debugDataBtree) {
            debugOut.print("    childPointer=" + childPointer[i] + " =0x" + Long.toHexString(childPointer[i]));
            for (long anOffset : offset[i]) debugOut.print(" " + anOffset);
            debugOut.println();
          }
        }
      }
    }

    // this finds the first entry we dont want to skip.
    // entry i goes from [offset(i),offset(i+1))
    // we want to skip any entries we dont need, namely those where want >= offset(i+1)
    // so keep skipping until want < offset(i+1)
    void first(int[] wantOrigin) throws IOException {
      if (debugChunkOrder && wantOrigin != null) System.out.printf("Level %d: Tile want %d%n", level, tiling.order(wantOrigin));
      if (level == 0) {
        currentEntry = 0;
        // note nentries-1 - assume dont skip the last one
       for (currentEntry = 0; currentEntry < nentries-1; currentEntry++) {
         DataChunk entry = myEntries.get(currentEntry + 1); // look at the next one
         if (debugChunkOrder) System.out.printf(" Entry=%d: Tile ending order= %d%n", currentEntry, tiling.order(entry.offset));
         if ((wantOrigin == null) || tiling.compare(wantOrigin, entry.offset) < 0) break;
       }
        if (debugChunkOrder) System.out.printf("Level %d use entry= %d%n", level, currentEntry);

      } else {
        currentNode = null;
        for (currentEntry = 0; currentEntry < nentries; currentEntry++) {
          if (debugChunkOrder) System.out.printf(" Entry=%3d offset [%-15s]: Tile order %d-%d%n", currentEntry,
                  Misc.showInts(offset[currentEntry]),
                  tiling.order(offset[currentEntry]), tiling.order(offset[currentEntry + 1]));
          if ((wantOrigin == null) || tiling.compare(wantOrigin, offset[currentEntry + 1]) < 0) {
            currentNode = new Node(childPointer[currentEntry], this.address);
            if (debugChunkOrder) System.out.printf("Level %d use entry= %d%n", level, currentEntry);
            currentNode.first(wantOrigin);
            break;
          }
        }

        // heres the case where its the last entry we want; the tiling.compare() above may fail
        if (currentNode == null) {
          currentEntry = nentries - 1;
          currentNode = new Node(childPointer[currentEntry], this.address);
          currentNode.first(wantOrigin);
        }
      }

      //if (currentEntry >= nentries)
      //  System.out.println("hah");
      assert (nentries == 0) || (currentEntry < nentries) : currentEntry + " >= " + nentries;
    }

    // LOOK - wouldnt be a bad idea to terminate if possible instead of running through all subsequent entries
    boolean hasNext() {
      if (level == 0) {
        return currentEntry < nentries;

      } else {
        if (currentNode.hasNext()) return true;
        return currentEntry < nentries - 1;
      }
    }

    DataChunk next() throws IOException {
      if (level == 0) {
        return myEntries.get(currentEntry++);

      } else {
        if (currentNode.hasNext())
          return currentNode.next();

        currentEntry++;
        currentNode = new Node(childPointer[currentEntry], this.address);
        currentNode.first(null);
        return currentNode.next();
      }
    }
  }

  /* private void dump(DataType dt, List<DataChunk> entries) {
   try {
     for (DataChunk node : entries) {
       if (dt == DataType.STRING) {
         HeapIdentifier heapId = new HeapIdentifier(node.address);
         GlobalHeap.HeapObject ho = heapId.getHeapObject();
         byte[] pa = new byte[(int) ho.dataSize];
         raf.seek(ho.dataPos);
         raf.read(pa);
         debugOut.println(" data at " + ho.dataPos + " = " + new String(pa));
       }
     }
   }
   catch (IOException e) {
     e.printStackTrace();
   }
 } */

  // these are part of the level 1A data structure, type 1
  // see http://www.hdfgroup.org/HDF5/doc/H5.format.html#V1Btrees,
  // see "Key" field (type 1) p 10
  // this is only for leaf nodes (level 0)
  class DataChunk {
    int size;       // size of chunk in bytes; need storage layout dimensions to interpret
    int filterMask; // bitfield indicating which filters have been skipped for this chunk
    int[] offset;   // offset index of this chunk, reletive to entire array
    long filePos;   // filePos of a single raw data chunk, already shifted by the offset if needed

    DataChunk(int ndim, boolean last) throws IOException {
      this.size = h5.raf.readInt();
      this.filterMask = h5.raf.readInt();
      offset = new int[ndim];
      for (int i = 0; i < ndim; i++) {
        long loffset = h5.raf.readLong();
        assert loffset < Integer.MAX_VALUE;
        offset[i] = (int) loffset;
      }
      this.filePos = last ? -1 : h5.readAddress(); //
      if (memTracker != null) memTracker.addByLen("Chunked Data (" + owner + ")", filePos, size);
    }

    public String toString() {
      StringBuilder sbuff = new StringBuilder();
      sbuff.append("  ChunkedDataNode size=").append(size).append(" filterMask=").append(filterMask).append(" filePos=").append(filePos).append(" offsets= ");
      for (long anOffset : offset) sbuff.append(anOffset).append(" ");
      return sbuff.toString();
    }
  }

}


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

package ucar.nc2.iosp.mcidas;

import edu.wisc.ssec.mcidas.AREAnav;
import edu.wisc.ssec.mcidas.AreaFile;
import edu.wisc.ssec.mcidas.McIDASException;

import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;
import ucar.unidata.util.Parameter;

/**
 * McIDASAreaProjection is the ProjectionImpl for McIDAS Area navigation
 * modules.
 *
 * @author dmurray
 */
public class McIDASAreaProjection extends ucar.unidata.geoloc.ProjectionImpl {

  /**
   * Attribute for the Area Directory
   */
  public static String ATTR_AREADIR = "AreaDirectory";

  /**
   * Attribute for the Navigation Block
   */
  public static String ATTR_NAVBLOCK = "NavBlock";

  /**
   * Attribute for the Navigation Block
   */
  public static String ATTR_AUXBLOCK = "AuxBlock";

  /**
   * Attribute for the Navigation Block
   */
  public static String GRID_MAPPING_NAME = "mcidas_area";

  /**
   * Area navigation
   */
  private AREAnav anav = null;

  /**
   * number of lines
   */
  private int lines;

  /**
   * number of elements
   */
  private int elements;

  /**
   * directory block
   */
  private int[] dirBlock;

  /**
   * navigation block
   */
  private int[] navBlock;

  /**
   * aux block
   */
  private int[] auxBlock;

  /**
   * copy constructor - avoid clone !!
   *
   * @return construct a copy of this
   */
  public ProjectionImpl constructCopy() {
    return new McIDASAreaProjection(dirBlock, navBlock, auxBlock);
  }

  /**
   * Default bean constructor
   */
  public McIDASAreaProjection() {
    super("McIDASAreaProjection", false);
  }

  /**
   * create a McIDAS AREA projection from the Area file's
   * directory and navigation blocks.
   *

   * This routine uses a flipped Y axis (first line of
   * the image file is number 0)
   *
   * @param af is the associated AreaFile
   */
  public McIDASAreaProjection(AreaFile af) {
    this(af.getDir(), af.getNav(), af.getAux());
  }

  /**
   * Create a AREA coordinate system from the Area file's
   * directory and navigation blocks.
   *

   * This routine uses a flipped Y axis (first line of
   * the image file is number 0)
   *
   * @param dir is the AREA file directory block
   * @param nav is the AREA file navigation block
   * @param aux is the AREA file auxillary block
   */
  public McIDASAreaProjection(int[] dir, int[] nav, int[] aux) {
    super("McIDASAreaProjection", false);

    try {
      anav = AREAnav.makeAreaNav(nav, aux);
    } catch (McIDASException excp) {
      throw new IllegalArgumentException("McIDASAreaProjection: problem creating projection" + excp);
    }
    dirBlock = dir;
    navBlock = nav;
    auxBlock = aux;
    anav.setImageStart(dir[5], dir[6]);
    anav.setRes(dir[11], dir[12]);
    anav.setStart(0, 0);
    anav.setMag(1, 1);
    lines = dir[8];
    elements = dir[9];
    anav.setFlipLineCoordinates(dir[8]);  // invert Y axis coordinates

    addParameter(CF.GRID_MAPPING_NAME, GRID_MAPPING_NAME);
    addParameter(new Parameter(ATTR_AREADIR, makeDoubleArray(dir)));
    addParameter(new Parameter(ATTR_NAVBLOCK, makeDoubleArray(nav)));
    if (aux != null) {
      addParameter(new Parameter(ATTR_AUXBLOCK, makeDoubleArray(aux)));
    }
  }

  /**
   * Get the directory block used to initialize this McIDASAreaProjection
   *
   * @return the area directory
   */
  public int[] getDirBlock() {
    return dirBlock;
  }

  /**
   * Get the navigation block used to initialize this McIDASAreaProjection
   *
   * @return the navigation block
   */
  public int[] getNavBlock() {
    return navBlock;
  }

  /**
   * Get the auxilliary block used to initialize this McIDASAreaProjection
   *
   * @return the auxilliary block (may be null)
   */
  public int[] getAuxBlock() {
    return auxBlock;
  }

    /*MACROBODY
      latLonToProj {} {
        double[][] xy = anav.toLinEle(new double[][] {{fromLat},{fromLon}});
        toX = xy[0][0];
        toY = xy[1][0];
      }

      projToLatLon {} {
        double[][] latlon = anav.toLatLon(new double[][] {{fromX},{fromY}});
        toLat = latlon[0][0];
        toLon = latlon[1][0];
      }

    MACROBODY*/
    /*BEGINGENERATED*/

    /*
    Note this section has been generated using the convert.tcl script.
    This script, run as:
    tcl convert.tcl McIDASAreaProjection.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[][] xy = anav.toLinEle(new double[][]{{fromLat}, {fromLon}});
    if (xy == null) return null;
    toX = xy[0][0];
    toY = xy[1][0];

    result.setLocation(toX, toY);
    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();
    double fromY = world.getY();

    double[][] latlon = anav.toLatLon(new double[][]{{fromX}, {fromY}});
    toLat = latlon[0][0];
    toLon = latlon[1][0];

    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[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */
  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex, int lonIndex) {
    float[] fromLatA = from[latIndex];
    float[] fromLonA = from[lonIndex];

    float[][] xy = anav.toLinEle(new float[][]{fromLatA, fromLonA});
    if (xy == null) return null;
    to[INDEX_X] = xy[0];
    to[INDEX_Y] = xy[1];
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public float[][] projToLatLon(float[][] from, float[][] to) {
    float[] fromXA = from[INDEX_X];
    float[] fromYA = from[INDEX_Y];
    float[][] latlon = anav.toLatLon(new float[][]{fromXA, fromYA});
    to[INDEX_LAT] = latlon[0];
    to[INDEX_LON] = latlon[1];
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n],
   *                 where from[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */
  public double[][] latLonToProj(double[][] from, double[][] to, int latIndex, int lonIndex) {
    double[] fromLatA = from[latIndex];
    double[] fromLonA = from[lonIndex];

    double[][] xy = anav.toLinEle(new double[][]{fromLatA, fromLonA});
    if (xy == null) return null;
    to[INDEX_X] = xy[0];
    to[INDEX_Y] = xy[1];
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public double[][] projToLatLon(double[][] from, double[][] to) {
    double[] fromXA = from[INDEX_X];
    double[] fromYA = from[INDEX_Y];
    double[][] latlon = anav.toLatLon(new double[][]{fromXA, fromYA});
    to[INDEX_LAT] = latlon[0];
    to[INDEX_LON] = latlon[1];
    return to;
  }

    /*ENDGENERATED*/

  /**
   * Get the bounds for this image
   *
   * @return the projection bounds
   */
  public ProjectionRect getDefaultMapArea() {
    return new ProjectionRect(new ProjectionRect(0, 0, elements, lines));
  }

  /**
   * 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.
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }
    if (Double.isNaN(pt1.getX()) || Double.isNaN(pt1.getY()) || Double.isNaN(pt2.getX()) || Double.isNaN(pt2.getY())) {
      return true;
    }
    // opposite signed X values, larger then 5000 km
    return (pt1.getX() * pt2.getX() < 0)
            && (Math.abs(pt1.getX() - pt2.getX()) > 5000.0);
  }

  /*
   * Determines whether or not the Object in question is
   * the same as this McIDASAreaProjection.  The specified
   * Object is equal to this McIDASAreaProjection
   * if it is an instance of McIDASAreaProjection and it has
   * the same navigation module and default map area as this one.
   *
   * @param obj the Object in question
   * @return true if they are equal
   *
  public boolean equals(Object obj) {
    if (!(obj instanceof McIDASAreaProjection)) {
      return false;
    }
    McIDASAreaProjection that = (McIDASAreaProjection) obj;
    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 (this == that)
            || (anav.equals(that.anav) && (this.lines == that.lines)
            && (this.elements == that.elements));
  }  */

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

    McIDASAreaProjection that = (McIDASAreaProjection) o;

    if (elements != that.elements) return false;
    if (lines != that.lines) return false;
    if (!anav.equals(that.anav)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = anav.hashCode();
    result = 31 * result + lines;
    result = 31 * result + elements;
    return result;
  }

  /**
   * Return a String which tells some info about this navigation
   *
   * @return wordy String
   */
  public String toString() {
    return "Image (" + anav.toString() + ") Projection";
  }

  /**
   * Get the parameters as a String
   *
   * @return the parameters as a String
   */
  public String paramsToString() {
    return " nav " + anav.toString();
  }

  /**
   * make a double array out of an int array
   *
   * @param ints array of ints
   * @return array of doubles
   */
  private double[] makeDoubleArray(int[] ints) {
    double[] newArray = new double[ints.length];
    for (int i = 0; i < ints.length; i++) {
      newArray[i] = ints[i];
    }
    return newArray;
  }
}


/*
 * Copyright (c) 1998-2017 University Corporation for Atmospheric Research/Unidata
 * See LICENSE.txt for license information.
 */
package thredds.server.ncss.controller;

import org.jdom2.Document;
import org.jdom2.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import thredds.core.AllowedServices;
import thredds.core.StandardService;
import thredds.core.TdsRequestedDataset;
import thredds.server.config.ThreddsConfig;
import thredds.server.exception.RequestTooLargeException;
import thredds.server.exception.ServiceNotAllowed;
import thredds.server.ncss.exception.*;
import thredds.server.ncss.format.SupportedFormat;
import thredds.server.ncss.format.SupportedOperation;
import thredds.server.ncss.params.NcssGridParamsBean;
import thredds.server.ncss.params.NcssParamsBean;
import thredds.server.ncss.view.dsg.DsgSubsetWriter;
import thredds.server.ncss.view.dsg.DsgSubsetWriterFactory;
import thredds.util.Constants;
import thredds.util.ContentType;
import thredds.util.TdsPathUtils;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.NetcdfFileWriter;
import ucar.nc2.ft.FeatureDatasetPoint;
import ucar.nc2.ft2.coverage.*;
import ucar.nc2.ft2.coverage.writer.CFGridCoverageWriter2;
import ucar.nc2.ft2.coverage.writer.CoverageAsPoint;
import ucar.nc2.ft2.coverage.writer.CoverageDatasetCapabilities;
import ucar.nc2.util.IO;
import ucar.nc2.util.Optional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Handles NCSS Grid Requests
 * Validation done here.
 *
 * @author caron
 * @since 4/29/2015
 */
@Controller
@RequestMapping("/ncss/grid")
public class NcssGridController extends AbstractNcssController {
  // Compression rate used to estimate the filesize of netcdf4 compressed files
  static private final short ESTIMATED_COMPRESION_RATE = 4;

  @Autowired
  private AllowedServices allowedServices;

  protected String getBase() {
    return StandardService.netcdfSubsetGrid.getBase();
  }

  @RequestMapping("**")     // data request
  public void handleRequest(HttpServletRequest req, HttpServletResponse res, @Valid NcssGridParamsBean params,
          BindingResult validationResult) throws Exception {
    if (!allowedServices.isAllowed(StandardService.netcdfSubsetGrid))
      throw new ServiceNotAllowed(StandardService.netcdfSubsetGrid.toString());

    if (validationResult.hasErrors())
      throw new BindException(validationResult);

    String datasetPath = getDatasetPath(req);
    try (CoverageCollection gcd = TdsRequestedDataset.getCoverageCollection(req, res, datasetPath)) {
      if (gcd == null) return;

      Formatter errs = new Formatter();
      if (!params.intersectsTime(gcd.getCalendarDateRange(), errs)) {
        handleValidationErrorMessage(res, HttpServletResponse.SC_BAD_REQUEST, errs.toString());
        return;
      }

      // throws exception if grid names not valid
      checkRequestedVars(gcd, params);

      if (params.hasLatLonPoint()) {
        handleRequestGridAsPoint(res, params, datasetPath, gcd);
      } else {
        handleRequestGrid(res, params, datasetPath, gcd);
      }
    }
  }

  private void handleRequestGrid(HttpServletResponse res, NcssGridParamsBean params, String datasetPath,
          CoverageCollection gcd) throws IOException, NcssException, InvalidRangeException {
    // Supported formats are netcdf3 (default) and netcdf4 (if available)
    SupportedFormat sf = SupportedOperation.GRID_REQUEST.getSupportedFormat(params.getAccept());
    NetcdfFileWriter.Version version =
            (sf == SupportedFormat.NETCDF3) ? NetcdfFileWriter.Version.netcdf3 : NetcdfFileWriter.Version.netcdf4;

    // all variables have to have the same vertical axis if a vertical coordinate was set. LOOK can we relax this ?
    if (params.getVertCoord() != null && !checkVarsHaveSameVertAxis(gcd, params) ) {
        throw new NcssException("The variables requested: " + params.getVar() + " have different vertical levels. " +
                "Grid requests with vertCoord must have variables with same vertical levels.");
      }

    String responseFile = getResponseFileName(datasetPath, version);
    File netcdfResult = makeCFNetcdfFile(gcd, responseFile, params, version);

    // filename download attachment
    String suffix = version.getSuffix();
    int pos = datasetPath.lastIndexOf("/");
    String filename = (pos >= 0) ? datasetPath.substring(pos + 1) : datasetPath;
    if (!filename.endsWith(suffix)) {
      filename += suffix;
    }

    // Headers...
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set(ContentType.HEADER, sf.getMimeType());
    httpHeaders.set(Constants.Content_Disposition, Constants.setContentDispositionValue(filename));

    // set content length
    httpHeaders.set(Constants.Content_Length, Constants.getContentLengthValue(netcdfResult));

    setResponseHeaders(res, httpHeaders);

    IO.copyFileB(netcdfResult, res.getOutputStream(), 60000);
    res.flushBuffer();
    res.getOutputStream().close();
    res.setStatus(HttpServletResponse.SC_OK);
  }

  private File makeCFNetcdfFile(CoverageCollection gcd, String responseFilename, NcssGridParamsBean params,
          NetcdfFileWriter.Version version) throws InvalidRangeException, IOException {
    SubsetParams subset = params.makeSubset(gcd);

    // Test maxFileDownloadSize
    long maxFileDownloadSize = ThreddsConfig.getBytes("NetcdfSubsetService.maxFileDownloadSize", -1L);
    if (maxFileDownloadSize > 0) {
      Optional<Long> estimatedSizeo = CFGridCoverageWriter2.writeOrTestSize(
              gcd, params.getVar(), subset, params.isAddLatLon(), true, null);
      if (!estimatedSizeo.isPresent())
        throw new InvalidRangeException("Request contains no data: " + estimatedSizeo.getErrorMessage());

      long estimatedSize = estimatedSizeo.get();
      if (version == NetcdfFileWriter.Version.netcdf4)
        estimatedSize /= ESTIMATED_COMPRESION_RATE;

      if (estimatedSize > maxFileDownloadSize)
        throw new RequestTooLargeException(
                "NCSS response too large = " + estimatedSize + " max = " + maxFileDownloadSize);
    }

    // write the file
    NetcdfFileWriter writer = NetcdfFileWriter.createNew(
            version, responseFilename, null);  // default chunking - let user control at some point

    Optional<Long> estimatedSizeo = CFGridCoverageWriter2.writeOrTestSize(
            gcd, params.getVar(), subset, params.isAddLatLon(), false, writer);
    if (!estimatedSizeo.isPresent())
      throw new InvalidRangeException("Request contains no data: " + estimatedSizeo.getErrorMessage());

    return new File(responseFilename);
  }

  private String getResponseFileName(String requestPathInfo, NetcdfFileWriter.Version version) {
    Random random = new Random(System.currentTimeMillis());
    int randomInt = random.nextInt();

    String filename = TdsPathUtils.getFileNameForResponse(requestPathInfo, version);
    String pathname = Integer.toString(randomInt) + "/" + filename;
    File ncFile = ncssDiskCache.getDiskCache().getCacheFile(pathname);
    if (ncFile == null)
      throw new IllegalStateException("NCSS misconfigured cache");
    return ncFile.getPath();
  }

  private void handleRequestGridAsPoint(HttpServletResponse res, NcssGridParamsBean params, String datasetPath,
          CoverageCollection gcd) throws Exception {
    SupportedFormat sf = SupportedOperation.POINT_REQUEST.getSupportedFormat(params.getAccept());

    CoverageAsPoint covp = new CoverageAsPoint(gcd, params.getVar(), params.makeSubset(gcd));
    try (FeatureDatasetPoint fd = covp.asFeatureDatasetPoint()) {

      // all subsetting is done in CoverageAsPoint
      //SubsetParams ncssParams = params.makeSubset(gcd);
      SubsetParams ncssParams = new SubsetParams()
              .set(SubsetParams.timeAll, true)
              .set(SubsetParams.variables, params.getVar());
      DsgSubsetWriter pds = DsgSubsetWriterFactory.newInstance(
              fd, ncssParams, ncssDiskCache, res.getOutputStream(), sf);
      setResponseHeaders(res, pds.getHttpHeaders(datasetPath, sf.isStream()));
      pds.respond(res, fd, datasetPath, ncssParams, sf);
    }
  }

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

  @RequestMapping(value = {"**/dataset.xml", "**/pointDataset.xml"})
  // Same response for both Grid and GridAsPoint.
  public ModelAndView getDatasetDescriptionXml(HttpServletRequest req, HttpServletResponse res) throws IOException {
    String datasetPath = getDatasetPath(req);

    try (CoverageCollection gcd = TdsRequestedDataset.getCoverageCollection(req, res, datasetPath)) {
      if (gcd == null) return null; // restricted dataset
      String datasetUrlPath = buildDatasetUrl(datasetPath);

      CoverageDatasetCapabilities writer = new CoverageDatasetCapabilities(gcd, "path");
      Document doc = writer.makeDatasetDescription();
      Element root = doc.getRootElement();
      root.setAttribute("location", datasetUrlPath);
      root.addContent(makeAcceptXML(SupportedOperation.GRID_REQUEST));

      return new ModelAndView("threddsXmlView", "Document", doc);
    }
  }

  @RequestMapping(value = "**/dataset.html")
  public ModelAndView getGridDatasetDescriptionHtml(HttpServletRequest req, HttpServletResponse res)
          throws IOException {
    return getDatasetDescriptionHtml(req, res, SupportedOperation.GRID_REQUEST);
  }

  @RequestMapping(value = "**/pointDataset.html")
  public ModelAndView getGridAsPointDatasetDescriptionHtml(HttpServletRequest req, HttpServletResponse res)
          throws IOException {
    return getDatasetDescriptionHtml(req, res, SupportedOperation.GRID_AS_POINT_REQUEST);
  }

  private ModelAndView getDatasetDescriptionHtml(HttpServletRequest req, HttpServletResponse res,
          SupportedOperation op) throws IOException {
    String datasetPath = getDatasetPath(req);

    try (CoverageCollection gcd = TdsRequestedDataset.getCoverageCollection(req, res, datasetPath)) {
      if (gcd == null) return null; // restricted dataset
      String datasetUrlPath = buildDatasetUrl(datasetPath);

      Map<String, Object> model = new HashMap<>();
      model.put("gcd", gcd);
      model.put("datasetPath", datasetUrlPath);
      model.put("horizExtentWKT", gcd.getHorizCoordSys().getLatLonBoundaryAsWKT(50, 100));
      model.put("accept", makeAcceptList(op));

      switch (op) {
        case GRID_REQUEST: return new ModelAndView("templates/ncssGrid", model);
        case GRID_AS_POINT_REQUEST: return new ModelAndView("templates/ncssGridAsPoint", model);
        default: throw new AssertionError("Who passed in a " + op + "?");
      }
    }
  }

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

  // Supported for backwards compatibility. We prefer that datasetBoundaries.wkt or datasetBoundaries.json are used.
  @RequestMapping("**/datasetBoundaries.xml")
  public void getDatasetBoundaries(NcssParamsBean params, HttpServletRequest req, HttpServletResponse res)
          throws IOException, UnsupportedResponseFormatException {
    SupportedFormat format = SupportedOperation.DATASET_BOUNDARIES_REQUEST.getSupportedFormat(params.getAccept());

    switch (format) {
      case WKT:  getDatasetBoundariesWKT(req, res);     break;
      case JSON: getDatasetBoundariesGeoJSON(req, res); break;
      default: throw new IllegalArgumentException(String.format(
              "Expected %s or %s, but got %s", SupportedFormat.WKT, SupportedFormat.JSON, format));
    }
  }

  @RequestMapping("**/datasetBoundaries.wkt")
  public void getDatasetBoundariesWKT(HttpServletRequest req, HttpServletResponse res) throws IOException {
    try (CoverageCollection gcd = TdsRequestedDataset.getCoverageCollection(req, res, getDatasetPath(req))) {
      if (gcd == null) return;

      res.setContentType(SupportedFormat.WKT.getMimeType());
      res.getWriter().write(gcd.getHorizCoordSys().getLatLonBoundaryAsWKT());
      res.getWriter().flush();
    }
  }

  @RequestMapping("**/datasetBoundaries.json")
  public void getDatasetBoundariesGeoJSON(HttpServletRequest req, HttpServletResponse res) throws IOException {
    try (CoverageCollection gcd = TdsRequestedDataset.getCoverageCollection(req, res, getDatasetPath(req))) {
      if (gcd == null) return;

      res.setContentType(SupportedFormat.JSON.getMimeType());
      res.getWriter().write(gcd.getHorizCoordSys().getLatLonBoundaryAsGeoJSON());
      res.getWriter().flush();
    }
  }

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

  /**
   * Checks that all the requested vars exist. If "all", fills out the param.vars with all grid names
   * Throws exception if some of the variables in the request are not contained in the dataset
   */
  private void checkRequestedVars(CoverageCollection gcd, NcssGridParamsBean params)
          throws VariableNotContainedInDatasetException {

    // if var == all --> all variables requested
    if (params.getVar().get(0).equalsIgnoreCase("all")) {
      params.setVar(getAllGridNames(gcd));
      return;
    }

    // Check vars are contained in the grid
    for (String gridName : params.getVar()) {
      Coverage grid = gcd.findCoverage(gridName);
      if (grid == null)
        throw new VariableNotContainedInDatasetException(
                "Variable: " + gridName + " is not contained in the requested dataset");
    }
  }

  private List<String> getAllGridNames(CoverageCollection gcd) {
    List<String> result = new ArrayList<>();
    for (Coverage var : gcd.getCoverages())
      result.add(var.getName());
    return result;
  }

  /**
   * Returns true if all the variables have the same vertical axis (if they have an axis).
   * Could be broadened to allow all with same coordinate unites? coordinate value??
   */
  protected boolean checkVarsHaveSameVertAxis(CoverageCollection gcd, NcssGridParamsBean params) {
    String zaxisName = null;
    for (String gridName : params.getVar()) {
      Coverage grid = gcd.findCoverage(gridName);
      CoverageCoordAxis zaxis = grid.getCoordSys().getZAxis();
      if (zaxis != null) {
        if (zaxisName == null)
          zaxisName = zaxis.getName();
        else if (!zaxisName.equals(zaxis.getName()))
          return false;
      }
    }
    return true;
  }
}