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

import ucar.nc2.dt.*;
import ucar.nc2.units.DateUnit;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.units.DateFormatter;
import ucar.nc2.Variable;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.util.CancelTask;
import ucar.ma2.StructureData;
import ucar.ma2.DataType;

import java.util.List;
import java.util.Date;
import java.io.IOException;
import java.text.ParseException;

/**
 * Superclass for implementations of PointObsDataset.
 *
 *
 * @deprecated use ucar.nc2.ft.point
 * @author caron
 */

abstract public class PointObsDatasetImpl extends TypedDatasetImpl implements PointObsDataset {

  /**
   * Get conversion factor for this unit into meters.
   * @param unitsString unit you want to convert
   * @return conversion factor : value in meters = factor * (value in units)
   * @throws Exception if not valid unit, or not convertible to meters
   */
  protected static double getMetersConversionFactor( String unitsString) throws Exception {
    SimpleUnit unit = SimpleUnit.factoryWithExceptions(unitsString);
    return unit.convertTo(1.0, SimpleUnit.meterUnit);
  }

  protected DateUnit timeUnit;

  public PointObsDatasetImpl() {
    super();
  }

  public PointObsDatasetImpl(String title, String description, String location) {
    super(title, description, location);
  }

  public PointObsDatasetImpl(NetcdfDataset ncfile) {
    super( ncfile);
  }

  protected abstract void setTimeUnits(); // reminder for subclasses to set this

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

  public String getDetailInfo() {
    StringBuffer sbuff = new StringBuffer();
    sbuff.append("PointObsDataset\n");
    sbuff.append("  adapter   = ").append(getClass().getName()).append("\n");
    sbuff.append("  timeUnit  = ").append(getTimeUnits()).append("\n");
    sbuff.append("  dataClass = ").append(getDataClass()).append("\n");
    sbuff.append("  dataCount = ").append(getDataCount()).append("\n");
    sbuff.append(super.getDetailInfo());

    return sbuff.toString();
  }

  public FeatureType getScientificDataType() {
    return FeatureType.POINT;
  }

  public Class getDataClass() {
    return PointObsDatatype.class;
  }

  public DateUnit getTimeUnits() {
    return timeUnit;
  }

  public List getData() throws IOException {
    return getData( (CancelTask) null);
  }

  public List getData( ucar.unidata.geoloc.LatLonRect boundingBox) throws IOException {
    return getData( boundingBox, null);
  }

  public List getData( ucar.unidata.geoloc.LatLonRect boundingBox, Date start, Date end) throws IOException {
    return getData( boundingBox, start, end, null);
  }

  protected DateFormatter formatter;
  protected double getTime(Variable timeVar, StructureData sdata) throws ParseException {
    if (timeVar == null) return 0.0;

    if ((timeVar.getDataType() == DataType.CHAR) || (timeVar.getDataType() == DataType.STRING)) {
      String time = sdata.getScalarString(timeVar.getShortName());
      if (null == formatter) formatter = new DateFormatter();
      Date date = formatter.getISODate(time);
      return date.getTime() / 1000.0;
    } else {
      return sdata.convertScalarFloat(timeVar.getShortName());
    }
  }

}


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

import ucar.ma2.*;

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

/**
 * Sequence is a one-dimensional Structure with indeterminate length.
 * The only data access is through getStructureIterator().
 * However, read() will read in the entire data and return an ArraySequence.
 *
 * @author caron
 * @since Feb 23, 2008
 */
public class Sequence extends Structure {

  /* Sequence Constructor
  *
  * @param ncfile    the containing NetcdfFile.
  * @param group     the containing group; if null, use rootGroup
  * @param parent    parent Structure, may be null
  * @param shortName variable shortName, must be unique within the Group
  */
  public Sequence(NetcdfFile ncfile, Group group, Structure parent, String shortName) {
    super(ncfile, group, parent, shortName);

    List<Dimension> dims = new ArrayList<>();
    dims.add(Dimension.VLEN);
    setDimensions(dims);
    setDataType(DataType.SEQUENCE);
  }

  public StructureDataIterator getStructureIterator(int bufferSize) throws java.io.IOException {
    return ncfile.getStructureIterator(this, bufferSize);
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Array read(int[] origin, int[] shape) throws IOException, InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Array read(String sectionSpec) throws IOException, InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Array read(List<Range> ranges) throws IOException, InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Array read(ucar.ma2.Section section) throws java.io.IOException, ucar.ma2.InvalidRangeException {
    return read();
  }

  /*
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   *
  @Override
  public Array read() throws IOException {
    throw new UnsupportedOperationException();
  } */

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public StructureData readStructure() throws IOException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public StructureData readStructure(int index) throws IOException, ucar.ma2.InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public ArrayStructure readStructure(int start, int count) throws IOException, ucar.ma2.InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Variable slice(int dim, int value) throws InvalidRangeException {
    throw new UnsupportedOperationException();
  }

  /**
   * UnsupportedOperation
   *
   * @throws UnsupportedOperationException
   */
  @Override
  public Variable section(Section subsection) throws InvalidRangeException {
    throw new UnsupportedOperationException();
  }

}


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

package ucar.nc2.grib.grib1;

import ucar.nc2.grib.GribData;
import ucar.nc2.grib.GribNumbers;
import ucar.nc2.grib.QuasiRegular;
import ucar.nc2.grib.grib1.tables.Grib1Customizer;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;
import java.util.Formatter;

/**
 * A Grib1 message.
 *
 * @author John
 * @since 9/3/11
 */
public class Grib1Record {

  private final Grib1SectionIndicator is;
  private final Grib1SectionGridDefinition gdss;
  private final Grib1SectionProductDefinition pdss;
  private final Grib1SectionBitMap bitmap;
  private final Grib1SectionBinaryData dataSection;

  private final byte[] header; // anything in between the records - eg idd header
  private int file; // for multiple files in same dataset

  /**
   * Construction for Grib1Record.
   *
   * @param header      Grib header
   * @param is          Grib1IndicatorSection
   * @param gdss        Grib1GridDefinitionSection
   * @param pdss        Grib1ProductDefinitionSection
   * @param bitmap      Grib1SectionBitMap
   * @param dataSection Grib1SectionData
   */
  public Grib1Record(byte[] header, Grib1SectionIndicator is,
                     Grib1SectionGridDefinition gdss,
                     Grib1SectionProductDefinition pdss,
                     Grib1SectionBitMap bitmap,
                     Grib1SectionBinaryData dataSection) {

    this.header = header;
    this.is = is;
    this.gdss = gdss;
    this.pdss = pdss;
    this.bitmap = bitmap;
    this.dataSection = dataSection;
  }

  /**
   * Construct record by reading raf, no checking
   *
   * @param raf positioned at start of message: the 'G' in "GRIB"
   * @throws IOException on read error
   */
  public Grib1Record(RandomAccessFile raf) throws IOException {
    this.header = null;
    is = new Grib1SectionIndicator(raf);
    pdss = new Grib1SectionProductDefinition(raf);
    gdss = pdss.gdsExists() ? new Grib1SectionGridDefinition(raf) : new Grib1SectionGridDefinition(pdss);
    bitmap = pdss.bmsExists() ? new Grib1SectionBitMap(raf) : null;
    dataSection = new Grib1SectionBinaryData(raf);
  }

  // copy constructor
  Grib1Record(Grib1Record from) {
    this.header = from.header;
    this.is = from.is;
    this.gdss = from.gdss;
    this.pdss = from.pdss;
    this.bitmap = from.bitmap;
    this.dataSection = from.dataSection;
  }

  public byte[] getHeader() {
    return header;
  }

  public Grib1SectionIndicator getIs() {
    return is;
  }

  public Grib1SectionGridDefinition getGDSsection() {
    return gdss;
  }

  public Grib1Gds getGDS() {
    return gdss.getGDS();
  }

  public Grib1SectionProductDefinition getPDSsection() {
    return pdss;
  }

  public Grib1SectionBitMap getBitMapSection() {
    return bitmap;
  }

  public Grib1SectionBinaryData getDataSection() {
    return dataSection;
  }

  public CalendarDate getReferenceDate() {
    return pdss.getReferenceDate();
  }

  public int getFile() {
    return file;
  }

  public void setFile(int file) {
    this.file = file;
  }

  private Grib1ParamTime ptime;

  public Grib1ParamTime getParamTime(Grib1Customizer cust) {
    if (ptime == null) ptime = cust.getParamTime(pdss);
    return ptime;
  }

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

