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

package ucar.nc2.geotiff;

import java.io.Closeable;
import java.io.IOException;

import ucar.ma2.Array;
import ucar.ma2.ArrayByte;
import ucar.ma2.ArrayFloat;
import ucar.ma2.DataType;
import ucar.ma2.Index;
import ucar.ma2.IndexIterator;
import ucar.ma2.IsMissingEvaluator;
import ucar.ma2.MAMath;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.dt.GridDatatype;
import ucar.nc2.ft2.coverage.CoverageCoordAxis1D;
import ucar.nc2.ft2.coverage.CoverageCoordSys;
import ucar.nc2.ft2.coverage.GeoReferencedArray;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.Projection;
import ucar.unidata.geoloc.projection.AlbersEqualArea;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.geoloc.projection.Mercator;
import ucar.unidata.geoloc.projection.Stereographic;
import ucar.unidata.geoloc.projection.TransverseMercator;
import ucar.unidata.geoloc.projection.proj4.AlbersEqualAreaEllipse;

/**
 * Write GeoTIFF files.
 * Regular data only
 *
 * @author caron, yuan
 */
public class GeotiffWriter implements Closeable {

  protected GeoTiff geotiff;
  protected short pageNumber = 1;

  /**
   * Constructor
   *
   * @param fileOut name of output file.
   */
  public GeotiffWriter(String fileOut) {
    geotiff = new GeoTiff(fileOut);
  }

  public void close() throws IOException {
    geotiff.close();
  }

  /**
   * Write GridDatatype data to the geotiff file.
   *
   * @param dataset   grid in contained in this dataset
   * @param grid      data is in this grid
   * @param data      2D array in YX order
   * @param greyScale if true, write greyScale image, else dataSample.
   * @throws IOException on i/o error
   */
  public void writeGrid(GridDataset dataset, GridDatatype grid, Array data, boolean greyScale) throws IOException {
    GridCoordSystem gcs = grid.getCoordinateSystem();

    if (!gcs.isRegularSpatial()) {
      throw new IllegalArgumentException("Must have 1D x and y axes for " + grid.getFullName());
    }

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

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

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

    if (yaxis.getCoordValue(0) < yaxis.getCoordValue(1)) {
      data = data.flip(0);
      yStart = yaxis.getCoordEdge((int) yaxis.getSize()) * scaler;
    }

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

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

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

  /**
   * Write Grid data to the geotiff file.
   * Grid currently must:
   * <ol>
   * <li> have a 1D X and Y coordinate axes.
   * <li> be lat/lon or Lambert Conformal Projection
   * <li> be equally spaced
   * </ol>
   *
   * @param grid        original grid
   * @param data        2D array in YX order
   * @param greyScale   if true, write greyScale image, else dataSample.
   * @param xStart
   * @param yStart
   * @param xInc
   * @param yInc
   * @param imageNumber
   * @throws IOException              on i/o error
   * @throws IllegalArgumentException if above assumptions not valid
   */
  protected void writeGrid(GridDatatype grid, Array data, boolean greyScale, double xStart, double yStart, double xInc,
                        double yInc, int imageNumber) throws IOException {

    int nextStart = 0;
    GridCoordSystem gcs = grid.getCoordinateSystem();

    // get rid of this when all projections are implemented
    if (!gcs.isLatLon()
            && !(gcs.getProjection() instanceof LambertConformal)
            && !(gcs.getProjection() instanceof Stereographic)
            && !(gcs.getProjection() instanceof Mercator)
            //  && !(gcs.getProjection() instanceof TransverseMercator)   LOOK broken ??
            && !(gcs.getProjection() instanceof AlbersEqualAreaEllipse)
            && !(gcs.getProjection() instanceof AlbersEqualArea)) {
      throw new IllegalArgumentException("Unsupported projection = " + gcs.getProjection().getClass().getName());
    }

    // write the data first
    MAMath.MinMax dataMinMax = grid.getMinMaxSkipMissingData(data);
    if (greyScale) {
      ArrayByte result = replaceMissingValuesAndScale(grid, data, dataMinMax);
      nextStart = geotiff.writeData((byte[]) result.getStorage(), imageNumber);
    } else {
      ArrayFloat result = replaceMissingValues(grid, data, dataMinMax);
      nextStart = geotiff.writeData((float[]) result.getStorage(), imageNumber);
    }

    // set the width and the height
    int height = data.getShape()[0];         // Y
    int width = data.getShape()[1];         // X

    writeMetadata(greyScale, xStart, yStart, xInc, yInc, height, width, imageNumber, nextStart, dataMinMax, gcs.getProjection());
  }

  private void writeMetadata(boolean greyScale, double xStart, double yStart, double xInc, double yInc, int height, int width, int imageNumber, int nextStart,
                     MAMath.MinMax dataMinMax, Projection proj) throws IOException {

    geotiff.addTag(new IFDEntry(Tag.ImageWidth, FieldType.SHORT).setValue(width));
    geotiff.addTag(new IFDEntry(Tag.ImageLength, FieldType.SHORT).setValue(height));

    // set the multiple images tag
    int ff = 1 << 1;
    int page = imageNumber - 1;
    geotiff.addTag(new IFDEntry(Tag.NewSubfileType, FieldType.SHORT).setValue(ff));
    geotiff.addTag(new IFDEntry(Tag.PageNumber, FieldType.SHORT).setValue(page, 2));

    // just make it all one big "row"
    geotiff.addTag(new IFDEntry(Tag.RowsPerStrip, FieldType.SHORT).setValue(1));  //height));
    // the following changes to make it viewable in ARCMAP
        /*
        int size = elemSize * height * width;  // size in bytes
        geotiff.addTag( new IFDEntry(Tag.StripByteCounts, FieldType.LONG).setValue( size));
        // data starts here, header is written at the end
        if( imageNumber == 1 )
          geotiff.addTag( new IFDEntry(Tag.StripOffsets, FieldType.LONG).setValue( 8));
        else
          geotiff.addTag( new IFDEntry(Tag.StripOffsets, FieldType.LONG).setValue(nextStart));
        */

    int elemSize = greyScale ? 1 : 4;

    int[] soffset = new int[height];
    int[] sbytecount = new int[height];
    if (imageNumber == 1) {
      soffset[0] = 8;
    } else {
      soffset[0] = nextStart;
    }
    sbytecount[0] = width * elemSize;
    for (int i = 1; i < height; i++) {
      soffset[i] = soffset[i - 1] + width * elemSize;
      sbytecount[i] = width * elemSize;
    }
    geotiff.addTag(new IFDEntry(Tag.StripByteCounts, FieldType.LONG, width).setValue(sbytecount));
    geotiff.addTag(new IFDEntry(Tag.StripOffsets, FieldType.LONG, width).setValue(soffset));
    // standard tags
    geotiff.addTag(new IFDEntry(Tag.Orientation, FieldType.SHORT).setValue(1));
    geotiff.addTag(new IFDEntry(Tag.Compression, FieldType.SHORT).setValue(1));  // no compression
    geotiff.addTag(new IFDEntry(Tag.Software, FieldType.ASCII).setValue("nc2geotiff"));
    geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(1));  // black is zero : not used?
    geotiff.addTag(new IFDEntry(Tag.PlanarConfiguration, FieldType.SHORT).setValue(1));

    if (greyScale) {
      // standard tags for Greyscale images ( see TIFF spec, section 4)
      geotiff.addTag(new IFDEntry(Tag.BitsPerSample, FieldType.SHORT).setValue(8));  // 8 bits per sample
      geotiff.addTag(new IFDEntry(Tag.SamplesPerPixel, FieldType.SHORT).setValue(1));

      geotiff.addTag(new IFDEntry(Tag.XResolution, FieldType.RATIONAL).setValue(1, 1));
      geotiff.addTag(new IFDEntry(Tag.YResolution, FieldType.RATIONAL).setValue(1, 1));
      geotiff.addTag(new IFDEntry(Tag.ResolutionUnit, FieldType.SHORT).setValue(1));

    } else {
      // standard tags for SampleFormat ( see TIFF spec, section 19)
      geotiff.addTag(new IFDEntry(Tag.BitsPerSample, FieldType.SHORT).setValue(32));  // 32 bits per sample
      geotiff.addTag(new IFDEntry(Tag.SampleFormat, FieldType.SHORT).setValue(3));  // Sample Format
      geotiff.addTag(new IFDEntry(Tag.SamplesPerPixel, FieldType.SHORT).setValue(1));
      float min = (float) (dataMinMax.min);
      float max = (float) (dataMinMax.max);
      geotiff.addTag(new IFDEntry(Tag.SMinSampleValue, FieldType.FLOAT).setValue(min));
      geotiff.addTag(new IFDEntry(Tag.SMaxSampleValue, FieldType.FLOAT).setValue(max));
      geotiff.addTag(new IFDEntry(Tag.GDALNoData, FieldType.FLOAT).setValue(min - 1.f));
    }

        /*
              geotiff.addTag( new IFDEntry(Tag.Geo_ModelPixelScale, FieldType.DOUBLE).setValue(
                new double[] {5.0, 2.5, 0.0} ));
              geotiff.addTag( new IFDEntry(Tag.Geo_ModelTiepoint, FieldType.DOUBLE).setValue(
                new double[] {0.0, 0.0, 0.0, -180.0, 90.0, 0.0 } ));
              //  new double[] {0.0, 0.0, 0.0, 183.0, 90.0, 0.0} ));
              IFDEntry ifd = new IFDEntry(Tag.Geo_KeyDirectory, FieldType.SHORT).setValue(
                new int[] {1, 1, 0, 4, 1024, 0, 1, 2, 1025, 0, 1, 1, 2048, 0, 1, 4326, 2054, 0, 1, 9102} );
              geotiff.addTag( ifd);
        */

    // set the transformation from projection to pixel, add tie point tag
    geotiff.setTransform(xStart, yStart, xInc, yInc);

    if (proj instanceof LatLonProjection) {
      addLatLonTags();
    } else if (proj instanceof LambertConformal) {
      addLambertConformalTags((LambertConformal) proj, xStart, yStart);
    } else if (proj instanceof Stereographic) {
      addPolarStereographicTags((Stereographic) proj, xStart, yStart);
    } else if (proj instanceof Mercator) {
      addMercatorTags((Mercator) proj);
    } else if (proj instanceof TransverseMercator) {
      addTransverseMercatorTags((TransverseMercator) proj);
    } else if (proj instanceof AlbersEqualArea) {
      addAlbersEqualAreaTags((AlbersEqualArea) proj);
    } else if (proj instanceof AlbersEqualAreaEllipse) {
      addAlbersEqualAreaEllipseTags((AlbersEqualAreaEllipse) proj);
    } else {
      throw new IllegalArgumentException("Unsupported projection = " + proj.getClass().getName());
    }

    geotiff.writeMetadata(imageNumber);
  }

