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

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.DataFormatType;
import ucar.nc2.iosp.*;
import ucar.unidata.io.RandomAccessFile;

import java.io.File;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.Formatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * IOServiceProvider implementation abstract base class to read/write "version 3" netcdf files.
 * AKA "file format version 1" files.
 *
 * @author caron
 * @see N3raf concrete class
 */

public abstract class N3iosp extends AbstractIOServiceProvider implements IOServiceProviderWriter {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(N3iosp.class);

  // Default fill values, used unless _FillValue variable attribute is set.
  static public final byte NC_FILL_BYTE = -127;
  static public final char NC_FILL_CHAR = (char) 0;
  static public final short NC_FILL_SHORT = (short) -32767;
  static public final int NC_FILL_INT = -2147483647;
  static public final float NC_FILL_FLOAT = 9.9692099683868690e+36f; /* near 15 * 2^119 */
  static public final double NC_FILL_DOUBLE = 9.9692099683868690e+36;

  static public final byte NC_FILL_UBYTE = (byte) 255;
  static public final short NC_FILL_USHORT = (short) 65535;
  static public final int NC_FILL_UINT = (int) 4294967295L;
  static public final long NC_FILL_INT64 = -9223372036854775806L;  // 0x8000000000000002. Only bits 63 and 1 set.

  // We want to use 18446744073709551614ULL here (see https://goo.gl/buBal9), but that's too big to fit into a
  // signed long (Java doesn't have unsigned types). So, assign the hex string that WOULD correspond to that value
  // *if it were treated as unsigned*. Java will treat it as signed and see "-2", but we don't much care.
  static public final long NC_FILL_UINT64 = 0xfffffffffffffffeL;

  static public final String NC_FILL_STRING  = "";

  static public Number getFillValueDefault(DataType dtype) {
    if ((dtype == DataType.BYTE) || (dtype == DataType.ENUM1)) return N3iosp.NC_FILL_BYTE;
    if (dtype == DataType.UBYTE) return N3iosp.NC_FILL_UBYTE;
    if (dtype == DataType.CHAR) return (byte) 0;
    if ((dtype == DataType.SHORT) || (dtype == DataType.ENUM2)) return N3iosp.NC_FILL_SHORT;
    if (dtype == DataType.USHORT) return N3iosp.NC_FILL_USHORT;
    if ((dtype == DataType.INT) || (dtype == DataType.ENUM4)) return N3iosp.NC_FILL_INT;
    if (dtype == DataType.UINT) return N3iosp.NC_FILL_UINT;
    if (dtype == DataType.LONG) return N3iosp.NC_FILL_INT64;
    if (dtype == DataType.ULONG) return N3iosp.NC_FILL_UINT64;
    if (dtype == DataType.FLOAT) return N3iosp.NC_FILL_FLOAT;
    if (dtype == DataType.DOUBLE) return N3iosp.NC_FILL_DOUBLE;
    return null;
  }

  /* CLASSIC
     The maximum size of a record in the classic format in versions 3.5.1 and earlier is 2^32 - 4 bytes.
     In versions 3.6.0 and later, there is no such restriction on total record size for the classic format
     or 64-bit offset format.

     If you don't use the unlimited dimension, only one variable can exceed 2 GiB in size, but it can be as
       large as the underlying file system permits. It must be the last variable in the dataset, and the offset
       to the beginning of this variable must be less than about 2 GiB.

     The limit is really 2^31 - 4. If you were to specify a variable size of 2^31 -3, for example, it would be
       rounded up to the nearest multiple of 4 bytes, which would be 2^31, which is larger than the largest
       signed integer, 2^31 - 1.

     If you use the unlimited dimension, record variables may exceed 2 GiB in size, as long as the offset of the
       start of each record variable within a record is less than 2 GiB - 4.
   */

  /* LARGE FILE
     Assuming an operating system with Large File Support, the following restrictions apply to the netCDF 64-bit offset format.

     No fixed-size variable can require more than 2^32 - 4 bytes of storage for its data, unless it is the last
     fixed-size variable and there are no record variables. When there are no record variables, the last
     fixed-size variable can be any size supported by the file system, e.g. terabytes.

     A 64-bit offset format netCDF file can have up to 2^32 - 1 fixed sized variables, each under 4GiB in size.
     If there are no record variables in the file the last fixed variable can be any size.

     No record variable can require more than 2^32 - 4 bytes of storage for each record's worth of data,
     unless it is the last record variable. A 64-bit offset format netCDF file can have up to 2^32 - 1 records,
     of up to 2^32 - 1 variables, as long as the size of one record's data for each record variable except the
     last is less than 4 GiB - 4.

     Note also that all netCDF variables and records are padded to 4 byte boundaries.
   */

  /**
   * Each fixed-size variable and the data for one record's worth of a single record variable are limited
   *    to a little less than 4 GiB.
   */
  static public final long MAX_VARSIZE = (long) 2 * Integer.MAX_VALUE - 2; // 4,294,967,292

  /**
   * The maximum number of records is 2^32-1.
   */
  static public final int MAX_NUMRECS = Integer.MAX_VALUE;

  static private boolean syncExtendOnly = false;

  /**
   * Set a static property.
   * Supported static properties:

   *
 syncExtendOnly = "true" : assume all file changes are syncExtend only.
   *

   *
   * @param name  property name
   * @param value property value
   */
  static public void setProperty(String name, String value) {
    if (name.equalsIgnoreCase("syncExtendOnly"))
      syncExtendOnly = value.equalsIgnoreCase("true");
  }

  /**
   * Determine if the given name can be used for a NetCDF object, i.e. a Dimension, Attribute, or Variable.
   * The allowed name syntax (in RE form) is:
   *
   *
([a-zA-Z0-9_]|{UTF8})([^\x00-\x1F\x7F/]|{UTF8})*

   *
   * where UTF8 represents a multi-byte UTF-8 encoding. Also, no trailing spaces are permitted in names. We do not
   * allow '/' because HDF5 does not permit slashes in names as slash is used as a group separator. If UTF-8 is
   * supported, then a multi-byte UTF-8 character can occur anywhere within an identifier.
   *
   * @param name  the name to validate.
   * @return  {@code true} if the name is valid.
   */
  // Implements "int NC_check_name(const char *name)" from NetCDF-C:
  // https://github.com/Unidata/netcdf-c/blob/v4.3.3.1/libdispatch/dstring.c#L169
  // Should match makeValidNetcdfObjectName()
  public static boolean isValidNetcdfObjectName(String name) {
    if (name == null || name.isEmpty()) {  // Null and empty names disallowed
      return false;
    }

    int cp = name.codePointAt(0);

    // First char must be [a-z][A-Z][0-9]_ | UTF8
    if (cp <= 0x7f) {
      if (!('A' <= cp && cp <= 'Z')
              && !('a' <= cp && cp <= 'z')
              && !('0' <= cp && cp <= '9')
              && cp != '_') {
        return false;
      }
    }

    for (int i = 1; i < name.length(); ++i) {
      cp = name.codePointAt(i);

      // handle simple 0x00-0x7f characters here
      if (cp <= 0x7f) {
        if (cp < ' ' || cp > 0x7E || cp == '/') {  // control char, DEL, or forward-slash
          return false;
        }
      }
    }

    if (cp <= 0x7f && Character.isWhitespace(cp)) {  // trailing spaces disallowed
      return false;
    }

    return true;
  }

  /**
   * Convert a name to a legal netcdf-3 name.
   *
   * @param name  the name to convert.
   * @return  the converted name.
   * @see #isValidNetcdfObjectName(String)
   */
  public static String makeValidNetcdfObjectName(String name) {
    StringBuilder sb = new StringBuilder(name);

    while (sb.length() > 0) {
      int cp = sb.codePointAt(0);

      // First char must be [a-z][A-Z][0-9]_ | UTF8
      if (cp <= 0x7f) {
        if (!('A' <= cp && cp <= 'Z')
                && !('a' <= cp && cp <= 'z')
                && !('0' <= cp && cp <= '9')
                && cp != '_') {
          sb.deleteCharAt(0);
          continue;
        }
      }
      break;
    }

    for (int pos = 1; pos < sb.length(); ++pos) {
      int cp = sb.codePointAt(pos);

      // handle simple 0x00-0x7F characters here
      if (cp <= 0x7F) {
        if (cp < ' ' || cp > 0x7E || cp == '/') {  // control char, DEL, or forward-slash
          sb.deleteCharAt(pos);
          --pos;
        }
      }
    }

    while (sb.length() > 0) {
      int cp = sb.codePointAt(sb.length() - 1);

      if (cp <= 0x7f && Character.isWhitespace(cp)) {
        sb.deleteCharAt(sb.length() - 1);
      } else {
        break;
      }
    }

    if (sb.length() == 0) {
      throw new IllegalArgumentException(String.format("Illegal NetCDF object name: '%s'", name));
    }

    return sb.toString();
  }

  /////////////////////////////////////////////////////////////////
  // I think these go together - consider deprecated

  /**
   * @deprecated use makeValidNetcdfObjectName
   */
  static public String makeValidNetcdf3ObjectName(String name) {
    StringBuilder sb = new StringBuilder(name);

    while (sb.length() > 0) {
      char c = sb.charAt(0);
      if (Character.isLetter(c) || (c == '_')) break;
      if (Character.isDigit(c)) {
        sb.insert(0, 'N');
        break;
      }
      sb.deleteCharAt(0);
    }

    int i = 1;
    while (i < sb.length()) {
      char c = sb.charAt(i);
      if (c == ' ')
        sb.setCharAt(i, '_');
      else {
        boolean ok = Character.isLetterOrDigit(c) || (c == '-') || (c == '_');
        //        || (c == '@') || (c == ':') || (c == '(') || (c == ')') || (c == '+') || (c == '.');
        if (!ok) {
          sb.delete(i, i + 1);
          i--;
          // sb.setCharAt(i, '-');
        }
      }
      i++;
    }

    return sb.toString();
  }

  //static private final String special1 = "_\\.@\\+\\-";
  //static private final String special2 = " ";
  //static private final Pattern objectNamePattern = Pattern.compile("[a-zA-Z0-9_][a-zA-Z0-9_@\\.\\-\\+]*");
  static private final Pattern objectNamePatternOld = Pattern.compile("[a-zA-Z0-9_][a-zA-Z0-9_@\\:\\(\\)\\.\\-\\+]*");

