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

import ucar.ma2.*;
import ucar.nc2.iosp.netcdf3.SPFactory;
import ucar.nc2.iosp.netcdf3.N3iosp;
import ucar.nc2.iosp.IOServiceProviderWriter;

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

/**
 * Create/Write netCDF-3 formatted files.

 * When a file is first created, it is in is "define mode", where the header objects (Dimensions, Attributes and Variables)
 *  may be added, deleted and modified, but no data may be written.
 * Once create() is called, you can no longer modify the header, but you can now write data.
 * An existing file is opened in write mode.

 *
 * If setRedefine(true) is called, the file goes into define mode, and header objects can be changed.
 * When setRedefine(false) is called, the new header is written, and the old file data is copied to the new file.
 * This can be quite costly.
 *
 * @author caron
 * @see NetcdfFile
 * @deprecated use NetcdfFileWriter
 */

public class NetcdfFileWriteable extends NetcdfFile {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NetcdfFileWriteable.class);
  static private Set<DataType> valid = EnumSet.of(DataType.BYTE, DataType.CHAR, DataType.SHORT, DataType.INT,
          DataType.DOUBLE, DataType.FLOAT);

  /**
   * Open an existing Netcdf file for writing data. Fill mode is true.
   * Cannot add new objects, you can only read/write data to existing Variables.
   *
   * @param location name of existing file to open.
   * @return existing file that can be written to
   * @throws java.io.IOException on I/O error
   */
  static public NetcdfFileWriteable openExisting(String location) throws IOException {
    return new NetcdfFileWriteable(location, true, true);
  }

  /**
   * Open an existing Netcdf file for writing data.
   * Cannot add new objects, you can only read/write data to existing Variables.
   * Setting fill = false is more efficient, use when you know you will write all data.
   *
   * @param location name of existing file to open.
   * @param fill     if true, the data is first written with fill values.
   * @return existing file that can be written to
   * @throws IOException on I/O error
   */
  static public NetcdfFileWriteable openExisting(String location, boolean fill) throws IOException {
    return new NetcdfFileWriteable(location, fill, true);
  }

  /**
   * Create a new Netcdf file, with fill mode true.
   *
   * @param location name of new file to open; if it exists, will overwrite it.
   * @return new file that can be written to
   * @throws IOException on I/O error
   */
  static public NetcdfFileWriteable createNew(String location) throws IOException {
    return new NetcdfFileWriteable(location, true, false);
  }

  /**
   * Create a new Netcdf file, put it into define mode. Make calls to addXXX(), then
   * when all objects are added, call create(). You cannot read or write data until create() is called.
   * Setting fill = false is more efficient, use when you know you will write all data.
   *
   * @param location name of new file to open; if it exists, will overwrite it.
   * @param fill     if true, the data is first written with fill values.
   *                 Leave false if you expect to write all data values, set to true if you want to be
   *                 sure that unwritten data values have the fill value in it. (default is false)
   * @return new file that can be written to
   * @throws IOException on I/O error
   */
  static public NetcdfFileWriteable createNew(String location, boolean fill) throws IOException {
    return new NetcdfFileWriteable(location, fill, false);
  }

  ////////////////////////////////////////////////////////////////////////////////
  private IOServiceProviderWriter spiw;
  private IOServiceProviderWriter cached_spiw = null; // Hold the IOSP for deferred use in create()

  // modes
  private boolean defineMode;

  // state
  private boolean isNewFile;
  private boolean isLargeFile;
  private boolean fill;
  private int extraHeader;
  private long preallocateSize;

  /**
   * Open or create a new Netcdf file, put it into define mode to allow writing.
   *
   * @param location open a new file at this location
   * @param fill set fill mode
   * @param isExisting true if file already exists
   * @throws IOException on I/O error
   */
  private NetcdfFileWriteable(String location, boolean fill, boolean isExisting) throws IOException {
    this(null, null, location, fill, isExisting);
  }

  /**
   * Open or create a new Netcdf file, put it into define mode to allow
   * writing, using the provided IOSP and RAF.
   *
   * @param iospw IO service provider to use, if null use standard
   * @param raf Random access file to use, may be null if iospw is
   * @param location open a new file at this location
   * @param fill set fill mode
   * @param isExisting true if file already exists
   * @throws IOException on I/O error
   */
  protected NetcdfFileWriteable(IOServiceProviderWriter iospw, ucar.unidata.io.RandomAccessFile raf,
          String location, boolean fill, boolean isExisting) throws IOException {
    super();
    this.location = location;
    this.fill = fill;

    if (isExisting) {
      if (iospw == null) {
        raf = new ucar.unidata.io.RandomAccessFile(location, "rw");
        spi = SPFactory.getServiceProvider();
        spiw = (IOServiceProviderWriter) spi;
      } else {
        spiw = iospw;
        spi = spiw;
      }
      spiw.open(raf, this, null);
      spiw.setFill( fill);

    } else {
      cached_spiw = iospw; // save for use later in create()
      defineMode = true;
      isNewFile = true;
    }
  }

  /**
   * Set the fill flag: call before calling create() or doing any data writing.
   * If true, the data is first written with fill values.
   * Default is fill = false.
   * Leave false if you expect to write all data values, set to true if you want to be
   * sure that unwritten data values have the fill value in it.
   *
   * @param fill set fill mode true or false
   */
  public void setFill(boolean fill) {
    this.fill = fill;
    if (spiw != null)
      spiw.setFill( fill);
  }

  /**
   * Preallocate the file size, for efficiency.
   * Must be in define mode
   * Must call before create() to have any affect.
   * @param size if set to > 0, set length of file to this upon creation - this (usually) pre-allocates contiguous storage.
   */
  public void setLength(long size) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    this.preallocateSize = size;
  }

  /**
   * Set if this should be a "large file" (64-bit offset) format.
   * Must be in define mode
   * @param isLargeFile true if large file
   */
  public void setLargeFile(boolean isLargeFile) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    this.isLargeFile = isLargeFile;
  }

  /**
   * Set extra bytes to reserve in the header.
   * This can prevent rewriting the entire file on redefine.
   * Must be in define mode
   * @param extraHeaderBytes # bytes extra for the header
   */
  public void setExtraHeaderBytes(int extraHeaderBytes) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    this.extraHeader = extraHeaderBytes;
  }

  /**
   * Is the file in define mode, which allows objects to be added and changed?
   * @return true if the file in define mode
   */
  public boolean isDefineMode() { return defineMode; }

  ////////////////////////////////////////////
  //// use these calls in define mode

  /**
   * Add a Dimension to the file. Must be in define mode.
   *
   * @param dimName name of dimension
   * @param length  size of dimension.
   * @return the created dimension
   */
  public Dimension addDimension(String dimName, int length) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");
    if (length <= 0)
      throw new IllegalArgumentException("dimension length must be > 0 :"+length);
    if (!N3iosp.isValidNetcdfObjectName(dimName))
      throw new IllegalArgumentException("illegal netCDF-3 dimension name: "+dimName);

    Dimension dim = new Dimension(dimName, length, true, false, false);
    super.addDimension(null, dim);
    return dim;
  }

  /**
   * Add a Dimension to the file. Must be in define mode.
   *
   * @param dimName          name of dimension
   * @param length           size of dimension.
   * @param isShared         if dimension is shared
   * @param isUnlimited      if dimension is unlimited
   * @param isVariableLength if dimension is variable length
   * @return the created dimension
   */
  public Dimension addDimension(String dimName, int length, boolean isShared, boolean isUnlimited, boolean isVariableLength) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");
    if (!N3iosp.isValidNetcdfObjectName(dimName))
      throw new IllegalArgumentException("illegal netCDF-3 dimension name "+dimName);

    Dimension dim = new Dimension(dimName, length, isShared, isUnlimited, isVariableLength);
    super.addDimension(null, dim);
    return dim;
  }

  /**
   * Add an unlimited Dimension to the file. Must be in define mode.
   *
   * @param dimName name of unlimited dimension
   * @return the created dimension
   */
  public Dimension addUnlimitedDimension(String dimName) {
    return addDimension(dimName, 0, true, true, false);
  }

  /**
   * Rename a Dimension. Must be in define mode.
   * @param oldName existing dimension has this name
   * @param newName rename to this
   * @return renamed dimension, or null if not found
   */
  public Dimension renameDimension(String oldName, String newName) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");

    Dimension dim = findDimension(oldName);
    if (null != dim) dim.setName(newName);
    return dim;
  }

  /**
   * Add a Global attribute to the file. Must be in define mode.
   * @param att the attribute.
   * @return the created attribute
   */
  public Attribute addGlobalAttribute(Attribute att) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");

    if (!N3iosp.isValidNetcdfObjectName(att.getShortName())) {
      String attName = N3iosp.makeValidNetcdfObjectName(att.getShortName());
      log.warn("illegal netCDF-3 attribute name= "+att.getShortName() + " change to "+ attName);
      att = new Attribute(attName, att.getValues());
    }

    return super.addAttribute(null, att);
  }

  /**
   * Add a Global attribute of type String to the file. Must be in define mode.
   *
   * @param name  name of attribute.
   * @param value value of atribute.
   * @return the created attribute
   */
  public Attribute addGlobalAttribute(String name, String value) {
    return addGlobalAttribute( new Attribute(name, value));
  }

  /**
   * Add a Global attribute of type Number to the file. Must be in define mode.
   *
   * @param name  name of attribute.
   * @param value must be of type Float, Double, Integer, Short or Byte
   * @return the created attribute
   */
  public Attribute addGlobalAttribute(String name, Number value) {
    return addGlobalAttribute( new Attribute(name, value));
  }

  /**
   * Add a Global attribute of type Array to the file. Must be in define mode.
   *
   * @param name   name of attribute.
   * @param values Array of values
   * @return the created attribute
   */
  public Attribute addGlobalAttribute(String name, Array values) {
    return addGlobalAttribute( new Attribute(name, values));
  }

  /**
   * Delete a global Attribute. Must be in define mode.
   * @param attName existing Attribute has this name
   * @return deleted Attribute, or null if not found
   */
  public Attribute deleteGlobalAttribute(String attName) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    Attribute att = findGlobalAttribute(attName);
    if (null == att) return null;

    rootGroup.remove(att);
    return att;
  }

  /**
   * Rename a global Attribute. Must be in define mode.
   * @param oldName existing Attribute has this name
   * @param newName rename to this
   * @return renamed Attribute, or null if not found
   */
  public Attribute renameGlobalAttribute(String oldName, String newName) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    Attribute att = findGlobalAttribute(oldName);
    if (null == att) return null;

    rootGroup.remove(att);
    att = new Attribute( newName, att.getValues());
    rootGroup.addAttribute( att);
    return att;
  }

  /**
   * Add a variable to the file. Must be in define mode.
   *
   * @param varName  name of Variable, must be unique with the file.
   * @param dataType type of underlying element
   * @param dims     array of Dimensions for the variable, must already have been added. Use an array of length 0
   *                 for a scalar variable.
   * @return the Variable that has been added
   */
  public Variable addVariable(String varName, DataType dataType, Dimension[] dims) {
    ArrayList<Dimension> list = new ArrayList<Dimension>();
    list.addAll(Arrays.asList(dims));
    return addVariable(varName, dataType, list);
  }

  /**
   * Add a variable to the file. Must be in define mode.
   *
   * @param varName  name of Variable, must be unique with the file.
   * @param dataType type of underlying element
   * @param dims     names of Dimensions for the variable, blank seperated.
   *                 Must already have been added. Use an empty string for a scalar variable.
   * @return the Variable that has been added
   */
  public Variable addVariable(String varName, DataType dataType, String dims) {
    // parse the list
    ArrayList<Dimension> list = new ArrayList<Dimension>();
    StringTokenizer stoker = new StringTokenizer(dims);
    while (stoker.hasMoreTokens()) {
      String tok = stoker.nextToken();
      Dimension d = rootGroup.findDimension(tok);
      if (null == d)
        throw new IllegalArgumentException("Cant find dimension " + tok);
      list.add(d);
    }

    return addVariable(varName, dataType, list);
  }

  /**
   * Add a variable to the file. Must be in define mode.
   *
   * @param shortName  name of Variable, must be unique with the file.
   * @param dataType type of underlying element
   * @param dims     list of Dimensions for the variable, must already have been added. Use a list of length 0
   *                 for a scalar variable.
   * @return the Variable that has been added
   */
  public Variable addVariable(String shortName, DataType dataType, List<Dimension> dims) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");
    if (!N3iosp.isValidNetcdfObjectName(shortName))
      throw new IllegalArgumentException("illegal netCDF-3 variable name: "+shortName);
    if (!valid.contains(dataType))
      throw new IllegalArgumentException("illegal dataType for netcdf-3 format: "+dataType);

    // check unlimited
    int count = 0;
    for (Dimension d : dims) {
      if (d.isUnlimited())
        if (count != 0)
          throw new IllegalArgumentException("Unlimited dimension "+d+" must be first instead its  ="+count);
      count++;
    }

    Variable v = new Variable(this, rootGroup, null, shortName);
    v.setDataType(dataType);
    v.setDimensions(dims);

    long size = v.getSize() * v.getElementSize();
    if (size > N3iosp.MAX_VARSIZE)
      throw new IllegalArgumentException("Variable size in bytes "+size+" may not exceed "+ N3iosp.MAX_VARSIZE);

    super.addVariable(null, v);
    return v;
  }

  /**
   * Add a variable with DataType = String to the file. Must be in define mode.
   * The variable will be stored in the file as a CHAR variable.
   * A new dimension with name "varName_strlen" is automatically added, with length max_strlen.
   *
   * @param varName    name of Variable, must be unique within the file.
   * @param dims       list of Dimensions for the variable, must already have been added. Use a list of length 0
   *                   for a scalar variable. Do not include the string length dimension.
   * @param max_strlen maximum string length.
   * @return the Variable that has been added
   */
  public Variable addStringVariable(String varName, List<Dimension> dims, int max_strlen) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");
    if (!N3iosp.isValidNetcdfObjectName(varName))
      throw new IllegalArgumentException("illegal netCDF-3 variable name: "+varName);

    Variable v = new Variable(this, rootGroup, null, varName);
    v.setDataType(DataType.CHAR);

    Dimension d = addDimension(varName + "_strlen", max_strlen);
    ArrayList<Dimension> sdims = new ArrayList<Dimension>(dims);
    sdims.add(d);
    v.setDimensions(sdims);

    super.addVariable(null, v);
    return v;
  }

  /**
   * Rename a Variable. Must be in define mode.
   * @param oldName existing Variable has this name
   * @param newName rename to this
   * @return renamed Variable, or null if not found
   */
  public Variable renameVariable(String oldName, String newName) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    Variable v = findVariable(oldName);
    if (null != v) v.setName(newName);
    return v;
  }

  /**
   * Add an attribute to the named Variable. Must be in define mode.
   *
   * @param varName name of variable. must already have been added to the file.
   * @param att     Attribute to add.
   */
  public void addVariableAttribute(String varName, Attribute att) {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");

    if (!N3iosp.isValidNetcdfObjectName(att.getShortName())) {
      String attName = N3iosp.makeValidNetcdfObjectName(att.getShortName());
      log.warn("illegal netCDF-3 attribute name= "+att.getShortName() + " change to "+ attName);
      att = new Attribute(attName, att.getValues());
    }

    Variable v = rootGroup.findVariable(varName);
    if (null == v)
      throw new IllegalArgumentException("addVariableAttribute variable name not found = <" + varName + ">");
    v.addAttribute(att);
  }

  /**
   * Add an attribute of type String to the named Variable. Must be in define mode.
   *
   * @param varName name of variable. must already have been added to the file.
   * @param attName name of attribute.
   * @param value   String value of atribute.
   */
  public void addVariableAttribute(String varName, String attName, String value) {
    addVariableAttribute(varName, new Attribute(attName, value));
  }

  /**
   * Add an attribute of type Number to the named Variable. Must be in define mode.
   *
   * @param varName name of variable. IllegalArgumentException if not valid name.
   * @param attName name of attribute.
   * @param value   must be of type Float, Double, Integer, Short or Byte
   */
  public void addVariableAttribute(String varName, String attName, Number value) {
    addVariableAttribute(varName, new Attribute(attName, value));
  }

  /**
   * Add an attribute of type Array to the named Variable. Must be in define mode.
   *
   * @param varName name of variable. IllegalArgumentException if not valid name.
   * @param attName name of attribute.
   * @param value   Array of valkues
   */
  public void addVariableAttribute(String varName, String attName, Array value) {
    Attribute att = new Attribute(attName, value);
    addVariableAttribute(varName, att);
  }

  /**
   * Delete a variable Attribute. Must be in define mode.
   * @param varName existing Variable name
   * @param attName existing Attribute has this name
   * @return deleted Attribute, or null if not found
   */
  public Attribute deleteVariableAttribute(String varName, String attName) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    Variable v = findVariable(varName);
    if (v == null) return null;

    Attribute att = v.findAttribute(attName);
    if (null == att) return null;

    v.remove(att);
    return att;
  }

  /**
   * Rename a variable Attribute. Must be in define mode.
   * @param varName existing Variable name
   * @param attName existing Attribute has this name
   * @param newName rename to this
   * @return renamed Attribute, or null if not found
   */
  public Attribute renameVariableAttribute(String varName, String attName, String newName) {
    if (!defineMode) throw new UnsupportedOperationException("not in define mode");
    Variable v = findVariable(varName);
    if (v == null) return null;

    Attribute att = v.findAttribute(attName);
    if (null == att) return null;

    v.remove(att);
    att = new Attribute( newName, att.getValues());
    v.addAttribute( att);
    return att;
  }

  /**
   * 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 global attribute
   * @param att replace with this value
   * @throws IOException if I/O error
   */
  public void updateAttribute(ucar.nc2.Variable v2, Attribute att) throws IOException {
    if (defineMode)
      throw new UnsupportedOperationException("in define mode");
    spiw.updateAttribute(v2, att);
  }

  /**
   * After you have added all of the Dimensions, Variables, and Attributes,
   *   call create() to actually create the file. You must be in define mode.
   * After this call, you are no longer in define mode.
   * @throws java.io.IOException if I/O error
   */
  public void create() throws java.io.IOException {
    if (!defineMode)
      throw new UnsupportedOperationException("not in define mode");

    if (cached_spiw == null) {
      spi = SPFactory.getServiceProvider();
      spiw = (IOServiceProviderWriter) spi;
    } else {
      spiw = cached_spiw;
      spi = spiw;
    }
    spiw.setFill( fill);
    spiw.create(location, this, extraHeader, preallocateSize, isLargeFile);

    defineMode = false;
  }

  ////////////////////////////////////////////
  // redefine

  /**
   * Set the redefine mode.
   * Designed to emulate nc_redef (redefineMode = true) and
   * nc_enddef (redefineMode = false)
   * @param redefineMode start or end define mode
   * @return true if it had to rewrite the entire file, false if it wrote the header in place
   * @throws java.io.IOException on read/write error
   */
  public boolean setRedefineMode(boolean redefineMode) throws IOException {
    if (redefineMode && !defineMode) {
      defineMode = true;

    } else if (!redefineMode && defineMode) {  // we were in defineMode, and now we are ending.
      defineMode = false;
      finish();

      // try to rewrite header, if it fails, then we have to rewrite entire file
      boolean ok = spiw.rewriteHeader(isLargeFile);  // LOOK seems like we should be using isNewFile
      if (!ok)
        rewrite();
      return !ok;
    }

    return false;
  }

  // rewrite entire file
  private void rewrite() throws IOException {
    // close existing file, rename and open as read-only
    spiw.flush();
    spiw.close();

    File prevFile = new File(location);
    File tmpFile = new File(location+".tmp");
    if (tmpFile.exists()) tmpFile.delete();
    if (!prevFile.renameTo(tmpFile)) {
      System.out.printf("%50s prevFile.exists=%s canRead=%s canWrite=%s%n", prevFile.getPath(), prevFile.exists(), prevFile.canRead(), prevFile.canWrite());
      System.out.printf("%50s  tmpFile.exists=%s canRead=%s canWrite=%s%n", tmpFile.getPath(), tmpFile.exists(), tmpFile.canRead(), tmpFile.canWrite());
      throw new RuntimeException("Cant rename "+prevFile.getAbsolutePath()+" to "+ tmpFile.getAbsolutePath());
    }

    NetcdfFile oldFile = NetcdfFile.open(tmpFile.getPath());

    // use record dimension if it has one
    Structure recordVar = null;
    if (oldFile.hasUnlimitedDimension()) {
      oldFile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
      recordVar = (Structure) oldFile.findVariable("record");
      /* if (recordVar != null) {
        Boolean result = (Boolean) spiw.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
        if (!result)
          recordVar = null;
      } */
    }

    // create new file with current set of objects
    spiw.create(location, this, extraHeader, preallocateSize, isLargeFile);
    spiw.setFill( fill);
    //isClosed = false;

    // wait till header is written before adding the record variable to the file
    if (recordVar != null) {
      Boolean result = (Boolean) spiw.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
      if (!result)
        recordVar = null;
    }

    // copy old file to new
    List<Variable> oldList = new ArrayList<Variable>(getVariables().size());
    for (Variable v : getVariables()) {
      Variable oldVar = oldFile.findVariable(v.getFullNameEscaped());
      if (oldVar != null)
        oldList.add(oldVar);
    }
    FileWriter.copyVarData(this, oldList, recordVar, null);
    flush();

    // delete old
    oldFile.close();
    if (!tmpFile.delete())
      throw new RuntimeException("Cant delete "+location);
  }

  ////////////////////////////////////////////
  //// use these calls to write data to the file

  /**
   * Write data to the named variable, origin assumed to be 0. Must not be in define mode.
   *
   * @param fullNameEsc name of variable. IllegalArgumentException if variable name does not exist.
   * @param values  write this array; must be same type and rank as Variable
   * @throws IOException if I/O error
   * @throws ucar.ma2.InvalidRangeException if values Array has illegal shape
   */
  public void write(String fullNameEsc, Array values) throws java.io.IOException, InvalidRangeException {
    write(fullNameEsc, new int[values.getRank()], values);
  }

  /**
   * Write data to the named variable. Must not be in define mode.
   *
   * @param fullNameEsc full, escaped name of variable. IllegalArgumentException if variable name does not exist.
   * @param origin  offset within the variable to start writing.
   * @param values  write this array; must be same type and rank as Variable
   * @throws IOException if I/O error
   * @throws ucar.ma2.InvalidRangeException if values Array has illegal shape
   */
  public void write(String fullNameEsc, int[] origin, Array values) throws java.io.IOException, InvalidRangeException {
    if (defineMode)
      throw new UnsupportedOperationException("in define mode");
    ucar.nc2.Variable v2 = findVariable(fullNameEsc);
    if (v2 == null)
      throw new IllegalArgumentException("NetcdfFileWriteable.write illegal variable name = " + fullNameEsc);

    spiw.writeData(v2, new Section(origin, values.getShape()), values);
    v2.invalidateCache();
  }

  /**
   * Write String data to a CHAR variable, origin assumed to be 0. Must not be in define mode.
   *
   * @param varName name of variable, must be of type CHAR.
   * @param values  write this array; must be ArrayObject of String
   * @throws IOException if I/O error
   * @throws ucar.ma2.InvalidRangeException if values Array has illegal shape
   */
  public void writeStringData(String varName, Array values) throws java.io.IOException, InvalidRangeException {
    writeStringData(varName, new int[values.getRank()], values);
  }

  /**
   * Write String data to a CHAR variable. Must not be in define mode.
   *
   * @param fullNameEsc name of variable, must be of type CHAR.
   * @param origin  offset to start writing, ignore the strlen dimension.
   * @param values  write this array; must be ArrayObject of String
   * @throws IOException if I/O error
   * @throws ucar.ma2.InvalidRangeException if values Array has illegal shape
   */
  public void writeStringData(String fullNameEsc, int[] origin, Array values) throws java.io.IOException, InvalidRangeException {

    if (values.getElementType() != String.class)
      throw new IllegalArgumentException("Must be ArrayObject of String ");

    ucar.nc2.Variable v2 = findVariable(fullNameEsc);
    if (v2 == null)
      throw new IllegalArgumentException("illegal variable name = " + fullNameEsc);

    if (v2.getDataType() != DataType.CHAR)
      throw new IllegalArgumentException("variable " + fullNameEsc + " is not type CHAR");
    int rank = v2.getRank();
    int strlen = v2.getShape(rank-1);

    // turn it into an ArrayChar
    ArrayChar cvalues = ArrayChar.makeFromStringArray((ArrayObject) values, strlen);

    int[] corigin = new int[rank];
    System.arraycopy(origin, 0, corigin, 0, rank - 1);

    write(fullNameEsc, corigin, cvalues);
  }

  /**
   * Flush anything written to disk.
   * @throws IOException if I/O error
   */
  public void flush() throws java.io.IOException {
    spiw.flush();
  }

  /**
   * close the file.
   * @throws IOException if I/O error
   */
  @Override
  public synchronized void close() throws java.io.IOException {
    if (spiw != null) {
      flush();
      spiw.close();
      spiw = null;
    }
    spi = null;
  }

  public String getFileTypeId() {
    return "netCDF";
  }

  public String getFileTypeDescription() {
    return "netCDF classic format - writer";
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // deprecated

  /**
   * Create a new Netcdf file, put it into define mode.
   *
   * @param location open a new file at this location
   * @param fill set fill mode
   * @deprecated use createNew(String filename, boolean fill)
   */
  public NetcdfFileWriteable(String location, boolean fill) {
    super();
    this.location = location;
    this.fill = fill;
    defineMode = true;
  }

  /**
   * Open a new Netcdf file, put it into define mode.
   *
   * @deprecated use createNew(String filename, boolean fill)
   */
  public NetcdfFileWriteable() {
    super();
    defineMode = true;
  }

  /**
   * Open an existing Netcdf file for writing data.
   *
   * @param location open an existing file at this location
   * @deprecated use openExisting(String filename, boolean fill)
   * @throws java.io.IOException on read error
   */
  public NetcdfFileWriteable(String location) throws IOException {
    super();
    this.location = location;
    ucar.unidata.io.RandomAccessFile raf = new ucar.unidata.io.RandomAccessFile(location, "rw");
    spi = SPFactory.getServiceProvider();
    spiw = (IOServiceProviderWriter) spi;
    spiw.open(raf, this, null);
  }

  /**
   * Set the filename of a new file to be created: call before calling create().
   * @param filename name of new file to create.
   * @deprecated use NetcdfFileWriteable.createNew(String filename);
   */
  public void setName(String filename) {
    this.location = filename;
  }

  /**
   * Add a variable to the file. Must be in define mode.
   *
   * @param varName       name of Variable, must be unique with the file.
   * @param componentType type of underlying element: String, double or Double, etc.
   * @param dims          array of Dimensions for the variable, must already have been added.
   * @deprecated use addVariable(String varName, DataType dataType, ArrayList dims);
   * @return the Varible added
   */
  public Variable addVariable(String varName, Class componentType, Dimension[] dims) {
    List<Dimension> list = new ArrayList<Dimension>();
    list.addAll(Arrays.asList(dims));
    return addVariable(varName, DataType.getType(componentType, false), list);   // LOOK unsigned
  }

}


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