  /**
   * Replace missing values with dataMinMax.min - 1.0; return a floating point data array.
   *
   * @param grid GridDatatype
   * @param data input data array
   * @return floating point data array with missing values replaced.
   */
  private ArrayFloat replaceMissingValues(IsMissingEvaluator grid, Array data, MAMath.MinMax dataMinMax) {
    float minValue = (float) (dataMinMax.min - 1.0);

    ArrayFloat floatArray = (ArrayFloat) Array.factory(DataType.FLOAT, data.getShape());
    IndexIterator dataIter = data.getIndexIterator();
    IndexIterator floatIter = floatArray.getIndexIterator();
    while (dataIter.hasNext()) {
      float v = dataIter.getFloatNext();
      if (grid.isMissing((double) v)) {
        v = minValue;
      }
      floatIter.setFloatNext(v);
    }

    return floatArray;
  }

  /**
   * Replace missing values with 0; scale other values between 1 and 255, return a byte data array.
   *
   * @param grid GridDatatype
   * @param data input data array
   * @return byte data array with missing values replaced and data scaled from 1- 255.
   */
  private ArrayByte replaceMissingValuesAndScale(IsMissingEvaluator grid, Array data, MAMath.MinMax dataMinMax) {
    double scale = 254.0 / (dataMinMax.max - dataMinMax.min);

    ArrayByte byteArray = (ArrayByte) Array.factory(DataType.BYTE, data.getShape());
    IndexIterator dataIter = data.getIndexIterator();
    IndexIterator resultIter = byteArray.getIndexIterator();

    byte bv;
    while (dataIter.hasNext()) {
      double v = dataIter.getDoubleNext();
      if (grid.isMissing(v)) {
        bv = 0;
      } else {
        int iv = (int) ((v - dataMinMax.min) * scale + 1);
        bv = (byte) (iv & 0xff);
      }
      resultIter.setByteNext(bv);
    }

    return byteArray;
  }

