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

import ucar.nc2.Attribute;
import ucar.nc2.NCdumpW;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.*;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.*;

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

/**
 * fork ucar.nc2.dt.grid.GridCoordSys for adaption of GridCoverage.
 * Minimalist, does not do subsetting, vertical transform.
 *
 * @author caron
 * @since 5/26/2015
 */
public class DtCoverageCS {
  //static private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DtCoverageCS.class);
  //static private final boolean warnUnits = false;

  /////////////////////////////////////////////////////////////////////////////
  protected DtCoverageCSBuilder builder;
  private String name;
  private ProjectionImpl proj;
  private GeoGridCoordinate2D g2d;
  private boolean isLatLon = false;

  /**
   * Create a GeoGridCoordSys from an existing Coordinate System.
   * This will choose which axes are the XHoriz, YHoriz, Vertical, Time, RunTIme, Ensemble.
   * If theres a Projection, it will set its map area
   *
   * @param builder create from this
   */
  public DtCoverageCS(DtCoverageCSBuilder builder) {
    super();
    this.builder = builder;

    // make name based on coordinate
    this.name = CoordinateSystem.makeName(builder.allAxes);

    // WRF NMM
    Attribute att = getXHorizAxis().findAttribute(_Coordinate.Stagger);
    if (att != null)
      setHorizStaggerType(att.getStringValue());

    if (builder.orgProj != null) {
      proj = builder.orgProj.constructCopy();
    }
  }

  public String getName() {
    return name;
  }

  public FeatureType getCoverageType() {
    return builder.type;
  }

  public List<CoordinateAxis> getCoordAxes() {
    return builder.allAxes;
  }

  public CoordinateAxis findCoordAxis(String shortName) {
    for (CoordinateAxis axis : builder.allAxes) {
      if (axis.getShortName().equals(shortName)) return axis;
    }
    return null;
  }

  public List<CoordinateTransform> getCoordTransforms() {
    return builder.coordTransforms;
  }

  /**
   * get the X Horizontal axis (either GeoX or Lon)
   */
  public CoordinateAxis getXHorizAxis() {
    return builder.xaxis;
  }

  /**
   * get the Y Horizontal axis (either GeoY or Lat)
   */
  public CoordinateAxis getYHorizAxis() {
    return builder.yaxis;
  }

  /**
   * get the Vertical axis (either Geoz, Height, or Pressure)
   */
  public CoordinateAxis1D getVerticalAxis() {
    return builder.vertAxis;
  }

  public CoordinateAxis getTimeAxis() {
    return builder.timeAxis;
  }

  public CoordinateAxis1DTime getRunTimeAxis() {
    return builder.rtAxis;
  }

  public CoordinateAxis1D getEnsembleAxis() {
    return builder.ensAxis;
  }

  public ProjectionImpl getProjection() {
    return proj;
  }

  /**
   * is this a Lat/Lon coordinate system?
   */
  public boolean isLatLon() {
    return isLatLon;
  }

  /**
   * Is this a global coverage over longitude ?
   *
   * @return true if isLatLon and longitude wraps
   */
  public boolean isGlobalLon() {
    if (!isLatLon) return false;
    if (!(getXHorizAxis() instanceof CoordinateAxis1D)) return false;
    CoordinateAxis1D lon = (CoordinateAxis1D) getXHorizAxis();
    double first = lon.getCoordEdge(0);
    double last = lon.getCoordEdge((int) lon.getSize());
    double min = Math.min(first, last);
    double max = Math.max(first, last);
    return (max - min) >= 360;
  }

  /**
   * true if x and y axes are CoordinateAxis1D and are regular
   */
  public boolean isRegularSpatial() {
    if (!isRegularSpatial(getXHorizAxis())) return false;
    if (!isRegularSpatial(getYHorizAxis())) return false;
    return true;
  }

  private boolean isRegularSpatial(CoordinateAxis axis) {
    if (axis == null) return true;
    if (!(axis instanceof CoordinateAxis1D)) return false;
    return ((CoordinateAxis1D) axis).isRegular();
  }

  private String horizStaggerType;

  public String getHorizStaggerType() {
    return horizStaggerType;
  }

  public void setHorizStaggerType(String horizStaggerType) {
    this.horizStaggerType = horizStaggerType;
  }

  private ProjectionRect mapArea = null;

  /**
   * Get the x,y bounding box in projection coordinates.
   */
  public ProjectionRect getBoundingBox() {
    if (mapArea == null) {

      CoordinateAxis horizXaxis = getXHorizAxis();
      CoordinateAxis horizYaxis = getYHorizAxis();
      if ((horizXaxis == null) || !horizXaxis.isNumeric() || (horizYaxis == null) || !horizYaxis.isNumeric())
        return null; // impossible

      // x,y may be 2D
      if ((horizXaxis instanceof CoordinateAxis2D) && (horizYaxis instanceof CoordinateAxis2D)) {
        // could try to optimize this - just get corners or something
        CoordinateAxis2D xaxis2 = (CoordinateAxis2D) horizXaxis;
        CoordinateAxis2D yaxis2 = (CoordinateAxis2D) horizYaxis;

        mapArea = null; // getBBfromCorners(xaxis2, yaxis2);  LOOK LOOK

        // mapArea = new ProjectionRect(horizXaxis.getMinValue(), horizYaxis.getMinValue(),
        //         horizXaxis.getMaxValue(), horizYaxis.getMaxValue());

      } else {

        CoordinateAxis1D xaxis1 = (CoordinateAxis1D) horizXaxis;
        CoordinateAxis1D yaxis1 = (CoordinateAxis1D) horizYaxis;

        /* add one percent on each side if its a projection. WHY?
        double dx = 0.0, dy = 0.0;
        if (!isLatLon()) {
          dx = .01 * (xaxis1.getCoordEdge((int) xaxis1.getSize()) - xaxis1.getCoordEdge(0));
          dy = .01 * (yaxis1.getCoordEdge((int) yaxis1.getSize()) - yaxis1.getCoordEdge(0));
        }

        mapArea = new ProjectionRect(xaxis1.getCoordEdge(0) - dx, yaxis1.getCoordEdge(0) - dy,
            xaxis1.getCoordEdge((int) xaxis1.getSize()) + dx,
            yaxis1.getCoordEdge((int) yaxis1.getSize()) + dy); */

        mapArea = new ProjectionRect(xaxis1.getCoordEdge(0), yaxis1.getCoordEdge(0),
                xaxis1.getCoordEdge((int) xaxis1.getSize()),
                yaxis1.getCoordEdge((int) yaxis1.getSize()));
      }
    }

    return mapArea;
  }

  /**
   * Get the Lat/Lon coordinates of the midpoint of a grid cell, using the x,y indices
   *
   * @param xindex x index
   * @param yindex y index
   * @return lat/lon coordinate of the midpoint of the cell
   */
  public LatLonPoint getLatLon(int xindex, int yindex) {
    double x, y;

    CoordinateAxis horizXaxis = getXHorizAxis();
    CoordinateAxis horizYaxis = getYHorizAxis();
    if (horizXaxis instanceof CoordinateAxis1D) {
      CoordinateAxis1D horiz1D = (CoordinateAxis1D) horizXaxis;
      x = horiz1D.getCoordValue(xindex);
    } else {
      CoordinateAxis2D horiz2D = (CoordinateAxis2D) horizXaxis;
      x = horiz2D.getCoordValue(yindex, xindex);
    }

    if (horizYaxis instanceof CoordinateAxis1D) {
      CoordinateAxis1D horiz1D = (CoordinateAxis1D) horizYaxis;
      y = horiz1D.getCoordValue(yindex);
    } else {
      CoordinateAxis2D horiz2D = (CoordinateAxis2D) horizYaxis;
      y = horiz2D.getCoordValue(yindex, xindex);
    }

    return isLatLon() ? new LatLonPointImpl(y, x) : getLatLon(x, y);
  }

  public LatLonPoint getLatLon(double xcoord, double ycoord) {
    Projection dataProjection = getProjection();
    return dataProjection.projToLatLon(new ProjectionPointImpl(xcoord, ycoord), new LatLonPointImpl());
  }

  private LatLonRect llbb = null;

  /**
   * Get horizontal bounding box in lat, lon coordinates.
   *
   * @return lat, lon bounding box.
   */
  public LatLonRect getLatLonBoundingBox() {

    if (llbb == null) {

      if ((getXHorizAxis() instanceof CoordinateAxis2D) && (getYHorizAxis() instanceof CoordinateAxis2D)) {
        return null;
      }

      CoordinateAxis horizXaxis = getXHorizAxis();
      CoordinateAxis horizYaxis = getYHorizAxis();
      if (isLatLon()) {
        double startLat = horizYaxis.getMinValue();
        double startLon = horizXaxis.getMinValue();

        double deltaLat = horizYaxis.getMaxValue() - startLat;
        double deltaLon = horizXaxis.getMaxValue() - startLon;

        LatLonPoint llpt = new LatLonPointImpl(startLat, startLon);
        llbb = new LatLonRect(llpt, deltaLat, deltaLon);

      } else {
        ProjectionImpl dataProjection = getProjection();
        ProjectionRect bb = getBoundingBox();
        if (bb != null)
          llbb = dataProjection.projToLatLonBB(bb);
      }
    }

    return llbb;

      /*  // look at all 4 corners of the bounding box
        LatLonPointImpl llpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getLowerLeftPoint(), new LatLonPointImpl());
        LatLonPointImpl lrpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getLowerRightPoint(), new LatLonPointImpl());
        LatLonPointImpl urpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getUpperRightPoint(), new LatLonPointImpl());
        LatLonPointImpl ulpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getUpperLeftPoint(), new LatLonPointImpl());

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

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

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

          llpt.set(latMin, lonMin);
          urpt.set(latMax, lonMax);

          llbb = new LatLonRect(llpt, urpt);
        }
      }
    }  */

  }

  @Override
  public String toString() {
    Formatter buff = new Formatter();
    show(buff, false);
    return buff.toString();
  }

  public void show(Formatter f, boolean showCoords) {
    f.format("Coordinate System (%s)%n", getName());

    showCoordinateAxis(getRunTimeAxis(), f, showCoords);
    showCoordinateAxis(getEnsembleAxis(), f, showCoords);
    showCoordinateAxis(getTimeAxis(), f, showCoords);
    showCoordinateAxis(getVerticalAxis(), f, showCoords);
    showCoordinateAxis(getYHorizAxis(), f, showCoords);
    showCoordinateAxis(getXHorizAxis(), f, showCoords);

    if (proj != null)
      f.format(" Projection: %s %s%n", proj.getName(), proj.paramsToString());
  }

  private void showCoordinateAxis(CoordinateAxis axis, Formatter f, boolean showCoords) {
    if (axis == null) return;
    f.format(" rt=%s (%s)", axis.getNameAndDimensions(), axis.getClass().getName());
    if (showCoords) showCoords(axis, f);
    f.format("%n");
  }