package ucar.nc2.iosp.gempak;

import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.util.Format;
import ucar.unidata.util.StringUtil2;

import java.io.*;

import java.util.List;

/**
 * Read a Gempak surface file
 */
public class GempakSurfaceFileReader extends AbstractGempakStationFileReader {

  /**
   * Surface Text identifier
   */
  public static final String SFTX = "SFTX";

  /**
   * Surface Data identifier
   */
  public static final String SFDT = "SFDT";

  /**
   * Surface Data identifier
   */
  public static final String SFSP = "SFSP";

  /**
   * standard surface file id
   */
  public static final String STANDARD = "standard";

  /**
   * climate surface file id
   */
  public static final String CLIMATE = "climate";

  /**
   * ship surface file id
   */
  public static final String SHIP = "ship";

  /**
   * Default ctor
   */
  GempakSurfaceFileReader() {
  }

  /**
   * Initialize the file, read in all the metadata (ala DM_OPEN)
   *
   * @param raf       RandomAccessFile to read.
   * @param fullCheck if true, check entire structure
   * @return A GempakSurfaceFileReader
   * @throws IOException problem reading file
   */
  public static GempakSurfaceFileReader getInstance(RandomAccessFile raf,
                                                    boolean fullCheck)
          throws IOException {
    GempakSurfaceFileReader gsfr = new GempakSurfaceFileReader();
    gsfr.init(raf, fullCheck);
    return gsfr;
  }