  private void addLatLonTags1() {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey,
            GeoKey.TagValue.ModelType_Geographic));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogGeodeticDatumGeoKey,
            GeoKey.TagValue.GeogGeodeticDatum6267));
  }

  private void addLatLonTags() {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Geographic));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogPrimeMeridianGeoKey, GeoKey.TagValue.GeogPrimeMeridian_GREENWICH));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));
  }

  private void addPolarStereographicTags(Stereographic proj, double FalseEasting, double FalseNorthing) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogPrimeMeridianGeoKey, GeoKey.TagValue.GeogPrimeMeridian_GREENWICH));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Snyder"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for Polar Stereographic
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_Stereographic));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCenterLongGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, 90.0));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getTangentLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjScaleAtNatOriginGeoKey, 1.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void addLambertConformalTags(LambertConformal proj, double FalseEasting, double FalseNorthing) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogPrimeMeridianGeoKey, GeoKey.TagValue.GeogPrimeMeridian_GREENWICH));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Snyder"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for lambert conformal
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_LambertConfConic_2SP));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel1GeoKey, proj.getParallelOne()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel2GeoKey, proj.getParallelTwo()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCenterLongGeoKey, proj.getOriginLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, proj.getOriginLat()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getOriginLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjScaleAtNatOriginGeoKey, 1.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));  // LOOK why not FalseNorthing ??
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void addMercatorTags(Mercator proj) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    // geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    // geotiff.addGeoKey( new GeoKey( GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Mercator"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for mercator
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_Mercator));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getOriginLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, proj.getParallel()));
    //    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel1GeoKey, proj.getParallel()));
    //   geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjScaleAtNatOriginGeoKey, 1));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void addTransverseMercatorTags(TransverseMercator proj) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Transvers Mercator"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for mercator
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_TransverseMercator));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, proj.getOriginLat()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getTangentLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjScaleAtNatOriginGeoKey, proj.getScale()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjScaleAtNatOriginGeoKey, 1.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void addAlbersEqualAreaEllipseTags(AlbersEqualAreaEllipse proj) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));

    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogSemiMajorAxisGeoKey, proj.getEarth().getMajor()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogSemiMinorAxisGeoKey, proj.getEarth().getMinor()));

    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Albers Conial Equal Area"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for mercator
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_AlbersEqualAreaEllipse));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, proj.getOriginLat()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getOriginLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel1GeoKey, proj.getParallelOne()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel2GeoKey, proj.getParallelTwo()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void addAlbersEqualAreaTags(AlbersEqualArea proj) {
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTModelTypeGeoKey, GeoKey.TagValue.ModelType_Projected));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GTRasterTypeGeoKey, GeoKey.TagValue.RasterType_Area));

    // define the "geographic Coordinate System"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeographicTypeGeoKey, GeoKey.TagValue.GeographicType_WGS_84));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.GeogAngularUnitsGeoKey, GeoKey.TagValue.GeogAngularUnits_DEGREE));

    // define the "coordinate transformation"
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectedCSTypeGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.PCSCitationGeoKey, "Albers Conial Equal Area"));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjectionGeoKey, GeoKey.TagValue.ProjectedCSType_UserDefined));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjLinearUnitsGeoKey, GeoKey.TagValue.ProjLinearUnits_METER));
    //geotiff.addGeoKey( new GeoKey( GeoKey.Tag.ProjLinearUnitsSizeGeoKey, 1.0)); // units of km

    // the specifics for mercator
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjCoordTransGeoKey, GeoKey.TagValue.ProjCoordTrans_AlbersConicalEqualArea));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLatGeoKey, proj.getOriginLat()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjNatOriginLongGeoKey, proj.getOriginLon()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel1GeoKey, proj.getParallelOne()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjStdParallel2GeoKey, proj.getParallelTwo()));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseEastingGeoKey, 0.0));
    geotiff.addGeoKey(new GeoKey(GeoKey.Tag.ProjFalseNorthingGeoKey, 0.0));
  }

  private void dump(Array data, int col) {
    int[] shape = data.getShape();
    Index ima = data.getIndex();

    for (int j = 0; j < shape[0]; j++) {
      float dd = data.getFloat(ima.set(j, col));
      System.out.println(j + " value= " + dd);
    }
  }

  // LOOK WTF ?? is this the seam crossing ??
  private double geoShiftGetXstart(Array lon, double inc) {
    Index ilon = lon.getIndex();
    int[] lonShape = lon.getShape();
    IndexIterator lonIter = lon.getIndexIterator();
    double xlon = 0.0;

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

    xlon = p0.getLongitude();
    while (lonIter.hasNext()) {
      float l = lonIter.getFloatNext();
      LatLonPoint pn = new LatLonPointImpl(0, l);
      if (pn.getLongitude() < xlon) {
        xlon = pn.getLongitude();
      }
    }

    if (p0.getLongitude() == pN.getLongitude()) {
      xlon = xlon - inc;
    }

    return xlon;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Write GridCoverage data to the geotiff file.
   *
   * @param array      GeoReferencedArray array in YX order
   * @param greyScale if true, write greyScale image, else dataSample.
   * @throws IOException on i/o error
   */
  public void writeGrid(GeoReferencedArray array, boolean greyScale) throws IOException {

    CoverageCoordSys gcs = array.getCoordSysForData();
    if (!gcs.isRegularSpatial())
      throw new IllegalArgumentException("Must have 1D x and y axes for " + array.getCoverageName());

    Projection proj = gcs.getProjection();
    CoverageCoordAxis1D xaxis = (CoverageCoordAxis1D) gcs.getXAxis();
    CoverageCoordAxis1D yaxis = (CoverageCoordAxis1D) gcs.getYAxis();

    // latlon coord does not need to be scaled
    double scaler = (xaxis.getUnits().equalsIgnoreCase("km")) ? 1000.0 : 1.0;

    // data must go from top to bottom
    double xStart = xaxis.getCoordEdge1(0) * scaler;
    double yStart = yaxis.getCoordEdge1(0) * scaler;
    double xInc = xaxis.getResolution() * scaler;
    double yInc = Math.abs(yaxis.getResolution()) * scaler;

    Array data = array.getData().reduce();
    if (yaxis.getCoordMidpoint(0) < yaxis.getCoordMidpoint(1)) {
      data = data.flip(0);
      yStart = yaxis.getCoordEdgeLast();
    }

    /*  remove - i think unneeded, monotonic lon handled in CoordinateAxis1D. JC 3/18/2013
     if (gcs.isLatLon()) {
      Array lon = xaxis.read();
      data = geoShiftDataAtLon(data, lon);
      xStart = geoShiftGetXstart(lon, xInc);
      //xStart = -180.0;
    }  */

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

    // write the data first
    int nextStart = 0;
    MAMath.MinMax dataMinMax = MAMath.getMinMaxSkipMissingData(data, array);
    if (greyScale) {
      ArrayByte result = replaceMissingValuesAndScale(array, data, dataMinMax);
      nextStart = geotiff.writeData((byte[]) result.getStorage(), pageNumber);
    } else {
      ArrayFloat result = replaceMissingValues(array, data, dataMinMax);
      nextStart = geotiff.writeData((float[]) result.getStorage(), pageNumber);
    }

    // set the width and the height
    int height = data.getShape()[0];         // Y
    int width = data.getShape()[1];         // X

    writeMetadata(greyScale, xStart, yStart, xInc, yInc, height, width, pageNumber, nextStart, dataMinMax, proj);
    pageNumber++;
  }
}


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

package ucar.nc2.dataset.conv;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.AxisType;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.nc2.units.SimpleUnit;

import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;
import ucar.unidata.util.Format;
import ucar.unidata.util.StringUtil2;

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

/**
 * NUWG Convention (ad hoc).
 * see http://www.unidata.ucar.edu/packages/netcdf/NUWG/
 *
 * @author caron
 */

public class NUWGConvention extends CoordSysBuilder {
  private NavInfoList navInfo = new NavInfoList();
  private String xaxisName = "", yaxisName = "";
  private Grib1 grib;