  /**
   * Determine if the given name can be used for a Dimension, Attribute, or Variable name.
   * Should match makeValidNetcdf3ObjectName.
   * @param name test this.
   * @return  true if valid name.
   * @deprecated use isValidNetcdfObjectName
   */
  static public boolean isValidNetcdf3ObjectName(String name) {
    Matcher m = objectNamePatternOld.matcher(name);
    return m.matches();
  }

  static private final Pattern objectNamePattern = Pattern.compile("[a-zA-Z0-9_][^\\x00-\\x1F\\x2F\\x7F]*");

  /**
   * Valid Netcdf Object name as a regular expression.
   * @return regular expression pattern describing valid Netcdf Object names.
   */
  static public Pattern getValidNetcdf3ObjectNamePattern() {
    return objectNamePattern;
  }

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

  /**
   * Convert a name to a legal netcdf name.
   * From the user manual:
   * "The names of dimensions, variables and attributes consist of arbitrary sequences of
   * alphanumeric characters (as well as underscore '_' and hyphen '-'), beginning with a letter
   * or underscore. (However names commencing with underscore are reserved for system use.)
   * Case is significant in netCDF names."
   *

   * Algorithm:
   *

   *
leading character: if alpha or underscore, ok; if digit, prepend "N"; otherwise discard
   *
other characters: if space, change to underscore; other delete.
   *

   * @param name convert this name
   * @return converted name
   * @deprecated use makeValidNetcdfObjectName
   */
  static public String createValidNetcdf3ObjectName(String name) {
    StringBuilder sb = new StringBuilder(name);

    //LOOK: could escape characters, as in DODS (%xx) ??

    while (sb.length() > 0) {
      char c = sb.charAt(0);
      if (Character.isLetter(c) || (c == '_')) break;
      if (Character.isDigit(c)) {
        sb.insert(0, 'N');
        break;
      }
      sb.deleteCharAt(0);
    }

    int i = 1;
    while (i < sb.length()) {
      char c = sb.charAt(i);
      if ((c == ' ') || (c == '/'))
        sb.setCharAt(i, '_');
      else {
        boolean ok = Character.isLetterOrDigit(c) || (c == '-') || (c == '_') ||
                (c == '@') || (c == ':') || (c == '(') || (c == ')') || (c == '+') || (c == '.');
        if (!ok) {
          sb.delete(i, i + 1);
          i--;
          // sb.setCharAt(i, '-');
        }
      }
      i++;
    }

    return sb.toString();
  }

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

  protected boolean readonly;
  protected N3header header;
  //protected int numrecs;
  //protected long recsize;
  protected long lastModified; // used by sync

  // used for writing only
  // protected long fileUsed = 0; // how much of the file is written to ?
  // protected long recStart = 0; // where the record data starts

  protected boolean debug = false, debugSize = false, debugSPIO = false, debugRecord = false, debugRead = false;
  protected boolean showHeaderBytes = false;