  // isolate dependencies here - in case we have a "minimal I/O" mode where not all fields are available
  public float[] readData(RandomAccessFile raf) throws IOException {
    return readData(raf, GribData.getInterpolationMethod());
  }

  // dont convertQuasiGrid
  public float[] readDataRaw(RandomAccessFile raf) throws IOException {
    return readData(raf, GribData.InterpolationMethod.none);
  }

  // isolate dependencies here - in case we have a "minimal I/O" mode where not all fields are available
  public float[] readData(RandomAccessFile raf, GribData.InterpolationMethod method) throws IOException {
    Grib1Gds gds = getGDS();
    Grib1DataReader reader = new Grib1DataReader(pdss.getDecimalScale(), gds.getScanMode(), gds.getNxRaw(), gds.getNyRaw(), gds.getNpts(), dataSection.getStartingPosition());
    byte[] bm = (bitmap == null) ? null : bitmap.getBitmap(raf);
    float[] data = reader.getData(raf, bm);

    if (gdss.isThin()) {
      data = QuasiRegular.convertQuasiGrid(data, gds.getNptsInLine(), gds.getNxRaw(), gds.getNyRaw(), method);
    }

    lastRecordRead = this;
    return data;
  }

  public void showDataInfo(RandomAccessFile raf, Formatter f) throws IOException {
    Grib1Gds gds = getGDS();
    f.format(" decimal scale = %d%n", pdss.getDecimalScale());
    f.format("     scan mode = %d%n", gds.getScanMode());
    f.format("            nx = %d%n", gds.getNx());
    f.format("            ny = %d%n", gds.getNy());
    f.format("         nxRaw = %d%n", gds.getNxRaw());
    f.format("         nyRaw = %d%n", gds.getNyRaw());
    f.format("          Npts = %d%n", gds.getNpts());
    f.format("        isThin = %s%n", gdss.isThin());

    GribData.Info info = getBinaryDataInfo(raf);
    f.format("   dataLength = %d%n", info.dataLength);

    // octet 4, 1st half (packing flag)
    f.format("    ----flag = %s%n", Long.toBinaryString(info.flag));
    f.format("        data = %s%n", info.getGridPointS());
    f.format("     packing = %s%n", info.getPackingS());
    f.format("        type = %s%n", info.getDataTypeS());
    f.format(" moreOctet14 = %s%n", info.hasOctet14());
    f.format("    binscale = %d%n", info.binaryScaleFactor);
    f.format("    decscale = %d%n", info.decimalScaleFactor);
    f.format("reference value = %f%n", info.referenceValue);
    f.format("      nbits = %d%n", info.numberOfBits);

    Grib1DataReader.showComplexPackingInfo(f, raf, dataSection.getStartingPosition());
  }

  /**
   * Read data array by first reading in GribRecord.
   * All sections are read in, so scanMode is from the datafile, not the index.
   *
   * @param raf      from this RandomAccessFile
   * @param startPos message starts here
   * @return data as float[] array
   * @throws IOException on read error
   */
  public static float[] readData(RandomAccessFile raf, long startPos) throws IOException {
    raf.seek(startPos);
    Grib1Record gr = new Grib1Record(raf);
    return gr.readData(raf);
  }

  // debugging, do not use
  public static Grib1Record lastRecordRead;

  /*
   * Read data array: use when you want to be independent of the GribRecord
   *
   * @param raf          from this RandomAccessFile
   * @param bmPos        bitmap.start
   * @param decimalScale pds.decimalScale
   * @param scanMode     gds.scanMode
   * @param nx           gds.nx
   * @param ny           gds.ny
   * @return data as float[] array
   * @throws IOException on read error
   *
  public static float[] readData(RandomAccessFile raf, long bmPos, int decimalScale, int scanMode, int nx, int ny) throws IOException {
    raf.seek(bmPos);
    Grib1SectionBitMap bms = new Grib1SectionBitMap(raf);
    Grib1DataReader reader = new Grib1DataReader(decimalScale, scanMode, nx, ny, raf.getFilePointer());

    boolean[] bitmap = bms.getBitmap(raf);

    return reader.getData(raf, bitmap);
  } */

  public GribData.Info getBinaryDataInfo(RandomAccessFile raf) throws IOException {
    GribData.Info info = dataSection.getBinaryDataInfo(raf);
    info.decimalScaleFactor = pdss.getDecimalScale();
    info.bitmapLength = (bitmap == null) ? 0 : bitmap.getLength(raf);
    info.nPoints = getGDS().getNpts();
    info.msgLength = is.getMessageLength();

    if (bitmap == null) {
      info.ndataPoints = info.nPoints;
    } else {
      byte[] bm = bitmap.getBitmap(raf);
      if (bm == null) {
        info.ndataPoints = info.nPoints;
      } else { // have to count the bits to see how many data values are stored
        info.ndataPoints = GribNumbers.countBits(bm);
      }
    }

    return info;
  }

  // debugging - do not use
  public int[] readRawData(RandomAccessFile raf) throws IOException {
    Grib1Gds gds = getGDS();
    Grib1DataReader reader = new Grib1DataReader(pdss.getDecimalScale(), gds.getScanMode(), gds.getNxRaw(), gds.getNyRaw(), gds.getNpts(), dataSection.getStartingPosition());
    byte[] bm = (bitmap == null) ? null : bitmap.getBitmap(raf);
    return reader.getDataRaw(raf, bm);
  }
}


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

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import thredds.util.DodsURLExtractor;

/**
 * A description
 *
 * @author Ethan Davis
 * @author Bas Retsios
 * @since Jun 8, 2005 15:34:04 -0600
 */
public class CrawlableDatasetDods implements CrawlableDataset {
	static private org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(CrawlableDatasetDods.class);

	private static DodsURLExtractor urlExtractor = null;

	private static Map listDatasetsMap = null; // maintain an in-memory copy for performance reasons .. TODO: add a version-check

	private String path;

	private URLConnection pathUrlConnection = null; // store this, for performance reasons

	private String name;

	private Object configObj = null;

	protected CrawlableDatasetDods() {
	}

	protected CrawlableDatasetDods(String path, Object configObj)
	{

		if (urlExtractor == null)
			urlExtractor = new DodsURLExtractor();

		if (listDatasetsMap == null) // for performance
			listDatasetsMap = new HashMap();

		if (configObj != null) {
			log.debug("CrawlableDatasetDods(): config object not null, it will be ignored <"
							+ configObj.toString() + ">.");
			this.configObj = configObj;
		}

		if (path.startsWith("http:")) {
			this.path = path;

			try {
				new URI(path); // check syntax .. URISyntaxException if its not good
				name = getName(path);
			} catch (URISyntaxException e) {
		        String tmpMsg = "Bad URI syntax for path <" + path + ">: " + e.getMessage();
		        log.debug( "CrawlableDatasetDods(): " + tmpMsg);
		        throw new IllegalArgumentException( tmpMsg);
			}

		    // Check if this accessPoint URL is an OPeNDAP server URL.
			// For now commented-out because it takes far too long when expanding a directory:
			// all links would be tested, because a CrawlableDataset is new-ed too fast (when its parent is expanded).
			/*
		    String apVersionString = path + (path.endsWith("/") ? "version" : "/version");
		    String apVersionResultContent = null;
		    try
		    {
		      apVersionResultContent = urlExtractor.getTextContent( apVersionString);
		    }
		    catch (java.io.IOException e)
		    {
		      String tmpMsg = "The accessPoint URL is not an OPeNDAP server URL (no version info) <" + apVersionString + ">";
		      log.error( "CrawlableDatasetDods(): " + tmpMsg, e);
		    }
		    if ( apVersionResultContent == null ||
		    	 (apVersionResultContent.indexOf( "DODS") == -1 &&
		         apVersionResultContent.indexOf( "OPeNDAP") == -1 &&
		         apVersionResultContent.indexOf( "DAP") == -1))
		    {
		      String tmpMsg = "The accessPoint URL version info is not valid <" + apVersionResultContent + ">";
		      log.error(  "CrawlableDatasetDods(): " + tmpMsg);
		    }
		    */
		} else {
			String tmpMsg = "Invalid url <" + path + ">.";
			log.debug("CrawlableDatasetDods(): " + tmpMsg);
			throw new IllegalArgumentException(tmpMsg);
		}
	}