  private void showCoords(CoordinateAxis axis, Formatter f) {
    try {
      if (axis instanceof CoordinateAxis1D && axis.isNumeric()) {
        CoordinateAxis1D axis1D = (CoordinateAxis1D) axis;
        if (!axis1D.isInterval()) {
          double[] e = axis1D.getCoordEdges();
          for (double anE : e) {
            f.format("%f,", anE);
          }
        } else {
          double[] b1 = axis1D.getBound1();
          double[] b2 = axis1D.getBound2();
          for (int i = 0; i < b1.length; i++) {
            f.format("(%f,%f) = %f%n", b1[i], b2[i], b2[i] - b1[i]);
          }
        }
      } else {
        f.format("%s", NCdumpW.printVariableData(axis, null));
      }
    } catch (IOException ioe) {
      f.format(ioe.getMessage());
    }
    f.format(" %s%n", axis.getUnitsString());
  }

  public CalendarDateRange getCalendarDateRange() {
    CoordinateAxis timeTaxis = getTimeAxis();
    if (timeTaxis != null && timeTaxis instanceof CoordinateAxis1DTime)
      return ((CoordinateAxis1DTime) timeTaxis).getCalendarDateRange();

    CoordinateAxis1DTime rtaxis = getRunTimeAxis();
    if (rtaxis != null) {
      return rtaxis.getCalendarDateRange();
    }

    return null;
  }

  public int getDomainRank() {
    return CoordinateSystem.makeDomain(builder.independentAxes).size();
  }