  /**
   * Initialize this reader.  Read all the metadata
   *
   * @return true if successful
   * @throws IOException problem reading the data
   */
  protected boolean init() throws IOException {
    return init(true);
  }

  /**
   * Initialize this reader.  Get the Grid specific info
   *
   * @param fullCheck check to make sure there are grids we can handle
   * @return true if successful
   * @throws IOException problem reading the data
   */
  protected boolean init(boolean fullCheck) throws IOException {

    if (!super.init(fullCheck)) {
      return false;
    }

    // Modeled after SF_OFIL
    if (dmLabel.kftype != MFSF) {
      logError("not a surface data file ");
      return false;
    }

    int numParams = 0;
    String partType = ((dmLabel.kfsrce == 100) && (dmLabel.kprt == 1))
            ? SFTX
            : SFDT;

    DMPart part = getPart(partType);

    if (part == null) {
      logError("No part named " + partType + " found");
      return false;
    } else {
      numParams = part.kparms;
    }

    if (!readStationsAndTimes(true)) {
      logError("Unable to read stations and times");
      return false;
    }
    // since the reads are ob by ob, set buffer size small
    if (subType.equals(STANDARD)) rf.setBufferSize(256);
    return true;

  }

  /**
   * Make the list of dates.  Override superclass to make the
   * value based on the subtype
   *
   * @param uniqueTimes true to make a unique list
   * @return the list of times
   */
  protected List<String> makeDateList(boolean uniqueTimes) {
    return super.makeDateList(!getFileSubType().equals(SHIP));
  }