  private static final boolean dumpNav = false;

  public NUWGConvention() {
    this.conventionName = "NUWG";
  }

  public void augmentDataset( NetcdfDataset ds, CancelTask cancelTask) {
    if (null != ds.findGlobalAttribute("_enhanced")) return; // check if its already been done - aggregating enhanced datasets.
    ds.addAttribute(null, new Attribute("_enhanced", "")); // LOOK

    // find all variables that have the nav dimension
    // put them into a NavInfoList
    // make their data into metadata
    List<Variable> vars = ds.getVariables();
    for (Variable v : vars) {
      if (0 <= v.findDimensionIndex("nav")) {

        if (dumpNav) parseInfo.format("NUWG has NAV var = %s%n", v);
        try {
          navInfo.add(new NavInfo(v));
        } catch (IOException ex) {
          parseInfo.format("ERROR NUWG reading NAV var = %s%n", v);
        }
      }
    }
    java.util.Collections.sort( navInfo, new NavComparator());
    parseInfo.format("%s%n%n", navInfo);

    // is this pathetic or what ?
    // problem is NUWG doesnt identify the x, y coords.
    // so we get to hack it in here
    int mode = 3; // default is LambertConformal
    try {
      mode = navInfo.getInt( "grid_type_code");
    } catch (NoSuchElementException e) {
      log.warn("No mode in navInfo - assume 3");
    }

    try {
     if (mode == 0) {
        xaxisName = navInfo.getString( "i_dim");
        yaxisName = navInfo.getString( "j_dim");
      } else {
        xaxisName = navInfo.getString( "x_dim");
        yaxisName = navInfo.getString( "y_dim");
      }
    } catch (NoSuchElementException e) {
      log.warn("No mode in navInfo - assume = 1");
      // could match variable grid_type, data = "tangential lambert conformal  "
    }
    grib = new Grib1( mode);

    if (null == ds.findVariable( xaxisName)) {
      grib.makeXCoordAxis( ds, xaxisName);
      parseInfo.format("Generated x axis from NUWG nav= %s%n", xaxisName);

    } else if (xaxisName.equalsIgnoreCase("lon")) {

      try {
          // check monotonicity
        boolean ok = true;
        Variable dc = ds.findVariable( xaxisName);
        Array coordVal = dc.read();
        IndexIterator coordIndex = coordVal.getIndexIterator();

        double coord1 = coordIndex.getDoubleNext();
        double coord2 = coordIndex.getDoubleNext();
        boolean increase = coord1 > coord2;
        coord1 = coord2;
        while (coordIndex.hasNext()) {
          coord2 = coordIndex.getDoubleNext();
          if ((coord1 > coord2) ^ increase) {
            ok = false;
            break;
          }
          coord1 = coord2;
        }

        if (!ok) {
          parseInfo.format( "ERROR lon axis is not monotonic, regen from nav%n");
          grib.makeXCoordAxis( ds, xaxisName);
        }
      } catch (IOException ioe) {
        log.warn("IOException when reading xaxis = "+xaxisName);
      }
    }

    if (null == ds.findVariable( yaxisName)) {
      grib.makeYCoordAxis( ds, yaxisName);
      parseInfo.format("Generated y axis from NUWG nav=%s%n", yaxisName);
    }

      // "referential" variables
    List<Dimension> dims = ds.getRootGroup().getDimensions();
    for (Dimension dim : dims) {
      String dimName = dim.getShortName();
      if (null != ds.findVariable( dimName)) // already has a coord axis
        continue;
      List<Variable> ncvars = searchAliasedDimension( ds, dim);
      if ((ncvars == null) || (ncvars.size() == 0)) // no alias
          continue;

      if (ncvars.size() == 1) {
        Variable ncvar = ncvars.get(0);
        if (!(ncvar instanceof VariableDS))
          continue; // cant be a structure
        if (makeCoordinateAxis( ncvar, dim)) {
          parseInfo.format("Added referential coordAxis = ");
          ncvar.getNameAndDimensions(parseInfo, true, false);
          parseInfo.format("%n");
        } else {
          parseInfo.format("Couldnt add referential coordAxis = %s%n", ncvar.getFullName());
        }

      } else if (ncvars.size() == 2) {

        if (dimName.equals("record")) {
          Variable ncvar0 = ncvars.get(0);
          Variable ncvar1 = ncvars.get(1);
          Variable ncvar = ncvar0.getShortName().equalsIgnoreCase("valtime") ? ncvar0 : ncvar1;

          if (makeCoordinateAxis( ncvar, dim)) {
            parseInfo.format("Added referential coordAxis (2) = ");
            ncvar.getNameAndDimensions(parseInfo, true, false);
            parseInfo.format("%n");

            // the usual crap - clean up time units
            String units = ncvar.getUnitsString();
            if (units != null) {
              units = StringUtil2.remove(units, '(');
              units = StringUtil2.remove(units, ')');
              ncvar.addAttribute(new Attribute(CDM.UNITS, units));
            }
          } else {
            parseInfo.format("Couldnt add referential coordAxis = %s%n", ncvar.getFullName());
          }

          //ncvar = (Variable) ncvars.get(0);
          //CoordinateAxis refTime = ds.addCoordinateAxis( ncvar);
          //vaidTime.setAuxilary( refTime);
          //parseInfo.append("Added referential record coordAxis = "+ncvar.getNameAndDimensions()+"%n");

        } else {
          // lower(?) bound
          Variable ncvar = ncvars.get(0);
          if (!(ncvar instanceof VariableDS)) continue; // cant be a structure

          if (makeCoordinateAxis( ncvar, dim)) {
            parseInfo.format("Added referential boundary coordAxis (2) = ");
            ncvar.getNameAndDimensions(parseInfo, true, false);
            parseInfo.format("%n");
          } else {
            parseInfo.format("Couldnt add referential coordAxis = %s%n", ncvar.getFullName());
          }

          /*  CoordinateAxis bound1 = ds.addCoordinateAxis( (VariableDS) ncvar);
          parseInfo.append("Added referential boundary coordAxis = ");
          ncvar.getNameAndDimensions(parseInfo, true, false);
          parseInfo.append("%n");

           // upper(?) bound
          ncvar = (Variable) ncvars.get(1);
          if (!(ncvar instanceof VariableDS)) continue; // cant be a structure
          CoordinateAxis bound2 = ds.addCoordinateAxis( (VariableDS) ncvar);
          //bound1.setAuxilary( bound2);

          parseInfo.append("Added referential boundary coordAxis = ");
          ncvar.getNameAndDimensions(parseInfo, true, false);
          parseInfo.append("%n");

          /* DimCoordAxis dc = addCoordAxisFromTopBotVars( dim, (Variable) ncvars.get(0),
              (Variable) ncvars.get(1));
          if (null != dc)
            dc.isReferential = true; */
        }
      } // 2
    } // loop over dims

    if (grib.ct != null) {
      VariableDS v = makeCoordinateTransformVariable(ds, grib.ct);
      v.addAttribute( new Attribute(_Coordinate.Axes, xaxisName+" "+yaxisName));
      ds.addVariable(null, v);
    }

    ds.finish();
  }