  public int getRangeRank() {
    return builder.allAxes.size(); // not right
  }
}

  ///////////////////////////////////////////////////////////////////////
  /* experimental

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

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

  static public LatLonRect getLatLonBoundingBox(Projection proj, double startx, double starty, double endx, double endy) {

    if (proj instanceof LatLonProjection) {
      double deltaLat = endy - starty;
      double deltaLon = endx - startx;

      LatLonPoint llpt = new LatLonPointImpl(starty, startx);
      return new LatLonRect(llpt, deltaLat, deltaLon);

    }

    ProjectionRect bb = new ProjectionRect(startx, starty, endx, endy);

    // look at all 4 corners of the bounding box
    LatLonPointImpl llpt = (LatLonPointImpl) proj.projToLatLon(bb.getLowerLeftPoint(), new LatLonPointImpl());
    LatLonPointImpl lrpt = (LatLonPointImpl) proj.projToLatLon(bb.getLowerRightPoint(), new LatLonPointImpl());
    LatLonPointImpl urpt = (LatLonPointImpl) proj.projToLatLon(bb.getUpperRightPoint(), new LatLonPointImpl());
    LatLonPointImpl ulpt = (LatLonPointImpl) proj.projToLatLon(bb.getUpperLeftPoint(), new LatLonPointImpl());

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

    boolean includesSouthPole = false;
    /* int[] resultSP = new int[2];
    resultSP = findXYindexFromLatLon(-90.0, 0, null);
    if (resultSP[0] != -1 && resultSP[1] != -1)
      includesSouthPole = true;  //

    LatLonRect llbb;

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

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

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

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

      llpt.set(latMin, lonMin);
      urpt.set(latMax, lonMax);

      llbb = new LatLonRect(llpt, urpt);
    }
    return llbb;
  }

}

/*

  public List<CalendarDate> getCalendarDates() {
    CoordinateAxis timeTaxis = getTimeAxis();
    if (timeTaxis != null && timeTaxis instanceof CoordinateAxis1DTime)
      return ((CoordinateAxis1DTime) timeTaxis).getCalendarDates();

    CoordinateAxis1DTime rtaxis = getRunTimeAxis();
    if (rtaxis != null) {
      return rtaxis.getCalendarDates();
    }

    return null;
  }

 */

 /**
   * Get the list of time names, to be used for user selection.
   * The ith one refers to the ith time coordinate.
   *
   * @return List of ucar.nc2.util.NamedObject, or empty list.
   *
public List<NamedObject> getTimes() {
  List<CalendarDate> cdates = getCalendarDates();
  if (cdates == null) throw new IllegalStateException("No CalendarDates");
  List<NamedObject> times = new ArrayList<>(cdates.size());
  for (CalendarDate cd : cdates) {
    times.add(new ucar.nc2.util.NamedAnything(cd.toString(), "calendar date"));
  }
  return times;
}

*/

  /**
   * Get the list of level names, to be used for user selection.
   * The ith one refers to the ith level coordinate.
   *
   * @return List of ucar.nc2.util.NamedObject, or empty list.
   *
public List<NamedObject> getLevels() {
  CoordinateAxis1D vertZaxis = getVerticalAxis();
  if (vertZaxis == null)
    return new ArrayList<>(0);

  int n = (int) vertZaxis.getSize();
  List<NamedObject> levels = new ArrayList<>(n);
  for (int i = 0; i < n; i++)
    levels.add(new ucar.nc2.util.NamedAnything(vertZaxis.getCoordName(i), vertZaxis.getUnitsString()));

  return levels;
}

*/
  /**
   * Get the String name for the ith level(z) coordinate.
   *
   * @param index which level coordinate
   * @return level name
   *
public String getLevelName(int index) {
  CoordinateAxis1D vertZaxis = getVerticalAxis();
  if ((vertZaxis == null) || (index < 0) || (index >= vertZaxis.getSize()))
    throw new IllegalArgumentException("getLevelName = " + index);
  return vertZaxis.getCoordName(index).trim();
}

  /**
   * Get the index corresponding to the level name.
   *
   * @param name level name
   * @return level index, or -1 if not found
   *
  public int getLevelIndex(String name) {
    CoordinateAxis1D vertZaxis = getVerticalAxis();
    if ((vertZaxis == null) || (name == null)) return -1;

    for (int i = 0; i < vertZaxis.getSize(); i++) {
      if (vertZaxis.getCoordName(i).trim().equals(name))
        return i;
    }
    return -1;
  }

*/
  /*
   * Create a GeoGridCoordSys as a section of an existing GeoGridCoordSys.
   * This will create sections of the corresponding CoordinateAxes.
   *
   * @param rt_range subset the runtime dimension, or null if you want all of it
   * @param e_range  subset the ensemble dimension, or null if you want all of it
   * @param t_range  subset the time dimension, or null if you want all of it
   * @param z_range  subset the vertical dimension, or null if you want all of it
   * @param y_range  subset the y dimension, or null if you want all of it
   * @param x_range  subset the x dimension, or null if you want all of it
   * @throws InvalidRangeException if any of the ranges are illegal
   *
  public DtCoverageCS subset(Range rt_range, Range e_range, Range t_range, Range z_range, Range y_range, Range x_range) throws InvalidRangeException {
    /* super();

    CoordinateAxis xaxis = from.getXHorizAxis();
    CoordinateAxis yaxis = from.getYHorizAxis();

    if ((xaxis instanceof CoordinateAxis1D) && (yaxis instanceof CoordinateAxis1D)) {
      CoordinateAxis1D xaxis1 = (CoordinateAxis1D) xaxis;
      CoordinateAxis1D yaxis1 = (CoordinateAxis1D) yaxis;

      horizXaxis = (x_range == null) ? xaxis1 : xaxis1.section(x_range);
      horizYaxis = (y_range == null) ? yaxis : yaxis1.section(y_range);

    } else if ((xaxis instanceof CoordinateAxis2D) && (yaxis instanceof CoordinateAxis2D) && from.isLatLon()) {
      CoordinateAxis2D lon_axis = (CoordinateAxis2D) xaxis;
      CoordinateAxis2D lat_axis = (CoordinateAxis2D) yaxis;

      horizXaxis = lon_axis.section(y_range, x_range);
      horizYaxis = lat_axis.section(y_range, x_range);

    } else
      throw new IllegalArgumentException("must be 1D or 2D/LatLon ");

    if (from.isGeoXY()) {
      xAxis = horizXaxis;
      yAxis = horizYaxis;
    } else {
      lonAxis = horizXaxis;
      latAxis = horizYaxis;
      isLatLon = true;
    }

    coordAxes.add(horizXaxis);
    coordAxes.add(horizYaxis);

    // set canonical area
    ProjectionImpl projOrig = from.getProjection();
    if (projOrig != null) {
      proj = projOrig.constructCopy();
      proj.setDefaultMapArea(getBoundingBox()); // LOOK expensive for 2D
    }

    CoordinateAxis1D zaxis = from.getVerticalAxis();
    if (zaxis != null) {
      vertZaxis = (z_range == null) ? zaxis : zaxis.section(z_range);
      coordAxes.add(vertZaxis);
      // LOOK assign hAxis, pAxis or zAxis ??
    }

    if (from.getVerticalCT() != null) {
      VerticalTransform vtFrom = from.getVerticalTransform(); // LOOK may need to make sure this exists?
      if (vtFrom != null)
        vt = vtFrom.subset(t_range, z_range, y_range, x_range);

      vCT = from.getVerticalCT();
    }

    CoordinateAxis1D eaxis = from.getEnsembleAxis();
    if (eaxis != null) {
      ensembleAxis = (e_range == null) ? eaxis : eaxis.section(e_range);
      coordAxes.add(ensembleAxis);
    }

    CoordinateAxis taxis = from.getTimeAxis();
    CoordinateAxis1DTime taxis1D = null;
    if (taxis != null) {
      if (taxis instanceof CoordinateAxis1DTime) {
        taxis1D = (CoordinateAxis1DTime) taxis;
        tAxis = timeTaxis = (t_range == null) ? taxis1D : taxis1D.section(t_range);
        coordAxes.add(timeTaxis);
        timeDim = timeTaxis.getDimension(0);
      } else {
        if ((rt_range == null) && (t_range == null))
          tAxis = taxis;
        else {
          Section timeSection = new Section().appendRange(rt_range).appendRange(t_range);
          tAxis = (CoordinateAxis) taxis.section(timeSection);
        }
        coordAxes.add(tAxis);
      }
    }

    CoordinateAxis1DTime rtaxis = from.getRunTimeAxis();
    if (rtaxis != null) {
      if (taxis1D != null) {
        Dimension tDim = taxis1D.getDimension(0);
        Dimension rtDim = rtaxis.getDimension(0);
        if (tDim.getShortName().equals(rtDim.getShortName())) { // must usetime subset range if dims match - FMRC 1D has this a lot
          runTimeAxis = (t_range == null) ? rtaxis : rtaxis.section(t_range);
        }
      }
      if (runTimeAxis == null)  // regular case of a run tim axis
        runTimeAxis = (rt_range == null) ? rtaxis : rtaxis.section(rt_range);

      coordAxes.add(runTimeAxis);
    }

    // make name based on coordinate
    Collections.sort(builder.standardAxes, new CoordinateAxis.AxisComparator()); // canonical ordering of axes
    this.name = makeName(builder.standardAxes);

    this.coordTrans = new ArrayList<>(from.getCoordinateTransforms());

    // collect dimensions
    for (CoordinateAxis axis : coordAxes) {
      List<Dimension> dims = axis.getDimensions();
      for (Dimension dim : dims) {
        dim.setShared(true); // make them shared (section will make them unshared)
        if (!domain.contains(dim))
          domain.add(dim);
      }
    }

    setHorizStaggerType(from.getHorizStaggerType()); //

    return null;
  } */

  /*
  private CoordinateAxis convertUnits(CoordinateAxis axis) {
    String units = axis.getUnitsString();
    SimpleUnit axisUnit = SimpleUnit.factory(units);
    if (axisUnit == null) return axis;

    double factor;
    try {
      factor = axisUnit.convertTo(1.0, SimpleUnit.kmUnit);
    } catch (IllegalArgumentException e) {
      if (warnUnits) log.warn("convertUnits failed", e);
      return axis;
    }
    if (factor == 1.0) return axis;

    Array data;
    try {
      data = axis.read();
    } catch (IOException e) {
      log.warn("convertUnits read failed", e);
      return axis;
    }

    DataType dtype = axis.getDataType();
    if (dtype.isFloatingPoint()) {
      IndexIterator ii = data.getIndexIterator();
      while (ii.hasNext())
        ii.setDoubleCurrent(factor * ii.getDoubleNext());

      CoordinateAxis newAxis = axis.copyNoCache();
      newAxis.setCachedData(data, false);
      newAxis.setUnitsString("km");
      return newAxis;

    } else {  // convert to DOUBLE
      Array newData = Array.factory(DataType.DOUBLE, axis.getShape());
      IndexIterator newi = newData.getIndexIterator();
      IndexIterator ii = data.getIndexIterator();
      while (ii.hasNext() && newi.hasNext())
        newi.setDoubleNext(factor * ii.getDoubleNext());

      CoordinateAxis newAxis = axis.copyNoCache();
      newAxis.setDataType(DataType.DOUBLE);
      newAxis.setCachedData(newData, false);
      newAxis.setUnitsString("km");
      return newAxis;
    }
  }

  /*
   * Given a point in x,y coordinate space, find the x,y index in the coordinate system.
   *
   * @param x_coord position in x coordinate space.
   * @param y_coord position in y coordinate space.
   * @param result  put result (x,y) index in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point. These will be -1 if out of range.
   *
  public int[] findXYindexFromCoord(double x_coord, double y_coord, int[] result) {
    if (result == null)
      result = new int[2];

    CoordinateAxis horizXaxis = getXHorizAxis();
    CoordinateAxis horizYaxis = getYHorizAxis();
    if ((horizXaxis instanceof CoordinateAxis1D) && (horizYaxis instanceof CoordinateAxis1D)) {
      result[0] = ((CoordinateAxis1D) horizXaxis).findCoordElement(x_coord);
      result[1] = ((CoordinateAxis1D) horizYaxis).findCoordElement(y_coord);
      return result;

    } else if ((horizXaxis instanceof CoordinateAxis2D) && (horizYaxis instanceof CoordinateAxis2D)) {
      if (g2d == null)
        g2d = new GeoGridCoordinate2D((CoordinateAxis2D) horizYaxis, (CoordinateAxis2D) horizXaxis);
      int[] result2 = new int[2];
      boolean found = g2d.findCoordElement(y_coord, x_coord, result2);
      if (found) {
        result[0] = result2[1];
        result[1] = result2[0];
      } else {
        result[0] = -1;
        result[1] = -1;
      }
      return result;
    }

    // cant happen
    throw new IllegalStateException("GridCoordSystem.findXYindexFromCoord");
  }

  /**
   * Given a point in x,y coordinate space, find the x,y index in the coordinate system.
   * If outside the range, the closest point is returned, eg, 0 or n-1 depending on if the coordinate is too small or too large.
   *
   * @param x_coord position in x coordinate space.
   * @param y_coord position in y coordinate space.
   * @param result  put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point.
   *
  public int[] findXYindexFromCoordBounded(double x_coord, double y_coord, int[] result) {
    if (result == null)
      result = new int[2];

    CoordinateAxis horizXaxis = getXHorizAxis();
    CoordinateAxis horizYaxis = getYHorizAxis();
    if ((horizXaxis instanceof CoordinateAxis1D) && (horizYaxis instanceof CoordinateAxis1D)) {
      result[0] = ((CoordinateAxis1D) horizXaxis).findCoordElementBounded(x_coord);
      result[1] = ((CoordinateAxis1D) horizYaxis).findCoordElementBounded(y_coord);
      return result;

    } else if ((horizXaxis instanceof CoordinateAxis2D) && (horizYaxis instanceof CoordinateAxis2D)) {
      if (g2d == null)
        g2d = new GeoGridCoordinate2D((CoordinateAxis2D) horizYaxis, (CoordinateAxis2D) horizXaxis);

      int[] result2 = new int[2];
      g2d.findCoordElement(y_coord, x_coord, result2); // returns best guess
      result[0] = result2[1];
      result[1] = result2[0];
      return result;
    }

    // cant happen
    throw new IllegalStateException("GridCoordSystem.findXYindexFromCoord");
  }

  /**
   * Given a lat,lon point, find the x,y index in the coordinate system.
   *
   * @param lat    latitude position.
   * @param lon    longitude position.
   * @param result put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point. These will be -1 if out of range.
   *
  public int[] findXYindexFromLatLon(double lat, double lon, int[] result) {
    Projection dataProjection = getProjection();
    ProjectionPoint pp = dataProjection.latLonToProj(new LatLonPointImpl(lat, lon), new ProjectionPointImpl());

    return findXYindexFromCoord(pp.getX(), pp.getY(), result);
  }

  /**
   * Given a lat,lon point, find the x,y index in the coordinate system.
   * If outside the range, the closest point is returned
   *
   * @param lat    latitude position.
   * @param lon    longitude position.
   * @param result put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point.
   *
  public int[] findXYindexFromLatLonBounded(double lat, double lon, int[] result) {
    Projection dataProjection = getProjection();
    ProjectionPoint pp = dataProjection.latLonToProj(new LatLonPointImpl(lat, lon), new ProjectionPointImpl());

    return findXYindexFromCoordBounded(pp.getX(), pp.getY(), result);
  }

    /**
   * Get Index Ranges for the given lat, lon bounding box.
   * For projection, only an approximation based on latlon corners.
   * Must have CoordinateAxis1D or 2D for x and y axis.
   *
   * @param rect the requested lat/lon bounding box
   * @return list of 2 Range objects, first y then x.
   *
  public List<Range> getRangesFromLatLonRect(LatLonRect rect) throws InvalidRangeException {
    double minx, maxx, miny, maxy;

    ProjectionImpl proj = getProjection();
    if (proj != null && !(proj instanceof VerticalPerspectiveView) && !(proj instanceof MSGnavigation)
            && !(proj instanceof Geostationary)) { // LOOK kludge - how to do this generrally ??
      // first clip the request rectangle to the bounding box of the grid
      LatLonRect bb = getLatLonBoundingBox();
      if (bb == null) throw new IllegalStateException("No LatLonBoundingBox");
      LatLonRect rect2 = bb.intersect(rect);
      if (null == rect2)
        throw new InvalidRangeException("Request Bounding box does not intersect Grid ");
      rect = rect2;
    }

    CoordinateAxis xaxis = getXHorizAxis();
    CoordinateAxis yaxis = getYHorizAxis();

    if (isLatLon()) {

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

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

      // normalize to [minLon,minLon+360]
      double minLon = xaxis.getMinValue();
      minx = LatLonPointImpl.lonNormalFrom( minx, minLon);
      maxx = LatLonPointImpl.lonNormalFrom( maxx, minLon);

    } else {
      ProjectionRect prect = getProjection().latLonToProjBB(rect); // allow projection to override
      minx = prect.getMinPoint().getX();
      miny = prect.getMinPoint().getY();
      maxx = prect.getMaxPoint().getX();
      maxy = prect.getMaxPoint().getY();

      /*
      see ProjectionImpl.latLonToProjBB2()
      Projection dataProjection = getProjection();
      ProjectionPoint ll = dataProjection.latLonToProj(llpt, new ProjectionPointImpl());
      ProjectionPoint ur = dataProjection.latLonToProj(urpt, new ProjectionPointImpl());
      ProjectionPoint lr = dataProjection.latLonToProj(lrpt, new ProjectionPointImpl());
      ProjectionPoint ul = dataProjection.latLonToProj(ulpt, new ProjectionPointImpl());

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

    if ((xaxis instanceof CoordinateAxis1D) && (yaxis instanceof CoordinateAxis1D)) {
      CoordinateAxis1D xaxis1 = (CoordinateAxis1D) xaxis;
      CoordinateAxis1D yaxis1 = (CoordinateAxis1D) yaxis;

      int minxIndex = xaxis1.findCoordElementBounded(minx);
      int minyIndex = yaxis1.findCoordElementBounded(miny);

      int maxxIndex = xaxis1.findCoordElementBounded(maxx);
      int maxyIndex = yaxis1.findCoordElementBounded(maxy);

      List<Range> list = new ArrayList<>();
      list.add(new Range(Math.min(minyIndex, maxyIndex), Math.max(minyIndex, maxyIndex)));
      list.add(new Range(Math.min(minxIndex, maxxIndex), Math.max(minxIndex, maxxIndex)));
      return list;

    } else if ((xaxis instanceof CoordinateAxis2D) && (yaxis instanceof CoordinateAxis2D) && isLatLon()) {
      CoordinateAxis2D lon_axis = (CoordinateAxis2D) xaxis;
      CoordinateAxis2D lat_axis = (CoordinateAxis2D) yaxis;
      int shape[] = lon_axis.getShape();
      int nj = shape[0];
      int ni = shape[1];

      int mini = Integer.MAX_VALUE, minj = Integer.MAX_VALUE;
      int maxi = -1, maxj = -1;

      // margolis 2/18/2010
      //minx = LatLonPointImpl.lonNormal( minx ); // <-- THIS IS NEW
      //maxx = LatLonPointImpl.lonNormal( maxx ); // <-- THIS IS NEW

      // brute force, examine every point LOOK BAD
      for (int j = 0; j < nj; j++) {
        for (int i = 0; i < ni; i++) {
          double lat = lat_axis.getCoordValue(j, i);
          double lon = lon_axis.getCoordValue(j, i);
          //lon = LatLonPointImpl.lonNormal( lon ); // <-- THIS IS NEW

          if ((lat >= miny) && (lat <= maxy) && (lon >= minx) && (lon <= maxx)) {
            if (i > maxi) maxi = i;
            if (i < mini) mini = i;
            if (j > maxj) maxj = j;
            if (j < minj) minj = j;
            //System.out.println(j+" "+i+" lat="+lat+" lon="+lon);
          }
        }
      }

      // this is the case where no points are included
      if ((mini > maxi) || (minj > maxj)) {
        mini = 0;
        minj = 0;
        maxi = -1;
        maxj = -1;
      }

      ArrayList<Range> list = new ArrayList<>();
      list.add(new Range(minj, maxj));
      list.add(new Range(mini, maxi));
      return list;

    } else {
      throw new IllegalArgumentException("must be 1D or 2D/LatLon ");
    }

  }

  /* private int getCrossing(CoordinateAxis2D axis) {
    for (int i=0; i<n; i++)
      if (axis.getCoordValue(i,j) > min) return i;
  } */

  /* private GeneralPath bbShape = null;
 public Shape getLatLonBoundingShape() {
   if (isLatLon())
     return getBoundingBox();

   if (bbShape == null) {
     ProjectionRect bb = getBoundingBox();
     Projection displayProjection = displayMap.getProjection();
     Projection dataProjection = getProjection();

     bbShape = new GeneralPath();

     LatLonPoint llpt = dataProjection.projToLatLon( bb.getX(), bb.getY());
     ProjectionPoint pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX(), bb.getY()+bb.getHeight());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX()+bb.getWidth(), bb.getY()+bb.getHeight());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX()+bb.getWidth(), bb.getY());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     bbShape.closePath();
   }

   return bbShape;
 }

*/


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