  /**
   * Set the file subType.
   */
  protected void makeFileSubType() {
    // determine file type
    Key key = findKey(GempakStation.SLAT);
    if (key == null)
      throw new IllegalStateException("File does not have key="+GempakStation.SLAT);
    String latType = key.type;
    Key dateKey = findKey(DATE);
    if (dateKey != null && !dateKey.type.equals(latType)) {
      if (latType.equals(ROW)) {
        subType = CLIMATE;
      } else {
        subType = STANDARD;
      }
    } else {
      subType = SHIP;
    }
  }

  /**
   * Print the list of dates in the file
   *
   * @param row ob row
   * @param col ob column
   */
  public void printOb(int row, int col) {
    int stnIndex = (getFileSubType().equals(CLIMATE))
            ? row
            : col;
    List<GempakStation> stations = getStations();
    if (stations.isEmpty() || stnIndex > stations.size()) {
      System.out.println("\nNo data available");
      return;
    }
    GempakStation station = getStations().get(stnIndex - 1);
    StringBuilder builder = new StringBuilder();
    builder.append("\nStation:\n");
    builder.append(station.toString());
    builder.append("\nObs\n\t");
    List<GempakParameter> params = getParameters(SFDT);
    for (GempakParameter parm : params) {
      builder.append(StringUtil2.padLeft(parm.getName(), 7));
      builder.append("\t");
    }
    builder.append("\n");
    RData rd;
    try {
      rd = DM_RDTR(row, col, SFDT);
    } catch (IOException ioe) {
      ioe.printStackTrace();
      rd = null;
    }
    if (rd == null) {
      builder.append("No Data Available");
    } else {
      builder.append("\t");
      float[] data = rd.data;
      for (int i = 0; i < data.length; i++) {
        builder.append( StringUtil2.padLeft( Format.formatDouble(data[i], 7, 1), 7));
        builder.append("\t");
      }
      int[] header = rd.header;
      if (header.length > 0) {
        builder.append("\nOb Time = ");
        builder.append(header[0]);
      }
    }
    System.out.println(builder.toString());
  }

  /**
   * Get the type for this file
   *
   * @return file type (CLIMATE, STANDARD, SHIP)
   */
  public String getSurfaceFileType() {
    return getFileSubType();
  }

  /**
   * Run the program
   *
   * @param args [0] filename (required),
   *             [1] variable name (X for default),
   *             [2] X to not list grids
   * @throws IOException problem reading the file
   */
  public static void main(String[] args) throws IOException {
    if (args.length == 0) {
      System.out.println("need to supply a GEMPAK surface file name");
      System.exit(1);
    }
    try {
      GempakParameters.addParameters(
              "resources/nj22/tables/gempak/params.tbl");
    } catch (Exception e) {
      System.out.println("unable to init param tables");
    }

    GempakSurfaceFileReader gsfr = getInstance(getFile(args[0]), true);
    System.out.println("Type = " + gsfr.getSurfaceFileType());
    gsfr.printFileLabel();
    gsfr.printKeys();
    gsfr.printHeaders();
    gsfr.printParts();
    //gsfr.printDates();
    //gsfr.printStations(false);
    int row = 1;
    int col = 1;
    if (args.length > 1) {
      row = Integer.parseInt(args[1]);
    }
    if (args.length > 2) {
      col = Integer.parseInt(args[2]);
    }
    gsfr.printOb(row, col);
  }

}


package thredds.server.wfs;

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

import ucar.nc2.ft2.simpgeometry.SimpleGeometry;

/**
 * A writer for a WFS compliant Feature Collection GML file.
 * Answers to GetFeature requests.
 *
 * @author wchen@usgs.gov
 *
 */
public class WFSGetFeatureWriter {

	private PrintWriter response;
	private String fileOutput;
	private final String namespace;
	private final String server;
	private final String ftName;
	private List<SimpleGeometry> geometries;

	/**
	 * Writes headers and bounding box
	 */
	private void writeHeadersAndBB() {
		fileOutput += "<wfs:FeatureCollection xsi:schemaLocation=" + WFSXMLHelper.encQuotes("http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd " + namespace + " " + server
					+ "?request=DescribeFeatureType" + WFSXMLHelper.AMPERSAND + "service=wfs" + WFSXMLHelper.AMPERSAND + "version=2.0.0" + WFSXMLHelper.AMPERSAND + "typename="
					+ WFSController.TDSNAMESPACE + "%3A" + ftName)
				+ " xmlns:xsi=" + WFSXMLHelper.encQuotes("http://www.w3.org/2001/XMLSchema-instance")
				+ " xmlns:xlink=" + WFSXMLHelper.encQuotes("http://www.w3.org/1999/xlink")
				+ " xmlns:gml=" + WFSXMLHelper.encQuotes("http://opengis.net/gml/3.2")
				+ " xmlns:fes=" + WFSXMLHelper.encQuotes("http://www.opengis.net/fes/2.0")
				+ " xmlns:ogc=" + WFSXMLHelper.encQuotes("http://www.opengis.net/ogc")
				+ " xmlns:wfs=" + WFSXMLHelper.encQuotes("http://opengis.net/wfs/2.0")
				+ " xmlns:" + WFSController.TDSNAMESPACE +"=" + WFSXMLHelper.encQuotes(namespace)
				+ " xmlns=" + WFSXMLHelper.encQuotes("http://www.opengis.net/wfs/2.0")
				+ " version=\"2.0.0\" numberMatched=" + WFSXMLHelper.encQuotes(String.valueOf(geometries.size())) + " numberReturned="
				+ WFSXMLHelper.encQuotes(String.valueOf(geometries.size())) + ">";

			double[] boundLower;
			double[] boundUpper;
			if(geometries.isEmpty()) {
				boundLower = new double[2]; boundUpper = new double[2];
				boundLower[0] = -180;  boundLower[1] = -90;
				boundUpper[0] = 180; boundUpper[1] = 90;
			}

			else {
				boundLower = geometries.get(0).getBBLower();
				boundUpper = geometries.get(0).getBBUpper();
			}

		   // WFS Bounding Box
			for(SimpleGeometry item: geometries) {

				// Find the overall BB

				// Test Lower
				double[] low = item.getBBLower();
				if(boundLower[0] > low[0]) boundLower[0] = low[0];
				if(boundLower[1] > low[1]) boundLower[1] = low[1];

				// Test Upper
				double[] upper = item.getBBUpper();
				if(boundUpper[0] < upper[0]) boundUpper[0] = upper[0];
				if(boundUpper[1] < upper[1]) boundUpper[1] = upper[1];

				// Add some padding
				boundLower[0] -= 10;  boundLower[1] -= 10;
				boundUpper[0] += 10; boundUpper[1] += 10;
			}

		fileOutput	+= "<wfs:boundedBy>"
			+ "<wfs:Envelope srsName=" + "\"urn:ogc:def:crs:EPSG::4326\"" + ">"
					+ "<wfs:lowerCorner>" + boundLower[0] + " " + boundLower[1] + "</wfs:lowerCorner>"
					+ "<wfs:upperCorner>" + boundUpper[0] + " " + boundUpper[1] + "</wfs:upperCorner>"
			+ "</wfs:Envelope>"
			+ "</wfs:boundedBy>";
	}

	/**
	 * Initiate the response with an XML file with an XML header and the FeatureCollection tag. Write bounding box and namespace information.
	 */
	public void startXML() {
		fileOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
		writeHeadersAndBB();
	}

	/**
	 * In the WFS specification for GetFeature each feature type is its own
	 * member and so writeMembers add each member to the fileOutput
	 */
	public void writeMembers() {
		int index = 1;
		GMLFeatureWriter writer = new GMLFeatureWriter();
		for(SimpleGeometry geometryItem: geometries) {

			// Find bounding box information
			double[] lowerCorner = geometryItem.getBBLower();
			double[] upperCorner = geometryItem.getBBUpper();

			fileOutput
				   += "<wfs:member>"

					// Write Geometry Information
					+ "<" + WFSController.TDSNAMESPACE + ":" + ftName + " gml:id=\"" + ftName + "." + index + "\">"

					// GML Bounding Box
					+ "<gml:boundedBy>"
					+ "<gml:Envelope srsName=" + "\"urn:ogc:def:crs:EPSG::4326\"" + ">"
							+ "<gml:lowerCorner>" +  lowerCorner[0] + " " + lowerCorner[1] + "</gml:lowerCorner>"
							+ "<gml:upperCorner>" +  upperCorner[0] + " " + upperCorner[1] + "</gml:upperCorner>"
					+ "</gml:Envelope>"
					+ "</gml:boundedBy>"

					+ "<" + WFSController.TDSNAMESPACE + ":geometryInformation>";

			//write GML features
			fileOutput += writer.writeFeature(geometryItem);

			// Cap off headers
			fileOutput
					+="</" + WFSController.TDSNAMESPACE + ":geometryInformation>"
					+ "</" + WFSController.TDSNAMESPACE + ":" + ftName +">"
					+ "</wfs:member>";

			index++;
		}
	}