  @Override
  public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) throws IOException {
    return N3header.isValidFile(raf);
  }

  @Override
  public String getDetailInfo() {
    Formatter f = new Formatter();
    f.format("%s",super.getDetailInfo());

    try {
      header.showDetail(f);
    } catch (IOException e) {
      return e.getMessage();
    }

    return f.toString();

  }

  // properties
  protected boolean useRecordStructure;

  //////////////////////////////////////////////////////////////////////////////////////
  // read existing file

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

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

    String location = raf.getLocation();
    if (!location.startsWith("http:")) {
      File file = new File(location);
      if (file.exists())
        lastModified = file.lastModified();
    }

    // its a netcdf-3 file
    raf.order(RandomAccessFile.BIG_ENDIAN);
    header = new N3header();

    header.read(raf, ncfile, null); // read header here
    //numrecs = header.numrecs;
    //recsize = header.recsize;
    //recStart = header.recStart;

    _open(raf);

    ncfile.finish();
  }

  @Override
  public void setFill(boolean fill) {
    this.fill = fill;
  }

  /////////////////////////////////////////////////////////////////////////////
  // data reading

  @Override
  public Array readData(ucar.nc2.Variable v2, Section section) throws IOException, InvalidRangeException {
    /* if (debugRead) {
      System.out.printf("debugRead %s %s%n", v2.toStringDebug(), section);
      if (v2.getShortName().equals("cref"))
        System.out.println("HEY");
    } */
    if (v2 instanceof Structure)
      return readRecordData((Structure) v2, section);

    N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
    DataType dataType = v2.getDataType();

    Layout layout = (!v2.isUnlimited()) ? new LayoutRegular(vinfo.begin, v2.getElementSize(), v2.getShape(), section) :
      new LayoutRegularSegmented(vinfo.begin, v2.getElementSize(), header.recsize, v2.getShape(), section);

    if (layout.getTotalNelems() == 0) {
      return Array.factory(dataType, section.getShape());
    }

    Object data = readData(layout, dataType);
    return Array.factory(dataType, section.getShape(), data);
  }

  /**
   * Read data from record structure. For N3, this is the only possible structure, and there can be no nesting.
   * Read all variables for each record, put in ByteBuffer.
   *
   * @param s the record structure
   * @param section the record range to read
   * @return an ArrayStructure, with all the data read in.
   * @throws IOException on error
   */
  private ucar.ma2.Array readRecordData(ucar.nc2.Structure s, Section section) throws java.io.IOException {
    //if (s.isSubset())
    //  return readRecordDataSubset(s, section);

    // has to be 1D
    Range recordRange = section.getRange(0);

    // create the ArrayStructure
    StructureMembers members = s.makeStructureMembers();
    for (StructureMembers.Member m : members.getMembers()) {
      Variable v2 = s.findVariable(m.getName());
      N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
      m.setDataParam((int) (vinfo.begin - header.recStart));
    }

    // protect agains too large of reads
    if (header.recsize > Integer.MAX_VALUE)
      throw new IllegalArgumentException("Cant read records when recsize > "+Integer.MAX_VALUE);
    long nrecs = section.computeSize();
    if (nrecs * header.recsize > Integer.MAX_VALUE)
      throw new IllegalArgumentException("Too large read: nrecs * recsize= "+(nrecs * header.recsize) +"bytes exceeds "+Integer.MAX_VALUE);

    members.setStructureSize((int) header.recsize);
    ArrayStructureBB structureArray = new ArrayStructureBB(members, new int[]{recordRange.length()});

    // note dependency on raf; should probably defer to subclass
    // loop over records
    byte[] result = structureArray.getByteBuffer().array();
    int count = 0;
    for (int recnum : recordRange) {
      if (debugRecord) System.out.println(" read record " + recnum);
      raf.seek(header.recStart + recnum * header.recsize); // where the record starts

      if (recnum != header.numrecs - 1)
        raf.readFully(result, (int) (count * header.recsize), (int) header.recsize);
      else
        raf.read(result, (int) (count * header.recsize), (int) header.recsize); // "wart" allows file to be one byte short. since its always padding, we allow
      count++;
    }

    return structureArray;
  }

  /**
   * Read data from record structure, that has been subsetted.
   * Read one record at at time, put requested variable into ArrayStructureMA.
   *
   * @param s the record structure
   * @param section the record range to read
   * @return an ArrayStructure, with all the data read in.
   * @throws IOException on error
   */
  private ucar.ma2.Array readRecordDataSubset(ucar.nc2.Structure s, Section section) throws java.io.IOException {
    Range recordRange = section.getRange(0);
    int nrecords = recordRange.length();

    // create the ArrayStructureMA
    StructureMembers members = s.makeStructureMembers();
    for (StructureMembers.Member m : members.getMembers()) {
      Variable v2 = s.findVariable(m.getName());
      N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
      m.setDataParam((int) (vinfo.begin - header.recStart)); // offset from start of record

      // construct the full shape
      int rank = m.getShape().length;
      int[] fullShape = new int[rank + 1];
      fullShape[0] = nrecords;  // the first dimension
      System.arraycopy(m.getShape(), 0, fullShape, 1, rank); // the remaining dimensions

      Array data = Array.factory(m.getDataType(), fullShape);
      m.setDataArray( data);
      m.setDataObject( data.getIndexIterator());
    }

    //LOOK this is all wrong - why using recsize ???
    return null;
    /* members.setStructureSize(recsize);
    ArrayStructureMA structureArray = new ArrayStructureMA(members, new int[]{nrecords});

    // note dependency on raf; should probably defer to subclass
    // loop over records
    byte[] record = new byte[ recsize];
    ByteBuffer bb = ByteBuffer.wrap(record);
    for (int recnum = recordRange.first(); recnum <= recordRange.last(); recnum += recordRange.stride()) {
      if (debugRecord) System.out.println(" readRecordDataSubset recno= " + recnum);

      // read one record
      raf.seek(recStart + recnum * recsize); // where the record starts
      if (recnum != numrecs - 1)
        raf.readFully(record, 0, recsize);
      else
        raf.read(record, 0, recsize); // "wart" allows file to be one byte short. since its always padding, we allow

      // transfer desired variable(s) to result array(s)
      for (StructureMembers.Member m : members.getMembers()) {
        IndexIterator dataIter = (IndexIterator) m.getDataObject();
        IospHelper.copyFromByteBuffer(bb, m, dataIter);
      }
    }

    return structureArray;  */
  }

  private ucar.ma2.Array readNestedData(ucar.nc2.Variable v2, Section section) throws java.io.IOException, ucar.ma2.InvalidRangeException {
    N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
    DataType dataType = v2.getDataType();

    // construct the full shape for use by RegularIndexer
    int[] fullShape = new int[v2.getRank() + 1];
    fullShape[0] = header.numrecs;  // the first dimension
    System.arraycopy(v2.getShape(), 0, fullShape, 1, v2.getRank()); // the remaining dimensions

    Layout layout = new LayoutRegularSegmented(vinfo.begin, v2.getElementSize(), header.recsize, fullShape, section);
    Object dataObject = readData(layout, dataType);
    return Array.factory(dataType, section.getShape(), dataObject);
  }

  @Override
  public long readToByteChannel(ucar.nc2.Variable v2, Section section, WritableByteChannel channel)
      throws java.io.IOException, ucar.ma2.InvalidRangeException {

    if (v2 instanceof Structure)
      return readRecordData((Structure) v2, section, channel);

    N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
    DataType dataType = v2.getDataType();

    Layout layout = (!v2.isUnlimited()) ? new LayoutRegular(vinfo.begin, v2.getElementSize(), v2.getShape(), section) :
      new LayoutRegularSegmented(vinfo.begin, v2.getElementSize(), header.recsize, v2.getShape(), section);

    return readData(layout, dataType, channel);
  }

  private long readRecordData(ucar.nc2.Structure s, Section section, WritableByteChannel out) throws java.io.IOException, InvalidRangeException {
    long count = 0;

    /* RegularIndexer index = new RegularIndexer( s.getShape(), recsize, recStart, section, recsize);
    while (index.hasNext()) {
       Indexer.Chunk chunk = index.next();
       count += raf.readBytes( out, chunk.getFilePos(), chunk.getNelems() * s.getElementSize());
     }  */

    // LOOK this is the OTW layout based on netcdf-3
    // not sure this works but should give an idea of timing
    Range recordRange = section.getRange(0);
    /* int stride = recordRange.stride();
    if (stride == 1) {
      int first = recordRange.first();
      int n = recordRange.length();
      if (false) System.out.println(" read record " + first+" "+ n * header.recsize+" bytes ");
      return raf.readToByteChannel(out, header.recStart + first * header.recsize, n * header.recsize);

    } else {  */

    for (int recnum : recordRange) {
        if (debugRecord) System.out.println(" read record " + recnum);
        raf.seek(header.recStart + recnum * header.recsize); // where the record starts
        count += raf.readToByteChannel(out, header.recStart + recnum * header.recsize, header.recsize);
      }
    // }

    return count;
  }

  //////////////////////////////////////////////////////////////////////////////////////
  // create new file

  protected boolean fill = true;
  // protected HashMap dimHash = new HashMap(50);

  @Override
  public void create(String filename, ucar.nc2.NetcdfFile ncfile, int extra, long preallocateSize, boolean largeFile) throws IOException {
    this.ncfile = ncfile;
    this.readonly = false;

    // finish any structures
    ncfile.finish();

    raf = new ucar.unidata.io.RandomAccessFile(filename, "rw");
    raf.order(RandomAccessFile.BIG_ENDIAN);

    if (preallocateSize > 0) {
      java.io.RandomAccessFile myRaf = raf.getRandomAccessFile();
      myRaf.setLength(preallocateSize);
    }

    header = new N3header();
    header.create(raf, ncfile, extra, largeFile, null);

    //recsize = header.recsize;   // record size
    //recStart = header.recStart; // record variables start here
    //fileUsed = headerParser.getMinLength(); // track what is actually used

    _create(raf);

    if (fill)
      fillNonRecordVariables();
    //else
    //  raf.setMinLength(recStart); // make sure file length is long enough, even if not written to.
  }

  @Override
  public boolean rewriteHeader(boolean largeFile) throws IOException {
    return header.rewriteHeader(largeFile, null);
  }

  //////////////////////////////////////////////////////////////////////////////////////
  // write

  @Override
  public void writeData(Variable v2, Section section, Array values) throws java.io.IOException, InvalidRangeException {
    N3header.Vinfo vinfo = (N3header.Vinfo) v2.getSPobject();
    DataType dataType = v2.getDataType();

    if (v2.isUnlimited()) {
      Range firstRange = section.getRange(0);
      setNumrecs(firstRange.last() + 1);
    }

    if (v2 instanceof Structure) {
      if (!(values instanceof ArrayStructure))
        throw new IllegalArgumentException("writeData for Structure: data must be ArrayStructure");

      if (v2.getRank() == 0)
        throw new IllegalArgumentException("writeData for Structure: must have rank > 0");

      Dimension d = v2.getDimension(0);
      if (!d.isUnlimited())
        throw new IllegalArgumentException("writeData for Structure: must have unlimited dimension");

      writeRecordData((Structure) v2, section, (ArrayStructure) values);

    } else {
      Layout layout = (!v2.isUnlimited()) ? new LayoutRegular(vinfo.begin, v2.getElementSize(), v2.getShape(), section) :
        new LayoutRegularSegmented(vinfo.begin, v2.getElementSize(), header.recsize, v2.getShape(), section);
      writeData(values, layout, dataType);
    }
  }

  @Override
  public int appendStructureData(Structure s, StructureData sdata) throws IOException, InvalidRangeException {
    int recnum = header.numrecs;
    setNumrecs(recnum + 1);
    writeRecordData(s, recnum, sdata);
    return recnum;
  }

  private void writeRecordData(ucar.nc2.Structure s, Section section, ArrayStructure structureArray) throws java.io.IOException, ucar.ma2.InvalidRangeException {
    int countSrcRecnum = 0;
    Range recordRange = section.getRange(0);
    for (int recnum : recordRange) {
      StructureData sdata = structureArray.getStructureData(countSrcRecnum);
      writeRecordData(s, recnum, sdata);
      countSrcRecnum++;
    }
  }

  private void writeRecordData(ucar.nc2.Structure s, int recnum, StructureData sdata) throws java.io.IOException, ucar.ma2.InvalidRangeException {

    StructureMembers members = sdata.getStructureMembers();

    // loop over members
    for (Variable vm : s.getVariables()) {
      StructureMembers.Member m = members.findMember(vm.getShortName());
      if (null == m)
        continue; // this means that the data is missing from the ArrayStructure

      // convert String member data into CHAR data
      Array data = sdata.getArray(m);
      if (data instanceof ArrayObject && vm.getDataType() == DataType.CHAR && vm.getRank() > 0) {
        int strlen = vm.getShape(vm.getRank() - 1);
        data = ArrayChar.makeFromStringArray((ArrayObject) data, strlen); // turn it into an ArrayChar
      }

      // layout of the destination
      N3header.Vinfo vinfo = (N3header.Vinfo) vm.getSPobject();
      long begin = vinfo.begin + recnum * header.recsize;  // this assumes unlimited dimension
      Section memberSection = vm.getShapeAsSection();
      Layout layout = new LayoutRegular(begin, vm.getElementSize(), vm.getShape(), memberSection);

      try {
        writeData(data, layout, vm.getDataType());
      } catch (Exception e) {
        log.error("Error writing member="+vm.getShortName()+" in struct="+s.getFullName(), e);
        throw new IOException(e);
      }
    }
  }

  protected void setNumrecs(int n) throws IOException, InvalidRangeException {
    if (n <= header.numrecs) return;
    int startRec = header.numrecs;

    if (debugSize) System.out.println("extend records to = " + n);
    //fileUsed = recStart + recsize * n;
    header.setNumrecs(n);
    //this.numrecs = n;

    // need to let unlimited dimension know of new shape
    for (Dimension dim : ncfile.getDimensions()) {
      if (dim.isUnlimited())
        dim.setLength(n);
    }

    // need to let all unlimited variables know of new shape
    for (Variable v : ncfile.getVariables()) {
      if (v.isUnlimited()) {
        v.resetShape();
        v.setCachedData(null, false);
      }
    }

    // extend file, handle filling
    if (fill)
      fillRecordVariables(startRec, n);
    else
      raf.setMinLength( header.calcFileSize());
  }

  /**
   * Update the value of an existing attribute. Attribute is found by name, which must match exactly.
   * You cannot make an attribute longer, or change the number of values.
   * For strings: truncate if longer, zero fill if shorter.  Strings are padded to 4 byte boundaries, ok to use padding if it exists.
   * For numerics: must have same number of values.
   *
   * @param v2  variable, or null for fglobal attribute
   * @param att replace with this value
   * @throws IOException
   */
  @Override
  public void updateAttribute(ucar.nc2.Variable v2, Attribute att) throws IOException {
    header.updateAttribute(v2, att);
  }

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

  // fill buffer with fill value

  protected void fillNonRecordVariables() throws IOException {
    // run through each variable
    for (Variable v : ncfile.getVariables()) {
      if (v.isUnlimited()) continue;
      try {
        writeData(v, v.getShapeAsSection(), makeConstantArray(v));
      } catch (InvalidRangeException e) {
        e.printStackTrace();  // shouldnt happen
      }
    }
  }

  protected void fillRecordVariables(int recStart, int recEnd) throws IOException, InvalidRangeException {
    // do each record completely, should be a bit more efficient

    for (int i = recStart; i < recEnd; i++) { // do one record at a time
      Range r = new Range(i, i);

      // run through each variable
      for (Variable v : ncfile.getVariables()) {
        if (!v.isUnlimited() || (v instanceof Structure)) continue;
        Section recordSection = new Section( v.getRanges());
        recordSection.setRange(0, r);
        writeData(v, recordSection, makeConstantArray(v));
      }
    }
  }

  private Array makeConstantArray(Variable v) {
    Class classType = v.getDataType().getPrimitiveClassType();
    //int [] shape = v.getShape();
    Attribute att = v.findAttribute(CDM.FILL_VALUE);

    Object storage = null;
    if (classType == double.class) {
      double[] storageP = new double[1];
      storageP[0] = (att == null) ? NC_FILL_DOUBLE : att.getNumericValue().doubleValue();
      storage = storageP;

    } else if (classType == float.class) {
      float[] storageP = new float[1];
      storageP[0] = (att == null) ? NC_FILL_FLOAT : att.getNumericValue().floatValue();
      storage = storageP;

    } else if (classType == int.class) {
      int[] storageP = new int[1];
      storageP[0] = (att == null) ? NC_FILL_INT : att.getNumericValue().intValue();
      storage = storageP;

    } else if (classType == short.class) {
      short[] storageP = new short[1];
      storageP[0] = (att == null) ? NC_FILL_SHORT : att.getNumericValue().shortValue();
      storage = storageP;

    } else if (classType == byte.class) {
      byte[] storageP = new byte[1];
      storageP[0] = (att == null) ? NC_FILL_BYTE : att.getNumericValue().byteValue();
      storage = storageP;

    } else if (classType == char.class) {
      char[] storageP = new char[1];
      storageP[0] = (att != null) && (att.getStringValue().length() > 0) ? att.getStringValue().charAt(0) : NC_FILL_CHAR;
      storage = storageP;
    }

    return Array.factoryConstant(v.getDataType(), v.getShape(), storage);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  @Override
  public boolean syncExtend() throws IOException {
    boolean result = header.synchNumrecs();
    if (result && log.isDebugEnabled())
      log.debug(" N3iosp syncExtend " + raf.getLocation() + " numrecs =" + header.numrecs);
    return result;
  }

  /* @Override
  public boolean sync() throws IOException {
    if (syncExtendOnly)
      return syncExtend();

    if (lastModified == 0) // ?? HttpRandomAccessFile
      return false;

    File file = new File(raf.getLocation());
    if (file.exists()) {
      long currentModified = file.lastModified();
      if (currentModified == lastModified)
        return false;

      // so things have been modified, heres where we need to reread the header !!
      ncfile.empty();
      open(raf, ncfile, null);
      if (log.isDebugEnabled())
        log.debug(" N3iosp resynced " + raf.getLocation() + " currentModified=" + currentModified + " lastModified= " + lastModified);
      return true;
    }

    // can this happen ?
    throw new IOException("File does not exist");
  } */

  @Override
  public void flush() throws java.io.IOException {
    if (raf != null) {
      raf.flush();
      header.writeNumrecs();
      raf.flush();
    }
  }

  @Override
  public void close() throws java.io.IOException {
    if (raf != null) {
      long size = header.calcFileSize();
      raf.setMinLength(size);
      raf.close();
    }
    raf = null;
  }

  @Override
  public void reacquire() throws IOException {
    super.reacquire();
    header.raf = this.raf;
  }

  /**
   * Debug info for this object.
   */
  @Override
  public String toStringDebug(Object o) {
    return null;
  }

  @Override
  public Object sendIospMessage(Object message) {
    if (null == header)
      return null;
    if (message == NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE)
      return header.makeRecordStructure();
    else if (message == NetcdfFile.IOSP_MESSAGE_REMOVE_RECORD_STRUCTURE)
      return header.removeRecordStructure();

    return super.sendIospMessage(message);
  }

  @Override
  public String getFileTypeId() {
    return DataFormatType.NETCDF.getDescription();
  }

  @Override
  public String getFileTypeDescription()  { return "NetCDF-3/CDM"; }

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // stuff we need the subclass to implement

  /**
   * Read data subset from file for a variable, create primitive array.
   *
   * @param index    handles skipping around in the file.
   * @param dataType dataType of the variable
   * @return primitive array with data read in
   * @throws java.io.IOException on error
   */
  abstract protected Object readData(Layout index, DataType dataType) throws IOException;

  abstract protected long readData(Layout index, DataType dataType, WritableByteChannel out) throws IOException;

  /**
   * Write data subset to file for a variable, create primitive array.
   *
   * @param aa       write data from this Array.
   * @param index    handles skipping around in the file.
   * @param dataType dataType of the variable
   * @throws java.io.IOException on error
   */
  abstract protected void writeData(Array aa, Layout index, DataType dataType) throws IOException;

  abstract protected void _open(ucar.unidata.io.RandomAccessFile raf) throws java.io.IOException;

  abstract protected void _create(ucar.unidata.io.RandomAccessFile raf) throws java.io.IOException;

  private static void testValid(String test) {
    //boolean isValid = isValidNetcdf3ObjectName(test);
    //String convert = makeValidNetcdf3ObjectName(test);
    //boolean isValid2 = isValidNetcdf3ObjectName(convert);
    //System.out.printf("old: %s (%s) == %s (%s) %n", test, isValid, convert, isValid2);

    boolean isValid = isValidNetcdfObjectName(test);
    String convert = makeValidNetcdfObjectName(test);
    boolean isValid2 = isValidNetcdfObjectName(convert);
    System.out.printf("new: %s (%s) == %s (%s) %n%n", test, isValid, convert, isValid2);
  }
}


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