package ucar.nc2.grib;

import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.ma2.DataType;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Formatter;
import java.util.zip.Deflater;

/**
 * Abstraction for handling Grib 1 & 2 data in a uniform way.
 *
 * @author John
 * @since 9/1/2014
 */
public class GribData {
  private static Logger logger = LoggerFactory.getLogger(GribData.class);

  public enum InterpolationMethod {none, cubic, linear}

  private static GribData.InterpolationMethod useInterpolationMethod = GribData.InterpolationMethod.linear; // default

  public static GribData.InterpolationMethod getInterpolationMethod() {
    return useInterpolationMethod;
  }

  public static void setInterpolationMethod(GribData.InterpolationMethod interpolationMethod) {
    useInterpolationMethod = interpolationMethod;
  }

  public interface Bean {

    float[] readData() throws IOException;

    int getNBits();

    long getDataLength();

    long getMsgLength();

    int getBinScale();

    int getDecScale();

    double getMinimum();

    double getMaximum();

    double getScale();

  }

  /*
  Code table 11 – Flag
  Bit No. Value Meaning
  1 0 Grid-point data
    1 Spherical harmonic coefficients
  2 0 Simple packing
    1 Complex or second-order packing
  3 0 Floating point values (in the original data) are represented
    1 Integer values (in the original data) are represented
  4 0 No additional flags at octet 14
    1 Octet 14 contains additional flag bits

  The following gives the meaning of the bits in octet 14 ONLY if bit 4 is set to 1. Otherwise octet 14 contains
  regular binary data.
  Bit No. Value Meaning
  5 Reserved – set to zero
  6 0 Single datum at each grid point
    1 Matrix of values at each grid point
  7 0 No secondary bit-maps
    1 Secondary bit-maps present
  8 0 Second-order values constant width
    1 Second-order values different widths
  9–12 Reserved for future use
  Notes:
  (1) Bit 4 shall be set to 1 to indicate that bits 5 to 12 are contained in octet 14 of the Binary data section.
  (2) Bit 3 shall be set to 1 to indicate that the data represented are integer values; where integer values are
  represented, any reference values, if not zero, should be rounded to integer before being applied.
  (3) Where secondary bit-maps are present in the data (used in association with second-order packing and, optionally,
  with a matrix of values at each point), this shall be indicated by setting bit 7 to 1.
  (4) The indicated meaning of bit 6 shall be retained in anticipation of the future reintroduction of a system to define a
  matrix of values at each grid point.
  ____________
   */

  public static class Info {
    public int bitmapLength;     // length of the bitmap section if any
    public long msgLength;       // length of the entire GRIB message
    public long dataLength;       // length of the data section
    public int ndataPoints;      // for Grib1, gds.getNumberPoints; for GRIB2, n data points stored
    public int nPoints;         //  number of points in the GRID
    public float referenceValue;
    public int binaryScaleFactor, decimalScaleFactor, numberOfBits;
    public int originalType;  // code table 5.1

    // GRIB-1 only
    public int flag;

    public boolean isGridPointData() {
      return !GribNumbers.testGribBitIsSet(flag, 1);
    }

    public boolean isSimplePacking() {
      return !GribNumbers.testGribBitIsSet(flag, 2);
    }

    public boolean hasFloatingPointValues() {
      return !GribNumbers.testGribBitIsSet(flag, 3);
    }

    public boolean hasOctet14() {
      return GribNumbers.testGribBitIsSet(flag,  4);
    }

    public String getGridPointS() {
      return isGridPointData() ? "Grid-point" : "Spherical harmonic coefficients";
    }

    public String getPackingS() {
      return isSimplePacking() ? "simple" : "Complex / second order";
    }

    public String getDataTypeS() {
      return hasFloatingPointValues() ? "float" : "int";
    }

    private float DD, EE;
    private int missing_value;
    private boolean init = false;
    public float convert(int val) {
      if (!init) {
        DD = (float) java.lang.Math.pow((double) 10, decimalScaleFactor);
        EE = (float) java.lang.Math.pow( 2.0, binaryScaleFactor);
        missing_value = (2 << numberOfBits - 1) - 1;       // all ones - reserved for missing value
        init = true;
      }

      if (val == missing_value) return Float.NaN;
      return (referenceValue + val * EE) / DD;
    }
  }

  /** This reports how well different compression schemes would work on the specific data.
   * Should be renamed */
  public static void calcScaleOffset(GribData.Bean bean1, Formatter f) {
    float[] data;
    try {
      data = bean1.readData();
    } catch (IOException e) {
      f.format("IOException %s", e.getMessage());
      return;
    }
    int npoints = data.length;

    // we always use unsigned packed
    // "If the packed values are intended to be interpreted as signed/unsigned integers"
    // http://www.unidata.ucar.edu/software/netcdf/docs/BestPractices.html
    int nbits = bean1.getNBits();
    int width = (2 << nbits - 1) - 2;                // unsigned
    int missing_value = (2 << nbits - 1) - 1;       // all ones - reserved for missing value

    // int width2 = (2 << (nbits-1)) - 1;  // signed
    f.format(" nbits = %d%n", nbits);
    f.format(" npoints = %d%n", npoints);
    f.format(" width = %d (0x%s) %n", width, Long.toHexString(width));
    f.format(" scale = %g %n", bean1.getScale());
    f.format(" resolution = %g %n", bean1.getScale() / 2);
    f.format(" range = %f %n%n", bean1.getMaximum() - bean1.getMinimum());

    float dataMin = Float.MAX_VALUE;
    float dataMax = -Float.MAX_VALUE;
    for (float fd : data) {
      if (Float.isNaN(fd)) continue;
      dataMin = Math.min(dataMin, fd);
      dataMax = Math.max(dataMax, fd);
    }
    f.format("           actual    computed%n");
    f.format(" dataMin = %8f %8f%n", dataMin, bean1.getMinimum());
    f.format(" dataMax = %8f %8f%n", dataMax, bean1.getMaximum());

    f.format(" actual range = %f%n", (dataMax - dataMin));

    // scale_factor =(dataMax - dataMin) / (2^n - 1)
    // add_offset = dataMin + 2^(n-1) * scale_factor

    double scale_factor = (dataMax - dataMin) / width;
    // float add_offset = dataMin + width2 * scale_factor / 2; // signed
    double add_offset = dataMin;                             // unsigned

    f.format(" scale_factor = %g%n", scale_factor);
    f.format(" add_offset = %g%n", add_offset);

    // unpacked_data_value = packed_data_value * scale_factor + add_offset
    // packed_data_value = nint((unpacked_data_value - add_offset) / scale_factor)

    ByteBuffer bb = ByteBuffer.allocate(4 * npoints);
    IntBuffer intBuffer = bb.asIntBuffer();
    double diffMax = -Double.MAX_VALUE;
    double diffTotal = 0;
    double diffTotal2 = 0;
    for (float fd : data) {
      if (Float.isNaN(fd)) {
        intBuffer.put(missing_value);
        continue;
      }

      // otherwise pack it
      int packed_data = (int) Math.round((fd - add_offset) / scale_factor);   // nint((unpacked_data_value - add_offset) / scale_factor)
      double unpacked_data = packed_data * scale_factor + add_offset;
      double diff = Math.abs(fd - unpacked_data);
      if (diff > scale_factor / 2)
        f.format("***   org=%g, packed_data=%d unpacked=%g diff = %g%n", fd, packed_data, unpacked_data, diff);

      diffMax = Math.max(diffMax, diff);
      diffTotal += diff;
      diffTotal2 += diff * diff;
      intBuffer.put(packed_data);
    }

    f.format("%n max_diff = %g%n", diffMax);
    f.format(" avg_diff = %g%n", diffTotal / data.length);

    // Math.sqrt( sumsq/n - avg * avg)
    double mean = diffTotal / npoints;
    double var = (diffTotal2 / npoints - mean * mean);
    f.format(" std_diff = %g%n", Math.sqrt(var));

    f.format("%nCompression%n");
    f.format(" number of values = %d%n", npoints);
    f.format(" uncompressed as floats = %d%n", npoints * 4);
    int packedBitsLen = npoints * nbits / 8;
    f.format(" uncompressed packed bits = %d%n", packedBitsLen);
    f.format(" grib data length = %d%n", bean1.getDataLength());
    f.format(" grib msg length = %d%n", bean1.getMsgLength());

    byte[] bdata = convertToBytes(data);
    byte[] scaledData = bb.array();

    ////////////////////////////////////////////
    f.format("%ndeflate (float)%n");
    Deflater deflater = new Deflater();
    deflater.setInput(bdata);
    deflater.finish();
    int compressedSize = deflater.deflate(new byte[10 * npoints]);
    deflater.end();
    f.format(" compressedSize = %d%n", compressedSize);
    f.format(" ratio floats / size = %f%n", (float) (npoints * 4) / compressedSize);
    f.format(" ratio packed bits / size = %f%n", (float) packedBitsLen / compressedSize);
    f.format(" ratio size / grib = %f%n", (float) compressedSize / bean1.getMsgLength());

    /////////////////////////////////////////////////////////
    f.format("%ndeflate (scaled ints)%n");
    deflater = new Deflater();
    deflater.setInput(scaledData);
    deflater.finish();
    compressedSize = deflater.deflate(new byte[10 * npoints]);
    deflater.end();

    f.format(" compressedSize = %d%n", compressedSize);
    f.format(" ratio floats / size = %f%n", (float) (npoints * 4) / compressedSize);
    f.format(" ratio packed bits / size = %f%n", (float) packedBitsLen / compressedSize);
    f.format(" ratio size / grib = %f%n", (float) compressedSize / bean1.getMsgLength());
  }

  @Nullable
  public static byte[] compressScaled(GribData.Bean bean) throws IOException {
    float[] data = bean.readData();
    int npoints = data.length;

    // we always use unsigned packed
    // "If the packed values are intended to be interpreted as signed/unsigned integers"
    // http://www.unidata.ucar.edu/software/netcdf/docs/BestPractices.html
    int nbits = bean.getNBits();
    int width = (2 << nbits - 1) - 2;                // unsigned
    int missing_value = (2 << nbits - 1) - 1;       // all ones - reserved for missing value

    float dataMin = Float.MAX_VALUE;
    float dataMax = -Float.MAX_VALUE;
    for (float fd : data) {
      if (Float.isNaN(fd)) continue;
      dataMin = Math.min(dataMin, fd);
      dataMax = Math.max(dataMax, fd);
    }

    // scale_factor =(dataMax - dataMin) / (2^n - 1)
    // add_offset = dataMin + 2^(n-1) * scale_factor

    double scale_factor = (dataMax - dataMin) / width;
    // float add_offset = dataMin + width2 * scale_factor / 2; // signed
    double add_offset = dataMin;                             // unsigned

    // unpacked_data_value = packed_data_value * scale_factor + add_offset
    // packed_data_value = nint((unpacked_data_value - add_offset) / scale_factor)

    ByteBuffer bb = ByteBuffer.allocate(4 * npoints + 24);
    bb.putDouble(scale_factor);
    bb.putDouble(add_offset);

    bb.putInt(nbits);
    bb.putInt(npoints);

    for (float fd : data) {
      if (Float.isNaN(fd)) {
        bb.putInt(missing_value);
      } else {
        int packed_data = (int) Math.round((fd - add_offset) / scale_factor);   // nint((unpacked_data_value - add_offset) / scale_factor)
        bb.putInt(packed_data);
      }
    }

    return null;
  }