  private boolean makeCoordinateAxis( Variable ncvar, Dimension dim) {
    if (ncvar.getRank() != 1)
      return false;
    Dimension vdim = ncvar.getDimension(0);
    if (!vdim.equals(dim))
      return false;

    if (!dim.getShortName().equals(ncvar.getShortName())) {
      ncvar.addAttribute( new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    }

    /* if (dim.getCoordinateVariables().size() == 1) {
      dim.addCoordinateVariable( ncvar);
      ncvar.setIsCoordinateAxis( true);
    } */
    return true;
  }

  /* private int getDimensionIndex( Variable v, String dimName) {
    Iterator iter = v.getDimensions().iterator();
    int count = 0;
    while (iter.hasNext()) {
      Dimension dim = (Dimension) iter.next();
      if (dimName.equalsIgnoreCase( dim.getShortName()))
        return count;
      count++;
    }
    return -1;
  } */

  /** Search for an aliased coord that may have multiple variables
   *   :dimName = alias1, alias2;
   *   Variable alias1(dim);
   *   Variable alias2(dim);
   * @param ds search in this dataset
   * @param dim: look for this dimension name
   * @return Collection of nectdf variables, or null if none
   */
  private List<Variable> searchAliasedDimension( NetcdfDataset ds, Dimension dim) {
    String dimName = dim.getShortName();
    String alias = ds.findAttValueIgnoreCase(null, dimName, null);
    if (alias == null)
      return null;

    List<Variable> vars = new ArrayList<>();
    StringTokenizer parser = new StringTokenizer(alias, " ,");
    while (parser.hasMoreTokens()) {
      String token = parser.nextToken();
      Variable ncvar = ds.findVariable( token);
      if (ncvar == null)
        continue;
      if (ncvar.getRank() != 1)
        continue;
      Iterator dimIter = ncvar.getDimensions().iterator();
      Dimension dim2 = (Dimension) dimIter.next();
      if (dimName.equals(dim2.getShortName())) {
        vars.add(ncvar);
        if (debug) System.out.print(" "+token);
      }
    }
    if (debug) System.out.println();

    return vars;
  }

  private StringBuilder buf = new StringBuilder(2000);
  public String extraInfo() {
    buf.setLength(0);
    buf.append(navInfo).append("%n");
    return buf.toString();
  }

  protected void makeCoordinateTransforms( NetcdfDataset ds) {
    if ((grib != null) && (grib.ct != null)) {
      VarProcess vp = findVarProcess(grib.ct.getName(), null);
      vp.isCoordinateTransform = true;
      vp.ct = grib.ct;
    }
    super.makeCoordinateTransforms( ds);
  }

  protected AxisType getAxisType( NetcdfDataset ds, VariableEnhanced ve) {
    Variable v = (Variable) ve;
    String vname = v.getShortName();

    if (vname.equalsIgnoreCase("lat"))
      return AxisType.Lat;

    if (vname.equalsIgnoreCase("lon"))
      return AxisType.Lon;

    if (vname.equalsIgnoreCase(xaxisName))
      return AxisType.GeoX;

    if (vname.equalsIgnoreCase(yaxisName))
      return AxisType.GeoY;

    if (vname.equalsIgnoreCase("record"))
      return AxisType.Time;
    Dimension dim = v.getDimension(0);
    if ((dim != null) && dim.getShortName().equalsIgnoreCase("record")) { // wow thats bad!
      return AxisType.Time;
    }

    String unit = ve.getUnitsString();
    if (unit != null) {
      if ( SimpleUnit.isCompatible("millibar", unit))
        return AxisType.Pressure;

      if ( SimpleUnit.isCompatible("m", unit))
        return AxisType.Height;

      if ( SimpleUnit.isCompatible("sec", unit))
        return null;
    }

    return AxisType.GeoZ; // AxisType.GeoZ;
  }

  /**  @return "up" if this is a Vertical (z) coordinate axis which goes up as coords get bigger
   * @param v for this axis
   */
  public String getZisPositive( CoordinateAxis v) {

     // gotta have a length unit
    String unit = v.getUnitsString();
    if ((unit != null) && SimpleUnit.isCompatible("m", unit))
      return "up";

    return "down";

    // lame NUWG COnventions! units of millibar might be "millibars above ground" !
    // heres a kludge that should work
    // return v.getName().equalsIgnoreCase("fhg") ? "up" : "down";
  }

  private static class NavComparator implements java.util.Comparator<NavInfo> {
    public int compare(NavInfo n1, NavInfo n2) {
      return n1.getName().compareTo( n2.getName());
    }
    public boolean equals(Object obj) {
      return (this == obj);
    }
  }

  private class NavInfo {
    Variable ncvar;
    DataType valueType;
    String svalue;
    byte bvalue;
    int ivalue;
    double dvalue;

    NavInfo( Variable ncvar) throws IOException {
      this.ncvar = ncvar;
      valueType = ncvar.getDataType();
      try {
        if ((valueType == DataType.CHAR) || (valueType == DataType.STRING))
          svalue =  ncvar.readScalarString();
        else if (valueType == DataType.BYTE)
          bvalue = ncvar.readScalarByte();
        else if ((valueType == DataType.INT) || (valueType == DataType.SHORT))
          ivalue = ncvar.readScalarInt();
        else
          dvalue = ncvar.readScalarDouble();
      } catch (java.lang.UnsupportedOperationException e) {
        parseInfo.format("Nav variable %s  not a scalar%n", getName());
      }
      //List<String> values = new ArrayList<String>();
      //values.add( getStringValue());
      // ncDataset.setValues( ncvar, values); // WHY?
    }

    public String getName() { return ncvar.getShortName(); }
    public String getDescription() {
      Attribute att = ncvar.findAttributeIgnoreCase(CDM.LONG_NAME);
      return (att == null) ? getName() : att.getStringValue();
    }
    public DataType getValueType() { return valueType; }

    public String getStringValue(){
      if ((valueType == DataType.CHAR) || (valueType == DataType.STRING))
        return svalue;
      else if (valueType == DataType.BYTE)
        return Byte.toString(bvalue);
      else if ((valueType == DataType.INT) || (valueType == DataType.SHORT))
        return Integer.toString(ivalue);
      else
        return Double.toString(dvalue);
    }

    private StringBuilder buf = new StringBuilder(200);
    public String toString() {
      buf.setLength(0);
      buf.append(getName());
      buf.append(" ");
      Format.tab(buf, 15, true);
      buf.append(getStringValue());
      buf.append(" ");
      Format.tab(buf, 35, true);
      buf.append(getDescription());
      return buf.toString();
    }
  }

  private static class NavInfoList extends ArrayList<NavInfo> {

    public NavInfo findInfo( String name) {
      for (NavInfo nav : this) {
        if (name.equalsIgnoreCase(nav.getName()))
          return nav;
      }
      return null;
    }

    public double getDouble( String name) throws NoSuchElementException {
      NavInfo nav = findInfo( name);
      if (nav == null)
        throw new NoSuchElementException("GRIB1 "+name);

      if ((nav.valueType == DataType.DOUBLE) || (nav.valueType == DataType.FLOAT))
        return nav.dvalue;
      else if ((nav.valueType == DataType.INT) || (nav.valueType == DataType.SHORT))
        return (double) nav.ivalue;
      else if (nav.valueType == DataType.BYTE)
        return (double)  nav.bvalue;

      throw new IllegalArgumentException("NUWGConvention.GRIB1.getDouble "+name+" type = "+nav.valueType);
    }

    public int getInt( String name) throws NoSuchElementException {
      NavInfo nav = findInfo( name);
      if (nav == null)
        throw new NoSuchElementException("GRIB1 "+name);

      if ((nav.valueType == DataType.INT) || (nav.valueType == DataType.SHORT))
        return nav.ivalue;
      else if ((nav.valueType == DataType.DOUBLE) || (nav.valueType == DataType.FLOAT))
        return (int) nav.dvalue;
      else if (nav.valueType == DataType.BYTE)
        return (int)  nav.bvalue;

      throw new IllegalArgumentException("NUWGConvention.GRIB1.getInt "+name+" type = "+nav.valueType);
    }

    public String getString( String name) throws NoSuchElementException {
      NavInfo nav = findInfo( name);
      if (nav == null)
        throw new NoSuchElementException("GRIB1 "+name);
      return nav.svalue;
    }

    public String toString() {
      StringBuilder buf = new StringBuilder(2000);
      buf.append("\nNav Info\n");
      buf.append("Name___________Value_____________________Description\n");
      for (NavInfo nava : this) {
        buf.append(nava).append("\n");
      }
      buf.append("\n");
      return buf.toString();
    }

  }

  // encapsolates GRIB-specific processing
  private class Grib1 {
    private String grid_name;
    private int grid_code = 0;
    private ProjectionCT ct;

    private  int nx, ny;
    private double startx, starty;
    private double dx, dy;

    Grib1( int mode) {
      // horiz system
      grid_name = "Projection";
      if (grid_name.length() == 0) grid_name = "grid_var";

      grid_code = mode;
      if (0 == grid_code)
         processLatLonProjection();
       else if (3 == grid_code)
         ct = makeLCProjection();
       else if (5 == grid_code)
         ct = makePSProjection();
       else
         throw new IllegalArgumentException("NUWGConvention: unknown grid_code= "+grid_code);

      // vertical system
    }

    CoordinateAxis makeXCoordAxis( NetcdfDataset ds, String xname) {
      CoordinateAxis v = new CoordinateAxis1D( ds, null, xname, DataType.DOUBLE, xname,
          (0 == grid_code) ? CDM.LON_UNITS : "km", "synthesized X coord");
      v.addAttribute( new Attribute( _Coordinate.AxisType, (0 == grid_code) ? AxisType.Lon.toString() : AxisType.GeoX.toString()));
      v.setValues( nx, startx, dx);
      ds.addCoordinateAxis( v);
      return v;
    }

    CoordinateAxis makeYCoordAxis( NetcdfDataset ds, String yname) {
      CoordinateAxis v = new CoordinateAxis1D( ds, null, yname, DataType.DOUBLE, yname,
            ((0 == grid_code) ? CDM.LAT_UNITS : "km"), "synthesized Y coord");
      v.addAttribute( new Attribute( _Coordinate.AxisType, (0 == grid_code) ? AxisType.Lat.toString() : AxisType.GeoY.toString()));
      v.setValues(ny, starty, dy);
      ds.addCoordinateAxis( v);
      return v;
    }

    private ProjectionCT makeLCProjection() throws NoSuchElementException {
      double latin1 = navInfo.getDouble( "Latin1");
      double latin2 = navInfo.getDouble( "Latin2");
      double lov = navInfo.getDouble( "Lov");
      double la1 = navInfo.getDouble( "La1");
      double lo1 = navInfo.getDouble( "Lo1");

      // we have to project in order to find the origin
      LambertConformal lc = new LambertConformal(latin1, lov, latin1, latin2);
      ProjectionPointImpl start = (ProjectionPointImpl) lc.latLonToProj( new LatLonPointImpl( la1, lo1));
      if (debug) System.out.println("start at proj coord "+start);
      startx = start.getX();
      starty = start.getY();

      nx = navInfo.getInt( "Nx");
      ny = navInfo.getInt( "Ny");
      dx = navInfo.getDouble( "Dx")/1000.0; // need to be km : unit conversion LOOK;
      dy = navInfo.getDouble( "Dy")/1000.0; // need to be km : unit conversion LOOK;

      return new ProjectionCT(grid_name, "FGDC", lc);
    }

    // polar stereographic
    private ProjectionCT makePSProjection() throws NoSuchElementException {
      double lov = navInfo.getDouble( "Lov");
      double la1 = navInfo.getDouble( "La1");
      double lo1 = navInfo.getDouble( "Lo1");

      // Why the scale factor?. accordining to GRID docs:
      // "Grid lengths are in units of meters, at the 60 degree latitude circle nearest to the pole"
      // since the scale factor at 60 degrees = k = 2*k0/(1+sin(60))  [Snyder,Working Manual p157]
      // then to make scale = 1 at 60 degrees, k0 = (1+sin(60))/2 = .933
      Stereographic ps = new Stereographic(90.0, lov, .933);

      // we have to project in order to find the origin
      ProjectionPointImpl start = (ProjectionPointImpl) ps.latLonToProj( new LatLonPointImpl( la1, lo1));
      if (debug) System.out.println("start at proj coord "+start);
      startx = start.getX();
      starty = start.getY();

      nx = navInfo.getInt( "Nx");
      ny = navInfo.getInt( "Ny");
      dx = navInfo.getDouble( "Dx")/1000.0;
      dy = navInfo.getDouble( "Dy")/1000.0;

      return new ProjectionCT(grid_name, "FGDC", ps);
    }

    private void processLatLonProjection() throws NoSuchElementException {
        // get stuff we need to construct axes
      starty = navInfo.getDouble( "La1");
      startx = navInfo.getDouble( "Lo1");
      nx = navInfo.getInt( "Ni");
      ny = navInfo.getInt( "Nj");
      dx = navInfo.getDouble( "Di");
      dy = navInfo.getDouble( "Dj");
    }

  } // GRIB1 */

}

/*

  private void showPoint( int ix, int iy) {
    ProjectionPointImpl pt = new ProjectionPointImpl(xaxis.getCoordValue(ix), yaxis.getCoordValue(iy));
    if (debugPoint) System.out.println( ix+ " "+iy+" "+pt+ " --> " +proj.projToLatLon(pt));
  }

  private void showProj( double lat, double lon) {
    LatLonPointImpl llpt = new LatLonPointImpl(lat,lon);
    if (debugProj) System.out.println( llpt+ " --> " +proj.latLonToProj(llpt));
  }

  private void showLat( double x, double y) {
    ProjectionPointImpl pt = new ProjectionPointImpl(x, y);
    LatLonPoint llpt = proj.projToLatLon(pt);
    if (debugPoint) System.out.println( pt+ " --> " +llpt.getLatitude()+" "+llpt.getLongitude());
  }

*/


package thredds.server.wfs;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletResponse;

/**
 * A simple XML writer for XML-based WFS exceptions.
 *
 * @author wchen@usgs.gov
 *
 */
public class WFSExceptionWriter {
	private final String text;
	private final String ExceptionCode;
	private final String locator;