	/**
	 * Finish writing the XML file, write the end tag for FeatureCollection and append it all to the PrintWriter.
	 *
	 * Once a XML is finished, the WFSDataWriter is no longer usable.
	 */
	public void finishXML() {
		fileOutput += "</wfs:FeatureCollection>";
		response.append(fileOutput);
		fileOutput = null; response = null;
	}

	/**
	 * Opens a WFSGetFeatureWriter, writes to the response given.
	 *
	 * @param response to write to
	 * @param server WFS Server URI
	 * @param namespace WFS TDS Namespace URI
	 * @throws IOException
	 */
	public WFSGetFeatureWriter(PrintWriter response, String server, String namespace, List<SimpleGeometry> geometries, String ftName) {
		this.fileOutput = "";
		this.response = response;
		this.server = server;
		this.namespace = namespace;
		this.geometries = geometries;
		this.ftName = ftName;
	}
}


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

package ucar.nc2.dt.point;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateTransform;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.StationObsDataset;
import ucar.nc2.dt.TypedDatasetFactory;
import ucar.nc2.ncml.NcMLWriter;
import ucar.nc2.units.DateFormatter;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.util.Parameter;

/**
 * A helper class to StationObsDataset; creates XML documents.
 *
 * @deprecated use ucar.nc2.ft.point
 * @author caron
 */
public class StationObsDatasetInfo {
  private StationObsDataset sobs;
  private String path;

  public StationObsDatasetInfo(StationObsDataset sobs, String path) {
    this.sobs = sobs;
    this.path = path;
  }

  /**
   * Write stationObsDataset XML document
   */
  public String writeStationObsDatasetXML() {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    return fmt.outputString(makeStationObsDatasetDocument());
  }

  public void writeStationObsDatasetXML(OutputStream os) throws IOException {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    fmt.output(makeStationObsDatasetDocument(), os);
  }

  /**
   * Write stationCollection XML document
   */
  public String writeStationCollectionXML() throws IOException {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    return fmt.outputString(makeStationCollectionDocument());
  }

  public void writeStationCollectionXML(OutputStream os) throws IOException {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    fmt.output(makeStationCollectionDocument(), os);
  }

  /**
   * Create an XML document from this info
   */
  public Document makeStationCollectionDocument() throws IOException {
    Element rootElem = new Element("stationCollection");
    Document doc = new Document(rootElem);

    List stns = sobs.getStations();
    System.out.println("nstns = "+stns.size());
    for (int i = 0; i < stns.size(); i++) {
      ucar.unidata.geoloc.Station s = (ucar.unidata.geoloc.Station) stns.get(i);
      Element sElem = new Element("station");
      sElem.setAttribute("name",s.getName());
      if (s.getWmoId() != null)
        sElem.setAttribute("wmo_id",s.getWmoId());
      if (s.getDescription() != null)
        sElem.addContent(new Element("description").addContent(s.getDescription()));

      sElem.addContent(new Element("longitude").addContent( ucar.unidata.util.Format.d(s.getLongitude(), 6)));
      sElem.addContent(new Element("latitide").addContent( ucar.unidata.util.Format.d(s.getLatitude(), 6)));
      if (!Double.isNaN(s.getAltitude()))
        sElem.addContent(new Element("altitude").addContent( ucar.unidata.util.Format.d(s.getAltitude(), 6)));
      rootElem.addContent(sElem);
    }

    return doc;
  }

  /**
   * Create an XML document from this info
   */
  public Document makeStationObsDatasetDocument() {
    Element rootElem = new Element("stationObsDataset");
    Document doc = new Document(rootElem);
    rootElem.setAttribute("location", sobs.getLocationURI());
    if (null != path)
      rootElem.setAttribute("path", path);

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

    /* coordinate axes
    List coordAxes = getCoordAxes(sobs);
    for (int i = 0; i < coordAxes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) coordAxes.get(i);
      rootElem.addContent(writeAxis(axis));
    } */

    // grids
    List vars = sobs.getDataVariables();
    Collections.sort(vars);
    for (int i = 0; i < vars.size(); i++) {
      VariableSimpleIF v = (VariableSimpleIF) vars.get(i);
      rootElem.addContent(writeVariable(v));
    }

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

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

    // add date range
    Date start  = sobs.getStartDate();
    Date end  = sobs.getEndDate();
    if ((start != null) && (end != null)) {
      DateFormatter format = new DateFormatter();
      Element dateRange = new Element("TimeSpan");
      dateRange.addContent(new Element("begin").addContent(format.toDateTimeStringISO(start)));
      dateRange.addContent(new Element("end").addContent(format.toDateTimeStringISO(end)));
      rootElem.addContent( dateRange);
    }

    // add accept list
    Element elem = new Element("AcceptList");
    elem.addContent(new Element("accept").addContent("raw"));
    elem.addContent(new Element("accept").addContent("xml"));
    elem.addContent(new Element("accept").addContent("csv"));
    elem.addContent(new Element("accept").addContent("netcdf"));
    elem.addContent(new Element("accept").addContent("netcdfStream"));
    rootElem.addContent(elem);

    return doc;
  }

  /* private List getCoordAxes(StationObsDataset gds) {
    HashSet axesHash = new HashSet();
    List gridSets = gds.getGridsets();
    for (int i = 0; i < gridSets.size(); i++) {
      GridDataset.Gridset gridset = (GridDataset.Gridset) gridSets.get(i);
      GridCoordSystem gcs = gridset.getGeoCoordSystem();
      List axes = gcs.getCoordinateAxes();
      for (int j = 0; j < axes.size(); j++)
        axesHash.add(axes.get(j));
    }

    List list = Arrays.asList(axesHash.toArray());
    Collections.sort(list);
    return list;
  } */

  /* private List getDimensions(StationObsDataset gds) {
    HashSet dimHash = new HashSet();
    List vars = gds.getDataVariables();
    for (int i = 0; i < vars.size(); i++) {
      VariableSimpleIF v = (VariableSimpleIF) vars.get(i);
      List dims = v.getDimensions();
      for (int j = 0; j < dims.size(); j++) {
        Dimension dim = (Dimension) dims.get(j);
        dimHash.add(dim);
      }
    }
    List list = Arrays.asList(dimHash.toArray());
    Collections.sort(list);
    return list;
  }

  private Element writeAxis(CoordinateAxis axis) {

    Element varElem = new Element("axis");
    varElem.setAttribute("name", axis.getName());
    varElem.setAttribute("shape", axis.getDimensionsString());

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

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

    // attributes
    Iterator atts = axis.getAttributes().iterator();
    while (atts.hasNext()) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) atts.next();
      varElem.addContent(ucar.nc2.ncml.NcMLWriter.writeAttribute(att, "attribute", null));
    }

    if (axis.getRank() < 2)
      varElem.addContent(ucar.nc2.ncml.NcMLWriter.writeValues(axis, null, true));

    return varElem;
  } */

  private Element writeBoundingBox(LatLonRect bb) {
    Element bbElem = new Element("LatLonBox");
    bbElem.addContent(new Element("west").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMin(), 4)));
    bbElem.addContent(new Element("east").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMax(), 4)));
    bbElem.addContent(new Element("south").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMin(), 4)));
    bbElem.addContent(new Element("north").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMax(), 4)));
    return bbElem;
  }

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

  private Element writeCoordTransform(CoordinateTransform ct) {
    Element ctElem = new Element("coordTransform");
    ctElem.setAttribute("name", ct.getName());
    ctElem.setAttribute("transformType", ct.getTransformType().toString());
    List params = ct.getParameters();
    for (int i = 0; i < params.size(); i++) {
      Parameter param = (Parameter) params.get(i);
      Element pElem = new Element("parameter");
      pElem.setAttribute("name", param.getName());
      pElem.setAttribute("value", param.getStringValue());
      ctElem.addContent(pElem);
    }
    return ctElem;
  }

  private Element writeVariable(VariableSimpleIF v) {
    NcMLWriter ncmlWriter = new NcMLWriter();

    Element varElem = new Element("variable");
    varElem.setAttribute("name", v.getShortName());

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

    // attributes
    Iterator atts = v.getAttributes().iterator();
    while (atts.hasNext()) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) atts.next();
      varElem.addContent(ncmlWriter.makeAttributeElement(att));
    }

    return varElem;
  }

  /**
   * debug
   */
  public static void main(String args[]) throws IOException {
    String url = "C:/data/metars/Surface_METAR_20060326_0000.nc";

    StationObsDataset ncd = (StationObsDataset) TypedDatasetFactory.open(FeatureType.STATION, url, null, new StringBuilder());
    StationObsDatasetInfo info = new StationObsDatasetInfo(ncd, null);
    FileOutputStream fos2 = new FileOutputStream("C:/TEMP/stationCollection.xml");
    GZIPOutputStream zout =  new GZIPOutputStream( fos2);

    info.writeStationObsDatasetXML(System.out);
    info.writeStationCollectionXML(zout);

    zout.close();
    File f = new File("C:/TEMP/stationCollection.xml");
    System.out.println(" size="+f.length());
  }

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information.
*/

package dap4.servlet;