	private CrawlableDatasetDods(CrawlableDatasetDods parent, String childPath)
	{
		String normalChildPath = childPath.startsWith("/")?childPath.substring(1):childPath;
		this.path = parent.getPath();
		this.path += this.path.endsWith("/") ? normalChildPath : "/" + normalChildPath;
		this.name = getName(path);
		this.configObj = null;
	}

	private String getName(String path) {
		// Attempt to return the last name in the path name sequence.
		if (!path.equals("/")) {
			String tmpName = path.endsWith("/")
                       ? path.substring(0, path.length() - 1) : path;
      int index = tmpName.lastIndexOf("/");
			if (index != -1)
				tmpName = tmpName.substring(index + 1);
			return tmpName;
		} else
			return path;
	}

  /**
   * Provide access to the java.net.URI that this CrawlableDataset represents.
   *
   * @return the java.net.URI that this CrawlableDataset represents.
   */
  public URI getUri()
  {
    try
    {
      return new URI( this.path);
    }
    catch ( URISyntaxException e )
    {
      return null;
    }
  }

  public Object getConfigObject() {
		return configObj;
	}

	public String getPath() {
		return (this.path);
	}

	public String getName() {
		return (this.name);
	}

	public boolean isCollection() {
		return isCollection(path);
	}

	public CrawlableDataset getDescendant( String relativePath )
	{
    if ( relativePath.startsWith( "/"))
      throw new IllegalArgumentException( "Path must be relative <" + relativePath + ">.");
    return new CrawlableDatasetDods(this, relativePath);
	}

	// how do we determine if a url is a collection?
	// we can't count on a trailing backslash, as this was removed by CrawlableDatasetFactory
	// for now, assume collection unless a known file extension is encountered
	private static String [] knownFileExtensions = {".hdf", ".xml", ".nc", ".bz2", ".cdp", ".jpg"};

	private static boolean isCollection(String path)
	{
		String testPath = path.toLowerCase(); // otherwise our matches may fail
		if (isDodsDataset(testPath))
			return false;
		else
		{
			int i = 0;
			while ((i < (knownFileExtensions.length)) && !testPath.endsWith(knownFileExtensions[i]))
					++i;
			return (i >= knownFileExtensions.length); // i < length means we deal with a known file ==> no collection
		}
	}

	private static String [] dodsExtensions = {".html", ".htm", ".das", ".dds", ".info"};

	private static String getDodsExtension(String path)
	{
		String extension = "";
		String testPath = path.toLowerCase(); // otherwise our matches may fail

		int i = 0;
		while ((i < (dodsExtensions.length)) && !testPath.endsWith(dodsExtensions[i]))
			++i;
		if (i < dodsExtensions.length)
			extension = dodsExtensions[i];
		return extension;
	}

	private static boolean isDodsDataset(String path)
	{
		return getDodsExtension(path).length() > 0;
	}

	private static String removeDodsExtension(String path)
	{
		String dodsExtension = getDodsExtension(path);
		if (dodsExtension.length() > 0)
			path = path.substring(0, path.length() - dodsExtension.length());

		return path;
	}

	// This function shouldn't be here !!!
	// It is a workaround for many OPeNDAP servers that crop part of their urls (the /opendap-bin/nph-dods/ part)
	// e.g. of server with problem (2-Nov-2006): http://acdisc.sci.gsfc.nasa.gov/opendap-bin/nph-dods/OPENDAP/Giovanni/
	private String forceChild(String url)
	{
		String prefix = path;
		if (prefix.endsWith("/"))
			prefix = path.substring(0, path.length() - 1); // because the url also contains a '/' that we will use
		int j = url.substring(0, url.length() - 1).lastIndexOf('/'); // url.length() - 1 was intentional .. if the last char is a '/', we're interested in the previous one.
		if (j >= 0)
		{
			String ret = prefix + url.substring(j);
			return ret;
		}
		else // relative paths .. leave intact
			return url;
	}

	public List listDatasets() throws IOException {

		if (!this.isCollection()) {
			String tmpMsg = "This dataset <" + this.getPath()
					+ "> is not a collection dataset.";
			log.error("listDatasets(): " + tmpMsg);
			throw new IllegalStateException(tmpMsg);
		}

		if (listDatasetsMap.containsKey(path)) // shortcut .. for performance
			return (List)listDatasetsMap.get(path);
		else
		{
			List list = new ArrayList();
			List pathList = new ArrayList(); // only for detecting duplicates (after removing the extension, sometimes we end up with duplicates)

			// Get list of possible datasets from current URL.
			List possibleDsList = null;
			try {
				String openPath = path;
				if (!openPath.endsWith("/")) // if you skip this, you will find that relative URLs don't work (fails in "extract", and in particular in URL u = new URL(baseURL, value))
					openPath += "/";
				possibleDsList = urlExtractor.extract(openPath);
			} catch (java.io.IOException e) {
				log.warn("listDatasets(): IOException while extracting dataset info from given OPeNDAP directory <"
								+ path + ">, return empty list: " + e.getMessage());
				return (list);
			}

			// Handle each link in the current access path.
			String curDsUrlString = null;
			for (Iterator it = possibleDsList.iterator(); it.hasNext(); ) {
				curDsUrlString = (String) it.next();
				// Perform some tests on curDsUrlString
				// Skip datasets that aren't OPeNDAP datasets (".html") or
				// collection datasets ("/").
				if ((!isDodsDataset(curDsUrlString)) && (!isCollection(curDsUrlString))) {
					log.warn("expandThisLevel(): Dataset isn't an OPeNDAP dataset or collection dataset, skip <"
									+ path + ">.");
					continue;
				}

				curDsUrlString = removeDodsExtension(curDsUrlString);

        // This function goes a bit too far trying to recover from servers that drop part of URL path.
        // However, it also converts URLs that point to external servers to be subdirectories of this CrDS.
        //curDsUrlString = forceChild(curDsUrlString);

        // Skip any URLs that aren't children of this CrDs
        if ( !curDsUrlString.startsWith( path ) )
        {
          log.debug( "listDatasets(): skipping URL <" + curDsUrlString + ">, not child of this CrDs <" + path + ">." );
          continue;
        }

        if (pathList.contains(curDsUrlString))
					continue; // duplicate
				else
					pathList.add(curDsUrlString);

				// Avoid links back down the path hierarchy (i.e., parent directory links).
				// Comment: this call was taken over from CrawlableDatasetFile. Since we use forceChild, this call is currently useless.
				if (!curDsUrlString.startsWith(path)) {
					log.debug("listDatasets(): current path <" + curDsUrlString
							+ "> not child of given" + " location <" + path
							+ ">, skip.");
					continue;
				}

				try {
					new URI(curDsUrlString); // syntax check
				} catch (URISyntaxException e) {
					log.error("listDatasets(): Skipping dataset  <"
							+ curDsUrlString + "> due to URISyntaxException: "
							+ e.getMessage());
					continue;
				}

				log.debug("listDatasets(): handle dataset (" + curDsUrlString
								+ ")");

				// So far so good .. curDsUrlString passed all tests, thus add it to the list
        try {
          list.add(CrawlableDatasetFactory.createCrawlableDataset(
							curDsUrlString, this.getClass().getName(), null));
				} catch (ClassNotFoundException e) {
					log.warn("listDatasets(): Can't make CrawlableDataset for child url <"
									+ curDsUrlString + ">: " + e.getMessage());
				} catch (NoSuchMethodException e) {
					log.warn("listDatasets(): Can't make CrawlableDataset for child url <"
									+ curDsUrlString + ">: " + e.getMessage());
				} catch (IllegalAccessException e) {
					log.warn("listDatasets(): Can't make CrawlableDataset for child url <"
									+ curDsUrlString + ">: " + e.getMessage());
				} catch (InvocationTargetException e) {
					log.warn("listDatasets(): Can't make CrawlableDataset for child url <"
									+ curDsUrlString + ">: " + e.getMessage());
				} catch (InstantiationException e) {
					log.warn("listDatasets(): Can't make CrawlableDataset for child url <"
									+ curDsUrlString + ">: " + e.getMessage());
				}
			}

			listDatasetsMap.put(path, list); // remember it next time, for performance

			return list;
		}
	}