	/**
	 * Given the information on construction, writes the necessary exception information.
	 *
	 * @param hsr the Servlet Response to write to
	 * @throws IOException
	 */
	public void write(HttpServletResponse hsr) throws IOException{
		PrintWriter xmlResponse = hsr.getWriter();

		xmlResponse.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		xmlResponse.append("<ows:ExceptionReport xml:lang=\"en-US\" xsi:schemaLocation=\"http://www.opengis.net/ows/1.1"
				+ " http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd\" version=\"2.0.0\" xmlns:ows=\"http://www.opengis.net/ows/1.1\""
				+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">");
		xmlResponse.append("<ows:Exception ");
		if(locator != null) xmlResponse.append("locator=\"" + locator + "\" ");
		xmlResponse.append("exceptionCode=\"" + ExceptionCode + "\">");
		xmlResponse.append("<ows:ExceptionText>" + text + "</ows:ExceptionText>");
		xmlResponse.append("</ows:Exception>");
		xmlResponse.append("</ows:ExceptionReport>");
	}

	/**
	 * Creates a new OWS Exception Report based on a text and an exception code.
	 *
	 * Some exceptions do not specify locators. For these exceptions send in null for the locator.
	 *
	 * @param text the text associated with the exception
	 * @param locator the locator associated with the exception
	 * @param ExceptionCode the standardized exception code
	 * @param hsr http response to write to
	 */
	public WFSExceptionWriter(String text, String locator, String ExceptionCode) {
		this.text = text;
		this.locator = locator;
		this.ExceptionCode = ExceptionCode;
	}
}


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

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

/**
 * Concrete implementation of Array specialized for ints.
 * Data storage is with 1D java array of ints.
 * <p/>
 * issues: what should we do if a conversion loses accuracy? nothing ? Exception ?
 *
 * @author caron
 * @see Array
 */
public class ArrayInt extends Array {