package thredds.server.opendap;

import opendap.dap.InvalidDimensionException;
import ucar.ma2.*;
import ucar.nc2.*;

import opendap.servers.*;
import opendap.dap.BaseType;
import opendap.dap.DArrayDimension;
import opendap.dap.PrimitiveVector;

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

/**
 * Wraps a netcdf variable with rank > 0 as an SDArray.
 * For char arrays, use NcSDString (rank 0 or 1) or NcSDCharArray (rank > 1).
 *
 * @author jcaron
 * @see NcSDCharArray
 */
public class NcSDArray extends SDArray implements HasNetcdfVariable {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NcSDArray.class);

  private static final boolean debug = false, debugRead = false;
  private Variable ncVar = null;
  //ignore protected BaseType elemType;

  /**
   * Constructor: Wraps a netcdf variable in a DODS SDArray.
   *
   * @param v  : netcdf Variable
   * @param bt : DODS element type
   */
  NcSDArray(Variable v, BaseType bt) {
    super(Variable.getDAPName(v));
    this.ncVar = v;

    // set dimensions
    for (Dimension dim : v.getDimensions()) {
      appendDim(dim.getLength(), dim.getShortName());
    }

    // this seems to be how you set the type
    // it creates the "primitive vector"
    addVariable(bt);
    // ignore this.elemType = bt;
  }

  public Variable getVariable() {
    return ncVar;
  }

  /**
   * Read the data values (parameters are ignored).
   * Use the start, stop and stride values, typically set by the constraint evaluator.
   *
   * @param datasetName not used
   * @param specialO    not used
   * @return false (no more data to be read)
   * @throws IOException
   * @throws EOFException
   */
  public boolean read(String datasetName, Object specialO) throws IOException {
    long tstart = System.currentTimeMillis();

    Array a;
    try {
      if (log.isDebugEnabled())
        log.debug(getRequestedRange());

      // set up the netcdf read
      int n = numDimensions();
      List<Range> ranges = new ArrayList<>(n);
      for (int i = 0; i < n; i++)
        ranges.add(new Range(getStart(i), getStop(i), getStride(i)));

      try {
        a = ncVar.read(ranges);

      } catch (java.lang.ArrayIndexOutOfBoundsException t) {
        log.error(getRequestedRange(), t);
        throw new RuntimeException("NcSDArray java.lang.ArrayIndexOutOfBoundsException=" + t.getMessage()+
            " for request= "+ getRequestedRange()+" dataset= "+ datasetName, t);
      }

      if (debug)
        System.out.println("  NcSDArray Read " + getEncodedName() + " " + a.getSize() + " elems of type = " + a.getElementType());
      if (debugRead) System.out.println("  Read = " + a.getSize() + " elems of type = " + a.getElementType());
      if (log.isDebugEnabled()) {
        long tookTime = System.currentTimeMillis() - tstart;
        log.debug("NcSDArray read array: " + tookTime * .001 + " seconds");
      }

    } catch (InvalidDimensionException e) {
      log.error(getRequestedRange(), e);
      throw new IllegalStateException("NcSDArray InvalidDimensionException=" + e.getMessage());

    } catch (InvalidRangeException e) {
      log.error(getRequestedRange(), e);
      throw new IllegalStateException("NcSDArray InvalidRangeException=" + e.getMessage());
    }
    setData(a);

    if (debugRead) System.out.println(" PrimitiveVector len = " + getPrimitiveVector().getLength() +
            " type = " + getPrimitiveVector().getTemplate());

    return (false);
  }

  private String getRequestedRange() {
    try {
      StringBuilder sbuff = new StringBuilder();
      sbuff.append("NcSDArray read " + ncVar.getFullName());
      for (int i = 0; i < numDimensions(); i++) {
        DArrayDimension d = getDimension(i);
        sbuff.append(" " + d.getEncodedName() + "(" + getStart(i) + "," + getStride(i) + "," + getStop(i) + ")");
      }
      return sbuff.toString();

    } catch (InvalidDimensionException e) {
      e.printStackTrace();
      return e.getMessage();
    }
  }

  public void setData(Array data) {
    PrimitiveVector pv = getPrimitiveVector();
    if (debugRead)
      System.out.println(" PrimitiveVector type = " + pv.getTemplate() +
              " pv type = " + pv.getClass().getName());

    if (ncVar.getDataType() == DataType.STRING) {
      int size = (int) data.getSize();
      NcSDString[] dodsBT = new NcSDString[size];
      IndexIterator ii = data.getIndexIterator();
      int count = 0;
      while (ii.hasNext()) {
        dodsBT[count++] = new NcSDString(Variable.getDAPName(ncVar), (String) ii.getObjectNext());
      }
      pv.setInternalStorage(dodsBT);

    } else if (ncVar.getDataType() == DataType.STRUCTURE) {
      NcSDStructure sds = (NcSDStructure) pv.getTemplate();

      int size = (int) data.getSize();
      NcSDStructure[] dodsBT = new NcSDStructure[size];

      IndexIterator ii = data.getIndexIterator();
      int count = 0;
      while (ii.hasNext()) {
        StructureData sdata = (StructureData) ii.getObjectNext();
        dodsBT[count] = new NcSDStructure(sds, sdata); // stupid replication - need to override externalize
        count++;
      }
      pv.setInternalStorage(dodsBT);

    } else {

      // copy the data into the PrimitiveVector
      // this is optimized to (possibly) eliminate the copy
      Object pa = data.get1DJavaArray(data.getElementType());
      pv.setInternalStorage(pa);
    }

    setRead(true);
  }

  public void serialize(DataOutputStream sink, StructureData sdata, StructureMembers.Member m) throws IOException {
    long tstart = System.currentTimeMillis();

    setData(sdata.getArray(m));
    externalize(sink);

    if (log.isDebugEnabled()) {
      long tookTime = System.currentTimeMillis() - tstart;
      log.debug("NcSDArray serialize: " + tookTime * .001 + " seconds");
    }
  }

}


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

/**
 *
 */
package ucar.nc2.iosp.grads;

import ucar.nc2.units.DateUnit;

import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

/**
 * Class to hold the complexities of the GrADS time dimension
 *
 * @author Don Murray - CU/CIRES
 */
public class GradsTimeDimension extends GradsDimension {

    /** time increment periods */
    private final String[] incStr = { "mn", "hr", "dy", "mo", "yr" };

    /** corresponding Calendar periods */
    private final int[] calIncs = { Calendar.MINUTE, Calendar.HOUR,
                                    Calendar.DATE, Calendar.MONTH,
                                    Calendar.YEAR };

    /** time date formats (input is lowercased so Z is z) */
    private final String[] dateFormats = { "HH:mm'z'ddMMMyyyy",
                                           "HH'z'ddMMMyyyy", "ddMMMyyyy",
                                           "MMMyyyy" };

    /** The initial time as a TimeStruct */
    private GradsTimeStruct initialTime = null;