	public List listDatasets(CrawlableDatasetFilter filter) throws IOException {
		List list = this.listDatasets();
		if (filter == null)
			return list;
		List retList = new ArrayList();
		for (Iterator it = list.iterator(); it.hasNext();) {
			CrawlableDataset curDs = (CrawlableDataset) it.next();
			if (filter.accept(curDs)) {
				retList.add(curDs);
			}
		}
		return (retList);
	}

	public CrawlableDataset getParentDataset() {
		if (!path.equals("/")) {
      String parentPath = path;
      int index = parentPath.lastIndexOf( "/", parentPath.endsWith( "/") ? parentPath.length() - 2 : parentPath.length() - 1 );
      if ( index != -1 )
        parentPath = parentPath.substring( 0, index + 1 );

			return new CrawlableDatasetDods( parentPath, null);
		} else
			return null;
	}

  public boolean exists() {
	if (pathUrlConnection == null)
	try {
		URL u = new URL(path);
		pathUrlConnection = u.openConnection();
	} catch (MalformedURLException e) {
	} catch (IOException e) {
	}
  if ( pathUrlConnection != null )
  try {
		int responseCode = ((HttpURLConnection)pathUrlConnection).getResponseCode();
		if (responseCode >= 200 && responseCode < 300) // Successful
			return true;
	} catch (IOException e) {
	}
	return false;
  }

  public long length() {
    if (this.isCollection())
      return (0);
    if (pathUrlConnection == null)
    {
    	try {
	        URL u = new URL(path);
	        pathUrlConnection = u.openConnection();
	    } catch (MalformedURLException e) {
	    } catch (IOException e) {
	    }
    }
    if (pathUrlConnection != null)
    	return pathUrlConnection.getContentLength();
    else
    	return (-1);
  }

	public Date lastModified() {
		if (pathUrlConnection == null)
		{
			try {
				URL u = new URL(path);
				pathUrlConnection = u.openConnection();
			} catch (MalformedURLException e) {
			} catch (IOException e) {
			}
		}

		if (pathUrlConnection != null)
		{
			long lastModified = pathUrlConnection.getLastModified();
			if (lastModified != 0)
			{
				Calendar cal = Calendar.getInstance();
				cal.clear();
				cal.setTimeInMillis(lastModified);
				return (cal.getTime());
			}
			else
				return null;
		}
		else
			return null;
	}

  public String toString()
  {
    return this.path;
  }
}


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

package dap4.dap4lib;

import dap4.core.util.DapUtil;
import dap4.core.util.Escape;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Provide an extended form of URI parser that can handle
 * multiple protocols and can parse the query and fragment parts.
 */

public class XURI
{

    //////////////////////////////////////////////////
    // Constants
    static final String QUERYSEP = "&";
    static final String FRAGMENTSEP = "&";

    // Define assembly flags

    static public enum Parts
    {
        FORMAT, // format protocol
        BASE, // base protocol
        PWD,  // including user
        HOST, // including port
        PATH,
        QUERY,
        FRAG;
    }

    // Mnemonics
    static public final EnumSet<Parts> URLONLY = EnumSet.of(Parts.BASE, Parts.PWD, Parts.HOST, Parts.PATH);
    static public final EnumSet<Parts> URLALL = EnumSet.of(Parts.FORMAT, Parts.BASE, Parts.PWD, Parts.HOST, Parts.PATH, Parts.QUERY, Parts.FRAG);
    static public final EnumSet<Parts> URLBASE = EnumSet.of(Parts.BASE, Parts.PWD, Parts.HOST, Parts.PATH, Parts.QUERY, Parts.FRAG);
    static public final EnumSet<Parts> URLPATH = EnumSet.of(Parts.PATH, Parts.QUERY, Parts.FRAG);

    //////////////////////////////////////////////////
    // Instance variables

    protected String originaluri = null;
    protected boolean isfile = false;

    protected String baseprotocol = null; // rightmost protocol
    protected String formatprotocol = null; // leftmost protocol
    protected String userinfo = null;
    protected String host = null;
    protected String path = null;
    protected String query = null;
    protected String frag = null;

    // Following are url decoded
    protected Map<String, String> queryfields // decomposed query
            = new HashMap<String, String>();
    protected Map<String, String> fragfields // decomposed fragment
            = new HashMap<String, String>();

    //////////////////////////////////////////////////
    // Constructor

    public XURI(String xurl)
            throws URISyntaxException
    {
        if(xurl == null)
            throw new URISyntaxException(xurl, "Null URI");
        // save the original uri
        this.originaluri = xurl;
        // The uri may be multi-protocol: e.g. dap4:file:...
        // Additionally, this may be a windows path, so it
        // will look like it has a single character protocol
        // that is really the drive letter.

        int[] breakpoint = new int[1];
        List<String> protocols = DapUtil.getProtocols(xurl, breakpoint); // should handle drive letters also
        String remainder = xurl.substring(breakpoint[0], xurl.length());
        switch (protocols.size()) {
        case 0: // pretend it is a file
            this.formatprotocol = "file";
            this.baseprotocol = "file";
            break;
        case 1:
            this.formatprotocol = protocols.get(0);
            if("file".equalsIgnoreCase(this.formatprotocol))
                this.baseprotocol = "file";   // default conversion
            else
                this.baseprotocol = "http";   // default conversion
            break;
        case 2:
            this.baseprotocol = protocols.get(0);
            this.formatprotocol = protocols.get(1);
            break;
        default:
            throw new URISyntaxException(xurl, "Too many protocols: at most 2 allowed");
        }
        this.isfile = "file".equals(this.baseprotocol);
        // The standard URI parser does not handle 'file:' very well,
        // so handle specially
        URI uri;
        if(this.isfile)
            parsefile(remainder);
        else
            parsenonfile(remainder); // not a file: url
        if(this.query != null)
            parseQuery(this.query);
        if(this.frag != null)
            parseFragment(this.frag);
    }

    protected void
    parsenonfile(String remainder)
            throws URISyntaxException
    {
        // Construct a usable url and parse it
        URI uri = new URI(baseprotocol + ":" + remainder);
        // Extract the parts of the uri so they can
        // be modified and later reassembled
        this.userinfo = canonical(uri.getUserInfo());
        this.host = canonical(uri.getHost());
        if(uri.getPort() > 0)
            this.host += (":" + uri.getPort());
        this.path = canonical(uri.getPath());
        // Parse the raw query (before decoding)
        this.query = uri.getRawQuery();
        // Parse the raw fragment (before decoding)
        this.frag = canonical(uri.getFragment());
    }

    protected void
    parsefile(String remainder)
    {
        // Pull off the query and fragment parts, if any.
        String query = null;
        String fragment = null;
        int qindex = remainder.indexOf("?");
        int findex = remainder.lastIndexOf("#");
        if(qindex >= 0) { // query and maybe fragment
            if(findex >= 0 && findex > qindex) {// both
                fragment = remainder.substring(findex + 1, remainder.length());
                remainder = remainder.substring(0, findex);
            }
            query = remainder.substring(qindex + 1, remainder.length());
            remainder = remainder.substring(0, qindex);
        } else if(findex >= 0) { // fragment, no query
            fragment = remainder.substring(findex + 1, remainder.length());
            remainder = remainder.substring(0, findex);
        }

        // Standardize path part to be absolute
        // => single leading '/' or windows drive letter
        StringBuilder buf = new StringBuilder(remainder);
        for(int i = 0; i < remainder.length(); i++) { // remove all leading '/'
            if(buf.charAt(i) != '/') break;
            buf.deleteCharAt(i);
        }
        // check for drive letter
        if(DapUtil.DRIVELETTERS.indexOf(buf.charAt(0)) < 0
                || buf.charAt(1) != ':') { // no drive letter, prepend '/'
            buf.insert(0, '/');
        }

        remainder = buf.toString();
        this.path = remainder;
        this.frag = fragment;
        this.query = query;
    }

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

    public String getOriginal()
    {
        return originaluri;
    }

    public String getBaseProtocol()
    {
        return baseprotocol;
    }

    public String getFormatProtocol()
    {
        return this.formatprotocol;
    }

    public void setBaseProtocol(String base)
    {
        this.baseprotocol = base;
    }

    public boolean isFile()
    {
        return this.isfile;
    }

    public String getUserinfo()
    {
        return this.userinfo;
    }

    public String getHost()
    {
        return this.host;
    }

    public String getPath()
    {
        return this.path;
    }

    public String getQuery()
    {
        return this.query;
    }

    public String getFrag()
    {
        return this.frag;
    }

    public Map<String, String> getQueryFields()
    {
        return this.queryfields;
    }