  // only used by test code
  public static float[] uncompressScaled(byte[] bdata) {
    throw new UnsupportedOperationException("bzip2 compression no longer supported.");
  }

  public static byte[] convertToBytes(float[] data) {
    ByteBuffer bb = ByteBuffer.allocate(data.length * 4);
    for (float val : data) bb.putFloat(val);
    return bb.array();
  }

  public static byte[] convertToBytes(int[] data) {
    ByteBuffer bb = ByteBuffer.allocate(data.length * 4);
    for (int val : data) bb.putInt(val);
    return bb.array();
  }

  public static double entropy(byte[] data) {
    int[] p = new int[256];

    // count occurrences
    for (byte b : data) {
      short s = DataType.unsignedByteToShort(b);
      p[s]++;
    }

    double n = data.length;
    double iln2 = 1.0 / Math.log(2.0);
    double sum = 0.0;
    for (int i = 0; i < 256; i++) {
      if (p[i] == 0) continue;
      double prob = ((double) p[i]) / n;
      sum += Math.log(prob) * prob * iln2;
    }

    return (sum == 0) ? 0.0 : -sum;
  }

  public static double entropy(int nbits, int[] data) {
    if (data == null) return 0.0;

    int n = (int) Math.pow(2, nbits);
    int[] p = new int[n];

    // count occurrences
    int count = 0;
    for (int b : data) {
      if (b < 0 || b > n - 1) {
        logger.warn("BAD %d at index %d; max=%d%n", b, count, n - 1);
        // just skip
      } else {
        p[b]++;
      }
      count++;
    }

    double len = data.length;
    double iln2 = 1.0 / Math.log(2.0);
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
      if (p[i] == 0) continue;
      double prob = ((double) p[i]) / len;
      sum += Math.log(prob) * prob * iln2;
    }

    return (sum == 0) ? 0.0 : -sum;
  }
}


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

/**
 * Handles nested sequences: a 1D array of variable length 1D arrays of StructureData.
 * Uses same technique as ArrayStructureMA for the inner fields; data storage is in member arrays.
 * Used only by opendap internals.
 *
 * Example use:
 * <pre>
    ArraySequence aseq = new ArraySequence( members, outerLength);
    for (int seq=0; seq < outerLength; seq++) {
      aseq.setSequenceLength(seq, seqLength);
    }
    aseq.finish();
 </pre>
 *
 * @author caron
 */
public class ArraySequenceNested extends ArrayStructure {
  private int[] sequenceLen;
  private int[] sequenceOffset;
  private int total = 0;

  /**
   * This is used for inner sequences, ie variable length structures nested inside of another structure.
   * @param members the members of the STructure
   * @param nseq the number of sequences, ie the length of the outer structure.
   */
  public ArraySequenceNested(StructureMembers members, int nseq) {
    super(members, new int[] {nseq});
    sequenceLen = new int[nseq];
  }

  // not sure how this is used
  protected StructureData makeStructureData( ArrayStructure as, int index) {
    return new StructureDataA( as, index);
  }

  public StructureData getStructureData(int index) {
    return new StructureDataA( this, index);
  }

  /**
   * Set the length of one of the sequences.
   * @param outerIndex which sequence?
   * @param len what is its length?
   */
  public void setSequenceLength( int outerIndex, int len) {
    sequenceLen[outerIndex] = len;
  }

  /**
   * Get the length of the ith sequence.
   * @param outerIndex which sequence?
   * @return its length
   */
  public int getSequenceLength( int outerIndex) {
    return sequenceLen[outerIndex];
  }

  /**
   * Get the the starting index of the ith sequence.
   * @param outerIndex which sequence?
   * @return its starting index
   */
  public int getSequenceOffset( int outerIndex) {
    return sequenceOffset[outerIndex];
  }

  /**
   * Call this when you have set all the sequence lengths.
   */
  public void finish() {
    sequenceOffset = new int[nelems];

    total = 0;
    for (int i=0; i<nelems; i++) {
      sequenceOffset[i] = total;
      total += sequenceLen[i];
    }

    sdata = new StructureData[nelems];
    for (int i=0; i<nelems; i++)
      sdata[i] = new StructureDataA( this, sequenceOffset[i]);

    // make the member arrays
    for (StructureMembers.Member m : members.getMembers()) {
      int[] mShape = m.getShape();
      int[] shape = new int[mShape.length + 1];
      shape[0] = total;
      System.arraycopy(mShape, 0, shape, 1, mShape.length);

      // LOOK not doing nested structures
      Array data = Array.factory(m.getDataType(), shape);
      m.setDataArray(data);
    }
  }

  /**
   * @return the total number of Structures over all the nested sequences.
   */
  public int getTotalNumberOfStructures() { return total; }

  /**
   * Flatten the Structures into a 1D array of Structures of length getTotalNumberOfStructures().
   * @return Array of Structures
   */
  public ArrayStructure flatten() {
    ArrayStructureW aw = new ArrayStructureW( getStructureMembers(), new int[] {total});
    for (int i=0; i<total; i++) {
      StructureData sdata = new StructureDataA( this, i);
      aw.setStructureData(sdata, i);
    }
    return aw;
  }