import dap4.core.dmr.ErrorResponse;
import dap4.core.util.DapException;
import dap4.core.util.DapUtil;
import dap4.dap4lib.DapCodes;
import dap4.dap4lib.RequestMode;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class ChunkWriter extends OutputStream
{
    //////////////////////////////////////////////////
    // Constants

    static public boolean DEBUG = false;
    static public boolean DUMPDATA = false;
    static public boolean DEBUGHEADER = false;

    static final int MAXCHUNKSIZE = 0xFFFF;

    static final long DEFAULTWRITELIMIT = 1 * 1000000;

    static final int SIZEOF_INTEGER = 4;

    static public final byte[] CRLF8 = DapUtil.extract(DapUtil.UTF8.encode(DapUtil.CRLF));

    static public final String XMLDOCUMENTHEADER
              = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";

    //////////////////////////////////////////////////
    // Type declarations

    protected enum State
    {
        ERROR,    // after error chunk
        END,     // after last data chunk
        DATA,    // after at least 1 data chunk
        DMR,     // After DMR
        INITIAL; // before anything is written
    }

    //////////////////////////////////////////////////
    // Instance variable

    protected OutputStream saveoutput = null; // save the target stream if we are debugging
    protected OutputStream output = null;
    protected State state = State.INITIAL;

    protected int maxbuffersize = MAXCHUNKSIZE;
    protected long writelimit = DEFAULTWRITELIMIT; // Max amount we will accept
    protected long writecount = 0; // actual amount written so far
    protected ByteBuffer chunk = null; // give caller a chance to set the max buffersize
    protected ByteBuffer header = null;
    protected ByteOrder writeorder = null;
    protected RequestMode mode = null;
    protected byte[] dmr8 = null; // dmr in utf-8 form
    protected boolean closed = false;

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

    public ChunkWriter(OutputStream output, RequestMode mode, ByteOrder writeorder)
            throws IOException
    {
        if(DUMPDATA || DEBUG) {
            this.saveoutput = output;
            this.output = new ByteArrayOutputStream();
        } else
            this.output = output;
        setWriteOrder(writeorder);
        setMode(mode);
        // Header must always go out in network order (aka big-endian)
        header = ByteBuffer.allocate(SIZEOF_INTEGER).order(ByteOrder.BIG_ENDIAN);
    }

    //////////////////////////////////////////////////
    // Accessors

    public RequestMode getMode()
    {
        return this.mode;
    }

    public void setMode(RequestMode mode)
    {
        this.mode = mode;
    }

    public void setBufferSize(int maxsize)
    {
        if(maxsize < 0 || maxsize > MAXCHUNKSIZE)
            return;
        maxbuffersize = maxsize;
    }

    public ByteOrder getWriteOrder()
    {
        return this.writeorder;
    }

    public void setWriteOrder(ByteOrder writeorder)
    {
        this.writeorder = writeorder;
    }

    public void setWriteLimit(long limit)
    {
        writelimit = limit;
    }

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

    /**
     * Write the DSR; do not bother to cache.
     *
     * @param dsr The DSR string
     * @throws IOException on IO related errors
     */

    public void
    writeDSR(String dsr)
            throws IOException
    {
        if(state != State.INITIAL)
            throw new DapException("Attempt to write DSR twice");

        if(dsr == null)
            throw new DapException("Attempt to write empty DSR");

        // Strip off any trailing sequence of CR or LF.
        int len = dsr.length();
        while(len > 0) {
            char c = dsr.charAt(len - 1);
            if(c != '\r' && c != '\n') break;
            len--;
        }
        if(dsr.length() == 0)
            throw new DapException("Attempt to write empty DSR");

        dsr = dsr.substring(0, len) + DapUtil.CRLF;

        // Add <?xml...?> prefix
        dsr = XMLDOCUMENTHEADER + "\n" + dsr;

        // Convert the dsr to UTF-8 and then to byte[]
        byte[] dsr8 = DapUtil.extract(DapUtil.UTF8.encode(dsr));
        sendDXR(dsr8);
        state = State.END;
    }

    /**
     * Cache the DMR. What it really does is
     * cache the DMR and write it at the point
     * where it is needed; either in close(), if writing
     * the DMR only, or in writeChunk() if writing data as well.
     *
     * @param dmr The DMR string
     * @throws IOException on IO related errors
     */

    public void
    cacheDMR(String dmr)
            throws IOException
    {
        if(state != State.INITIAL)
            throw new DapException("Attempt to write DMR twice");

        if(dmr == null)
            throw new DapException("Attempt to write empty DMR");

        // Strip off any trailing sequence of CR or LF.
        int len = dmr.length();
        while(len > 0) {
            char c = dmr.charAt(len - 1);
            if(c != '\r' && c != '\n') break;
            len--;
        }
        if(dmr.length() == 0)
            throw new DapException("Attempt to write empty DMR");

        dmr = dmr.substring(0, len) + DapUtil.CRLF;

        // Prepend the <?xml...?> prefix
        dmr = XMLDOCUMENTHEADER + "\n" + dmr;

        // Convert the dmr to UTF-8 and then to byte[]
        this.dmr8 = DapUtil.extract(DapUtil.UTF8.encode(dmr));

        state = State.DMR;
    }

    /**
     * Output the specifiedd DMR or DSR or..., but xml only.
     *
     * @throws IOException on IO related errors
     */

    void
    sendDXR(byte[] dxr8)
            throws IOException
    {
        if(dxr8 == null || dxr8.length == 0)
            return; // do nothing

        if(mode == RequestMode.DMR || mode == RequestMode.DSR) {
            state = State.END;
        } else {//mode == DATA
            // Prefix with chunk header
            int flags = DapUtil.CHUNK_DATA;
            if(this.writeorder == ByteOrder.LITTLE_ENDIAN)
                flags |= DapUtil.CHUNK_LITTLE_ENDIAN;
            chunkheader(dxr8.length, flags, this.header);
            // write the header
            output.write(DapUtil.extract(this.header));
            state = State.DATA;
        }
        // write the DXR
        output.write(dxr8);
        output.flush();
    }

    /**
     * Write an error chunk.
     * If mode == DMR then replaces the dmr
     * else reset the current chunk thus
     * losing any partial write.
     *
     * @param httpcode The httpcode, 0 => ignore
     * @param msg      The <Error> <Message>, null => ignore
     * @param cxt      The <Error> <Context>, null => ignore
     * @param other    The <Error> <OtherInformation>, null => ignore
     * @throws IOException on IO related errors
     */
    public void
    writeError(int httpcode,
               String msg,
               String cxt,
               String other)
            throws IOException
    {
        dmr8 = null;
        ErrorResponse response = new ErrorResponse(httpcode, msg, cxt, other);
        String errorbody = response.buildXML();
        // Convert the error body into utf8 then to byte[]
        byte[] errbody8 = DapUtil.extract(DapUtil.UTF8.encode(errorbody));
        if(mode == RequestMode.DMR) {
            sendDXR(errbody8);
        } else {//mode == DATA
            // clear any partial chunk
            chunk.clear();
            // create an error header
            int flags = DapUtil.CHUNK_ERROR | DapUtil.CHUNK_END;
            chunkheader(errbody8.length, flags, header);
            output.write(DapUtil.extract(header));
            output.write(errbody8);
            output.flush();
        }
        state = State.ERROR;
    }

    /**
     * Write out the current chunk (with given set of flags).
     *
     * @param flags The flags for the header
     * @throws IOException on IO related errors
     */
    void writeChunk(int flags)
            throws IOException
    {
        // If flags indicate CHUNK_END
        // and amount to write is zero,
        // go ahead and write the zero size chunk.
        if(chunk == null)
            chunk = ByteBuffer.allocate(maxbuffersize);

        int buffersize = chunk.position();
        chunkheader(buffersize, flags, header);
        // output the header followed by the data (if any)
        // Zero size chunk is ok.
        output.write(DapUtil.extract(header));
        if(buffersize > 0)
            output.write(chunk.array(), 0, buffersize);
        chunk.clear();// reset
    }

    static public void
    chunkheader(int length, int flags, ByteBuffer hdrbuf)
            throws DapException
    {
        if(length > MAXCHUNKSIZE || length < 0)
            throw new DapException("Illegal chunk size: " + length);
        int hdr = ((flags << 24) | length);
        hdrbuf.clear();
        hdrbuf.putInt(hdr);
        if(DEBUGHEADER)
            System.err.printf("header: flags=%x length=%d header=%08x%n",
                    flags, length, hdr);
    }

    void verifystate()
            throws DapException
    {
        // Verify that we are in a proper state to write data
        switch (state) {
        case INITIAL:
            throw new DapException("Attempt to write data before DMR");
        case END:
            throw new DapException("Attempt to write data after END");
        case ERROR:
            throw new DapException("Attempt to write data after ERROR");
        case DMR:
        case DATA:
            break;
        }
    }

    //////////////////////////////////////////////////
    // OutputStream API

    /**
     * Closes this output stream and releases any system resources
     * associated with this stream. Except, the underlying stream is not
     * actually closed; that is left to the servlet level
     *
     * @throws IOException on IO related errors
     */

    public void close()
            throws IOException
    {
        if(closed)
            return;
        closed = true;

        if(dmr8 != null) {
            sendDXR(dmr8);
            dmr8 = null;
        }

        if(mode == RequestMode.DMR)
            return; // only DMR should be sent

        // If there is no partial chunk to write then
        // we are done; else verify we can write and write the last
        // chunk; => multiple closes are ok.
        if(chunk == null || chunk.position() == 0)
            return;

        // There is data left to write.
        verifystate(); // are we in a state supporting data write?

        // Force out the current chunk (might be empty)
        // but do not close the underlying output stream
        state = State.DATA; // pretend

        int flags = DapUtil.CHUNK_END;
        writeChunk(flags);
        state = State.END;
        this.output.flush(); // Do not close
        if(this.saveoutput != null) {
            // write to true output target
            this.saveoutput.write(((ByteArrayOutputStream) this.output).toByteArray());
        }
    }

    public byte[]
    getDump()
    {
        if((DUMPDATA || DEBUG) && this.output != null)
            return ((ByteArrayOutputStream) this.output).toByteArray();
        else
            return null;
    }

    /**
     * Overload flush to also write out the DMR
     *
     * @throws IOException
     */
    @Override
    public void
    flush()
            throws IOException
    {
        if(mode == RequestMode.DMR)
            return; // leave to close() to do this
        if(dmr8 != null) {
            sendDXR(dmr8);
            dmr8 = null;
        }
    }

    /**
     * Writes b.length bytes from the specified byte array
     * to this output stream.
     *
     * @param b the data
     * @throws IOException if an I/O error occurs
     */

    @Override
    public void write(byte[] b) throws IOException
    {
        write(b, 0, b.length);
    }

    /**
     * Writes the specified byte to the chunk
     *
     * @param b the byte to write
     * @throws IOException if an I/O error occurs
     */

    @Override
    public void write(int b)
            throws IOException
    {
        byte[] buf = new byte[1];
        buf[0] = (byte) (b & 0xff);
        write(buf, 0, 1);
    }

    /**
     * Writes len bytes from the specified byte array starting at
     * offset off to this output stream.
     * <p>
     * If this write fills up the chunk buffer,
     * then write out the buffer and put
     * the remaining bytes into the reset buffer.
     *
     * @param b   the data
     * @param off start point in b from which to write data
     * @param len number of bytes to write
     * @throws IOException if an I/O error occurs
     */

    @Override
    public void write(byte[] b, int off, int len)
            throws IOException
    {
        verifystate();
        if(writecount + len >= writelimit)
            throw new DapException("Attempt to write too much data: limit=" + writecount + len)
                    .setCode(DapCodes.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        if(chunk == null)
            chunk = ByteBuffer.allocate(maxbuffersize).order(getWriteOrder());
        if(state == State.DMR) {
            chunk.clear(); // reset
            state = State.DATA;
        }
        assert (state == State.DATA);
        if(b.length < off + len)
            throw new BufferUnderflowException();
        int left = len;
        int offset = off;
        while(left > 0) {
            int avail = chunk.remaining();
            do {
                if(avail == 0) {
                    writeChunk(DapUtil.CHUNK_DATA);
                    avail = chunk.remaining();
                }
                int towrite = (left < avail ? left : avail);
                chunk.put(b, off, towrite);
                left -= towrite;
                avail -= towrite;
            } while(left > 0);
        }
        writecount += len;
    }
}


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

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

import java.io.Serializable;

/**
 * Universal Transverse Mercator.
 * Ellipsoidal earth.
 *

 * Origin of coordinate system is reletive to the point where the
 * central meridian and the equator cross.
 * This point has x,y value = (500, 0) km for north hemisphere.
 * and (500, 10,0000) km for south hemisphere.
 * Increasing values always go north and east.
 *

 * The central meridian = (zone * 6 - 183) degrees, where zone in [1,60].
 *
 * @author John Caron
 */

public class UtmProjection extends ProjectionImpl {
  public static final String GRID_MAPPING_NAME = "universal_transverse_mercator";
  public static final String UTM_ZONE1 = "utm_zone_number";
  public static final String UTM_ZONE2 = "UTM_zone";

  private final Utm_To_Gdc_Converter convert2latlon;
  private final Gdc_To_Utm_Converter convert2xy;

  private static class SaveParams implements Serializable {
    final double a;
    final double f;
    final int zone;
    final boolean isNorth;

    private SaveParams(double a, double f, int zone, boolean isNorth) {
      this.a = a;
      this.f = f;
      this.zone = zone;
      this.isNorth = isNorth;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      SaveParams that = (SaveParams) o;
      if (Double.compare(that.a, a) != 0) return false;
      if (Double.compare(that.f, f) != 0) return false;
      if (isNorth != that.isNorth) return false;
      return zone == that.zone;
    }

    @Override
    public int hashCode() {
      int result;
      long temp;
      temp = Double.doubleToLongBits(a);
      result = (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(f);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      result = 31 * result + zone;
      result = 31 * result + (isNorth ? 1 : 0);
      return result;
    }
  }

  private final SaveParams saveParams; // needed for constructCopy

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result = (saveParams == null) ? new UtmProjection(getZone(), isNorth()) : new UtmProjection(saveParams.a, saveParams.f, getZone(), isNorth());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public UtmProjection() {
    this(5, true);
  }

  /**
   * Constructor with default WGS 84 ellipsoid.
   *
   * @param zone    the UTM zone number (1-60)
   * @param isNorth true if the UTM coordinate is in the northern hemisphere
   */
  public UtmProjection(int zone, boolean isNorth) {
    super("UtmProjection", false);
    convert2latlon = new Utm_To_Gdc_Converter(zone, isNorth);
    convert2xy = new Gdc_To_Utm_Converter(zone, isNorth);
    saveParams = new SaveParams(convert2latlon.getA(), 1/convert2latlon.getF(), zone, isNorth);

    addParameter(CF.GRID_MAPPING_NAME, GRID_MAPPING_NAME);
    addParameter(CF.SEMI_MAJOR_AXIS, convert2latlon.getA());
    addParameter(CF.INVERSE_FLATTENING, convert2latlon.getF());
    addParameter(UTM_ZONE1, zone);
  }

  /**
   * Construct a Universal Transverse Mercator Projection.
   *
   * @param a       the semi-major axis (meters) for the ellipsoid
   * @param f       the inverse flattening for the ellipsoid
   * @param zone    the UTM zone number (1-60)
   * @param isNorth true if the UTM coordinate is in the northern hemisphere
   */
  public UtmProjection(double a, double f, int zone, boolean isNorth) {
    super("UtmProjection", false);
    saveParams = new SaveParams(a, f, zone, isNorth);

    convert2latlon = new Utm_To_Gdc_Converter(a, f, zone, isNorth);
    convert2xy = new Gdc_To_Utm_Converter(a, f, zone, isNorth);

    addParameter(CF.GRID_MAPPING_NAME, GRID_MAPPING_NAME);
    addParameter(CF.SEMI_MAJOR_AXIS, a);
    addParameter(CF.INVERSE_FLATTENING, f);
    addParameter(UTM_ZONE1, zone);
  }

  /**
   * Get the zone number = [1,60]
   *
   * @return zone number
   */
  public int getZone() {
    return convert2latlon.getZone();
  }

  /**
   * Get whether in North or South Hemisphere.
   *
   * @return true if north
   */
  public boolean isNorth() {
    return convert2latlon.isNorth();
  }

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

  /*
   * Getting the central meridian in degrees.  depends on the zone
   * @return  the central meridian in degrees.
   */
  public double getCentralMeridian() {
    return convert2xy.getCentralMeridian();
  }

  /**
   * Get the parameters as a String
   *
   * @return the parameters as a String
   */
  public String paramsToString() {
    return getZone() + " " + isNorth();
  }

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

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

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

  /*
   * Returns true if this represents the same Projection as proj.
   *
   * @param proj projection in question
   * @return true if this represents the same Projection as proj.
   *
  public boolean equals(Object proj) {
    if (!(proj instanceof UtmProjection)) {
      return false;
    }

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

    return op.getZone() == getZone();
  }  */

  /**
   * 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 fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();

    return convert2xy.latLonToProj(fromLat, fromLon, result);
  }

  public double[][] latLonToProj(double[][] from, double[][] to, int latIndex, int lonIndex) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:null array argument or wrong dimension (to)");
    }
    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:from array not same length as to array");
    }

    return convert2xy.latLonToProj(from, to, latIndex, lonIndex);
  }

  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex, int lonIndex) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:null array argument or wrong dimension (to)");
    }
    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.latLonToProj:from array not same length as to array");
    }

    return convert2xy.latLonToProj(from, to, latIndex, lonIndex);
  }

  /**
   * 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) {
    return convert2latlon.projToLatLon(world.getX(), world.getY(), result);
  }

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) coordinate of
   *             the ith point
   * @return the "to" array
   */
  public float[][] projToLatLon(float[][] from, float[][] to) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:from array not same length as to array");
    }

    return convert2latlon.projToLatLon(from, to);
  }

  public double[][] projToLatLon(double[][] from, double[][] to) {
    if ((from == null) || (from.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:null array argument or wrong dimension (from)");
    }
    if ((to == null) || (to.length != 2)) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:null array argument or wrong dimension (to)");
    }

    if (from[0].length != to[0].length) {
      throw new IllegalArgumentException("ProjectionImpl.projToLatLon:from array not same length as to array");
    }

    return convert2latlon.projToLatLon(from, to);
  }

  /*
  roszelld@usgs.gov
  'm transforming coordinates (which are in UTM Zone 17N projection) to
lat/lon.

If I get the ProjectionImpl from the grid (stage) and use the projToLatLon
function with {{577.8000000000001}, {2951.8}} in kilometers for example, I
get {{26.553706785076937}, {-80.21754983617633}}, which is not very
accurate at all when I plot them.

If I use GeoTools to build a transform based on the same projection
parameters read from the projectionimpl, I get {{26.685132668190793},
{-80.21802662821469}} which appears to be MUCH more accurate when I plot
them on a map.
   */
  public static void main(String arg[]) {
    UtmProjection utm = new UtmProjection(17, true);
    LatLonPoint ll = utm.projToLatLon(577.8000000000001, 2951.8);
    System.out.printf("%15.12f %15.12f%n", ll.getLatitude(), ll.getLongitude());
    assert Misc.nearlyEquals(ll.getLongitude(), -80.21802662821469, 1.0e-8);
    assert Misc.nearlyEquals(ll.getLatitude(), 26.685132668190793, 1.0e-8);
  }

}


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