    public Map<String, String> getFragFields()
    {
        return this.fragfields;
    }

    public XURI
    parseQuery(String q)
    {
        if(q == null || q.length() == 0) return this;
        String[] params = q.split(QUERYSEP);
        this.query = q;
        for(String param : params) {
            String[] pair = param.split("[=]");
            String name = Escape.urlDecode(pair[0]);
            name = name.toLowerCase(); // for consistent lookup
            String value = "";
            if(pair.length > 1) {
                value = Escape.urlDecode(pair[1]);
                this.queryfields.put(name, value);
            }
        }
        return this;
    }

    public XURI
    parseFragment(String f)
    {
        if(f == null || f.length() == 0) return this;
        String[] params = f.split(FRAGMENTSEP);
        if(params != null && params.length > 0) {
            this.frag = f;
            for(String param : params) {
                String[] pair = param.split("=");
                String name = Escape.urlDecode(pair[0]);
                name = name.toLowerCase(); // for consistent lookup
                String value = (pair.length == 2 ? Escape.urlDecode(pair[1])
                        : "");
                this.fragfields.put(name, value);
            }
        }
        return this;
    }

    //////////////////////////////////////////////////
    // API

    /**
     * Reassemble the url using the specified parts
     *
     * @param parts to include
     * @return the assembled uri
     */

    public String
    assemble(EnumSet<Parts> parts)
    {
        StringBuilder uri = new StringBuilder();
        // Note that format and base may be same, so case it out
        int useformat = (parts.contains(Parts.FORMAT) ? 1 : 0);
        int usebase = (parts.contains(Parts.BASE) ? 2 : 0);
        switch (useformat + usebase) {
        case 0 + 0: // neither
            break;
        case 1 + 0: // FORMAT only
            uri.append(this.formatprotocol + ":");
            break;
        case 2 + 0: // BASE only
            uri.append(this.baseprotocol + ":");
            break;
        case 2 + 1: // both
            uri.append(this.formatprotocol + ":");
            if(!this.baseprotocol.equals(this.formatprotocol))
                uri.append(this.formatprotocol + ":");
            break;
        }
        uri.append(this.baseprotocol.equals("file") ? "/" : "//");

        if(userinfo != null && parts.contains(Parts.PWD))
            uri.append(this.userinfo + ":");
        if(this.host != null && parts.contains(Parts.HOST))
            uri.append(this.host);
        if(this.path != null && parts.contains(Parts.PATH))
            uri.append(this.path);
        if(this.query != null && parts.contains(Parts.QUERY))
            uri.append("?" + this.query);
        if(this.frag != null && parts.contains(Parts.FRAG))
            uri.append("#" + this.frag);
        return uri.toString();
    }

    /**
     * Canonicalize a part of a URL
     *
     * @param s part of the url
     */
    static public String
    canonical(String s)
    {
        if(s != null) {
            s = s.trim();
            if(s.length() == 0)
                s = null;
        }
        return s;
    }

    public String toString()
    {
        return originaluri;
    }
}


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

package ucar.nc2.dataset.conv;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.AxisType;
import ucar.nc2.time.*;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.nc2.dataset.transform.WRFEtaTransformBuilder;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;
import ucar.unidata.util.StringUtil2;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * WRF netcdf output files.
 *

 * Note: Apparently WRF netcdf files before version 2 didnt output the projection origin, so
 * we cant properly georeference them.
 *

 * This Convention currently only supports ARW output, identified as DYN_OPT=2 or GRIDTYPE=C
 *
 * @author caron
 */

public class WRFConvention extends CoordSysBuilder {

  // static private java.text.SimpleDateFormat dateFormat;

 // static {
 //   dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
 //   dateFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  //}

  public static boolean isMine(NetcdfFile ncfile) {
    if (null == ncfile.findDimension("south_north")) return false;

    // ARW only
    Attribute att = ncfile.findGlobalAttribute("DYN_OPT");
    if (att != null) {
      if (att.getNumericValue().intValue() != 2) return false;
    } else {
      att = ncfile.findGlobalAttribute("GRIDTYPE");
      if (att != null) {
        if (!att.getStringValue().equalsIgnoreCase("C") &&
                !att.getStringValue().equalsIgnoreCase("E"))
          return false;
      }
    }

    att = ncfile.findGlobalAttribute("MAP_PROJ");
    return att != null;
  }

  private double centerX = 0.0, centerY = 0.0;
  private ProjectionCT projCT = null;
  private boolean gridE = false;

  public WRFConvention() {
    this.conventionName = "WRF";
  }

  /* Implementation notes

    There are 2 WRFs : NMM ("Non-hydrostatic Mesoscale Model" developed at NOAA/NCEP) and
    1) NMM ("Non-hydrostatic Mesoscale Model" developed at NOAA/NCEP)
      GRIDTYPE="E"
      DYN_OPT = 4

      This is a staggered grid that requires special processing.

    2) ARW ("Advanced Research WRF", developed at MMM)
      GRIDTYPE="C"
      DYN_OPT = 2
      DX, DY grid spaceing in meters (must be equal)

      the Arakawa C staggered grid (see ARW 2.2 p 3-17)
      the + are the "non-staggered" grid:

      + V + V + V +
      U T U T U T U
      + V + V + V +
      U T U T U T U
      + V + V + V +
      U T U T U T U
      + V + V + V +
   */

  /* ARW Users Guide p 3-19

7. MAP_PROJ_NAME: Character string specifying type of map projection. Valid entries are:
  "polar" -> Polar stereographic
  "lambert" -> Lambert conformal (secant and tangent)
  "mercator" -> Mercator

8. MOAD_KNOWN_LAT/MOAD_KNOWN_LON (= CEN_LAT, CEN_LON):
  Real latitude and longitude of the center point in the grid.

9. MOAD_STAND_LATS (= TRUE_LAT1/2):
  2 real values for the "true" latitudes (where grid spacing is exact).
  Must be between -90 and 90, and the values selected depend on projection:
  Polar-stereographic: First value must be the latitude at which the grid
spacing is true. Most users will set this equal to their center latitude.
Second value must be +/-90. for NH/SH grids.
  Lambert Conformal: Both values should have the same sign as the center
latitude. For a tangential lambert conformal, set both to the same value
(often equal to the center latitude). For a secant Lambert Conformal, they
may be set to different values.
  Mercator: The first value should be set to the latitude you wish your grid
  spacing to be true (often your center latitude). Second value is not used.

10. MOAD_STAND_LONS (=STAND_LON): This is one entry specifying the longitude in degrees East (-
180->180) that is parallel to the y-axis of your grid, (sometimes referred to as the
orientation of the grid). This should be set equal to the center longitude in most cases.

---------------------------------
version 3.1
http://www.mmm.ucar.edu/wrf/users/docs/user_guide_V3.1/users_guide_chap5.htm

The definition for map projection options:

map_proj =  1: Lambert Conformal
            2: Polar Stereographic
            3: Mercator
            6: latitude and longitude (including global)
*/

  public void augmentDataset(NetcdfDataset ds, CancelTask cancelTask) {
    if (null != ds.findVariable("x")) return; // check if its already been done - aggregating enhanced datasets.

    Attribute att = ds.findGlobalAttribute("GRIDTYPE");
    gridE = att != null && att.getStringValue().equalsIgnoreCase("E");

    // kludge in fixing the units
    List<Variable> vlist = ds.getVariables();
    for (Variable v : vlist) {
      att = v.findAttributeIgnoreCase(CDM.UNITS);
      if (att != null) {
        String units = att.getStringValue();
        if (units != null)
          v.addAttribute(new Attribute(CDM.UNITS, normalize(units))); // removes the old
      }
    }

    // make projection transform
    att = ds.findGlobalAttribute("MAP_PROJ");
    int projType = att.getNumericValue().intValue();
    boolean isLatLon = false;

    if (projType == 203) {

      /* centerX = centralLon;
      centerY = centralLat;
      ds.addCoordinateAxis( makeLonCoordAxis( ds, "longitude", ds.findDimension("west_east")));
      ds.addCoordinateAxis( makeLatCoordAxis( ds, "latitude", ds.findDimension("south_north")));  */

      Variable glat = ds.findVariable("GLAT");
      if (glat == null) {
        parseInfo.format("Projection type 203 - expected GLAT variable not found%n");
      } else {
        glat.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));
        if (gridE) glat.addAttribute(new Attribute(_Coordinate.Stagger, CDM.ARAKAWA_E));
        glat.setDimensions("south_north west_east");
        glat.setCachedData(convertToDegrees(glat), false);
        glat.addAttribute(new Attribute(CDM.UNITS, CDM.LAT_UNITS));
      }