    //J-
    /** time templates */
    private static final String[] timeTemplates = {
        "%x1",   // 1 digit decade
        "%x3",   // 3 digit decade
        "%y2",   // 2 digit year
        "%y4",   // 4 digit year
        "%m1",   // 1 or 2 digit month
        "%m2",   // 2 digit month (leading zero if needed)
        "%mc",   // 3 character month abbreviation
        "%d1",   // 1 or 2 digit day
        "%d2",   // 2 digit day (leading zero if needed)
        "%h1",   // 1 or 2 digit hour
        "%h2",   // 2 digit hour
        "%h3",   // 3 digit hour (e.g., 120 or 012)
        "%n2",   // 2 digit minute; leading zero if needed
        "%f2",   // 2 digit forecast hour; leading zero if needed; more digits added for hours >99; hour values increase indefinitely
        "%f3",   // 3 digit forecast hour; leading zeros if needed; more digits added for hours >999; hour values increase indefinitely
        "%fn2",  // 2 digit forecast minute; leading zero if needed; more digits added for minutes > 99; minute values increase indefinitely (2.0.a9+)
        "%fhn",  // forecast time expressed in hours and minutes (hhnn) where minute value (nn) is always <=59
                 //  and hour value (hh) increases indefinitely. If hh or nn are <=9, they are padded with a 0
                 // so they are always at least 2 digits; more digits added for hours >99. (2.0.a9+)
        "%fdhn", // forecast time expressed in days, hours, and minutes (ddhhnn) where minute value (nn) is always <=59,
                 // hour value (hh) is always <=23 and day value (dd) increases indefinitely. If dd, hh, or nn are <=9,
                 // they are padded with a 0 so they are always at least 2 digits; more digits added for days >99. (2.0.a9+)
        "%j3",   // 3 digit julian day (day of year) (2.0.a7+)
        "%t1",   // 1 or 2 digit time index (file names contain number sequences that begin with 1 or 01) (2.0.a7+)
        "%t2",   // 2 digit time index (file names contain number sequences that begin with 01) (2.0.a7+)
        "%t3",   // 3 digit time index (file names contain number sequences that begin with 001) (2.0.a7+)
        "%t4",   // 4 digit time index (file names contain number sequences that begin with 0001) (2.0.a8+)
        "%t5",   // 5 digit time index (file names contain number sequences that begin with 00001) (2.0.a8+)
        "%t6",   // 6 digit time index (file names contain number sequences that begin with 000001) (2.0.a8+)
        "%tm1",  // 1 or 2 digit time index (file names contain number sequences that begin with 0 or 00) (2.0.a7+)
        "%tm2",  // 2 digit time index (file names contain number sequences that begin with 00) (2.0.a7+)
        "%tm3",  // 3 digit time index (file names contain number sequences that begin with 000) (2.0.a7+)
        "%tm4",  // 4 digit time index (file names contain number sequences that begin with 0000) (2.0.a8+)
        "%tm5",  // 5 digit time index (file names contain number sequences that begin with 00000) (2.0.a8+)
        "%tm6",  // 6 digit time index (file names contain number sequences that begin with 000000) (2.0.a8+)

  //When specifying the initial time (e.g., NWP model output), use these substitutions:

        "%ix1",  //initial 1 digit decade
        "%ix3",  //initial 3 digit decade
        "%iy2",  //initial 2 digit year
        "%iy4",  //initial 4 digit year
        "%im1",  //initial 1 or 2 digit month
        "%im2",  //initial 2 digit month (leading zero if needed)
        "%imc",  //initial 3 character month abbreviation
        "%id1",  //initial 1 or 2 digit day (leading zero if needed)
        "%id2",  //initial 2 digit day
        "%ih1",  //initial 1 or 2 digit hour
        "%ih2",  //initial 2 digit hour
        "%ih3",  //initial 3 digit hour
        "%in2"  //initial 2 digit minute (leading zero if needed)

    };
//J+

    /**
     * Create new GradsTimeDimension
     *
     * @param name  the dimension name
     * @param size  the dimension size
     * @param mapping  the dimension mapping type
     */
    public GradsTimeDimension(String name, int size, String mapping) {
        super(name, size, mapping);
    }

    /**
     * Make the level values from the specifications
     *
     * @return the level values
     */
    protected double[] makeLevelValues() {
        List<String> levels = getLevels();
        if (levels == null) {
            return null;
        }
        if (levels.size() != getSize()) {
            // do someting
        }
        // Time is always LINEAR
        int      inc     = 0;
        double[] vals    = new double[getSize()];
        String   tstart  = levels.get(0).trim().toLowerCase();
        String   pattern = null;
        if (tstart.indexOf(":") >= 0) {                     // HH:mmZddMMMyyyy
            pattern = dateFormats[0];
        } else if (tstart.indexOf("z") >= 0) {              // mmZddMMMyyyy
            pattern = dateFormats[1];
        } else if (Character.isLetter(tstart.charAt(0))) {  // MMMyyyy
            pattern = dateFormats[3];
        } else {
            pattern = dateFormats[2];                       // ddMMMyyyy
        }
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        //sdf.setLenient(true);
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        ParsePosition p = new ParsePosition(0);
        Date          d = sdf.parse(tstart, p);
        if (d == null) {
            System.out.println("couldn't parse at " + p.getErrorIndex());
            d = new Date(0);
        }
        //System.out.println("start = " + d);
        // set the unit
        sdf.applyPattern("yyyy-MM-dd HH:mm:ss Z");
        setUnit("hours since "
                + sdf.format(d, new StringBuffer(), new FieldPosition(0)));
        // parse the increment
        // vvkk where
        // vv     =       an integer number, 1 or 2 digits
        // kk     =       mn (minute)
        //                hr (hour)
        //                dy (day)
        //                mo (month)
        //                yr (year)
        String tinc     = levels.get(1).toLowerCase();
        int    incIndex = 0;
        for (int i = 0; i < incStr.length; i++) {
            int index = tinc.indexOf(incStr[i]);
            if (index < 0) {
                continue;
            }
            int numOf = Integer.parseInt(tinc.substring(0, index));
            inc      = numOf;
            incIndex = i;

            break;
        }
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        calendar.setTime(d);
        vals[0]     = 0;
        initialTime = makeTimeStruct(calendar);
        //System.out.println("initial time = " + initialTime);
        int calInc = calIncs[incIndex];
        double hours =  (double) 1000 * 60 * 60;
        for (int i = 1; i < getSize(); i++) {
            calendar.add(calInc, inc);
            // subtract from origin, convert to hours
            double offset = (calendar.getTime().getTime() - d.getTime()) / hours;  //millis in an hour
            vals[i] = offset;
        }
        return vals;

    }

    /**
     * Make a time struct from the index.
     *
     * @param timeIndex  the time value index
     *
     * @return the corresponding TimeStruct
     */
    public GradsTimeStruct makeTimeStruct(int timeIndex) {
        double   tVal     = getValues()[timeIndex];
        Date     d        = DateUnit.getStandardDate(tVal + " " + getUnit());
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
        calendar.setTime(d);
        return makeTimeStruct(calendar);
    }

    /**
     * Make a GradsTimeStruct from the calendar state
     *
     * @param calendar  the calendar
     *
     * @return the corresponding GradsTimeStruct
     */
    private GradsTimeStruct makeTimeStruct(Calendar calendar) {
        GradsTimeStruct ts = new GradsTimeStruct();
        ts.year   = calendar.get(Calendar.YEAR);
        ts.month  = calendar.get(Calendar.MONTH) + 1;  // MONTH is zero based
        ts.day    = calendar.get(Calendar.DAY_OF_MONTH);
        ts.hour   = calendar.get(Calendar.HOUR_OF_DAY);
        ts.minute = calendar.get(Calendar.MINUTE);
        ts.jday   = calendar.get(Calendar.DAY_OF_YEAR);
        return ts;
    }