   // package private. use Array.factory()
  static ArrayInt factory(Index index, boolean isUnsigned) {
    return ArrayInt.factory(index,  isUnsigned, null);
  }

  /* create new ArrayInt with given indexImpl and backing store.
   * Should be private.
   * @param index use this Index
   * @param stor. use this storage. if null, allocate.
   * @return. new ArrayInt.D<rank> or ArrayInt object.
   */
  static ArrayInt factory( Index index, boolean isUnsigned, int [] storage) {
    if (index instanceof Index0D) {
      return new ArrayInt.D0(index, isUnsigned, storage);
    } else if (index instanceof Index1D) {
      return new ArrayInt.D1(index, isUnsigned, storage);
    } else if (index instanceof Index2D) {
      return new ArrayInt.D2(index, isUnsigned, storage);
    } else if (index instanceof Index3D) {
      return new ArrayInt.D3(index, isUnsigned, storage);
    } else if (index instanceof Index4D) {
      return new ArrayInt.D4(index, isUnsigned, storage);
    } else if (index instanceof Index5D) {
      return new ArrayInt.D5(index, isUnsigned, storage);
    } else if (index instanceof Index6D) {
      return new ArrayInt.D6(index, isUnsigned, storage);
    } else if (index instanceof Index7D) {
      return new ArrayInt.D7(index, isUnsigned, storage);
    } else {
      return new ArrayInt(index, isUnsigned, storage);
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  protected int[] storage;

  /**
   * Create a new Array of type int and the given shape.
   * dimensions.length determines the rank of the new Array.
   *
   * @param dimensions the shape of the Array.
   */
  public ArrayInt(int[] dimensions, boolean isUnsigned) {
    super(isUnsigned ? DataType.UINT : DataType.INT, dimensions);
    storage = new int[(int) indexCalc.getSize()];
  }

  /**
   * Create a new Array using the given IndexArray and backing store.
   * used for sections. Trusted package private.
   *
   * @param ima  use this IndexArray as the index
   * @param data use this as the backing store
   */
  ArrayInt(Index ima, boolean isUnsigned, int[] data) {
    super(isUnsigned ? DataType.UINT : DataType.INT, ima);
    /* replace by something better
    if (ima.getSize() != data.length)
      throw new IllegalArgumentException("bad data length"); */
    if (data != null)
      storage = data;
    else
      storage = new int[(int) ima.getSize()];
  }

  /**
   * create new Array with given indexImpl and same backing store
   */
  protected Array createView(Index index) {
    return ArrayInt.factory(index, isUnsigned(), storage);
  }

  /* Get underlying primitive array storage. CAUTION! You may invalidate your warrentee! */
  public Object getStorage() {
    return storage;
  }

  // copy from javaArray to storage using the iterator: used by factory( Object);
  protected void copyFrom1DJavaArray(IndexIterator iter, Object javaArray) {
    int[] ja = (int[]) javaArray;
    for (int aJa : ja) iter.setIntNext(aJa);
  }

  // copy to javaArray from storage using the iterator: used by copyToNDJavaArray;
  protected void copyTo1DJavaArray(IndexIterator iter, Object javaArray) {
    int[] ja = (int[]) javaArray;
    for (int i = 0; i < ja.length; i++)
      ja[i] = iter.getIntNext();
  }

  public ByteBuffer getDataAsByteBuffer() {return getDataAsByteBuffer(null);}

  public ByteBuffer getDataAsByteBuffer(ByteOrder order) {
    ByteBuffer bb = super.getDataAsByteBuffer((int) (4 * getSize()),order);
    IntBuffer ib = bb.asIntBuffer();
    ib.put((int[]) get1DJavaArray(int.class)); // make sure its in canonical order
    return bb;
  }

  /**
   * Return the element class type
   */
  public Class getElementType() {
    return int.class;
  }

  /**
   * Get the value at the specified index.
   * @param i the index
   * @return the value at the specified index.
   */
  public int get(Index i) {
    return storage[i.currentElement()];
  }

  /**
   * Set the value at the specified index.
   * @param i the index
   * @param value set to this value
   */
  public void set(Index i, int value) {
    storage[i.currentElement()] = value;
  }

  public double getDouble(Index i) {
    int val = storage[i.currentElement()];
    return (double) (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setDouble(Index i, double value) {
    storage[i.currentElement()] = (int) value;
  }

  public float getFloat(Index i) {
    int val = storage[i.currentElement()];
    return (float) (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setFloat(Index i, float value) {
    storage[i.currentElement()] = (int) value;
  }

  public long getLong(Index i) {
    int val = storage[i.currentElement()];
    return (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setLong(Index i, long value) {
    storage[i.currentElement()] = (int) value;
  }

  public int getInt(Index i) {
    return storage[i.currentElement()];
  }

  public void setInt(Index i, int value) {
    storage[i.currentElement()] = value;
  }

  public short getShort(Index i) {
    return (short) storage[i.currentElement()];
  }

  public void setShort(Index i, short value) {
    storage[i.currentElement()] = (int) value;
  }

  public byte getByte(Index i) {
    return (byte) storage[i.currentElement()];
  }

  public void setByte(Index i, byte value) {
    storage[i.currentElement()] = (int) value;
  }

  public char getChar(Index i) {
    return (char) storage[i.currentElement()];
  }

  public void setChar(Index i, char value) {
    storage[i.currentElement()] = (int) value;
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public boolean getBoolean(Index i) {
    throw new ForbiddenConversionException();
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public void setBoolean(Index i, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(Index i) {
    return storage[i.currentElement()];
  }

  public void setObject(Index i, Object value) {
    storage[i.currentElement()] = ((Number) value).intValue();
  }

  // package private : mostly for iterators
  public double getDouble(int index) {
    int val = storage[index];
    return (double) (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setDouble(int index, double value) {
    storage[index] = (int) value;
  }

  public float getFloat(int index) {
    int val = storage[index];
    return (float) (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setFloat(int index, float value) {
    storage[index] = (int) value;
  }

  public long getLong(int index) {
    int val = storage[index];
    return (isUnsigned() ? DataType.unsignedIntToLong(val) : val);
  }

  public void setLong(int index, long value) {
    storage[index] = (int) value;
  }

  public int getInt(int index) {
    return storage[index];
  }

  public void setInt(int index, int value) {
    storage[index] = value;
  }

  public short getShort(int index) {
    return (short) storage[index];
  }

  public void setShort(int index, short value) {
    storage[index] = (int) value;
  }

  public byte getByte(int index) {
    return (byte) storage[index];
  }

  public void setByte(int index, byte value) {
    storage[index] = (int) value;
  }

  public char getChar(int index) {
    return (char) storage[index];
  }

  public void setChar(int index, char value) {
    storage[index] = (int) value;
  }

  public boolean getBoolean(int index) {
    throw new ForbiddenConversionException();
  }

  public void setBoolean(int index, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(int index) {
    return getInt(index);
  }

  public void setObject(int index, Object value) {
    storage[index] = ((Number) value).intValue();
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 0.
   */
  public static class D0 extends ArrayInt {
    private Index0D ix;

    public D0(boolean isUnsigned) {
      super(new int[]{},isUnsigned);
      ix = (Index0D) indexCalc;
    }

    private D0(Index i, boolean isUnsigned, int[] store) {
      super(i,isUnsigned, store);
      ix = (Index0D) indexCalc;
    }

    public int get() {
      return storage[ix.currentElement()];
    }

    public void set(int value) {
      storage[ix.currentElement()] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 1.
   */
  public static class D1 extends ArrayInt {
    private Index1D ix;

    public D1(int len0, boolean isUnsigned) {
      super(new int[]{len0}, isUnsigned);
      ix = (Index1D) indexCalc;
    }

    private D1(Index i, boolean isUnsigned, int[] store) {
      super(i,isUnsigned, store);
      ix = (Index1D) indexCalc;
    }

    public int get(int i) {
      return storage[ix.setDirect(i)];
    }

    public void set(int i, int value) {
      storage[ix.setDirect(i)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 2.
   */
  public static class D2 extends ArrayInt {
    private Index2D ix;

    public D2(int len0, int len1, boolean isUnsigned) {
      super(new int[]{len0, len1}, isUnsigned);
      ix = (Index2D) indexCalc;
    }

    private D2(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index2D) indexCalc;
    }

    public int get(int i, int j) {
      return storage[ix.setDirect(i, j)];
    }

    public void set(int i, int j, int value) {
      storage[ix.setDirect(i, j)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 3.
   */
  public static class D3 extends ArrayInt {
    private Index3D ix;

    public D3(int len0, int len1, int len2, boolean isUnsigned) {
      super(new int[]{len0, len1, len2}, isUnsigned);
      ix = (Index3D) indexCalc;
    }

    private D3(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index3D) indexCalc;
    }

    public int get(int i, int j, int k) {
      return storage[ix.setDirect(i, j, k)];
    }

    public void set(int i, int j, int k, int value) {
      storage[ix.setDirect(i, j, k)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 4.
   */
  public static class D4 extends ArrayInt {
    private Index4D ix;

    public D4(int len0, int len1, int len2, int len3, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3}, isUnsigned);
      ix = (Index4D) indexCalc;
    }

    private D4(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index4D) indexCalc;
    }

    public int get(int i, int j, int k, int l) {
      return storage[ix.setDirect(i, j, k, l)];
    }

    public void set(int i, int j, int k, int l, int value) {
      storage[ix.setDirect(i, j, k, l)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 5.
   */
  public static class D5 extends ArrayInt {
    private Index5D ix;

    public D5(int len0, int len1, int len2, int len3, int len4, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4}, isUnsigned);
      ix = (Index5D) indexCalc;
    }

    private D5(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index5D) indexCalc;
    }

    public int get(int i, int j, int k, int l, int m) {
      return storage[ix.setDirect(i, j, k, l, m)];
    }

    public void set(int i, int j, int k, int l, int m, int value) {
      storage[ix.setDirect(i, j, k, l, m)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 6.
   */
  public static class D6 extends ArrayInt {
    private Index6D ix;

    public D6(int len0, int len1, int len2, int len3, int len4, int len5, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4, len5}, isUnsigned);
      ix = (Index6D) indexCalc;
    }

    private D6(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index6D) indexCalc;
    }

    public int get(int i, int j, int k, int l, int m, int n) {
      return storage[ix.setDirect(i, j, k, l, m, n)];
    }

    public void set(int i, int j, int k, int l, int m, int n, int value) {
      storage[ix.setDirect(i, j, k, l, m, n)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for ints, rank 7.
   */
  public static class D7 extends ArrayInt {
    private Index7D ix;

    public D7(int len0, int len1, int len2, int len3, int len4, int len5, int len6, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4, len5, len6}, isUnsigned);
      ix = (Index7D) indexCalc;
    }

    private D7(Index i, boolean isUnsigned, int[] store) {
      super(i, isUnsigned, store);
      ix = (Index7D) indexCalc;
    }

    public int get(int i, int j, int k, int l, int m, int n, int o) {
      return storage[ix.setDirect(i, j, k, l, m, n, o)];
    }

    public void set(int i, int j, int k, int l, int m, int n, int o, int value) {
      storage[ix.setDirect(i, j, k, l, m, n, o)] = value;
    }
  }

}