      Variable glon = ds.findVariable("GLON");
      if (glon == null) {
        parseInfo.format("Projection type 203 - expected GLON variable not found%n");
      } else {
        glon.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
        if (gridE) glon.addAttribute(new Attribute(_Coordinate.Stagger, CDM.ARAKAWA_E));
        glon.setDimensions("south_north west_east");
        glon.setCachedData(convertToDegrees(glon), false);
        glon.addAttribute(new Attribute(CDM.UNITS, CDM.LON_UNITS));
      }

      VariableDS v = new VariableDS(ds, null, null, "LatLonCoordSys", DataType.CHAR, "", null, null);
      v.addAttribute(new Attribute(_Coordinate.Axes, "GLAT GLON Time"));
      Array data = Array.factory(DataType.CHAR, new int[]{}, new char[]{' '});
      v.setCachedData(data, true);
      ds.addVariable(null, v);

      Variable dataVar = ds.findVariable("LANDMASK");
      dataVar.addAttribute(new Attribute(_Coordinate.Systems, "LatLonCoordSys"));

    } else {
      double lat1 = findAttributeDouble(ds, "TRUELAT1");
      double lat2 = findAttributeDouble(ds, "TRUELAT2");
      double centralLat = findAttributeDouble(ds, "CEN_LAT");  // center of grid
      double centralLon = findAttributeDouble(ds, "CEN_LON");  // center of grid

      double standardLon = findAttributeDouble(ds, "STAND_LON"); // true longitude
      double standardLat = findAttributeDouble(ds, "MOAD_CEN_LAT");

      ProjectionImpl proj = null;
      switch (projType) {
        case 0: // for diagnostic runs with no georeferencing
          proj = new FlatEarth();
          projCT = new ProjectionCT("flat_earth", "FGDC", proj);
          // System.out.println(" using LC "+proj.paramsToString());
          break;
        case 1:
          proj = new LambertConformal(standardLat, standardLon, lat1, lat2, 0.0, 0.0, 6370);
          projCT = new ProjectionCT("Lambert", "FGDC", proj);
          // System.out.println(" using LC "+proj.paramsToString());
          break;
        case 2:
          // Thanks to Heiko Klein for figuring out WRF Stereographic
          double lon0 = (Double.isNaN(standardLon)) ? centralLon : standardLon;
          double lat0 = (Double.isNaN(centralLat)) ? lat2 : centralLat;  // ?? 7/20/2010
          double scaleFactor = (1 + Math.abs( Math.sin(Math.toRadians(lat1)))) / 2.;  // R Schmunk 9/10/07
          // proj = new Stereographic(lat2, lon0, scaleFactor);
          proj = new Stereographic(lat0, lon0, scaleFactor, 0.0, 0.0, 6370);
          projCT = new ProjectionCT("Stereographic", "FGDC", proj);
          break;
        case 3:
          proj = new Mercator(standardLon, lat1, 0.0, 0.0, 6370); // thanks to Robert Schmunk with edits for non-MOAD grids
          projCT = new ProjectionCT("Mercator", "FGDC", proj);
          // proj = new TransverseMercator(standardLat, standardLon, 1.0);
          //projCT = new ProjectionCT("TransverseMercator", "FGDC", proj);
          break;
        case 6:
          // version 3 "lat-lon", including global
          // http://www.mmm.ucar.edu/wrf/users/workshops/WS2008/presentations/1-2.pdf
          // use 2D XLAT, XLONG
          isLatLon = true;
          for (Variable v : vlist) {
            if (v.getShortName().startsWith("XLAT")) {
              v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));
              removeConstantTimeDim(ds, v);

            } else if (v.getShortName().startsWith("XLONG")) {
              v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
              removeConstantTimeDim(ds, v);

            } else if (v.getShortName().equals("T")) { // ANOTHER MAJOR KLUDGE to pick up 4D fields
              v.addAttribute(new Attribute(_Coordinate.Axes, "Time XLAT XLONG z"));
            } else if (v.getShortName().equals("U")) {
              v.addAttribute(new Attribute(_Coordinate.Axes, "Time XLAT_U XLONG_U z"));
            } else if (v.getShortName().equals("V")) {
              v.addAttribute(new Attribute(_Coordinate.Axes, "Time XLAT_V XLONG_V z"));
            } else if (v.getShortName().equals("W")) {
              v.addAttribute(new Attribute(_Coordinate.Axes, "Time XLAT XLONG z_stag"));
            }
          }
          break;
        default:
          parseInfo.format("ERROR: unknown projection type = %s%n", projType);
          break;
      }

      if (proj != null) {
        LatLonPointImpl lpt1 = new LatLonPointImpl(centralLat, centralLon); // center of the grid
        ProjectionPoint ppt1 = proj.latLonToProj(lpt1, new ProjectionPointImpl());
        centerX = ppt1.getX();
        centerY = ppt1.getY();
        if (debug) {
          System.out.println("centerX=" + centerX);
          System.out.println("centerY=" + centerY);
        }
      }

      // make axes
      if (!isLatLon) {
        ds.addCoordinateAxis(makeXCoordAxis(ds, "x", ds.findDimension("west_east")));
        ds.addCoordinateAxis(makeXCoordAxis(ds, "x_stag", ds.findDimension("west_east_stag")));
        ds.addCoordinateAxis(makeYCoordAxis(ds, "y", ds.findDimension("south_north")));
        ds.addCoordinateAxis(makeYCoordAxis(ds, "y_stag", ds.findDimension("south_north_stag")));
      }
      ds.addCoordinateAxis(makeZCoordAxis(ds, "z", ds.findDimension("bottom_top")));
      ds.addCoordinateAxis(makeZCoordAxis(ds, "z_stag", ds.findDimension("bottom_top_stag")));

      if (projCT != null) {
        VariableDS v = makeCoordinateTransformVariable(ds, projCT);
        v.addAttribute(new Attribute(_Coordinate.AxisTypes, "GeoX GeoY"));
        if (gridE) v.addAttribute(new Attribute(_Coordinate.Stagger, CDM.ARAKAWA_E));
        ds.addVariable(null, v);
      }
    }

    // time coordinate variations
    if (ds.findVariable("Time") == null) { // Can skip this if its already there, eg from NcML
      CoordinateAxis taxis = makeTimeCoordAxis(ds, "Time", ds.findDimension("Time"));
      if (taxis == null)
        taxis = makeTimeCoordAxis(ds, "Time", ds.findDimension("Times"));
      if (taxis != null)
        ds.addCoordinateAxis(taxis);
    }

    ds.addCoordinateAxis(makeSoilDepthCoordAxis(ds, "ZS"));

    ds.finish();
  }

  private void removeConstantTimeDim(NetcdfDataset ds, Variable v) {
    int[] shape = v.getShape();
    if (v.getRank() == 3 && shape[0] == 1) { // remove time dependcies - MAJOR KLUDGE
      Variable view;
      try {
        view = v.slice(0, 0);
      } catch (InvalidRangeException e) {
        log.error("Cant remove first dimension in variable {}", v);
        return;
      }
      ds.removeVariable(null, v.getShortName());
      ds.addVariable(null, view);
    }
  }

  private Array convertToDegrees(Variable v) {
    Array data;
    try {
      data = v.read();
      data = data.reduce();
    } catch (IOException ioe) {
      throw new RuntimeException("data read failed on " + v.getFullName() + "=" + ioe.getMessage());
    }
    IndexIterator ii = data.getIndexIterator();
    while (ii.hasNext()) {
      ii.setDoubleCurrent(Math.toDegrees(ii.getDoubleNext()));
    }
    return data;
  }

  // pretty much WRF specific
  private String normalize(String units) {
    switch (units) {
      case "fraction":
        units = "";
        break;
      case "dimensionless":
        units = "";
        break;
      case "NA":
        units = "";
        break;
      case "-":
        units = "";
        break;
      default:
        units = StringUtil2.substitute(units, "**", "^");
        units = StringUtil2.remove(units, '}');
        units = StringUtil2.remove(units, '{');
        break;
    }
    return units;
  }
  /////////////////////////////////////////////////////////////////////////

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

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

    if (vname.equalsIgnoreCase("x") || vname.equalsIgnoreCase("x_stag"))
      return AxisType.GeoX;

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

    if (vname.equalsIgnoreCase("y") || vname.equalsIgnoreCase("y_stag"))
      return AxisType.GeoY;

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

    if (vname.equalsIgnoreCase("z") || vname.equalsIgnoreCase("z_stag"))
      return AxisType.GeoZ;

    if (vname.equalsIgnoreCase("Z"))
      return AxisType.Height;

    if (vname.equalsIgnoreCase("time") || vname.equalsIgnoreCase("times"))
      return AxisType.Time;

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

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

    return null;
  }

  /**
   * Does increasing values of Z go vertical  up?
   *
   * @param v for thsi axis
   * @return "up" if this is a Vertical (z) coordinate axis which goes up as coords get bigger,
   *         else return "down"
   */
  public String getZisPositive(CoordinateAxis v) {
    return "down"; //eta coords decrease upward
  }

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

  private CoordinateAxis makeLonCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    double dx = 4 * findAttributeDouble(ds, "DX");
    int nx = dim.getLength();
    double startx = centerX - dx * (nx - 1) / 2;

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(), "degrees_east", "synthesized longitude coordinate");
    v.setValues(nx, startx, dx);
    v.addAttribute(new Attribute(_Coordinate.AxisType, "Lon"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    return v;
  }

  private CoordinateAxis makeLatCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    double dy = findAttributeDouble(ds, "DY");
    int ny = dim.getLength();
    double starty = centerY - dy * (ny - 1) / 2;

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(), "degrees_north", "synthesized latitude coordinate");
    v.setValues( ny, starty, dy);
    v.addAttribute(new Attribute(_Coordinate.AxisType, "Lat"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    return v;
  }

  private CoordinateAxis makeXCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    double dx = findAttributeDouble(ds, "DX") / 1000.0; // km ya just gotta know
    int nx = dim.getLength();
    double startx = centerX - dx * (nx - 1) / 2; // ya just gotta know
    //System.out.println(" originX= "+originX+" startx= "+startx);

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(), "km", "synthesized GeoX coordinate from DX attribute");
    v.setValues(nx, startx, dx);
    v.addAttribute(new Attribute(_Coordinate.AxisType, "GeoX"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    if (gridE) v.addAttribute(new Attribute(_Coordinate.Stagger, CDM.ARAKAWA_E));
    return v;
  }

  private CoordinateAxis makeYCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    double dy = findAttributeDouble(ds, "DY") / 1000.0;
    int ny = dim.getLength();
    double starty = centerY - dy * (ny - 1) / 2; // - dy/2; // ya just gotta know
    //System.out.println(" originY= "+originY+" starty= "+starty);

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(), "km", "synthesized GeoY coordinate from DY attribute");
    v.setValues(ny, starty, dy);
    v.addAttribute(new Attribute(_Coordinate.AxisType, "GeoY"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    if (gridE) v.addAttribute(new Attribute(_Coordinate.Stagger, CDM.ARAKAWA_E));
    return v;
  }

  private CoordinateAxis makeZCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;

    String fromWhere = axisName.endsWith("stag") ? "ZNW" : "ZNU";

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(), "", "eta values from variable " + fromWhere);
    v.addAttribute(new Attribute(CF.POSITIVE, CF.POSITIVE_DOWN)); // eta coordinate is 1.0 at bottom, 0 at top
    v.addAttribute(new Attribute(_Coordinate.AxisType, "GeoZ"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    // create eta values from file variables: ZNU, ZNW
    // But they are a function of time though the values are the same in the sample file
    // NOTE: Use first time sample assuming all are the same!
    Variable etaVar = ds.findVariable(fromWhere);
    if (etaVar == null) return makeFakeCoordAxis(ds, axisName, dim);

    int n = etaVar.getShape(1); //number of eta levels
    int[] origin = new int[]{0, 0};
    int[] shape = new int[]{1, n};
    try {
      Array array = etaVar.read(origin, shape);//read first time slice
      ArrayDouble.D1 newArray = new ArrayDouble.D1(n);
      IndexIterator it = array.getIndexIterator();
      int count = 0;
      while (it.hasNext()) {
        double d = it.getDoubleNext();
        newArray.set(count++, d);
      }
      v.setCachedData(newArray, true);
    } catch (Exception e) {
      e.printStackTrace();
    }//ADD: error?

    return v;
  }

  private CoordinateAxis makeFakeCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.SHORT, dim.getShortName(), "", "synthesized coordinate: only an index");
    v.addAttribute(new Attribute(_Coordinate.AxisType, "GeoZ"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    v.setValues(dim.getLength(), 0, 1);
    return v;
  }

  private CoordinateAxis makeTimeCoordAxis(NetcdfDataset ds, String axisName, Dimension dim) {
    if (dim == null) return null;
    int nt = dim.getLength();
    Variable timeV = ds.findVariable("Times");
    if (timeV == null) return null;

    Array timeData;
    try {
      timeData = timeV.read();
    } catch (IOException ioe) {
      return null;
    }

    ArrayDouble.D1 values = new ArrayDouble.D1(nt);
    int count = 0;

    if (timeData instanceof ArrayChar) {
      ArrayChar.StringIterator iter = ((ArrayChar) timeData).getStringIterator();
      String testTimeStr = ((ArrayChar) timeData).getString(0);
      boolean isCanonicalIsoStr = true;
      // Maybe too specific to require WRF to give 10 digits or
      //  dashes for the date (e.g. yyyy-mm-dd)?
      final String wrfDateWithUnderscore = "([\\-\\d]{10})_";
      final Pattern wrfDateWithUnderscorePattern = Pattern.compile(wrfDateWithUnderscore);
      Matcher m = wrfDateWithUnderscorePattern.matcher(testTimeStr);
      if (wrfDateWithUnderscorePattern.matcher(testTimeStr) != null) {
    	  isCanonicalIsoStr = false;
      }

      while (iter.hasNext()) {
        String dateS = iter.next();
        try {
          CalendarDate cd;
          if (isCanonicalIsoStr) {
            cd = CalendarDateFormatter.isoStringToCalendarDate(null, dateS);
          } else {
        	cd = CalendarDateFormatter.isoStringToCalendarDate(null, dateS.replaceFirst("_", "T"));
          }

          values.set(count++, (double) cd.getMillis() / 1000);

        } catch (IllegalArgumentException e) {
          parseInfo.format("ERROR: cant parse Time string = <%s> err= %s%n", dateS, e.getMessage());

          // one more try
          String startAtt = ds.findAttValueIgnoreCase(null, "START_DATE", null);
          if ((nt == 1) && (null != startAtt)) {
            try {
              CalendarDate cd = CalendarDateFormatter.isoStringToCalendarDate(null, startAtt);
              values.set(0, (double) cd.getMillis() / 1000);
            } catch (IllegalArgumentException e2) {
              parseInfo.format("ERROR: cant parse global attribute START_DATE = <%s> err=%s%n", startAtt, e2.getMessage());
            }
          }
        }
      }
    } else {
      IndexIterator iter = timeData.getIndexIterator();
      while (iter.hasNext()) {
        String dateS = (String) iter.next();
        try {
          CalendarDate cd = CalendarDateFormatter.isoStringToCalendarDate(null, dateS);
          values.set(count++, (double) cd.getMillis() / 1000);
        } catch (IllegalArgumentException e) {
          parseInfo.format("ERROR: cant parse Time string = %s%n", dateS);
        }
      }

    }

    CoordinateAxis v = new CoordinateAxis1D(ds, null, axisName, DataType.DOUBLE, dim.getShortName(),
            "secs since 1970-01-01 00:00:00", "synthesized time coordinate from Times(time)");
    v.addAttribute(new Attribute(_Coordinate.AxisType, "Time"));
    if (!axisName.equals(dim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, dim.getShortName()));

    v.setCachedData(values, true);
    return v;
  }

  private VariableDS makeSoilDepthCoordAxis(NetcdfDataset ds, String coordVarName) {
    Variable coordVar = ds.findVariable(coordVarName);
    if (null == coordVar)
      return null;

    Dimension soilDim = null;
    List<Dimension> dims = coordVar.getDimensions();
    for (Dimension d : dims) {
      if (d.getShortName().startsWith("soil_layers"))
        soilDim = d;
    }
    if (null == soilDim)
      return null;

    if (coordVar.getRank() == 1) {
      coordVar.addAttribute(new Attribute(CF.POSITIVE, CF.POSITIVE_DOWN)); // soil depth gets larger as you go down
      coordVar.addAttribute(new Attribute(_Coordinate.AxisType, "GeoZ"));
      if (!coordVarName.equals(soilDim.getShortName()))
        coordVar.addAttribute(new Attribute(_Coordinate.AliasForDimension, soilDim.getShortName()));
      return (VariableDS) coordVar;
    }

    String units = ds.findAttValueIgnoreCase(coordVar, CDM.UNITS, "");

    CoordinateAxis v = new CoordinateAxis1D(ds, null, "soilDepth", DataType.DOUBLE, soilDim.getShortName(), units, "soil depth");
    v.addAttribute(new Attribute(CF.POSITIVE, CF.POSITIVE_DOWN)); // soil depth gets larger as you go down
    v.addAttribute(new Attribute(_Coordinate.AxisType, "GeoZ"));
    v.addAttribute(new Attribute(CDM.UNITS, CDM.UNITS));
    if (!v.getShortName().equals(soilDim.getShortName()))
      v.addAttribute(new Attribute(_Coordinate.AliasForDimension, soilDim.getShortName()));

    //read first time slice
    int n = coordVar.getShape(1);
    int[] origin = new int[]{0, 0};
    int[] shape = new int[]{1, n};
    try {
      Array array = coordVar.read(origin, shape);
      ArrayDouble.D1 newArray = new ArrayDouble.D1(n);
      IndexIterator it = array.getIndexIterator();
      int count = 0;
      while (it.hasNext()) {
        double d = it.getDoubleNext();
        newArray.set(count++, d);
      }
      v.setCachedData(newArray, true);
    } catch (Exception e) {
      e.printStackTrace();
    }

    return v;
  }

  private double findAttributeDouble(NetcdfDataset ds, String attname) {
    Attribute att = ds.findGlobalAttributeIgnoreCase(attname);
    if (att == null) return Double.NaN;
    return att.getNumericValue().doubleValue();
  }

  /**
   * Assign CoordinateTransform objects to Coordinate Systems.
   */
  protected void assignCoordinateTransforms(NetcdfDataset ncDataset) {
    super.assignCoordinateTransforms(ncDataset);

    // any cs whose got a vertical coordinate with no units
    List<CoordinateSystem> csys = ncDataset.getCoordinateSystems();
    for (CoordinateSystem cs : csys) {
      if (cs.getZaxis() != null) {
        String units = cs.getZaxis().getUnitsString();
        if ((units == null) || (units.trim().length() == 0)) {
          VerticalCT vct = makeWRFEtaVerticalCoordinateTransform(ncDataset, cs);
          if (vct != null)
            cs.addCoordinateTransform(vct);
          parseInfo.format("***Added WRFEta verticalCoordinateTransform to %s%n", cs.getName());
        }
      }
    }
  }

  private VerticalCT makeWRFEtaVerticalCoordinateTransform(NetcdfDataset ds, CoordinateSystem cs) {
    if ((null == ds.findVariable("PH")) || (null == ds.findVariable("PHB")) ||
            (null == ds.findVariable("P")) || (null == ds.findVariable("PB")))
      return null;

    WRFEtaTransformBuilder builder = new WRFEtaTransformBuilder(cs);
    return builder.makeCoordinateTransform(ds, null);
  }
}