    /**
     * Replace the time template parameters in a filename
     *
     * @param filespec  the file template
     * @param timeIndex the time index
     *
     * @see "http://www.iges.org/grads/gadoc/templates.html"
     *
     * @return  the filled in template
     */
    public String replaceFileTemplate(String filespec, int timeIndex) {

        GradsTimeStruct ts = makeTimeStruct(timeIndex);
        //System.out.println(ts);
        String retString = filespec;
        String format;
        while (hasTimeTemplate(retString)) {
            // initial time
            if (retString.indexOf("%ix1") >= 0) {
                retString = retString.replaceAll("%ix1",
                        String.format("%d", initialTime.year / 10));
            }
            if (retString.indexOf("%ix3") >= 0) {
                retString = retString.replaceAll("%ix3",
                        String.format("%03d", initialTime.year / 10));
            }
            if (retString.indexOf("%iy2") >= 0) {
                int cent = initialTime.year / 100;
                int val  = initialTime.year - cent * 100;
                retString = retString.replaceAll("%iy2",
                        String.format("%02d", val));
            }
            if (retString.indexOf("%iy4") >= 0) {
                retString = retString.replaceAll("%iy4",
                        String.format("%d", initialTime.year));
            }
            if (retString.indexOf("%im1") >= 0) {
                retString = retString.replaceAll("%im1",
                        String.format("%d", initialTime.month));
            }
            if (retString.indexOf("%im2") >= 0) {
                retString = retString.replaceAll("%im2",
                        String.format("%02d", initialTime.month));
            }
            if (retString.indexOf("%imc") >= 0) {
                retString = retString.replaceAll("%imc",
                        GradsTimeStruct.months[initialTime.month - 1]);
            }
            if (retString.indexOf("%id1") >= 0) {
                retString = retString.replaceAll("%id1",
                        String.format("%d", initialTime.day));
            }
            if (retString.indexOf("%id2") >= 0) {
                retString = retString.replaceAll("%id2",
                        String.format("%02d", initialTime.day));
            }
            if (retString.indexOf("%ih1") >= 0) {
                retString = retString.replaceAll("%ih1",
                        String.format("%d", initialTime.hour));
            }
            if (retString.indexOf("%ih2") >= 0) {
                retString = retString.replaceAll("%ih2",
                        String.format("%02d", initialTime.hour));
            }
            if (retString.indexOf("%ih3") >= 0) {
                retString = retString.replaceAll("%ih3",
                        String.format("%03d", initialTime.hour));
            }
            if (retString.indexOf("%in2") >= 0) {
                retString = retString.replaceAll("%in2",
                        String.format("%02d", initialTime.minute));
            }
            // any time
            // decade
            if (retString.indexOf("%x1") >= 0) {
                retString = retString.replaceAll("%x1",
                        String.format("%d", ts.year / 10));
            }
            if (retString.indexOf("%x3") >= 0) {
                retString = retString.replaceAll("%x3",
                        String.format("%03d", ts.year / 10));
            }
            // year
            if (retString.indexOf("%y2") >= 0) {
                int cent = ts.year / 100;
                int val  = ts.year - cent * 100;
                retString = retString.replaceAll("%y2",
                        String.format("%02d", val));
            }
            if (retString.indexOf("%y4") >= 0) {
                retString = retString.replaceAll("%y4",
                        String.format("%d", ts.year));
            }
            // month
            if (retString.indexOf("%m1") >= 0) {
                retString = retString.replaceAll("%m1",
                        String.format("%d", ts.month));
            }
            if (retString.indexOf("%m2") >= 0) {
                retString = retString.replaceAll("%m2",
                        String.format("%02d", ts.month));
            }
            if (retString.indexOf("%mc") >= 0) {
                retString = retString.replaceAll("%mc",
                        GradsTimeStruct.months[ts.month - 1]);
            }
            // day
            if (retString.indexOf("%d1") >= 0) {
                retString = retString.replaceAll("%d1",
                        String.format("%d", ts.day));
            }
            if (retString.indexOf("%d2") >= 0) {
                retString = retString.replaceAll("%d2",
                        String.format("%02d", ts.day));
            }
            // hour
            if (retString.indexOf("%h1") >= 0) {
                retString = retString.replaceAll("%h1",
                        String.format("%d", ts.hour));
            }
            if (retString.indexOf("%h2") >= 0) {
                retString = retString.replaceAll("%h2",
                        String.format("%02d", ts.hour));
            }
            if (retString.indexOf("%h3") >= 0) {
                retString = retString.replaceAll("%h3",
                        String.format("%03d", ts.hour));
            }
            // minute
            if (retString.indexOf("%n2") >= 0) {
                retString = retString.replaceAll("%n2",
                        String.format("%02d", ts.minute));
            }
            // julian day
            if (retString.indexOf("%j3") >= 0) {
                retString = retString.replaceAll("%j3",
                        String.format("%03d", ts.jday));
            }
            // time index (1 based)
            if (retString.indexOf("%t1") >= 0) {
                retString = retString.replaceAll("%t1",
                        String.format("%d", timeIndex + 1));
            }
            if (retString.indexOf("%t2") >= 0) {
                retString = retString.replaceAll("%t2",
                        String.format("%02d", timeIndex + 1));
            }
            if (retString.indexOf("%t3") >= 0) {
                retString = retString.replaceAll("%t3",
                        String.format("%03d", timeIndex + 1));
            }
            if (retString.indexOf("%t4") >= 0) {
                retString = retString.replaceAll("%t4",
                        String.format("%04d", timeIndex + 1));
            }
            if (retString.indexOf("%t5") >= 0) {
                retString = retString.replaceAll("%t5",
                        String.format("%05d", timeIndex + 1));
            }
            if (retString.indexOf("%t6") >= 0) {
                retString = retString.replaceAll("%t6",
                        String.format("%06d", timeIndex + 1));
            }
            // time index (0 based)
            if (retString.indexOf("%tm1") >= 0) {
                retString = retString.replaceAll("%tm1",
                        String.format("%d", timeIndex));
            }
            if (retString.indexOf("%tm2") >= 0) {
                retString = retString.replaceAll("%tm2",
                        String.format("%02d", timeIndex));
            }
            if (retString.indexOf("%tm3") >= 0) {
                retString = retString.replaceAll("%tm3",
                        String.format("%03d", timeIndex));
            }
            if (retString.indexOf("%tm4") >= 0) {
                retString = retString.replaceAll("%tm4",
                        String.format("%04d", timeIndex));
            }
            if (retString.indexOf("%tm5") >= 0) {
                retString = retString.replaceAll("%tm5",
                        String.format("%05d", timeIndex));
            }
            if (retString.indexOf("%tm6") >= 0) {
                retString = retString.replaceAll("%tm6",
                        String.format("%06d", timeIndex));
            }
            // forecast hours
            if (retString.indexOf("%f") >= 0) {
                int mins = (int) getValues()[timeIndex] * 60;
                int tdif;
                if (retString.indexOf("%f2") >= 0) {
                    format = "%02d";
                    tdif   = mins / 60;
                    if (tdif > 99) {
                        format = "%d";
                    }
                    retString = retString.replaceAll("%f2",
                            String.format(format, tdif));
                }
                if (retString.indexOf("%f3") >= 0) {
                    format = "%03d";
                    tdif   = mins / 60;
                    if (tdif > 999) {
                        format = "%d";
                    }
                    retString = retString.replaceAll("%f3",
                            String.format(format, tdif));
                }
                if (retString.indexOf("%fn2") >= 0) {
                    format = "%02d";
                    if (mins > 99) {
                        format = "%d";
                    }
                    retString = retString.replaceAll("%fn2",
                            String.format(format, mins));
                }
                if (retString.indexOf("%fhn2") >= 0) {
                    tdif = mins;
                    int hrs = tdif / 60;
                    int mns = tdif - (hrs * 60);
                    format = "%02d%02d";
                    if (hrs > 99) {
                        format = "%d%02d";
                    }
                    retString = retString.replaceAll("%fhn2",
                            String.format(format, hrs, mns));
                }
                if (retString.indexOf("%fdhn2") >= 0) {
                    tdif = mins;
                    int dys = tdif / 1440;
                    int hrs = (tdif - (dys * 1440)) / 60;
                    int mns = tdif - (dys * 1440) - (hrs * 60);
                    format = "%02d%02d%02d";
                    if (dys > 99) {
                        format = "%d%02d%02d";
                    }
                    retString = retString.replaceAll("%fdhn2",
                            String.format(format, dys, hrs, mns));
                }
            }
        }
        return retString;

    }

    /**
     * Does this file definition have a time template in it?
     * @param template  the file template
     * @return true if it does
     */
    public static boolean hasTimeTemplate(String template) {
        for (int i = 0; i < timeTemplates.length; i++) {
            if (template.indexOf(timeTemplates[i]) >= 0) {
                return true;
            }
        }
        return false;
    }

}


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

package ucar.unidata.geoloc.projection;

import com.google.common.math.DoubleMath;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;
import ucar.unidata.util.SpecialMathFunction;

/**
 * Transverse Mercator projection, spherical earth.
 * Projection plane is a cylinder tangent to the earth at tangentLon.
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 53
 *
 * @author John Caron
 */

public class TransverseMercator extends ProjectionImpl {

  private double lat0, lon0, scale, earthRadius;
  private double falseEasting, falseNorthing;

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

  /**
   * Constructor with default parameteres
   */
  public TransverseMercator() {
    this(40.0, -105.0, .9996);
  }

  /**
   * Construct a TransverseMercator Projection.
   *
   * @param lat0       origin of projection coord system is at (lat0, tangentLon)
   * @param tangentLon longitude that the cylinder is tangent at ("central meridian")
   * @param scale      scale factor along the central meridian
   */
  public TransverseMercator(double lat0, double tangentLon, double scale) {
    this(lat0, tangentLon, scale, 0.0, 0.0, EARTH_RADIUS);
  }

  /**
   * Construct a TransverseMercator Projection.
   *
   * @param lat0       origin of projection coord system is at (lat0, tangentLon)
   * @param tangentLon longitude that the cylinder is tangent at ("central meridian")
   * @param scale      scale factor along the central meridian
   * @param east       false easting in km
   * @param north      false northing in km
   */
  public TransverseMercator(double lat0, double tangentLon, double scale, double east, double north) {
    this(lat0, tangentLon, scale, east, north, EARTH_RADIUS);
  }