import ucar.unidata.geoloc.Station;

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

/**
 * Implementation of Station
 *
 * @author caron
 * @deprecated use ucar.unidata.geoloc.StationImpl
 */

public class StationImpl extends ucar.unidata.geoloc.StationImpl {
  protected List<StationObsDatatype> obsList;
  protected int count = -1;

  public StationImpl() {
  }

  public StationImpl( String name, String desc, double lat, double lon, double alt) {
    super( name, desc, "", lat, lon, alt);
  }

  public StationImpl( String name, String desc, double lat, double lon, double alt, int count) {
    this( name, desc, lat, lon, alt);
    this.count = count;
  }

  public int getNumObservations() { return (obsList == null) ? count : obsList.size(); }

  /////

  public void incrCount() { count++; }

  public List getObservations() throws IOException {
    if (obsList == null)
        obsList = readObservations();
    return obsList;
  }

  // got to use this or subclass readObservations()
  public void addObs( StationObsDatatype sobs) {
    if (null == obsList) obsList = new ArrayList<StationObsDatatype>();
    obsList.add( sobs);
  }

  protected List<StationObsDatatype> readObservations()  throws IOException { return null; }

}


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

import com.google.common.eventbus.EventBus;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import thredds.inventory.CollectionUpdateEvent;
import thredds.inventory.CollectionUpdateType;