/*
 NMM . output from gridgen. these are the input files to WRF i think
netcdf Q:/grid/netcdf/wrf/geo_nmm.d01.nc {
 dimensions:
   Time = UNLIMITED;   // (1 currently)
   DateStrLen = 19;
   west_east = 136;
   south_north = 309;
   land_cat = 24;
   soil_cat = 16;
   month = 12;
 variables:
   char Times(Time=1, DateStrLen=19);
   float XLAT_M(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "degrees latitude";
     :description = "Latitude on mass grid";
     :stagger = "M";
   float XLONG_M(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "degrees longitude";
     :description = "Longitude on mass grid";
     :stagger = "M";
   float XLAT_V(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "degrees latitude";
     :description = "Latitude on velocity grid";
     :stagger = "V";
   float XLONG_V(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "degrees longitude";
     :description = "Longitude on velocity grid";
     :stagger = "V";
   float E(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "-";
     :description = "Coriolis E parameter";
     :stagger = "M";
   float F(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "-";
     :description = "Coriolis F parameter";
     :stagger = "M";
   float LANDMASK(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "none";
     :description = "Landmask : 1=land, 0=water";
     :stagger = "M";
   float LANDUSEF(Time=1, land_cat=24, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XYZ";
     :units = "category";
     :description = "24-category USGS landuse";
     :stagger = "M";
   float LU_INDEX(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "category";
     :description = "Dominant category";
     :stagger = "M";
   float HGT_M(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "meters MSL";
     :description = "Topography height";
     :stagger = "M";
   float HGT_V(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "meters MSL";
     :description = "Topography height";
     :stagger = "V";
   float SOILTEMP(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "Kelvin";
     :description = "Annual mean deep soil temperature";
     :stagger = "M";
   float SOILCTOP(Time=1, soil_cat=16, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XYZ";
     :units = "category";
     :description = "16-category top-layer soil type";
     :stagger = "M";
   float SOILCBOT(Time=1, soil_cat=16, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XYZ";
     :units = "category";
     :description = "16-category bottom-layer soil type";
     :stagger = "M";
   float ALBEDO12M(Time=1, month=12, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XYZ";
     :units = "percent";
     :description = "Monthly surface albedo";
     :stagger = "M";
   float GREENFRAC(Time=1, month=12, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XYZ";
     :units = "fraction";
     :description = "Monthly green fraction";
     :stagger = "M";
   float SNOALB(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "percent";
     :description = "Maximum snow albedo";
     :stagger = "M";
   float SLOPECAT(Time=1, south_north=309, west_east=136);
     :FieldType = 104; // int
     :MemoryOrder = "XY ";
     :units = "category";
     :description = "Dominant category";
     :stagger = "M";

 :TITLE = "OUTPUT FROM GRIDGEN";
 :SIMULATION_START_DATE = "0000-00-00_00:00:00";
 :WEST-EAST_GRID_DIMENSION = 136; // int
 :SOUTH-NORTH_GRID_DIMENSION = 309; // int
 :BOTTOM-TOP_GRID_DIMENSION = 0; // int
 :WEST-EAST_PATCH_START_UNSTAG = 1; // int
 :WEST-EAST_PATCH_END_UNSTAG = 136; // int
 :WEST-EAST_PATCH_START_STAG = 1; // int
 :WEST-EAST_PATCH_END_STAG = 136; // int
 :SOUTH-NORTH_PATCH_START_UNSTAG = 1; // int
 :SOUTH-NORTH_PATCH_END_UNSTAG = 309; // int
 :SOUTH-NORTH_PATCH_START_STAG = 1; // int
 :SOUTH-NORTH_PATCH_END_STAG = 309; // int
 :GRIDTYPE = "E";
 :DX = 0.111143f; // float
 :DY = 0.106776f; // float
 :DYN_OPT = 4; // int
 :CEN_LAT = 21.0f; // float
 :CEN_LON = 80.0f; // float
 :TRUELAT1 = 1.0E20f; // float
 :TRUELAT2 = 1.0E20f; // float
 :MOAD_CEN_LAT = 0.0f; // float
 :STAND_LON = 1.0E20f; // float
 :POLE_LAT = 90.0f; // float
 :POLE_LON = 0.0f; // float
 :corner_lats = 3.8832555f, 36.602543f, 36.602543f, 3.8832555f, 3.8931324f, 36.61482f, 36.59018f, 3.873307f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f; // float
 :corner_lons = 65.589096f, 61.982986f, 98.01701f, 94.410904f, 65.69548f, 62.114895f, 98.14889f, 94.51727f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f; // float
 :MAP_PROJ = 203; // int
 :MMINLU = "USGS";
 :ISWATER = 16; // int
 :ISICE = 24; // int
 :ISURBAN = 1; // int
 :ISOILWATER = 14; // int
 :grid_id = 1; // int
 :parent_id = 1; // int
 :i_parent_start = 0; // int
 :j_parent_start = 0; // int
 :i_parent_end = 136; // int
 :j_parent_end = 309; // int
 :parent_grid_ratio = 1; // int
}

*/