  /**
   * Construct a TransverseMercator Projection.
   *
   * @param lat0       origin of projection coord system is at (lat0, tangentLon)
   * @param tangentLon longitude that the cylinder is tangent at ("central meridian")
   * @param scale      scale factor along the central meridian
   * @param east       false easting in units of km
   * @param north      false northing in units of km
   * @param radius     earth radius in km
   */
  public TransverseMercator(double lat0, double tangentLon, double scale, double east, double north, double radius) {
    super("TransverseMercator", false);

    this.lat0 = Math.toRadians(lat0);
    this.lon0 = Math.toRadians(tangentLon);
    this.earthRadius = radius;
    this.scale = scale * earthRadius;
    this.falseEasting = (!Double.isNaN(east)) ? east : 0.0;
    this.falseNorthing = (!Double.isNaN(north)) ? north : 0.0;

    addParameter(CF.GRID_MAPPING_NAME, CF.TRANSVERSE_MERCATOR);
    addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, tangentLon);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, lat0);
    addParameter(CF.SCALE_FACTOR_AT_CENTRAL_MERIDIAN, scale);
    addParameter(CF.EARTH_RADIUS, earthRadius * 1000);

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

  // bean properties

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

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

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

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

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

  public double getEarthRadius() {
    return earthRadius;
  }

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

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

  /**
   * Set the origin latitude
   *
   * @param lat  the origin latitude
   */
  public void setOriginLat(double lat) {
      lat0 = Math.toRadians(lat);
  }

  /**
   * Set the tangent longitude
   *
   * @param lon the tangent longitude
   */
  public void setTangentLon(double lon) {
      lon0 = Math.toRadians(lon);
  }

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

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

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

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

  /**
   * Get the parameters as a String
   *
   * @return the parameters as a String
   */
  public String paramsToString() {
    return toString();
  }

  @Override
  public String toString() {
    return "TransverseMercator{" +
            "lat0=" + lat0 +
            ", lon0=" + lon0 +
            ", scale=" + scale +
            ", earthRadius=" + earthRadius +
            ", falseEasting=" + falseEasting +
            ", falseNorthing=" + falseNorthing +
            '}';
  }

  /**
   * Does the line between these two points cross the projection "seam".
   *
   * @param pt1 the line goes between these two points
   * @param pt2 the line goes between these two points
   * @return false if there is no seam
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (ProjectionPointImpl.isInfinite(pt1)
            || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }

    double y1 = pt1.getY() - falseNorthing;
    double y2 = pt2.getY() - falseNorthing;

    // opposite signed long lines
    return (y1 * y2 < 0) && (Math.abs(y1 - y2) > 2 * earthRadius);
  }

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

    TransverseMercator that = (TransverseMercator) o;
    double tolerance = 1e-6;

    if (DoubleMath.fuzzyCompare(that.earthRadius,   earthRadius,   tolerance) != 0) return false;
    if (DoubleMath.fuzzyCompare(that.falseEasting,  falseEasting,  tolerance) != 0) return false;
    if (DoubleMath.fuzzyCompare(that.falseNorthing, falseNorthing, tolerance) != 0) return false;
    if (DoubleMath.fuzzyCompare(that.lat0,          lat0,          tolerance) != 0) return false;
    if (DoubleMath.fuzzyCompare(that.lon0,          lon0,          tolerance) != 0) return false;
    if (DoubleMath.fuzzyCompare(that.scale,         scale,         tolerance) != 0) return false;

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

    return true;
  }

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

  /*MACROBODY
    projToLatLon {} {
    double x = (fromX-falseEasting)/scale;
    double d = (fromY-falseNorthing)/scale + lat0;
    toLon = Math.toDegrees(lon0 + Math.atan2(SpecialMathFunction.sinh(x), Math.cos(d)));
    toLat = Math.toDegrees(Math.asin( Math.sin(d)/ SpecialMathFunction.cosh(x)));
    }

    latLonToProj {} {
    double lon = Math.toRadians(fromLon);
    double lat = Math.toRadians(fromLat);
    double dlon = lon-lon0;
    double b = Math.cos( lat) * Math.sin(dlon);
    // infinite projection
    if ((Math.abs(Math.abs(b) - 1.0)) < TOLERANCE) {
        toX = 0.0; toY =  0.0;
    } else {
    toX = scale * SpecialMathFunction.atanh(b) + falseEasting;
    toY = scale * (Math.atan2(Math.tan(lat),Math.cos(dlon)) - lat0) + falseNorthing;
    }
}

  MACROBODY*/

  /*BEGINGENERATED*/

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

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latLon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  public ProjectionPoint latLonToProj(LatLonPoint latLon,
                                      ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();

    double lon = Math.toRadians(fromLon);
    double lat = Math.toRadians(fromLat);
    double dlon = lon - lon0;
    double b = Math.cos(lat) * Math.sin(dlon);

    if ((Math.abs(Math.abs(b) - 1.0)) < TOLERANCE) { // infinite projection
      toX = Double.POSITIVE_INFINITY;
      toY = Double.POSITIVE_INFINITY;
    } else {
      toX = scale * SpecialMathFunction.atanh(b);
      toY = scale * (Math.atan2(Math.tan(lat), Math.cos(dlon)) - lat0);
    }

    result.setLocation(toX + falseEasting, toY + falseNorthing);
    return result;
  }

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

    double x = (fromX - falseEasting) / scale;
    double d = (fromY - falseNorthing) / scale + lat0;
    toLon = Math.toDegrees(lon0 + Math.atan2(Math.sinh(x), Math.cos(d)));
    toLat = Math.toDegrees(Math.asin(Math.sin(d) / Math.cosh(x)));

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

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

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

      double lon = Math.toRadians(fromLon);
      double lat = Math.toRadians(fromLat);
      double dlon = lon - lon0;
      double b = Math.cos(lat) * Math.sin(dlon);
      // infinite projection
      if ((Math.abs(Math.abs(b) - 1.0)) < TOLERANCE) {
        toX = 0.0;
        toY = 0.0;
      } else {
        toX = scale * SpecialMathFunction.atanh(b) + falseEasting;
        toY = scale * (Math.atan2(Math.tan(lat), Math.cos(dlon)) - lat0) + falseNorthing;
      }

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

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

    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i];
      double fromY = fromYA[i];

      double x = (fromX - falseEasting) / scale;
      double d = (fromY - falseNorthing) / scale + lat0;
      toLon = Math.toDegrees(lon0 + Math.atan2(Math.sinh(x), Math.cos(d)));
      toLat = Math.toDegrees(Math.asin(Math.sin(d) / Math.cosh(x)));

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

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

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

      double lon = Math.toRadians(fromLon);
      double lat = Math.toRadians(fromLat);
      double dlon = lon - lon0;
      double b = Math.cos(lat) * Math.sin(dlon);
      // infinite projection
      if ((Math.abs(Math.abs(b) - 1.0)) < TOLERANCE) {
        toX = 0.0;
        toY = 0.0;
      } else {
        toX = scale * SpecialMathFunction.atanh(b) + falseEasting;
        toY = scale * (Math.atan2(Math.tan(lat), Math.cos(dlon)) - lat0) + falseNorthing;
      }

      resultXA[i] = (double) toX;
      resultYA[i] = (double) toY;
    }
    return to;
  }

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

    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i];
      double fromY = fromYA[i];

      double x = (fromX - falseEasting) / scale;
      double d = (fromY - falseNorthing) / scale + lat0;
      toLon = Math.toDegrees(lon0 + Math.atan2(Math.sinh(x), Math.cos(d)));
      toLat = Math.toDegrees(Math.asin(Math.sin(d) / Math.cosh(x)));

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

  /*ENDGENERATED*/

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.servers;

import java.util.*;

import opendap.dap.NoSuchFunctionException;

/**
 * Represents a library of available server-side functions for use
 * in evaluating constraint expressions.

 *

 * When created, a FunctionLibrary is empty.
 * There are two ways to populate it, described below. Once the
 * FunctionLibrary has been created and populated, it should be
 * used to create a ClauseFactory, which can then be given to
 * the CEEvaluator for use in parsing.

 *

 *
 The most straightforward is to pass an instance of each
 * ServerSideFunction class to the add() method.  If you have a large
 * number of ServerSideFunctions, or if you want to have choose class names
 * for your functions independent of the actual function name, this is the
 * approach to use.
 *

 * A second, more complex method resolves
 * server-side function names at runtime. The function library automatically
 * looks for a class whose
 * name matches the name requested (if a prefix is set, it will
 * look for a class whose name is prefix + name requested). If such a class
 * exists, and it implements the ServerSideFunction interface, a new instance
 * of the class is created using the default constructor, added to the list
 * of available functions, and returned to the caller.

 *

 * This is not quite as complicated as it sounds. For instance, say the
 * FunctionLibrary's prefix is set to "opendap.servers.test.SSF". Then the
 * first time
 * the server gets a constraint expression containing the function "dummy()",
 * the FunctionLibrary will
 * automatically load the class "opendap.servers.test.SSFdummy", and return
 * a new instance using the class's default constructor.

 *

 * This avoids the need to hardcode a list of server-side functions, and
 * allows you to create new functions at any time. For instance to create
 * a function "newfn()" you can simply
 * create the class "opendap.servers.test.SSFnewfn" and put it somewhere
 * in the server's classpath . Then the first CE containing this function will
 * cause that class to be automatically loaded, just the like the dummy()
 * function was.
 *
 * @author joew
 */
public class FunctionLibrary {

    /**
     * Creates a new FunctionLibrary with no prefix set.
     */
    public FunctionLibrary() {
        this("");
    }

    /**
     * Creates a new FunctionLibrary.
     *
     * @param prefix A string that will be prepended to function names in order
     *               to create a classname for that function. For example,
     */
    public FunctionLibrary(String prefix) {
        this.prefix = prefix;
        this.boolFunctions = new HashMap();
        this.btFunctions = new HashMap();
    }

    /**
     * Sets the prefix to use for classname lookup.
     * For instance, if the prefix
     * is "myserver.SSF", then when the library doesn't have an entry
     * for function "xyz" it will look for a class "myserver.SSFxyz".
     */
    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    /**
     * Returns the prefix being used for classname lookup.
     */
    public String getPrefix() {
        return prefix;
    }

    /**
     * Adds a function to the library. The function will be inspected
     * to determine whether it is a boolean or BaseType function.
     */
    public void add(ServerSideFunction function) {
        if (function instanceof BoolFunction) {
            boolFunctions.put(function.getName(), function);
        }
        if (function instanceof BTFunction) {
            btFunctions.put(function.getName(), function);
        }
    }

    /**
     * Retrieves a boolean function from the library. If the function
     * is not found the library will attempt to load it using the
     * mechanism described in the class documentation.
     *
     * @param name The name of the function being requested.
     * @return Null if the function is not
     *         in the library, and the attempt to load it fails.
     */
    public BoolFunction getBoolFunction(String name)
            throws NoSuchFunctionException {

        if (!boolFunctions.containsKey(name)) {
            loadNewFunction(name);
        }
        return (BoolFunction) boolFunctions.get(name);
    }

    /**
     * Retrieves a BaseType function from the library. If the function
     * is not found the library will attempt to load it using the
     * mechanism described in the class documentation.
     *
     * @param name The name of the function being requested.
     * @return Null if the function is not
     *         in the library, and the attempt to load it fails.
     */
    public BTFunction getBTFunction(String name)
            throws NoSuchFunctionException {

        if (!btFunctions.containsKey(name)) {
            loadNewFunction(name);
        }
        return (BTFunction) btFunctions.get(name);
    }

    /**
     * Tries to load a function with the given name.
     */
    protected void loadNewFunction(String name) {
        try {
            String fullName = prefix + name;
            Class value = Class.forName(fullName);
            if ((ServerSideFunction.class).isAssignableFrom(value)) {
                add((ServerSideFunction) value.newInstance());
                return;
            }
        } catch (ClassNotFoundException e) {
        } catch (IllegalAccessException e) {
        } catch (InstantiationException e) {
        }
    }

    protected Map boolFunctions;
    protected Map btFunctions;
    protected String prefix;
}


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

package ucar.nc2.dataset.conv;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.ma2.Array;
import ucar.ma2.ArrayObject;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.*;
import ucar.nc2.constants.CF;
import ucar.nc2.dataset.*;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.util.CancelTask;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.units.SimpleUnit;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.geoloc.projection.TransverseMercator;
import ucar.unidata.geoloc.projection.Stereographic;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Default Coordinate Conventions.
 * Split from GDV.
 * @author caron
 * @since Dec 17, 2008
 */
public class DefaultConvention extends CoordSysBuilder {
  static private final Logger logger = LoggerFactory.getLogger(DefaultConvention.class);

    protected ProjectionCT projCT = null;

    public DefaultConvention() {
      this.conventionName = "Default";
    }