  public double getScalarDouble(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.DOUBLE) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be double");
    Array data = m.getDataArray();
    return data.getDouble( recnum * m.getSize()); // gets first one in the array
  }

  public double[] getJavaArrayDouble(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.DOUBLE) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be double");
    int count = m.getSize();
    Array data = m.getDataArray();
    double[] pa = new double[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getDouble( recnum * count + i);
    return pa;
  }

  public float getScalarFloat(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.FLOAT) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be float");
    Array data = m.getDataArray();
    return data.getFloat( recnum * m.getSize()); // gets first one in the array
  }

  public float[] getJavaArrayFloat(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.FLOAT) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be float");
    int count = m.getSize();
    Array data = m.getDataArray();
    float[] pa = new float[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getFloat( recnum * count + i);
    return pa;
  }

  public byte getScalarByte(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == byte.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be byte");
    Array data = m.getDataArray();
    return data.getByte( recnum * m.getSize()); // gets first one in the array
  }

  public byte[] getJavaArrayByte(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == byte.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be byte");
    int count = m.getSize();
    Array data = m.getDataArray();
    byte[] pa = new byte[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getByte( recnum * count + i);
    return pa;
  }

  public short getScalarShort(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == short.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be short");
    Array data = m.getDataArray();
    return data.getShort( recnum * m.getSize()); // gets first one in the array
  }

  public short[] getJavaArrayShort(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == short.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be short");
    int count = m.getSize();
    Array data = m.getDataArray();
    short[] pa = new short[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getShort( recnum * count + i);
    return pa;
  }

  public int getScalarInt(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == int.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be int");
    Array data = m.getDataArray();
    return data.getInt( recnum * m.getSize()); // gets first one in the array
  }

  public int[] getJavaArrayInt(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == int.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be int");
    int count = m.getSize();
    Array data = m.getDataArray();
    int[] pa = new int[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getInt( recnum * count + i);
    return pa;
  }

  public long getScalarLong(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == long.class)) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be long");
    Array data = m.getDataArray();
    return data.getLong( recnum * m.getSize()); // gets first one in the array
  }

  public long[] getJavaArrayLong(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == long.class)) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be long");
    int count = m.getSize();
    Array data = m.getDataArray();
    long[] pa = new long[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getLong( recnum * count + i);
    return pa;
  }

  public char getScalarChar(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.CHAR) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be char");
    Array data = m.getDataArray();
    return data.getChar( recnum * m.getSize()); // gets first one in the array
  }

  public char[] getJavaArrayChar(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.CHAR) throw new IllegalArgumentException("Type is "+m.getDataType()+", must be char");
    int count = m.getSize();
    Array data = m.getDataArray();
    char[] pa = new char[count];
    for (int i=0; i<count; i++)
      pa[i] = data.getChar( recnum * count + i);
    return pa;
  }

  public String getScalarString(int recnum, StructureMembers.Member m) {
    if (m.getDataType() == DataType.CHAR) {
      ArrayChar data = (ArrayChar) m.getDataArray();
      return data.getString( recnum);
    }

    if (m.getDataType() == DataType.STRING) {
      ArrayObject data = (ArrayObject) m.getDataArray();
      return (String) data.getObject( recnum);
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be String or char");
  }

  public String[] getJavaArrayString(int recnum, StructureMembers.Member m) {
    int n = m.getSize();
    String[] result = new String[n];

    if (m.getDataType() == DataType.CHAR) {

      ArrayChar data = (ArrayChar) m.getDataArray();
      for (int i=0; i<n; i++)
        result[i] = data.getString( recnum * n + i);
      return result;

    } else if (m.getDataType() == DataType.STRING) {

      Array data = m.getDataArray();
      for (int i=0; i<n; i++)
        result[i] = (String) data.getObject( recnum * n + i);
      return result;
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be String or char");
  }

  public StructureData getScalarStructure(int recnum, StructureMembers.Member m) {
    if (m.getDataType() == DataType.STRUCTURE) {
      ArrayStructure data = (ArrayStructure) m.getDataArray();
      return data.getStructureData( recnum * m.getSize());  // gets first in the array
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be Structure");
  }

  public ArrayStructure getArrayStructure(int recnum, StructureMembers.Member m) {
    if (m.getDataType() == DataType.STRUCTURE) {
      ArrayStructure data = (ArrayStructure) m.getDataArray();
      // we need to subset this array structure to deal with just the subset for this recno
      // use "brute force" for now, see if we can finesse later
      int count = m.getSize();
      StructureData[] sdata = new StructureData[count];
      for (int i=0; i<count; i++)
        sdata[i] = data.getStructureData( recnum * count + i);

      return new ArrayStructureW( data.getStructureMembers(), m.getShape(), sdata);
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be Structure");
  }

}


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

import java.util.Formatter;

/**
 * static formatting utilities. Replace with standard java library when possible.
 *
 * @author John Caron
 */
public class Format {

  /**
   * Blank fill sbuff with blanks, until position tabStop.
   *
   * @param sbuff     StringBuffer to manipulate
   * @param tabStop   pad out to here
   * @param alwaysOne true if you want to guarentee at least one space.
   */
  public static void tab(StringBuffer sbuff, int tabStop, boolean alwaysOne) {
    int len = sbuff.length();
    if (tabStop > len) {
      sbuff.setLength(tabStop);
      for (int i = len; i < tabStop; i++) {
        sbuff.setCharAt(i, ' ');
      }
    } else if (alwaysOne) {
      sbuff.setLength(len + 1);
      sbuff.setCharAt(len, ' ');
    }
  }

  /**
   * Blank fill sbuff with blanks, until position tabStop.
   *
   * @param sbuff     StringBuilder to manipulate
   * @param tabStop   pad out to here
   * @param alwaysOne true if you want to guarentee at least one space.
   */
  public static void tab(StringBuilder sbuff, int tabStop, boolean alwaysOne) {
    int len = sbuff.length();
    if (tabStop > len) {
      sbuff.setLength(tabStop);
      for (int i = len; i < tabStop; i++) {
        sbuff.setCharAt(i, ' ');
      }
    } else if (alwaysOne) {
      sbuff.setLength(len + 1);
      sbuff.setCharAt(len, ' ');
    }
  }

  /**
   * Create a new string by padding the existing one with blanks to specified width.
   * Do nothing if length is already greater or equal to width.
   *
   * @param s     string to pad
   * @param width length of return string
   * @return padded string
   */
  public static String s(String s, int width) {
    return pad(s, width, false);
  }

  /**
   * Create a new string by padding the existing one with blanks to specified width.
   * Do nothing if length is already greater or equal to width.
   *
   * @param s            string to pad
   * @param width        length of return string
   * @param rightJustify if true, right justify, else left justify
   * @return padded string
   */
  public static String pad(String s, int width, boolean rightJustify) {
    if (s.length() >= width) {
      return s;
    }
    StringBuilder sbuff = new StringBuilder(width);
    int need = width - s.length();
    sbuff.setLength(need);
    for (int i = 0; i < need; i++) {
      sbuff.setCharAt(i, ' ');
    }

    if (rightJustify) {
      sbuff.append(s);
    } else {
      sbuff.insert(0, s);
    }

    return sbuff.toString();
  }

  /**
   * Format an integer value.
   *
   * @param v     : value
   * @param width pad to this width
   * @return formatted string
   */
  public static String i(int v, int width) {
    return pad(Integer.toString(v), width, true);
  }

  /**
   * Format a long value.
   *
   * @param v     : value
   * @param width pad to this width
   * @return formatted string
   */
  public static String l(long v, int width) {
    return pad(Long.toString(v), width, true);
  }

  /**
   * Double value formatting with minimum number of significant figures in a minimum width.
   * This will try to do a reasonable job of getting
   * a representation that has min_sigfig significant figures in minimum width.
   *
   * @param d          the number to format.
   * @param min_sigfig minimum number of significant figures
   * @return string representation
   */
  public static String d(double d, int min_sigfig) {
    return formatDouble(d, min_sigfig, -1).trim();
  }

  /**
   * Double value formatting with minimum number of significant figures in a specified width.
   * This will try to do a reasonable job of getting
   * a representation that has min_sigfig significant figures in the specified width.
   * Right now, all it does is call d( double d, int min_sigfig) and left pad out to width chars.
   *
   * @param d          the number to format.
   * @param min_sigfig minimum number of significant figures
   * @param width      width of the result
   * @return string representation, right justified in field of specified width
   */
  public static String d(double d, int min_sigfig, int width) {
    String s = Format.d(d, min_sigfig);
    return pad(s, width, true);
  }

  /**
   * Double value formatting with fixed number of digits to the right of the decimal point.
   *
   * @param d              the number to format.
   * @param fixed_decimals number of digits to the right of the decimal point
   * @return string representation, with specified number of decimal places
   */
  public static String dfrac(double d, int fixed_decimals) {
    return formatDouble(d, 100, fixed_decimals).trim();
    //String s = Double.toString( d);
    //s = sigfigFix( s, 100, num_dec);
    //return s.trim();
  }

  /* This dorks with Double.toString():
  *
  * From Double.toString() (m = magnitude of the number):
  *
  *  If m is greater than or equal to 10^-3 but less than 10^7, then it is represented as the
  *  integer part of m, in decimal form with no leading zeroes, followed by '.' (.),
  *  followed by one or more decimal digits representing the fractional part of m.
  *
  *  If m is less than 10^-3 or greater than 10^7, then it is represented in scientific notation.
  *  Let n be the unique integer such that 10n<=m<10n+1; then let a be the mathematically exact
  *  quotient of m and 10n so that 1<=a<10. The magnitude is then represented as the integer part
  *  of a, as a single decimal digit, followed by '.' (.), followed by decimal digits representing
  *  the fractional part of a, followed by the letter 'E' (E), followed by a representation of n
  *  as a decimal integer, as produced by the method Integer.toString(int).
  *
  *  How many digits must be printed for the fractional part of m or a? There must be
  *  at least one digit to represent the fractional part, and beyond that as many,
  *  but only as many, more digits as are needed to uniquely distinguish the argument
  *  value from adjacent values of type double. That is, suppose that x is the exact
  *  mathematical value represented by the decimal representation produced by this method
  *  for a finite nonzero argument d. Then d must be the double value nearest to x; or if
  *  two double values are equally close to x, then d must be one of them and the least
  *  significant bit of the significand of d must be 0.
  */

  /**
   * Format a double value
   *
   * @param d              value to format
   * @param min_sigFigs    minimum significant figures
   * @param fixed_decimals number of fixed decimals
   * @return double formatted as a string
   */
  public static String formatDouble(double d, int min_sigFigs, int fixed_decimals) {

    String s = Double.toString(d);

    // extract the sign
    String sign;
    String unsigned;
    if (s.startsWith("-") || s.startsWith("+")) {
      sign = s.substring(0, 1);
      unsigned = s.substring(1);
    } else {
      sign = "";
      unsigned = s;
    }

    // deal with exponential notation
    String mantissa;
    String exponent;
    int eInd = unsigned.indexOf('E');
    if (eInd == -1) {
      eInd = unsigned.indexOf('e');
    }
    if (eInd == -1) {
      mantissa = unsigned;
      exponent = "";
    } else {
      mantissa = unsigned.substring(0, eInd);
      exponent = unsigned.substring(eInd);
    }

    // deal with decimal point
    StringBuilder number, fraction;
    int dotInd = mantissa.indexOf('.');
    if (dotInd == -1) {
      number = new StringBuilder(mantissa);
      fraction = new StringBuilder("");
    } else {
      number = new StringBuilder(mantissa.substring(0, dotInd));
      fraction = new StringBuilder(mantissa.substring(dotInd + 1));
    }

    // number of significant figures
    int numFigs = number.length();
    int fracFigs = fraction.length();

    // can do either fixed_decimals or min_sigFigs
    if (fixed_decimals != -1) {
      if (fixed_decimals == 0) {
        fraction.setLength(0);
      } else if (fixed_decimals > fracFigs) {
        int want = fixed_decimals - fracFigs;
        for (int i = 0; i < want; i++) {
          fraction.append("0");
        }
      } else if (fixed_decimals < fracFigs) {
        int chop = fracFigs - fixed_decimals;  // LOOK should round !!
        fraction.setLength(fraction.length() - chop);
      }
      fracFigs = fixed_decimals;

    } else {
      // Don't count leading zeros in the fraction, if no number
      if (((numFigs == 0) || number.toString().equals("0"))
              && (fracFigs > 0)) {
        numFigs = 0;
        number = new StringBuilder("");
        for (int i = 0; i < fraction.length(); ++i) {
          if (fraction.charAt(i) != '0') {
            break;
          }
          --fracFigs;
        }
      }
      // Don't count trailing zeroes in the number if no fraction
      if ((fracFigs == 0) && (numFigs > 0)) {
        for (int i = number.length() - 1; i > 0; i--) {
          if (number.charAt(i) != '0') {
            break;
          }
          --numFigs;
        }
      }
      // deal with min sig figures
      int sigFigs = numFigs + fracFigs;
      if (sigFigs > min_sigFigs) {
        // Want fewer figures in the fraction; chop (should round? )
        int chop = Math.min(sigFigs - min_sigFigs, fracFigs);
        fraction.setLength(fraction.length() - chop);
        fracFigs -= chop;
      }
    }

    /*int sigFigs = numFigs + fracFigs;
   if (sigFigs > max_sigFigs) {

     if (numFigs >= max_sigFigs) {  // enough sig figs in just the number part
       fraction.setLength( 0 );
       for ( int i=max_sigFigs; i<numFigs; ++i )
         number.setCharAt( i, '0' );  // should round?
     } else {

       // Want fewer figures in the fraction; chop (should round? )
       int chop = sigFigs - max_sigFigs;
       fraction.setLength( fraction.length() - chop );
     }
   }

   /* may want a fixed decimal place
   if (dec_places != -1) {

     if (dec_places == 0) {
       fraction.setLength( 0 );
       fracFigs = 0;
     } else if (dec_places > fracFigs) {
       int want = dec_places - fracFigs;
       for (int i=0; i<want; i++)
         fraction.append("0");
     } else if (dec_places < fracFigs) {
       int chop = fracFigs - dec_places;
       fraction.setLength( fraction.length() - chop );
       fracFigs = dec_places;
     }

   } */

    if (fraction.length() == 0) {
      return sign + number + exponent;
    } else {
      return sign + number + "." + fraction + exponent;
    }
  }

  /**
   * Nicely formatted representation of bytes, eg turn 5.636E7 into
   *
   * @param size the size in bytes
   * @return formatted string
   */
  public static String formatByteSize(double size) {
    String unit = null;
    if (size > 1.0e15) {
      unit = "Pbytes";
      size *= 1.0e-15;
    } else if (size > 1.0e12) {
      unit = "Tbytes";
      size *= 1.0e-12;
    } else if (size > 1.0e9) {
      unit = "Gbytes";
      size *= 1.0e-9;
    } else if (size > 1.0e6) {
      unit = "Mbytes";
      size *= 1.0e-6;
    } else if (size > 1.0e3) {
      unit = "Kbytes";
      size *= 1.0e-3;
    } else {
      unit = "bytes";
    }

    return Format.d(size, 4) + " " + unit;
  }

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

  /**
   * Show the value of a double to the significant figures
   *
   * @param d      double value
   * @param sigfig number of significant figures
   */
  private static void show(double d, int sigfig) {
    System.out.println("Format.d(" + d + "," + sigfig + ") == "
            + Format.d(d, sigfig));
  }

  /**
   * Show the value of a double with specified number of decimal places
   *
   * @param d          double value
   * @param dec_places number of decimal places
   */
  private static void show2(double d, int dec_places) {
    System.out.println("Format.dfrac(" + d + "," + dec_places + ") == "
            + Format.dfrac(d, dec_places));
  }

  public static void doit(int scale, double n) {
    Formatter f = new Formatter();
    String format = "Prob_above_%f3." + scale;
    f.format(format, n);
    System.out.printf("%s %f == %s%n", format, n, f);
  }

  public static void main(String argv[]) {
    doit(1, 1.00003);
    System.out.printf("%f == %f3.1%n", 1.00003, 1.00003);
    System.out.printf("%s%n", dfrac(1.00003, 0));
    System.out.printf("%s%n", dfrac(1.00003, 1));
  }

}


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

package ucar.nc2.dt.grid;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateTransform;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.dt.GridDatatype;
import ucar.nc2.dt.grid.gis.GridBoundariesExtractor;
import ucar.nc2.ncml.NcMLWriter;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.ProjectionRect;
import ucar.unidata.util.Parameter;

/**
 * A helper class to GridDataset; creates a GridDataset XML document.
 * This is a candidate for the XML representation of the Grid SDT.
 * Used to create form for NetcdfSubsetService for Grids.
 *
 * @author caron
 */
public class GridDatasetInfo {
	private static final Logger logger = LoggerFactory.getLogger(GridDatasetInfo.class);

	private ucar.nc2.dt.GridDataset gds;
	private String path;

	public GridDatasetInfo(ucar.nc2.dt.GridDataset gds, String path) {
		this.gds = gds;
		this.path = path;
	}

	/**
	 * Write the information as an XML document
	 *
	 * @param doc write XML for this Document
	 * @return String output
	 */
	public String writeXML(Document doc) {
		XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
		return fmt.outputString(doc);
	}

	/**
	 * Write the information as an XML document
	 *
	 * @param doc write XML for this Document
	 * @param os  write to this output stream
	 * @throws java.io.IOException on write error
	 */
	public void writeXML(Document doc, OutputStream os) throws IOException {
		XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
		fmt.output(doc, os);
	}

	/**
	 * Create the "Dataset Description" XML document from this GridDataset
	 *
	 * @return a JDOM Document
	 */
	public Document makeDatasetDescription() {
		Element rootElem = new Element("gridDataset");
		Document doc = new Document(rootElem);
		rootElem.setAttribute("location", gds.getLocation());
		if (null != path)
			rootElem.setAttribute("path", path);

		/* dimensions
    List dims = getDimensions(gds);
    for (int j = 0; j < dims.size(); j++) {
      Dimension dim = (Dimension) dims.get(j);
      rootElem.addContent(ucar.nc2.ncml.NcMLWriter.writeDimension(dim, null));
    } */

		// coordinate axes
		for (CoordinateAxis axis : getCoordAxes(gds)) {
			rootElem.addContent(writeAxis(axis));
		}

		/* grids
    List grids = gds.getGrids();
    Collections.sort(grids, new GridComparator());
    for (int i = 0; i < grids.size(); i++) {
      GeoGrid grid = (GeoGrid) grids.get(i);
      rootElem.addContent(writeGrid(grid));
    } */

		/* coordinate systems
    List gridSets = gds.getGridsets();
    for (int i = 0; i < gridSets.size(); i++) {
      GridDataset.Gridset gridset = (GridDataset.Gridset) gridSets.get(i);
      rootElem.addContent(writeCoordSys(gridset.getGeoCoordSystem()));
    } */

		// gridSets
		List<GridDataset.Gridset> gridSets = gds.getGridsets();
		Collections.sort(gridSets, new GridSetComparator());
		for (GridDataset.Gridset gridset : gridSets) {
			rootElem.addContent(writeGridSet(gridset));
		}

		// coordinate transforms
		for (CoordinateTransform ct : getCoordTransforms(gds)) {
			rootElem.addContent(writeCoordTransform(ct));
		}

		/* global attributes
    Iterator atts = gds.getGlobalAttributes().iterator();
    while (atts.hasNext()) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) atts.next();
      rootElem.addContent(ucar.nc2.ncml.NcMLWriter.writeAttribute(att, "attribute", null));
    } */

		// add lat/lon bounding box
		LatLonRect bb = gds.getBoundingBox();
		if (bb != null)
			rootElem.addContent(writeBoundingBox(bb));

		// add date range
		CalendarDate start = gds.getCalendarDateStart();
		CalendarDate end = gds.getCalendarDateEnd();
		if ((start != null) && (end != null)) {
			Element dateRange = new Element("TimeSpan");
			dateRange.addContent(new Element("begin").addContent(start.toString()));
			dateRange.addContent(new Element("end").addContent(end.toString()));
			rootElem.addContent(dateRange);
		}

		// add accept list
		addAcceptList(rootElem);

		//    elem.addContent(new Element("accept").addContent("xml"));
		//    elem.addContent(new Element("accept").addContent("csv"));
		//    elem.addContent(new Element("accept").addContent("netcdf"));
		return doc;
	}

	/**
	 * Returns a WKT polygon with the dataset boundaries
	 *
	 * @return  WKT string
	 */
	public String getDatasetBoundariesWKT(){

		return GridBoundariesExtractor.valueOf(gds).getDatasetBoundariesWKT();
	}

	/**
	 * Create the "Grid Form" XML document from this GridDataset.
	 * Used to create the Grid HTML form, cause I dont know XSLT
	 *
	 * @return the JDOM Document
	 */
	public Document makeGridForm() {
		Element rootElem = new Element("gridForm");
		Document doc = new Document(rootElem);
		rootElem.setAttribute("location", gds.getLocation());
		if (null != path)
			rootElem.setAttribute("path", path);

		// its all about grids
		List<GridDatatype> grids = gds.getGrids();
		Collections.sort(grids, new GridComparator()); // sort by time axis, vert axis, grid name

		CoordinateAxis currentTime = null;
		CoordinateAxis currentVert = null;
		Element timeElem = null;
		Element vertElem = null;
		boolean newTime;

		for (int i = 0; i < grids.size(); i++) {
			GeoGrid grid = (GeoGrid) grids.get(i);
			GridCoordSystem gcs = grid.getCoordinateSystem();

			CoordinateAxis time = gcs.getTimeAxis();
			CoordinateAxis vert = gcs.getVerticalAxis();

			/* System.out.println(" grid "+grid.getName()
              +" time="+(time == null ? " null" : time.hashCode())
              +" vert="+(vert == null ? " null" : vert.hashCode())); */

			//Assuming all variables in dataset has ensemble dim if one has
			if(i==0){
				CoordinateAxis1D ens = gcs.getEnsembleAxis();
				if(ens != null){
					Element ensAxisEl = writeAxis2(ens, "ensemble");
					rootElem.addContent(ensAxisEl);
				}

			}

			if ((i == 0) || !compareAxis(time, currentTime)) {
				timeElem = new Element("timeSet");
				rootElem.addContent(timeElem);
				Element timeAxisElement = writeAxis2(time, "time");
				if (timeAxisElement != null)
					timeElem.addContent(timeAxisElement);
				currentTime = time;
				newTime = true;
			} else {
				newTime = false;
			}

			if (newTime || !compareAxis(vert, currentVert)) {
				vertElem = new Element("vertSet");
				timeElem.addContent(vertElem);
				Element vertAxisElement = writeAxis2(vert, "vert");
				if (vertAxisElement != null)
					vertElem.addContent(vertAxisElement);
				currentVert = vert;
			}

			vertElem.addContent(writeGrid(grid));
		}

		// add lat/lon bounding box
		LatLonRect bb = gds.getBoundingBox();
		if (bb != null)
			rootElem.addContent(writeBoundingBox(bb));

		// add projected bounding box
		//--> Asuming all gridSets have the same coordinates and bbox
		ProjectionRect rect = grids.get(0).getCoordinateSystem().getBoundingBox();

		Element projBBOX =new Element("projectionBox");
		Element minx = new Element("minx");
		minx.addContent( Double.valueOf(rect.getMinX()).toString() );
		projBBOX.addContent(minx);
		Element maxx = new Element("maxx");
		maxx.addContent( Double.valueOf(rect.getMaxX()).toString() );
		projBBOX.addContent(maxx);
		Element miny = new Element("miny");
		miny.addContent( Double.valueOf(rect.getMinY()).toString() );
		projBBOX.addContent(miny);
		Element maxy = new Element("maxy");
		maxy.addContent( Double.valueOf(rect.getMaxY()).toString() );
		projBBOX.addContent(maxy);

		rootElem.addContent(projBBOX);

		// add date range
		CalendarDate start = gds.getCalendarDateStart();
		CalendarDate end = gds.getCalendarDateEnd();
		if ((start != null) && (end != null)) {
			Element dateRange = new Element("TimeSpan");
			dateRange.addContent(new Element("begin").addContent(start.toString()));
			dateRange.addContent(new Element("end").addContent(end.toString()));
			rootElem.addContent(dateRange);
		}

		// add accept list
		addAcceptList(rootElem);

		//    Element elem = new Element("AcceptList");
		//    elem.addContent(new Element("accept").addContent("xml"));
		//    elem.addContent(new Element("accept").addContent("csv"));
		//    elem.addContent(new Element("accept").addContent("netcdf"));
		//    rootElem.addContent(elem);
		return doc;
	}

	private void addAcceptList(Element rootElement){

		// add accept list
		Element elem = new Element("AcceptList");
		//accept list for Grid As Point requests
		Element gridAsPoint = new Element("GridAsPoint");

        // LOOK this is wrong - should be using SupportedOperation class or something
		gridAsPoint.addContent(new Element("accept").addContent("xml").setAttribute("displayName", "xml") );
		gridAsPoint.addContent(new Element("accept").addContent("xml_file").setAttribute("displayName", "xml (file)"));
		gridAsPoint.addContent(new Element("accept").addContent("csv").setAttribute("displayName", "csv"));
		gridAsPoint.addContent(new Element("accept").addContent("csv_file").setAttribute("displayName", "csv (file)"));
		gridAsPoint.addContent(new Element("accept").addContent("geocsv").setAttribute("displayName", "geocsv"));
		gridAsPoint.addContent(new Element("accept").addContent("geocsv_file").setAttribute("displayName", "geocsv (file)"));
		gridAsPoint.addContent(new Element("accept").addContent("netcdf").setAttribute("displayName", "netcdf"));

		//accept list for Grid requests
		Element grids = new Element("Grid");
		grids.addContent(new Element("accept").addContent("netcdf").setAttribute("displayName", "netcdf"));
		//Check if netcdf4 is available
		//	    try{
		//	    	if( Nc4Iosp.isClibraryPresent() ){
		//	    		grids.addContent(new Element("accept").addContent("netcdf4"));
		//	    		gridAsPoint.addContent(new Element("accept").addContent("netcdf4"));
		//	    	}
		//	    }catch(UnsatisfiedLinkError e){
		//	    	//Log this in threddsServlet.log ??
		//	    }

		elem.addContent(gridAsPoint);
		elem.addContent(grids);
		rootElement.addContent(elem);
	}

	private Element writeAxis2(CoordinateAxis axis, String name) {
		if (axis == null) return null;

		NcMLWriter ncmlWriter = new NcMLWriter();

		Element varElem = new Element(name);
		varElem.setAttribute("name", axis.getFullName());
		varElem.setAttribute("shape", getShapeString(axis.getShape())); // axis.getDimensionsString());

		DataType dt = axis.getDataType();
		varElem.setAttribute("type", dt.toString());

		AxisType axisType = axis.getAxisType();
		if (null != axisType)
			varElem.setAttribute("axisType", axisType.toString());

		// attributes
		for (Attribute att : axis.getAttributes())
			varElem.addContent(ncmlWriter.makeAttributeElement(att));

		try {
			Element values = ncmlWriter.makeValuesElement(axis, false);
			values.setAttribute("npts", Long.toString(axis.getSize()));
			varElem.addContent(values);
		} catch (IOException e) {
			String message = String.format(
					"Couldn't read values for %s. Omitting <values> element.", axis.getFullName());
			logger.warn(message, e);
		}

		return varElem;
	}

	private boolean compareAxis(CoordinateAxis axis1, CoordinateAxis axis2) {
		if (axis1 == axis2)
			return true;

		if (axis1 == null) return false;
		if (axis2 == null) return false;

		return axis1.equals(axis2);
	}

	// sort by time, then vert, then name
	private static class GridComparator implements Comparator<GridDatatype> {

		// Returns a -1, 0, 1 if the first argument is less than, equal to, or greater than the second.
		public int compare(GridDatatype grid1, GridDatatype grid2) {
			GridCoordSystem gcs1 = grid1.getCoordinateSystem();
			GridCoordSystem gcs2 = grid2.getCoordinateSystem();

			CoordinateAxis time1 = gcs1.getTimeAxis();
			CoordinateAxis time2 = gcs2.getTimeAxis();
			int ret = compareAxis(time1, time2);
			if (ret != 0) return ret;

			CoordinateAxis vert1 = gcs1.getVerticalAxis();
			CoordinateAxis vert2 = gcs2.getVerticalAxis();
			ret = compareAxis(vert1, vert2);
			if (ret != 0) return ret;

			return grid1.getFullName().compareTo(grid2.getFullName());
		}

		private int compareAxis(CoordinateAxis axis1, CoordinateAxis axis2) {
			if (axis1 == axis2)
				return 0;

			if (axis1 == null) return -1;
			if (axis2 == null) return 1;

			return axis1.getFullName().compareTo(axis2.getFullName());
		}

	}

	private List<CoordinateAxis> getCoordAxes(ucar.nc2.dt.GridDataset gds) {
		Set<CoordinateAxis> axesHash = new HashSet<>();

		for (ucar.nc2.dt.GridDataset.Gridset gridset : gds.getGridsets()) {
			GridCoordSystem gcs = gridset.getGeoCoordSystem();
			for (CoordinateAxis axe : gcs.getCoordinateAxes())
				axesHash.add(axe);
		}

		List<CoordinateAxis> list = Arrays.asList((CoordinateAxis[]) axesHash.toArray( new CoordinateAxis[ axesHash.size()]));
		Collections.sort(list);
		return list;
	}

	private List<CoordinateTransform> getCoordTransforms(ucar.nc2.dt.GridDataset gds) {
		Set<CoordinateTransform> ctHash = new HashSet<>();

		for (ucar.nc2.dt.GridDataset.Gridset gridset : gds.getGridsets()) {
			GridCoordSystem gcs = gridset.getGeoCoordSystem();
			for (CoordinateTransform axe : gcs.getCoordinateTransforms())
				ctHash.add(axe);
		}

		List<CoordinateTransform> list = Arrays.asList((CoordinateTransform[]) ctHash.toArray( new CoordinateTransform[ ctHash.size()]));
		Collections.sort(list);
		return list;
	}

	/* private List getDimensions(ucar.nc2.dt.GridDataset gds) {
    HashSet dimHash = new HashSet();
    List grids = gds.getGrids();
    for (int i = 0; i < grids.size(); i++) {
      GeoGrid grid = (GeoGrid) grids.get(i);
      List dims = grid.getDimensions();
      for (int j = 0; j < dims.size(); j++) {
        Dimension dim = (Dimension) dims.get(j);
        dimHash.add(dim);
      }
    }
    List list = Arrays.asList(dimHash.toArray());
    Collections.sort(list);
    return list;
  }  */

	private Element writeAxis(CoordinateAxis axis) {
		NcMLWriter ncmlWriter = new NcMLWriter();

		Element varElem = new Element("axis");
		varElem.setAttribute("name", axis.getFullName());
		varElem.setAttribute("shape", getShapeString(axis.getShape())); // axis.getDimensionsString());

		DataType dt = axis.getDataType();
		varElem.setAttribute("type", dt.toString());

		AxisType axisType = axis.getAxisType();
		if (null != axisType)
			varElem.setAttribute("axisType", axisType.toString());

		// attributes
		for (Attribute att : axis.getAttributes()) {
			varElem.addContent(ncmlWriter.makeAttributeElement(att));
		}

		if (axis.getRank() == 1) {
			try {
				Element values = ncmlWriter.makeValuesElement(axis, true);
				varElem.addContent(values);
			} catch (IOException e) {
				String message = String.format(
						"Couldn't read values for %s. Omitting <values> element.", axis.getFullName());
				logger.warn(message, e);
			}
		}

		return varElem;
	}

	// display name plus the dimensions
	private String getShapeString(int[] shape) {
		StringBuilder buf = new StringBuilder();
		for (int i = 0; i < shape.length; i++) {
			if (i != 0) buf.append(" ");
			buf.append(shape[i]);
		}
		return buf.toString();
	}

	private Element writeBoundingBox(LatLonRect bb) {

		Element bbElem = new Element("LatLonBox");
		//LatLonPoint llpt = bb.getLowerLeftPoint();
		//LatLonPoint urpt = bb.getUpperRightPoint();

		//bbElem.addContent(new Element("west").addContent(ucar.unidata.util.Format.dfrac(llpt.getLongitude(), 4)));
		bbElem.addContent(new Element("west").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMin() , 4)));
		//bbElem.addContent(new Element("east").addContent(ucar.unidata.util.Format.dfrac(urpt.getLongitude(), 4)));
		bbElem.addContent(new Element("east").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMax(), 4)));
		//bbElem.addContent(new Element("south").addContent(ucar.unidata.util.Format.dfrac(llpt.getLatitude(), 4)));
		bbElem.addContent(new Element("south").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMin(), 4)));
		//bbElem.addContent(new Element("north").addContent(ucar.unidata.util.Format.dfrac(urpt.getLatitude(), 4)));
		bbElem.addContent(new Element("north").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMax(), 4)));

		return bbElem;

	}

	private Element writeGridSet(GridDataset.Gridset gridset) {
		Element csElem = new Element("gridSet");
		GridCoordSystem cs = gridset.getGeoCoordSystem();
		csElem.setAttribute("name", cs.getName());

		ProjectionRect rect = cs.getBoundingBox();
		Element projBBOX =new Element("projectionBox");
		Element minx = new Element("minx");
		minx.addContent( Double.valueOf(rect.getMinX()).toString() );
		projBBOX.addContent(minx);
		Element maxx = new Element("maxx");
		maxx.addContent( Double.valueOf(rect.getMaxX()).toString() );
		projBBOX.addContent(maxx);
		Element miny = new Element("miny");
		miny.addContent( Double.valueOf(rect.getMinY()).toString() );
		projBBOX.addContent(miny);
		Element maxy = new Element("maxy");
		maxy.addContent( Double.valueOf(rect.getMaxY()).toString() );
		projBBOX.addContent(maxy);

		csElem.addContent(projBBOX);

		for (CoordinateAxis axis : cs.getCoordinateAxes()) {
			Element axisElem = new Element("axisRef");
			axisElem.setAttribute("name", axis.getFullName());
			csElem.addContent(axisElem);
		}

		for (CoordinateTransform ct : cs.getCoordinateTransforms()) {
			Element elem = new Element("coordTransRef");
			elem.setAttribute("name", ct.getName());
			csElem.addContent(elem);
		}

		List<GridDatatype> grids = gridset.getGrids();
		Collections.sort(grids);
		for (GridDatatype grid : grids) {
			csElem.addContent(writeGrid(grid));
		}

		return csElem;
	}

	/* private Element writeCoordSys(GridCoordSystem cs) {
    Element csElem = new Element("coordSys");
    csElem.setAttribute("name", cs.getName());
    List axes = cs.getCoordinateAxes();
    for (int i = 0; i < axes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) axes.get(i);
      Element axisElem = new Element("axisRef");
      axisElem.setAttribute("name", axis.getName());
      csElem.addContent(axisElem);
    }
    List cts = cs.getCoordinateTransforms();
    for (int j = 0; j < cts.size(); j++) {
      CoordinateTransform ct = (CoordinateTransform) cts.get(j);
      Element elem = new Element("coordTransRef");
      elem.setAttribute("name", ct.getName());
      csElem.addContent(elem);
    }
    return csElem;
  } */

	private Element writeCoordTransform(CoordinateTransform ct) {
		Element ctElem = new Element("coordTransform");
		ctElem.setAttribute("name", ct.getName());
		ctElem.setAttribute("transformType", ct.getTransformType().toString());
		for (Parameter param : ct.getParameters()) {
			Element pElem = new Element("parameter");
			pElem.setAttribute("name", param.getName());
			pElem.setAttribute("value", param.getStringValue());
			ctElem.addContent(pElem);
		}
		return ctElem;
	}

	private Element writeGrid(GridDatatype grid) {
		NcMLWriter ncmlWriter = new NcMLWriter();

		Element varElem = new Element("grid");
		varElem.setAttribute("name", grid.getFullName());

        String desc = grid.getDescription() != null ? grid.getDescription() : "No description";
        varElem.setAttribute("desc", desc);

		StringBuilder buff = new StringBuilder();
		List dims = grid.getDimensions();
		for (int i = 0; i < dims.size(); i++) {
			Dimension dim = (Dimension) dims.get(i);
			if (i > 0) buff.append(" ");
			if (dim.isShared())
				buff.append(dim.getShortName());
			else
				buff.append(dim.getLength());
		}
		if (buff.length() > 0)
			varElem.setAttribute("shape", buff.toString());

		DataType dt = grid.getDataType();
		if (dt != null)
			varElem.setAttribute("type", dt.toString());

		//GridCoordSystem cs = grid.getCoordinateSystem();
		//varElem.setAttribute("coordSys", cs.getName());

		// attributes
		for (ucar.nc2.Attribute att : grid.getAttributes()) {
			varElem.addContent(ncmlWriter.makeAttributeElement(att));
		}

		return varElem;
	}

	// sort by domain size, then name
	private static class GridSetComparator implements Comparator<GridDataset.Gridset> {

		public int compare(GridDataset.Gridset gridset1, GridDataset.Gridset gridset2) {
			GridCoordSystem cs1 = gridset1.getGeoCoordSystem();
			GridCoordSystem cs2 = gridset2.getGeoCoordSystem();
			if (cs1.getDomain().size() != cs2.getDomain().size())
				return cs1.getDomain().size() - cs2.getDomain().size();
			return cs1.getName().compareTo(cs2.getName());
		}
	}

	/**
	 * debug
	 *
	 * @param args ignored
	 * @throws java.io.IOException on io error
	 */
	public static void main(String args[]) throws IOException {
		String url = "cdmremote:http://localhost:8080/thredds/cdmremote/grib/NCDC/CFSR/NCDC-CFSR/PGB-LatLon0p5";

		GridDataset ncd = ucar.nc2.dt.grid.GridDataset.open(url);
		GridDatasetInfo info = new GridDatasetInfo(ncd, null);
		FileOutputStream fos2 = new FileOutputStream("C:/tmp2/gridInfo.xml");
		info.writeXML(info.makeGridForm(), fos2);
		fos2.close();

		String infoString = info.writeXML(info.makeGridForm());
		System.out.println(infoString);
	}

}