import javax.annotation.concurrent.ThreadSafe;
import java.util.Date;

/**
 * Handle background tasks for updating collections.
 * Singleton, thread safe.
 * Cover for quartz library.
 * Only used in tds/tdm.
 *
 * @author caron
 * @since Nov 21, 2010
 */
@ThreadSafe
@Component
public class CollectionUpdater {
  static private final org.slf4j.Logger fcLogger = org.slf4j.LoggerFactory.getLogger(CollectionUpdater.class);

  static private final String COLLECTION_NAME = "collectionName";
  static private final String EVENT_BUS = "eventBus";
  static private final String LOGGER = "logger";
  static private final String UpdateType = "updateType";
  static private final String Source = "Source";
  static private final long startupWait = 3 * 1000; // 3 secs
  static private boolean disabled = false;

  @Autowired
  @Qualifier("fcTriggerEventBus")
  private EventBus eventBus;

  private org.quartz.Scheduler scheduler = null;
  private boolean failed = false;
  private boolean isTdm = false;

  public void setTdm(boolean tdm) {
    isTdm = tdm;
  }

  public boolean isTdm() {
    return isTdm;
  }

  public CollectionUpdater() {
    try {
      scheduler = StdSchedulerFactory.getDefaultScheduler();
      scheduler.start();
      // scheduler.getListenerManager().addSchedulerListener(new MySchedListener());
    } catch (Throwable e) {
      failed = true;
      throw new RuntimeException("quartz scheduler failed to initialize", e);
    }
  }

  public void setEventBus(EventBus eventBus) {
    this.eventBus = eventBus;
  }

  // debugging
  public org.quartz.Scheduler getScheduler() {
    return scheduler;
  }

  /**
   * Look at config and decide if tasks need to be started
   */
  public void scheduleTasks(FeatureCollectionConfig config, Logger logger) {
    if (disabled || failed) return;
    if (logger == null) logger = fcLogger;

    FeatureCollectionConfig.UpdateConfig updateConfig = (isTdm) ? config.tdmConfig : config.updateConfig;
    if (updateConfig == null || updateConfig.updateType == CollectionUpdateType.never) return;

    String collectionName = config.getCollectionName();

    // prob dont need to set a job if theres no chron job ?

    // Job to update the collection
    org.quartz.JobDataMap map = new org.quartz.JobDataMap();
    map.put(EVENT_BUS, eventBus);
    map.put(COLLECTION_NAME, collectionName);
    map.put(LOGGER, logger);
    JobDetail updateJob = JobBuilder.newJob(UpdateCollectionJob.class)
            .withIdentity(collectionName, "UpdateCollection")
            .storeDurably()
            .usingJobData(map)
            .build();

    try {
      if(!scheduler.checkExists(updateJob.getKey())) {
      	scheduler.addJob(updateJob, false);
      } else {
        logger.warn("scheduler failed to add updateJob for " + updateJob.getKey() +". Another Job exists with that identification." );
      }
    } catch (Throwable e) {
      logger.error("scheduler failed to add updateJob for " + config, e);
      return;
    }

    // task to run the job on startup
    if (updateConfig.startupType != CollectionUpdateType.never) {
      map = new org.quartz.JobDataMap();
      map.put(UpdateType, updateConfig.startupType);
      map.put(Source, "startup");
      Date runTime = new Date(new Date().getTime() + startupWait); // wait startupWait before trigger
      SimpleTrigger startupTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
              .withIdentity(collectionName, "startup")
              .startAt(runTime)
              .forJob(updateJob)
              .usingJobData(map)
              .build();

      try {
        scheduler.scheduleJob(startupTrigger);
        logger.info("scheduleJob startup scan force={} for '{}' at {}", updateConfig.startupType.toString(), config.collectionName, runTime);
      } catch (Throwable e) {
        logger.error("scheduleJob failed to schedule startup Job for " + config, e);
        return;
      }
    }

    // task to run the job periodically, with rescan
    if (updateConfig.rescan != null) {
        map = new org.quartz.JobDataMap();
        map.put(UpdateType, updateConfig.updateType);
        map.put(Source, "rescan");
        CronTrigger rescanTrigger = TriggerBuilder.newTrigger()
                .withIdentity(collectionName, "rescan")
                .withSchedule(CronScheduleBuilder.cronSchedule(updateConfig.rescan))
                .forJob(updateJob)
                .usingJobData(map)
                .build();

      try {
    		scheduler.scheduleJob(rescanTrigger);
        logger.info("scheduleJob recurring scan for '{}' cronExpr={}", config.collectionName, updateConfig.rescan);
      } catch (Throwable e) {
        logger.error("scheduleJob failed to schedule cron Job", e);
        // e.printStackTrace();
      }
    }

    /* updating the proto dataset
    FeatureCollectionConfig.ProtoConfig pconfig = config.protoConfig;
    if (pconfig.change != null) {
      org.quartz.JobDataMap pmap = new org.quartz.JobDataMap();
      pmap.put(DCM_NAME, manager);
      map.put(LOGGER, logger);
      JobDetail protoJob = JobBuilder.newJob(ChangeProtoJob.class)
              .withIdentity(jobName, "UpdateProto")
              .usingJobData(pmap)
              .storeDurably()
              .build();

      try {
        CronTrigger protoTrigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, "rereadProto")
                .withSchedule(CronScheduleBuilder.cronSchedule(pconfig.change))
                .build();
        scheduler.scheduleJob(protoJob, protoTrigger);
        if (logger != null)logger.info("Schedule proto update for '{}' cronExpr={}", config.collectionName, pconfig.change);

      } catch (Throwable e) {
        if (logger != null)logger.error("cronExecutor failed to schedule RereadProtoJob", e);
        // e.printStackTrace();
      }
    } */

  }

  public void shutdown() {
    if (scheduler == null) return;
    try {
      scheduler.shutdown(true);
      System.out.printf("Quartz scheduler shutdown%n");
    } catch (Throwable e) {
      fcLogger.error("Scheduler failed to shutdown", e);
      scheduler = null;
      //e.printStackTrace();
    }
  }

  /* Called by TDS collectionController when trigger is received externally
  public void triggerUpdate(String collectionName, CollectionUpdateType triggerType) {
    JobDataMap map = new org.quartz.JobDataMap();
    map.put(UpdateType, triggerType);
    Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity(collectionName+"-trigger", triggerType.toString()) // dorky
            .forJob(collectionName, "UpdateCollection") // ??
            .usingJobData(map)
            .startNow()
            .build();

    try {
      // logger.debug("Trigger Update for {} type= {}", collectionName, triggerType);
      scheduler.scheduleJob(trigger);
    } catch (Throwable e) {
      startupLogger.error("triggerUpdate failed", e);
      // e.printStackTrace();
    }
  } */

  // do the work in a separate thread
  public static class UpdateCollectionJob implements org.quartz.Job {
    public UpdateCollectionJob() {
    }

    public void execute(JobExecutionContext context) throws JobExecutionException {
      EventBus eventBus = (EventBus) context.getJobDetail().getJobDataMap().get(EVENT_BUS);
      String collectionName = (String) context.getJobDetail().getJobDataMap().get(COLLECTION_NAME);
      org.slf4j.Logger loggerfc = (org.slf4j.Logger) context.getJobDetail().getJobDataMap().get(LOGGER);
      CollectionUpdateType type= (CollectionUpdateType) context.getTrigger().getJobDataMap().get(UpdateType);
      String source= (String) context.getTrigger().getJobDataMap().get(Source);
      String groupName = context.getTrigger().getKey().getGroup();

      try {
        eventBus.post( new CollectionUpdateEvent(type, collectionName, source));
        fcLogger.debug("CollectionUpdate post event {} on {}", type, collectionName);

      } catch (Throwable e) {
        if (loggerfc != null) loggerfc.error("UpdateCollectionJob.execute "+groupName+" failed collection=" + collectionName, e);
      }
    }
  }

  /* disabled LOOK
  public static class ChangeProtoJob implements org.quartz.Job {
    public ChangeProtoJob() {
    }

    public void execute(JobExecutionContext context) throws JobExecutionException {
      CollectionUpdateListener manager = (CollectionUpdateListener) context.getJobDetail().getJobDataMap().get(DCM_NAME);
      org.slf4j.Logger loggerfc = (org.slf4j.Logger) context.getJobDetail().getJobDataMap().get(LOGGER);

      try {
        if (loggerfc != null) loggerfc.info("ResetProto for {}", manager.getCollectionName());
        // manager.sendEvent(CollectionUpdateListener.TriggerType.resetProto);
      } catch (Throwable e) {
        if (loggerfc != null) loggerfc.error("ChangeProtoJob.execute failed collection=" + manager.getCollectionName(), e);
      }
    }
  } */
}