    public void augmentDataset(NetcdfDataset ds, CancelTask cancelTask) {
      projCT = makeProjectionCT(ds);
      if (projCT != null) {
        VariableDS v = makeCoordinateTransformVariable(ds, projCT);
        ds.addVariable(null, v);

        String xname = findCoordinateName(ds, AxisType.GeoX);
        String yname = findCoordinateName(ds, AxisType.GeoY);
        if (xname != null && yname != null)
          v.addAttribute(new Attribute(_Coordinate.Axes, xname + " " + yname));
      }
      ds.finish();
    }

    @Override
    protected void findCoordinateAxes(NetcdfDataset ds) {

      for (VarProcess vp : varList) {
        if (vp.isCoordinateVariable) continue;

        Variable ncvar = vp.v;
        if (!(ncvar instanceof VariableDS)) continue; // cant be a structure

        String dimName = findAlias(ds, ncvar);
        if (dimName.length() == 0) // none
          continue;
        Dimension dim = ds.findDimension(dimName);
        if (null != dim) {
          vp.isCoordinateAxis = true;
          parseInfo.format(" Coordinate Axis added (alias) = %s for dimension %s%n", vp.v.getFullName(), dimName);
        }
      }

      // coordinates is an alias for _CoordinateAxes
      for (VarProcess vp : varList) {
        if (vp.coordAxes == null) { // dont override if already set
          String coordsString = ds.findAttValueIgnoreCase(vp.v, CF.COORDINATES, null);
          if (coordsString != null) {
            vp.coordinates = coordsString;
          }
        }
      }

      super.findCoordinateAxes(ds);

      /////////////////////////
      // now we start forcing
      HashMap<AxisType, VarProcess> map = new HashMap<>();

      // find existing axes, so we dont duplicate
      for (VarProcess vp : varList) {
        if (vp.isCoordinateAxis) {
          AxisType atype = getAxisType(ds, (VariableEnhanced) vp.v);
          if (atype != null)
            map.put(atype, vp);
        }
      }

      // look for time axes based on units
      if (map.get(AxisType.Time) == null) {
        for (VarProcess vp : varList) {
          Variable ncvar = vp.v;
          if (!(ncvar instanceof VariableDS)) continue; // cant be a structure
          String unit = ncvar.getUnitsString();

          if (unit != null && SimpleUnit.isDateUnit(unit)) {
            vp.isCoordinateAxis = true;
            map.put(AxisType.Time, vp);
            parseInfo.format(" Time Coordinate Axis added (unit) = %s from unit %s%n", vp.v.getFullName(), unit);
            //break; // allow multiple time coords
          }
        }
      }

      // look for missing axes by using name hueristics
      for (VarProcess vp : varList) {
        if (vp.isCoordinateVariable) continue;
        Variable ncvar = vp.v;
        if (!(ncvar instanceof VariableDS)) continue; // cant be a structure

        AxisType atype = getAxisType(ds, (VariableEnhanced) vp.v);
        if (atype != null) {
          if (map.get(atype) == null) {
            vp.isCoordinateAxis = true;
            parseInfo.format(" Coordinate Axis added (Default forced) = %s for axis %s%n", vp.v.getFullName(), atype);
            map.put(atype, vp);
          }
        }
      }
    }

    /**
     * look for aliases.
     *
     * @param ds       containing dataset
     * @param axisType look for this axis type
     * @return name of axis of that type
     */
    private String findCoordinateName(NetcdfDataset ds, AxisType axisType) {
      List<Variable> vlist = ds.getVariables();
      for (Variable aVlist : vlist) {
        VariableEnhanced ve = (VariableEnhanced) aVlist;
        if (axisType == getAxisType(ds, ve)) {
          return ve.getFullName();
        }
      }
      return null;
    }

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

    protected AxisType getAxisType(NetcdfDataset ds, VariableEnhanced ve) {
      AxisType result = getAxisTypeCoards(ds, ve);
      if (result != null) return result;

      Variable v = (Variable) ve;
      String vname = v.getShortName();
      if (vname == null)
        return null;
      String unit = v.getUnitsString();
      if (unit == null) unit = "";
      String desc = v.getDescription();
      if (desc == null) desc = "";

      if (vname.equalsIgnoreCase("x") || findAlias(ds, v).equalsIgnoreCase("x"))
        return AxisType.GeoX;

      if (vname.equalsIgnoreCase("lon") || vname.equalsIgnoreCase("longitude") || findAlias(ds, v).equalsIgnoreCase("lon"))
        return AxisType.Lon;

      if (vname.equalsIgnoreCase("y") || findAlias(ds, v).equalsIgnoreCase("y"))
        return AxisType.GeoY;

      if (vname.equalsIgnoreCase("lat") || vname.equalsIgnoreCase("latitude") || findAlias(ds, v).equalsIgnoreCase("lat"))
        return AxisType.Lat;

      if (vname.equalsIgnoreCase("lev") || findAlias(ds, v).equalsIgnoreCase("lev") ||
              (vname.equalsIgnoreCase("level") || findAlias(ds, v).equalsIgnoreCase("level")))
        return AxisType.GeoZ;

      if (vname.equalsIgnoreCase("z") || findAlias(ds, v).equalsIgnoreCase("z") ||
          vname.equalsIgnoreCase("altitude") || desc.contains("altitude") || vname.equalsIgnoreCase("depth") ||
          vname.equalsIgnoreCase("elev") || vname.equalsIgnoreCase("elevation")) {
        if (SimpleUnit.isCompatible("m", unit)) // units of meters
          return AxisType.Height;
    }

      if (vname.equalsIgnoreCase("time") || findAlias(ds, v).equalsIgnoreCase("time"))  {
        if (SimpleUnit.isDateUnit(unit))
          return AxisType.Time;
      }

      if (vname.equalsIgnoreCase("time") && v.getDataType() == DataType.STRING) {
        try {
          Array firstValue = v.read("0");
          // System.out.printf("%s%n", NCdumpW.printArray(firstValue, "firstValue", null));
          if (firstValue instanceof ArrayObject.D1) {
            ArrayObject.D1 sarry = (ArrayObject.D1) firstValue;
            String firstStringValue = (String) sarry.get(0);
            if (CalendarDate.parseISOformat(null, firstStringValue) != null) // valid iso date string
              return AxisType.Time;
          }
        } catch (IOException | InvalidRangeException e) {
          logger.warn("time string error", e);
        }
      }

      return null;
    }

    // look for an coord_axis or coord_alias attribute
    private String findAlias(NetcdfDataset ds, Variable v) {
      String alias = ds.findAttValueIgnoreCase(v, "coord_axis", null);
      if (alias == null)
        alias = ds.findAttValueIgnoreCase(v, "coord_alias", "");
      if (alias == null) alias = "";
      return alias;
    }

  // replicated from COARDS, but we need to diverge from COARDS
    // we assume that coordinate axes get identified by being coordinate variables
   private AxisType getAxisTypeCoards( NetcdfDataset ncDataset, VariableEnhanced v) {

     String unit = v.getUnitsString();
     if (unit == null)
       return null;

     if( unit.equalsIgnoreCase("degrees_east") ||
             unit.equalsIgnoreCase("degrees_E") ||
             unit.equalsIgnoreCase("degreesE") ||
             unit.equalsIgnoreCase("degree_east") ||
             unit.equalsIgnoreCase("degree_E") ||
             unit.equalsIgnoreCase("degreeE"))
       return AxisType.Lon;

     if ( unit.equalsIgnoreCase("degrees_north") ||
             unit.equalsIgnoreCase("degrees_N") ||
             unit.equalsIgnoreCase("degreesN") ||
             unit.equalsIgnoreCase("degree_north") ||
             unit.equalsIgnoreCase("degree_N") ||
             unit.equalsIgnoreCase("degreeN"))
       return AxisType.Lat;

     if (SimpleUnit.isDateUnit(unit)) // || SimpleUnit.isTimeUnit(unit)) removed dec 18, 2008
       return AxisType.Time;

     // look for other z coordinate
     //if (SimpleUnit.isCompatible("m", unit))
     //  return AxisType.Height;
     if (SimpleUnit.isCompatible("mbar", unit))
       return AxisType.Pressure;
     if (unit.equalsIgnoreCase("level") || unit.equalsIgnoreCase("layer") || unit.equalsIgnoreCase("sigma_level"))
       return AxisType.GeoZ;

     String positive = ncDataset.findAttValueIgnoreCase((Variable) v, "positive", null);
     if (positive != null) {
       if (SimpleUnit.isCompatible("m", unit))
         return AxisType.Height;
       else
         return AxisType.GeoZ;
     }
     return null;
   }

    private ProjectionCT makeProjectionCT(NetcdfDataset ds) {
      // look for projection in global attribute
      String projection = ds.findAttValueIgnoreCase(null, "projection", null);
      if (null == projection) {
        parseInfo.format("Default Conventions error: NO projection name found %n");
        return null;
      }
      String params = ds.findAttValueIgnoreCase(null, "projection_params", null);
      if (null == params) params = ds.findAttValueIgnoreCase(null, "proj_params", null);
      if (null == params) {
        parseInfo.format("Default Conventions error: NO projection parameters found %n");
        return null;
      }

      // parse the parameters
      int count = 0;
      double[] p = new double[4];
      try {
        // new way : just the parameters
        StringTokenizer stoke = new StringTokenizer(params, " ,");
        while (stoke.hasMoreTokens() && (count < 4)) {
          p[count++] = Double.parseDouble(stoke.nextToken());
        }
      } catch (NumberFormatException e) {
        // old way : every other one
        StringTokenizer stoke = new StringTokenizer(params, " ,");
        while (stoke.hasMoreTokens() && (count < 4)) {
          stoke.nextToken(); // skip
          p[count++] = Double.parseDouble(stoke.nextToken());
        }
      }

      parseInfo.format("Default Conventions projection %s params = %f %f %f %f%n", projection, p[0],p[1],p[2],p[3]);

      ProjectionImpl proj;
      if (projection.equalsIgnoreCase("LambertConformal"))
        proj = new LambertConformal(p[0], p[1], p[2], p[3]);
      else if (projection.equalsIgnoreCase("TransverseMercator"))
        proj = new TransverseMercator(p[0], p[1], p[2]);
      else if (projection.equalsIgnoreCase("Stereographic") || projection.equalsIgnoreCase("Oblique_Stereographic"))
        proj = new Stereographic(p[0], p[1], p[2]);
      else {
        parseInfo.format("Default Conventions error: Unknown projection %s%n",projection);
        return null;
      }

      return new ProjectionCT(proj.getClassName(), "FGDC", proj);
    }

}