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

package thredds.catalog;

import thredds.catalog.parser.jdom.InvCatalogFactory10;

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

import ucar.nc2.constants.CDM;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;
import ucar.nc2.Attribute;
import ucar.nc2.constants.FeatureType;
import org.jdom2.output.XMLOutputter;
import org.jdom2.output.Format;
import org.jdom2.Element;

/**
 * Metadata for "enhanced catalogs", type THREDDS.
 *
 * @author John Caron
 */

public class ThreddsMetadata {
  protected boolean inherited;

  protected List<Source> creators = new ArrayList<Source>();
  protected List<Contributor> contributors = new ArrayList<Contributor>();
  protected List<DateType> dates = new ArrayList<DateType>();
  protected List<InvDocumentation> docs = new ArrayList<InvDocumentation>();
  protected List<Vocab> keywords = new ArrayList<Vocab>(); //
  protected List<InvMetadata> metadata = new ArrayList<InvMetadata>();
  protected List<Vocab> projects = new ArrayList<Vocab>();
  protected List<InvProperty> properties = new ArrayList<InvProperty>(); //
  protected List<Source> publishers = new ArrayList<Source>(); //
  protected List<Variables> variables = new ArrayList<Variables>();  //
  protected String variableMapLink;

  // singles
  protected GeospatialCoverage gc;
  protected DateRange timeCoverage;
  //protected CalendarDateRange cdateCoverage;
  protected String authorityName, serviceName;
  protected FeatureType dataType;
  protected DataFormatType dataFormat;
  protected double dataSize = 0.0;

  public ThreddsMetadata(boolean inherited) {
    this.inherited = inherited;
  }

  /**
   * Copy Constructor.
   *
   * @param from copy from here
   */
  public ThreddsMetadata(ThreddsMetadata from) {
    this.inherited = from.inherited;
    add(from, true);
  }

  /**
   * Add all the content from another ThreddsMetadata
   *
   * @param tmd              get content from here
   * @param includeInherited if false, dont add inherited InvMetadata
   */
  public void add(ThreddsMetadata tmd, boolean includeInherited) {
    creators.addAll(tmd.getCreators());
    contributors.addAll(tmd.getContributors());
    dates.addAll(tmd.getDates());
    docs.addAll(tmd.getDocumentation());
    keywords.addAll(tmd.getKeywords());
    projects.addAll(tmd.getProjects());
    properties.addAll(tmd.getProperties());
    publishers.addAll(tmd.getPublishers());
    variables.addAll(tmd.getVariables());

    if (includeInherited)
      metadata.addAll(tmd.getMetadata());
    else {
      for (InvMetadata mdata : tmd.getMetadata() ) {
        if (!mdata.isInherited())
          metadata.add(mdata);
      }
    }

    // LOOK! should be copies ??!!
    if (gc == null) gc = tmd.getGeospatialCoverage();
    if (timeCoverage == null) timeCoverage = tmd.getTimeCoverage();
    if (serviceName == null) serviceName = tmd.getServiceName();
    if (dataType == null) dataType = tmd.getDataType();
    if (dataSize == 0.0) dataSize = tmd.getDataSize();
    if (dataFormat == null) dataFormat = tmd.getDataFormatType();
    if (authorityName == null) authorityName = tmd.getAuthority();
    if (variableMapLink == null) variableMapLink = tmd.getVariableMap();
  }

  /**
   * Add a creator
   * @param c add this
   */
  public void addCreator(Source c) {
    if (c != null) creators.add(c);
  }

  /**
   * @return list of creators (type Source); may be empty, not null.
   */
  public List<ThreddsMetadata.Source> getCreators() {
    return creators;
  }

  /**
   * Set list of creators (type Source); may be empty, not null.
   * @param creators set creators to this list
   */
  public void setCreators(List<Source> creators) {
    this.creators = creators;
  }

  /**
   * Add a contributor
   * @param c add this
   */
  public void addContributor(Contributor c) {
    if (c != null) contributors.add(c);
  }

  /**
   * @return list of contributors (type Contributor); may be empty, not null.
   */
  public List<Contributor> getContributors() {
    return contributors;
  }

  /**
   * Set list of contributors (type Contributor); may be empty, not null.
   * @param contributors set contributers to this
   */
  public void setContributors(List<Contributor> contributors) {
    this.contributors = contributors;
  }

  /**
   * Add a date
   * @param d add this dateType
   */
  public void addDate(DateType d) {
    if (d != null) dates.add(d);
  }

  /**
   * @return list of DateType; may be empty, not null.
   */
  public List<DateType> getDates() {
    return dates;
  }

  /**
   * Add a documentation
   * @param d add this
   */
  public void addDocumentation(InvDocumentation d) {
    if (d != null) docs.add(d);
  }

  /**
   * @return list of InvDocumentation; may be empty, not null.
   */
  public List<InvDocumentation> getDocumentation() {
    return docs;
  }

  /**
   * Add a keyword
   * @param keyword add this
   */
  public void addKeyword(Vocab keyword) {
    if (keyword != null) keywords.add(keyword);
  }

  /**
   * @return list of keywords; may be empty, not null.
   */
  public List<Vocab> getKeywords() {
    return keywords;
  }

  /**
   * Set list of keywords; may be empty, not null.
   * @param keywords set list of keywords to this
   */
  public void setKeywords(List<Vocab> keywords) {
    this.keywords = keywords;
  }

  /**
   * Add InvMetadata
   * @param m add this
   */
  public void addMetadata(InvMetadata m) {
    if (m != null) metadata.add(m);
  }

  /**
   * remove an InvMetadata element from list, using equals() to locate it.
   * @param m remove this
   */
  public void removeMetadata(InvMetadata m) {
    metadata.remove(m);
  }

  /**
   * @return list of InvMetadata; may be empty, not null.
   */
  public List<InvMetadata> getMetadata() {
    return metadata;
  }

  /**
   * Add a project
   * @param project add this
   */
  public void addProject(Vocab project) {
    if (project != null) projects.add(project);
  }

  /**
   * @return list of projects (type Vocab); may be empty, not null.
   */
  public List<Vocab> getProjects() {
    return projects;
  }

  /**
   * Set list of projects (type Vocab); may be empty, not null.
   * @param projects set list of projects to this
   */
  public void setProjects(List<Vocab> projects) {
    this.projects = projects;
  }

  /**
   * Add a property
   * @param p add this
   */
  public void addProperty(InvProperty p) {
    if (p != null) properties.add(p);
  }

  public void addProperties(List<Attribute> atts) {
    for (Attribute att : atts)
      if (att.isString()) properties.add(new InvProperty(att.getShortName(), att.getStringValue()));
  }

  /**
   * @return list of properties; may be empty, not null.
   */
  public List<InvProperty> getProperties() {
    return properties;
  }

  /**
   * Add a publisher
   * @param p add this
   */
  public void addPublisher(Source p) {
    if (p != null) publishers.add(p);
  }

  /**
   * @return list of publishers (type Source); may be empty, not null.
   */
  public List<Source> getPublishers() {
    return publishers;
  }

  /**
   * Set list of publishers (type Source); may be empty, not null.
   * @param publishers set list of publishers to this
   */
  public void setPublishers(List<Source> publishers) {
    this.publishers = publishers;
  }

  /**
   * Add variables
   * @param vs add this
   */
  public void addVariables(Variables vs) {
    if (vs != null) variables.add(vs);
  }

  /**
   * @return list of Variables; may be empty, not null.
   */
  public List<Variables> getVariables() {
    return variables;
  }

  /**
   * Add a variable Map link
   * @param xlinkHref         : URI of xlink
   * */
  public void addVariableMapLink(String xlinkHref) {
    variableMapLink = xlinkHref;
  }

  public String getVariableMap() {
    return variableMapLink;
  }

  /**
   * set GeospatialCoverage element
   * @param gc set GeospatialCoverage to this
   */
  public void setGeospatialCoverage(GeospatialCoverage gc) {
    this.gc = gc;
  }

  /**
   * @return GeospatialCoverage element
   */
  public GeospatialCoverage getGeospatialCoverage() {
    return gc;
  }

  /**
   * set TimeCoverage element
   * @param tc set TimeCoverage to this
   */
  public void setTimeCoverage(DateRange tc) {
    this.timeCoverage = tc;
  }

  /**
   * set TimeCoverage from a CalendarDateRange
   * @param cdc set CalendarDateRange to this
   */
  public void setTimeCoverage(CalendarDateRange cdc) {
    if (cdc == null) return;
    this.timeCoverage = cdc.toDateRange();
  }

  /**
   * @return TimeCoverage element  as DateRange
   */
  public DateRange getTimeCoverage() {
    return timeCoverage;
  }

  /**
   * @return TimeCoverage element as CalendarDateRange
   */
  public CalendarDateRange getCalendarDateCoverage() {
    return timeCoverage == null ? null : CalendarDateRange.of(timeCoverage);
  }

  /**
   * @return true if this is inherited
   */
  public boolean isInherited() {
    return inherited;
  }

  /**
   * Set inherited
   * @param inherited set true if inherited
   */
  public void setInherited(boolean inherited) {
    this.inherited = inherited;
  }

  /**
   * @return serviceName
   */
  public String getServiceName() {
    return serviceName;
  }

  /**
   * Set serviceName
   * @param serviceName set service name to this, must be valid service
   */
  public void setServiceName(String serviceName) {
    this.serviceName = serviceName;
  }

  /**
   * @return dataType
   */
  public FeatureType getDataType() {
    return dataType;
  }

  /**
   * Set dataType
   * @param dataType set to this dataType
   */
  public void setDataType(FeatureType dataType) {
    this.dataType = dataType;
  }

  /**
   * @return dataType
   */
  public DataFormatType getDataFormatType() {
    return dataFormat;
  }

  /**
   * Set dataTypeFormat
   * @param dataFormat set to this dataTypeFormat
   */
  public void setDataFormatType(DataFormatType dataFormat) {
    this.dataFormat = dataFormat;
  }

  /**
   * @return authority
   */
  public String getAuthority() {
    return authorityName;
  }

  /**
   * Set authority
   * @param authorityName set to this
   */
  public void setAuthority(String authorityName) {
    this.authorityName = authorityName;
  }

  /**
   * @return specific type of documentation
   * @param type match on doc.getType()
   */
  public String getDocumentation(String type) {
    for (InvDocumentation doc : getDocumentation()) {
      String dtype = doc.getType();
      if ((dtype != null) && dtype.equalsIgnoreCase(type)) return doc.getInlineContent();
    }
    return null;
  }

  /* @return Documentation that are xlinks
  public List<InvDocumentation> getDocumentationLinks() {
    List<InvDocumentation> result = new ArrayList<InvDocumentation>();
    for (InvDocumentation doc : getDocumentation()) {
      if (doc.hasXlink())
        result.add(doc);
    }
    return result;
  }

  /* set the list of Documentation that are xlinks
  public void setDocumentationLinks(List newdocs) {
    ArrayList result = new ArrayList(newdocs);
    java.util.List olddocs = getDocumentation();
    for (int i=0; i<olddocs.size(); i++) {
      InvDocumentation doc = (InvDocumentation) olddocs.get(i);
      if (!doc.hasXlink()) // keep the non-links
        result.add(doc);
    }
    docs = result;
  } */

  /**
   * @return specific type of documentation = history
   */
  public String getHistory() {
    return getDocumentation("history");
  }

  /**
   * set specific type of documentation = history
   * @param history add (or replace) the history documentation element to this content
   */
  public void setHistory(String history) {
    addDocumentation("history", history);
  }

  /**
   * @return specific type of documentation = processing_level
   */
  public String getProcessing() {
    return getDocumentation("processing_level");
  }

  /**
   * set specific type of documentation = processing_level
   * @param processing add (or replace) the history documentation element to this content
   */
  public void setProcessing(String processing) {
    addDocumentation("processing_level", processing);
  }

  /**
   * @return specific type of documentation = rights
   */
  public String getRights() {
    return getDocumentation("rights");
  }

  /**
   * set specific type of documentation = rights
   * @param rights add (or replace) the history documentation element to this content
   */
  public void setRights(String rights) {
    addDocumentation("rights", rights);
  }

  /**
   * @return specific type of documentation = summary
   */
  public String getSummary() {
    return getDocumentation("summary");
  }

  /**
   * set specific type of documentation = summary
   * @param summary add (or replace) the history documentation element to this content
   */
  public void setSummary(String summary) {
    addDocumentation("summary", summary);
  }

  /**
   * @return the data size in bytes. A value of 0.0 or NaN means not set.
   */
  public double getDataSize() {
    return dataSize;
  }

  /**
   * Set size (bytes)
   * @param size the data size in bytes. A value of 0.0 or NaN means not set.
   */
  public void setDataSize(double size) {
    this.dataSize = size;
  }

  public boolean hasDataSize() {
    return dataSize != 0.0 && !Double.isNaN(dataSize);
  }

  /**
   * set specified type of documentation
   *
   * @param type    type of documentation
   * @param content documentation contenet
   */
  public void addDocumentation(String type, String content) {
    if (content == null) {
      removeDocumentation(type);
      return;
    }

    content = content.trim();
    for (InvDocumentation doc : getDocumentation()) {
      String dtype = doc.getType();
      if ((dtype != null) && dtype.equalsIgnoreCase(type)) {
        doc.setInlineContent(content);
        return;
      }
    }
    if (content.length() > 0)
      addDocumentation(new InvDocumentation(null, null, null, type, content));
  }

  /**
   * remove all instances of specified type of documentation
   * @param type type of documentation
   */
  public void removeDocumentation(String type) {
    Iterator iter = docs.iterator();
    while (iter.hasNext()) {
      InvDocumentation doc = (InvDocumentation) iter.next();
      String dtype = doc.getType();
      if ((dtype != null) && dtype.equalsIgnoreCase(type))
        iter.remove();
    }
  }

  String dump(int n) {
    StringBuilder buff = new StringBuilder(100);

    if (docs.size() > 0) {
      String indent = InvDatasetImpl.indent(n + 2);
      buff.append(indent);
      buff.append("Docs:\n");
      for (InvDocumentation doc : docs) {
        buff.append(InvDatasetImpl.indent(n + 4)).append(doc).append("\n");
      }
    }

    if (metadata.size() > 0) {
      String indent = InvDatasetImpl.indent(n + 2);
      buff.append(indent);
      buff.append("Metadata:\n");
      for (InvMetadata m  : metadata) {
        buff.append(InvDatasetImpl.indent(n + 4)).append(m).append("\n");
      }
    }

    if (properties.size() > 0) {
      String indent = InvDatasetImpl.indent(n + 2);
      buff.append(indent);
      buff.append("Properties:\n");
      for (InvProperty p : properties) {
        buff.append(InvDatasetImpl.indent(n + 4)).append(p).append("\n");
      }
    }

    return buff.toString();
  }

  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof ThreddsMetadata)) return false;
    return o.hashCode() == this.hashCode();
  }

  // Generated by IntelliJ (with a few minor changes).
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      long temp;
      result = 37 * result + (inherited ? 1 : 0);
      result = 29 * result + (creators != null ? creators.hashCode() : 0);
      result = 29 * result + (contributors != null ? contributors.hashCode() : 0);
      result = 29 * result + (dates != null ? dates.hashCode() : 0);
      result = 29 * result + (docs != null ? docs.hashCode() : 0);
      result = 29 * result + (keywords != null ? keywords.hashCode() : 0);
      result = 29 * result + (metadata != null ? metadata.hashCode() : 0);
      result = 29 * result + (projects != null ? projects.hashCode() : 0);
      result = 29 * result + (properties != null ? properties.hashCode() : 0);
      result = 29 * result + (publishers != null ? publishers.hashCode() : 0);
      result = 29 * result + (variables != null ? variables.hashCode() : 0);
      result = 29 * result + (gc != null ? gc.hashCode() : 0);
      result = 29 * result + (timeCoverage != null ? timeCoverage.hashCode() : 0);
      result = 29 * result + (authorityName != null ? authorityName.hashCode() : 0);
      result = 29 * result + (serviceName != null ? serviceName.hashCode() : 0);
      result = 29 * result + (dataType != null ? dataType.hashCode() : 0);
      result = 29 * result + (dataFormat != null ? dataFormat.hashCode() : 0);
      temp = dataSize != +0.0d ? Double.doubleToLongBits(dataSize) : 0l;
      result = 29 * result + (int) (temp ^ (temp >>> 32));
      hashCode = result;
    }
    return hashCode;
  }

  private volatile int hashCode = 0; // Bloch, item 8 - lazily initialize hash value

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements Contributor type.
   */
  static public class Contributor {
    private String name;
    private String role;

    public Contributor() {
    }

    public Contributor(String name, String role) {
      this.name = name;
      this.role = role;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
      hashCode = 0;
    }

    public String getRole() {
      return role;
    }

    public void setRole(String role) {
      this.role = role;
      hashCode = 0;
    }

    // for bean editing
    static public String editableProperties() {
      return "role name";
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Contributor)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        // result = 37*result + dataset.getName().hashCode();
        result = 37 * result + getName().hashCode();
        if (null != getRole())
          result = 37 * result + getRole().hashCode();
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements Source type, used by publisher and creator elements.
   */
  static public class Source {
    private Vocab name, long_name;
    private String url, email;

    // need no-arg constructor for bean handling
    public Source() {
      this.name = new Vocab();
      this.long_name = new Vocab();
    }

    public Source(Vocab name, String url, String email) {
      this.name = name;
      this.url = url;
      this.email = email;
    }

    public Vocab getNameVocab() {
      return this.name;
    }

    /**
     * @return name
     */
    public String getName() {
      return this.name.getText();
    }

    /**
     * Set name
     * @param name to set
     */
    public void setName(String name) {
      this.name.setText(name);
      hashCode = 0;
    }

    public String getUrl() {
      return url;
    }

    public void setUrl(String url) {
      this.url = url;
      hashCode = 0;
    }

    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
      hashCode = 0;
    }

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

    public void setVocabulary(String vocabulary) {
      this.name.setVocabulary(vocabulary);
      this.long_name.setVocabulary(vocabulary);
      hashCode = 0;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Source)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        // result = 37*result + dataset.getName().hashCode();
        result = 37 * result + getName().hashCode();
        if (null != getVocabulary())
          result = 37 * result + getVocabulary().hashCode();
        if (null != getUrl())
          result = 37 * result + getUrl().hashCode();
        if (null != getEmail())
          result = 37 * result + getEmail().hashCode();
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

    // for bean editing
    static public String hiddenProperties() {
      return "nameVocab";
    }

    static public String editableProperties() {
      return "name email url vocabulary";
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements Vocab type, just text with an optional "vocabulary" attribute.
   */
  static public class Vocab {
    private String text;
    private String vocabulary;

    // need no-arg constructor for bean handling
    public Vocab() {
    }

    public Vocab(String text, String vocabulary) {
      this.text = text;
      this.vocabulary = vocabulary;
    }

    public String getText() {
      return text;
    }

    public void setText(String text) {
      this.text = text;
      hashCode = 0;
    }

    public String getVocabulary() {
      return vocabulary;
    }

    public void setVocabulary(String vocabulary) {
      this.vocabulary = vocabulary;
      hashCode = 0;
    }

    // for bean editing
    static public String editableProperties() {
      return "text vocabulary";
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Vocab)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        // result = 37*result + dataset.getName().hashCode();
        if (null != getText())
          result = 37 * result + getText().hashCode();
        if (null != getVocabulary())
          result = 37 * result + getVocabulary().hashCode();
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

  }

  /* 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"));
  } */

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

  /**
   * Implements GeospatialCoverage type.
   */
  static public class GeospatialCoverage {
    static private Range defaultEastwest = new Range(0.0, 0.0, Double.NaN, CDM.LON_UNITS);
    static private Range defaultNorthsouth = new Range(0.0, 0.0, Double.NaN, CDM.LAT_UNITS);
    static private Range defaultUpdown = new Range(0.0, 0.0, Double.NaN, "km");
    static private GeospatialCoverage empty = new GeospatialCoverage();

    private Range eastwest, northsouth, updown;
    private boolean isGlobal = false;
    private String zpositive = "up";
    private List<Vocab> names = new ArrayList<Vocab>(); // Vocab

    // need no-arg constructor for bean handling
    public GeospatialCoverage() {
    }

    public GeospatialCoverage(Range eastwest, Range northsouth, Range updown, List<Vocab> names, String zpositive) {
      this.eastwest = eastwest; //  : new Range(defaultEastwest);
      this.northsouth = northsouth; // : new Range(defaultNorthsouth);
      this.updown = updown; //  : new Range(defaultUpdown);
      if (names != null) this.names = new ArrayList<Vocab>(names);
      if (zpositive != null) this.zpositive = zpositive;

      if (names != null) {
        for (Vocab name : names) {
          String elem = name.getText();
          if (elem.equalsIgnoreCase("global")) isGlobal = true;
        }
      }
    }

    public void extend(GeospatialCoverage gc) {
      this.eastwest.extend(gc.getEastWestRange());
      this.northsouth.extend(gc.getNorthSouthRange());
    }

    public boolean isEmpty() {
      return this.equals(empty);
    }

    public Range getEastWestRange() {
      return eastwest;
    }

    public Range getNorthSouthRange() {
      return northsouth;
    }

    public Range getUpDownRange() {
      return updown;
    }

    public List<ThreddsMetadata.Vocab> getNames() {
      return names;
    }

    /**
     * @return "up" or "down"
     */
    public String getZPositive() {
      return zpositive;
    }

    public void setZPositive(String positive) {
      this.zpositive = positive;
      hashCode = 0;
    }

    /**
     * @return true or false
     */
    public boolean getZPositiveUp() {
      return zpositive.equalsIgnoreCase("up");
    }

    public void setZPositiveUp(boolean positive) {
      this.zpositive = positive ? "up" : "down";
      hashCode = 0;
    }

    public boolean isValid() {
      return isGlobal || ((eastwest != null) && (northsouth != null));
    }

    /**
     * @return true if coverage is Global
     */
    public boolean isGlobal() {
      return isGlobal;
    }

    /**
     * Set isGlobal
     * @param isGlobal true if global coverage
     */
    public void setGlobal(boolean isGlobal) {
      this.isGlobal = isGlobal;
      hashCode = 0;
    }

    /**
     * @return starting latitude, or Double.NaN if not valie
     */
    public double getLatStart() {
      return (northsouth == null) ? Double.NaN : northsouth.start;
    }

    /**
     * Set starting latitude
     * @param start starting latitude, decimal degrees north
     */
    public void setLatStart(double start) {
      if (northsouth == null)
        northsouth = new Range(defaultNorthsouth);
      northsouth.start = start;
      hashCode = 0;
    }

    /**
     * @return latitude extent - may be positive or negetive
     */
    public double getLatExtent() {
      return (northsouth == null) ? Double.NaN : northsouth.size;
    }

    /**
     * Set latitude extent
     * @param size latitude extent, decimal degrees north
     */
    public void setLatExtent(double size) {
      if (northsouth == null)
        northsouth = new Range(defaultNorthsouth);
      northsouth.size = size;
      hashCode = 0;
    }

    /**
     * @return latitude resolution: 0.0 or NaN means not set.
     */
    public double getLatResolution() {
      return (northsouth == null) ? Double.NaN : northsouth.resolution;
    }

    /**
     * Set latitude resolution
     * @param resolution latitude resolution, decimal degrees north
     */
    public void setLatResolution(double resolution) {
      if (northsouth == null)
        northsouth = new Range(defaultNorthsouth);
      northsouth.resolution = resolution;
      hashCode = 0;
    }

    /**
     * @return latitude units
     */
    public String getLatUnits() {
      return (northsouth == null) ? null : northsouth.units;
    }

    /**
     * Set latitude units
     * @param units set units
     */
    public void setLatUnits(String units) {
      if (northsouth == null)
        northsouth = new Range(defaultNorthsouth);
      northsouth.units = units;
      hashCode = 0;
    }

    // LOOK not dealing with units degrees_south
    public double getLatNorth() {
      return Math.max(northsouth.start, northsouth.start + northsouth.size);
    }

    public double getLatSouth() {
      return Math.min(northsouth.start, northsouth.start + northsouth.size);
    }

    /**
     * @return starting longitude
     */
    public double getLonStart() {
      return (eastwest == null) ? Double.NaN : eastwest.start;
    }

    /**
     * Set starting longitude
     * @param start starting longitude
     */
    public void setLonStart(double start) {
      if (eastwest == null)
        eastwest = new Range(defaultEastwest);
      eastwest.start = start;
      hashCode = 0;
    }

    /**
     * @return longitude extent - may be positive or negetive
     */
    public double getLonExtent() {
      return (eastwest == null) ? Double.NaN : eastwest.size;
    }

    /**
     * Set longitude extent
     * @param size longitude extent
     */
    public void setLonExtent(double size) {
      if (eastwest == null)
        eastwest = new Range(defaultEastwest);
      eastwest.size = size;
      hashCode = 0;
    }

    /**
     * @return longitude resolution: 0.0 or NaN means not set.
     */
    public double getLonResolution() {
      return (eastwest == null) ? Double.NaN : eastwest.resolution;
    }

    /**
     * Set longitude resolution
     * @param resolution longitude resolution
     */
    public void setLonResolution(double resolution) {
      if (eastwest == null)
        eastwest = new Range(defaultEastwest);
      eastwest.resolution = resolution;
      hashCode = 0;
    }

    /**
     * @return longitude units
     */
    public String getLonUnits() {
      return (eastwest == null) ? null : eastwest.units;
    }

    /**
     * Set longitude units
     * @param units longitude units
     */
    public void setLonUnits(String units) {
      if (eastwest == null)
        eastwest = new Range(defaultEastwest);
      eastwest.units = units;
      hashCode = 0;
    }

    // LOOK not dealing with units degrees_west
    public double getLonEast() {
      if (eastwest == null) return Double.NaN;
      return Math.max(eastwest.start, eastwest.start + eastwest.size);
    }

    public double getLonWest() {
      if (eastwest == null) return Double.NaN;
      return Math.min(eastwest.start, eastwest.start + eastwest.size);
    }

    /**
     * @return starting height
     */
    public double getHeightStart() {
      return updown == null ? 0.0 : updown.start;
    }

    /**
     * Set starting height
     * @param start starting height
     */
    public void setHeightStart(double start) {
      if (updown == null)
        updown = new Range(defaultUpdown);
      updown.start = start;
      hashCode = 0;
    }

    /**
     * @return height extent - may be positive or negetive
     */
    public double getHeightExtent() {
      return updown == null ? 0.0 : updown.size;
    }

    /**
     * Set height extent
     * @param size height extent
     */
    public void setHeightExtent(double size) {
      if (updown == null)
        updown = new Range(defaultUpdown);
      updown.size = size;
      hashCode = 0;
    }

    /**
     * @return height resolution: 0.0 or NaN means not set.
     */
    public double getHeightResolution() {
      return updown == null ? 0.0 : updown.resolution;
    }

    /**
     * Set height resolution
     * @param resolution height resolution
     */
    public void setHeightResolution(double resolution) {
      if (updown == null)
        updown = new Range(defaultUpdown);
      updown.resolution = resolution;
      hashCode = 0;
    }

    /**
     * @return height units
     */
    public String getHeightUnits() {
      return updown == null ? null : updown.units;
    }

    /**
     * Set height units
     * @param units height units
     */
    public void setHeightUnits(String units) {
      if (updown == null)
        updown = new Range(defaultUpdown);
      updown.units = units;
      hashCode = 0;
    }

    public LatLonRect getBoundingBox() {
      return isGlobal ? new LatLonRect() :
          new LatLonRect(new LatLonPointImpl(getLatStart(), getLonStart()), getLatExtent(), getLonExtent());
    }

    public void setBoundingBox(LatLonRect bb) {
      LatLonPointImpl llpt = bb.getLowerLeftPoint();
      LatLonPointImpl urpt = bb.getUpperRightPoint();
      double height = urpt.getLatitude() - llpt.getLatitude();

      this.eastwest = new Range(llpt.getLongitude(), bb.getWidth(), 0.0, CDM.LON_UNITS);
      this.northsouth = new Range(llpt.getLatitude(), height, 0.0, CDM.LAT_UNITS);

      if ((bb.getWidth() > 358) && (height > 178))
        setGlobal(true); // LOOK ??
    }

    public void setVertical(CoordinateAxis1D vaxis) {
      int n = (int) vaxis.getSize();
      double size = vaxis.getCoordValue(n - 1) - vaxis.getCoordValue(0);
      double resolution = vaxis.getIncrement();
      String units = vaxis.getUnitsString();
      this.updown = new Range(vaxis.getCoordValue(0), size, resolution, units);
      if (units != null) {
        setZPositiveUp(SimpleUnit.isCompatible("m", units));
      }
    }

    public void setVertical(double start, double end, double resolution, String units, boolean isPositiveUp) {
      double size = end - start;
      this.updown = new Range(start, size, resolution, units);
      setZPositiveUp(isPositiveUp);
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof GeospatialCoverage)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        if (null != getEastWestRange())
          result = 37 * result + getEastWestRange().hashCode();
        if (null != getNorthSouthRange())
          result = 37 * result + getNorthSouthRange().hashCode();
        if (null != getUpDownRange())
          result = 37 * result + getUpDownRange().hashCode();
        if (null != getNames())
          result = 37 * result + getNames().hashCode();
        if (null != getZPositive())
          result = 2 * result + getZPositive().hashCode();
        result = 2 * result + (isGlobal() ? 1 : 0);
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

    public void toXML(java.io.OutputStream out) throws IOException {
      // hack, hack
      InvCatalogFactory10 converter = new InvCatalogFactory10();
      Element elem = converter.writeGeospatialCoverage(this);
      //XMLOutputter outputter = new XMLOutputter("  ", true);
      XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
      fmt.output(elem, out);
    }
  }

  /**
   * Implements spatialRange type.
   */
  static public class Range {
    private double start, size, resolution;
    private String units;

    /**
     * Constructor
     *
     * @param start      starting value
     * @param size       ending = start + size
     * @param resolution data resolution, or NaN if unknown
     * @param units      what units are start, size in?
     */
    public Range(double start, double size, double resolution, String units) {
      this.start = start;
      this.size = size;
      this.resolution = resolution;
      this.units = units;
    }

    /**
     * Copy constructor
     * @param from copy this
     */
    public Range(Range from) {
      this.start = from.start;
      this.size = from.size;
      this.resolution = from.resolution;
      this.units = from.units;
    }

    public double getStart() {
      return start;
    }

    public double getSize() {
      return size;
    }

    public double getResolution() {
      return resolution;
    }

    public String getUnits() {
      return units;
    }

    public boolean hasResolution() {
      return (resolution != 0.0) && !Double.isNaN(resolution);
    }

    public void extend(Range r) {
      double stop = Math.max(this.start+this.size, r.start+r.size);
      this.start = Math.min(this.start, r.start);
      this.size = this.start - stop;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Range)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        if (null != getUnits())
          result = 37 * result + getUnits().hashCode();
        result = 37 * result + (int) (getStart() * 1000.0);
        result = 37 * result + (int) (getSize() * 1000.0);
        if (hasResolution())
          result = 37 * result + (int) (getResolution() * 1000.0);
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements TimeCoverage type.
   *
   static public class TimeCoverage {
   static private TimeCoverage empty = new TimeCoverage();

   private DateType start, end;
   private TimeDuration duration, resolution;
   private double durationSecs;
   boolean hasStart, hasEnd, hasDuration, invalid;
   private DateType _start, _end;

   // need no-arg constructor for bean handling
   public TimeCoverage() {
   }

   public TimeCoverage(DateType start, DateType end, TimeDuration duration, TimeDuration resolution) {
   this.start = start;
   this.end = end;
   this.duration = duration;
   this.resolution = resolution;
   recalc();
   }

   private void recalc() {
   hasStart = (start != null) && !start.isBlank();
   hasEnd = (end != null) && !end.isBlank();
   hasDuration = (duration != null) && !duration.isBlank();

   invalid = true;
   if (hasStart && hasEnd) {
   invalid = false;
   } else if (hasStart && hasDuration) {
   invalid = false;
   _end = start.add( duration);
   } else if (hasEnd && hasDuration) {
   invalid = false;
   _start = end.subtract( duration);
   }
   hashCode = 0;
   }

   public boolean isEmpty() { return this.equals( empty); }

   public boolean included( Date d) {
   if (invalid) return false;

   if (hasStart && hasEnd) {
   if (start.after( d)) return false;
   if (end.before( d)) return false;

   } else if (hasStart && hasDuration) {
   if (start.after( d)) return false;
   if (_end.before( d)) return false;

   } else if (hasEnd && hasDuration) {
   if (_start.after( d)) return false;
   if (end.before( d)) return false;

   }

   return true;
   }

   public DateType getStart() { return start; }
   public void setStart(DateType start) {
   this.start = start;
   recalc();
   }

   public DateType getEnd() { return end; }
   public void setEnd(DateType end) {
   this.end = end;
   recalc();
   }

   public TimeDuration getDuration() { return duration; }
   public void setDuration(TimeDuration duration) {
   this.duration = duration;
   recalc();
   }

   public TimeDuration getResolution() { return resolution; }
   public void setResolution(TimeDuration resolution) {
   this.resolution = resolution;
   recalc();
   }

   public String toString() { return "start= "+start +" end= "+end+ " duration= "+ duration
   + " resolution= "+ resolution; }
   public boolean equals(Object o) {
   if (this == o) return true;
   if (!(o instanceof TimeCoverage)) return false;
   return o.hashCode() == this.hashCode();
   }

   public int hashCode() {
   if (hashCode == 0) {
   int result = 17;
   if (hasStart && !start.isBlank())
   result = 37*result + start.hashCode();
   if (hasEnd && !end.isBlank())
   result = 37*result + end.hashCode();
   if (hasDuration && !duration.isBlank())
   result = 37*result + duration.hashCode();
   if (resolution != null && !resolution.isBlank())
   result = 37*result + resolution.hashCode();
   hashCode = result;
   }
   return hashCode;
   }
   private volatile int hashCode = 0; // Bloch, item 8

   } */

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements Variable type.
   */
  static public class Variable implements Comparable {
    private String name, desc, vocabulary_name, units, id;

    // no-arg constructor for beans
    public Variable() {
      this.name = "";
      this.desc = "";
      this.vocabulary_name = "";
      this.units = "";
      this.id = "";
    }

    public Variable(String name, String desc, String vocabulary_name, String units, String id) {
      this.name = name;
      this.desc = desc;
      this.vocabulary_name = vocabulary_name;
      this.units = units;
      this.id = id;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getDescription() {
      return desc;
    }

    public void setDescription(String desc) {
      this.desc = desc;
    }

    public String getVocabularyName() {
      return vocabulary_name;
    }

    public void setVocabularyName(String vocabulary_name) {
      this.vocabulary_name = vocabulary_name;
    }

    // need unique id, cant count on name, eg because of variant GRIB tables
    public String getVocabularyId() {
      return id;
    }

    public void setVocabularyId(String id) {
      this.id = id;
    }

    public void setVocabularyId(Attribute id) {
      if (id == null) return;
      StringBuilder sbuff = new StringBuilder();
      for (int i = 0; i < id.getLength(); i++) {
        if (i > 0) sbuff.append(",");
        sbuff.append(id.getNumericValue(i));
      }
      this.id = sbuff.toString();
    }

    public String getUnits() {
      return units;
    }

    public void setUnits(String units) {
      this.units = units;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Variable)) return false;
      return o.hashCode() == this.hashCode();
    }

    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        if (null != getName())
          result = 37 * result + getName().hashCode();
        if (null != getDescription())
          result = 37 * result + getDescription().hashCode();
        if (null != getVocabularyName())
          result = 37 * result + getVocabularyName().hashCode();
        if (null != getUnits())
          result = 37 * result + getUnits().hashCode();
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

    public String toString()
    {
      return "Variable [" + name + "]";
    }

    public int compareTo(Object o) {
      Variable ov = (Variable) o;
      return name.compareTo(ov.name);
    }

    // for bean editing
    static public String hiddenProperties() {
      return "nameVocab";
    }

    static public String editableProperties() {
      return "name description units vocabularyName";
    }
  }

  /**
   * Implements Variables type.
   */
  static public class Variables {
    private String vocabulary, vocabHref, mapHref;
    private URI vocabUri, mapUri;
    private List<Variable> variables = new ArrayList<Variable>();

    public Variables(String vocab) {
      this.vocabulary = vocab;
    }

    public Variables(String vocab, String vocabHref, URI vocabUri, String mapHref, URI mapUri) {
      this.vocabulary = vocab;
      this.vocabHref = vocabHref;
      this.vocabUri = vocabUri;
      this.mapHref = mapHref;
      this.mapUri = mapUri;
    }

    public String getVocabulary() {
      return vocabulary;
    }

    public String getVocabHref() {
      return vocabHref;
    }

    public URI getVocabUri() {
      return vocabUri;
    }

    public URI getMapUri() {
      return mapUri;
    }

    public String getMapHref() {
      return mapHref;
    }

    public void addVariable(Variable v) {
      variables.add(v);
    }

    public List<Variable> getVariableList() {
      init();
      return variables;
    }

    public void sort() {
      Collections.sort(variables);
    }

    private boolean isInit = false;

    private void init() {
      if (isInit || (mapUri != null)) return;
      isInit = true;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Variables)) return false;
      return o.hashCode() == this.hashCode();
    }

    /**
     * Override Object.hashCode() to implement equals.
     */
    public int hashCode() {
      if (hashCode == 0) {
        int result = 17;
        if (null != getVocabulary())
          result = 37 * result + getVocabulary().hashCode();
        if (null != getVocabUri())
          result = 37 * result + getVocabUri().hashCode();
        if (null != getMapUri())
          result = 37 * result + getMapUri().hashCode();
        if (null != getVariableList())
          result = 37 * result + getVariableList().hashCode();
        hashCode = result;
      }
      return hashCode;
    }

    private volatile int hashCode = 0; // Bloch, item 8

    public String toString()
    {
      StringBuilder sb = new StringBuilder( "Variables" );
      sb.append( " [" )
        .append( getVocabulary() != null ? getVocabulary() : "" )
        .append( "]" );
      if ( mapUri != null )
        sb.append( "map [").append( mapUri.toString()).append("]");
      else
        for ( Variable v : variables )
          sb.append( " " ).append( v.toString() );
      return sb.toString();
    }
  }

  /**
   * Factory for metadata of type THREDDS.
   *
   static public class Parser implements thredds.catalog.MetadataConverterIF {
   private DOMBuilder builder = new DOMBuilder();

   /**
   * Read metadata from an XLink href, and convert to Java object representing
   * the metadata.
   * @param dataset : InvDataset that this metadata refers to.
   * @param urlString : the location of the metadata: this must be an XML document whose
   *    root Element contains the proper metadata.
   * @return ThreddsMetadata object
   *
  public Object readMetadataContentFromURL(InvDataset dataset, String urlString) throws java.net.MalformedURLException, java.io.IOException {
  Document doc;
  try {
  SAXBuilder builder = new SAXBuilder(true);
  doc = builder.build(urlString);
  }
  catch (JDOMException e) {
  System.out.println("ThreddsMetadataFactory parsing error= \n" + e.getMessage());
  throw new IOException("ThreddsMetadataFactory parsing error= " + e.getMessage());
  }

  if (showParsedXML) {
  XMLOutputter xmlOut = new XMLOutputter();
  System.out.println("*** catalog/showParsedXML = \n" + xmlOut.outputString(doc) + "\n*******");
  }

  return readMetadataContent(dataset, doc.getRootElement());
  }

  /**
   * Read metadata from a org.w3c.dom.Element, and convert to Java object representing
   * the metadata.
   * @param dataset : InvDataset that this metadata refers to.
   * @param domElement : the DOM Element containing metadata info.
   * @return ThreddsMetadata object
   *
  public Object readMetadataContent(InvDataset dataset, org.w3c.dom.Element domElement) {
  // convert to JDOM element
  Element mdataElement = builder.build(domElement);
  return readMetadataContent(dataset, mdataElement);
  }

  /**
   * Read metadata from a org.w3c.dom.Element, and convert to Java object representing
   * the metadata.
   * @param dataset  : InvDataset that this metadata refers to.
   * @param mdataElement : the JDOM Element containing aggregation children.
   * @return List of objects of type Aggregation
   *
  public Object readMetadataContent(InvDataset dataset, Element mdataElement) {

  ThreddsMetadata tm = new ThreddsMetadata();
  boolean ok = true;

  Element spatialElem = mdataElement.getChild("spatialExtent", defNS);
  if (null == spatialElem) {
  tm.err.append("** Warning: THREDDS Metadata record has no SpatialExtent element\n");
  }
  else if (null != spatialElem.getAttributeValue("isGlobal"))
  tm.isGlobal = true;
  else {
  String minLat = spatialElem.getChildText("minLat", defNS);
  String minLon = spatialElem.getChildText("minLon", defNS);
  String maxLat = spatialElem.getChildText("maxLat", defNS);
  String maxLon = spatialElem.getChildText("maxLon", defNS);

  if (minLat == null) {
  tm.err.append("Missing SpatialExtent minLat element\n");
  } else {
  try { tm.minLat = Double.parseDouble( minLat); }
  catch (NumberFormatException e) {
  tm.err.append("Bad format for SpatialExtent minLat = "+minLat+"\n");
  }
  }
  if (minLon == null) {
  tm.err.append("Missing SpatialExtent minLon element\n");
  } else {
  try { tm.minLon = Double.parseDouble( minLon); }
  catch (NumberFormatException e) {
  tm.err.append("Bad format for SpatialExtent minLon = "+minLon+"\n");
  }
  }
  if (maxLat == null) {
  tm.err.append("Missing SpatialExtent maxLat element\n");
  } else {
  try { tm.maxLat = Double.parseDouble( maxLat); }
  catch (NumberFormatException e) {
  tm.err.append("Bad format for SpatialExtent maxLat = "+maxLat+"\n");
  }
  }
  if (maxLon == null) {
  tm.err.append("Missing SpatialExtent maxLon element\n");
  } else {
  try { tm.maxLon = Double.parseDouble( maxLon); }
  catch (NumberFormatException e) {
  tm.err.append("Bad format for SpatialExtent maxLon = "+maxLon+"\n");
  }
  }
  }

  Element timeElem = mdataElement.getChild("timeExtent", defNS);
  if (null == timeElem) {
  tm.err.append("** Warning: THREDDS Metadata record has no TimeExtent element\n");
  }
  else {
  String minDate = timeElem.getChildText("minDate", defNS);
  String maxDate = timeElem.getChildText("maxDate", defNS);
  if (minDate == null) {
  tm.err.append("Missing TimeExtent minDate element\n");
  } else {
  try { tm.minDate = dateFormat.parse( minDate); }
  catch (ParseException e) {
  tm.err.append("Bad format for TimeExtent minDate = "+minDate+"\n");
  }
  }
  if (maxDate == null) {
  tm.err.append("Missing TimeExtent maxDate element\n");
  } else {
  try { tm.maxDate = dateFormat.parse( maxDate); }
  catch (ParseException e) {
  tm.err.append("Bad format for TimeExtent maxDate = "+maxDate+"\n");
  }
  }
  }

  // get fileAccess elements
  java.util.List list = mdataElement.getChildren("param", defNS);
  for (int j = 0; j < list.size(); j++) {
  Element paramElement = (Element) list.get(j);
  String name = paramElement.getText();
  tm.params.add(name);
  }

  if (debug) System.out.println (" ThredddsMetadata added: "+ tm);
  return ok ? tm : null;
  }

  /**
   * Create a JDOM element from an InvMetadata content object
   *
   * @param contentObject : the content object
   * @return an object representing the metadata content as a JDOM element
   *
  public void addMetadataContent(org.w3c.dom.Element dome, Object contentObject) {

  Element mdataElem = builder.build(dome);
  ThreddsMetadata tm = (ThreddsMetadata) contentObject;

  Element spatialElem = new Element("spatialExtent", defNS);
  if (tm.isGlobal)
  spatialElem.setAttribute("isGlobal", "true");
  else {
  spatialElem.addContent( new Element("minLat", defNS).setText(Double.toString(tm.minLat)));
  spatialElem.addContent( new Element("minLon", defNS).setText(Double.toString(tm.minLon)));
  spatialElem.addContent( new Element("maxLat", defNS).setText(Double.toString(tm.maxLat)));
  spatialElem.addContent( new Element("maxLon", defNS).setText(Double.toString(tm.maxLon)));
  }
  mdataElem.addContent(spatialElem);

  Element timeElem = new Element("timeExtent", defNS);
  timeElem.addContent( new Element("minDate", defNS).setText(dateFormat.format( tm.minDate)));
  timeElem.addContent( new Element("maxDate", defNS).setText(dateFormat.format( tm.maxDate)));
  mdataElem.addContent(timeElem);

  for (int i=0; i<tm.params.size(); i++) {
  mdataElem.addContent( new Element("param", defNS).setText((String)tm.params.get(i)));
  }
  }

  /**
   * Validate internal data structures.
   * @param contentObject : the content object
   * @param out : print error message here
   * @return true if no fatl validation errors.
   *
  public boolean validateMetadataContent(Object contentObject, StringBuffer out) {
  if (contentObject == null) return false;

  ThreddsMetadata tm = (ThreddsMetadata) contentObject;
  boolean hasErr = tm.err.length() > 0;
  if (hasErr) out.append( tm.err);
  return !hasErr;
  }
  } // Factory */

  /**
   * ********************************************************************
   */

  /* testing
  public static void main (String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(false);
    catFactory.registerMetadataConverter( MetadataType.THREDDS, new ThreddsMetadata.Parser());

    InvCatalogImpl cat = (InvCatalogImpl) catFactory.readXML("file:///C:/dev/thredds/catalog/test/data/enhancedCat.xml");
    System.out.println("\nDump=\n" + cat.dump());

    StringBuffer buff = new StringBuffer();
    boolean isValid = cat.check( buff);
    System.out.println("catalog <" + cat.getName()+ "> "+ (isValid ? "is" : "is not") + " valid");
    System.out.println("  messages=\n" + buff.toString());

    if (isValid) System.out.println("\nBack to XML=\n" + catFactory.writeXML( cat));
  } */
  static public void main(String[] args) throws IOException {
    GeospatialCoverage gc = new GeospatialCoverage();
    LatLonRect bb = new LatLonRect();
    gc.setBoundingBox(bb);
    gc.toXML(System.out);
  }

}


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

import ucar.ma2.Index;
import ucar.ma2.Section;
import ucar.ma2.InvalidRangeException;

/**
 * LayoutSegmented has data stored in segments.
 * Assume that each segment size is a multiple of elemSize.
 *
 * @author caron
 * @since Dec 31, 2007
 */
public class LayoutSegmented implements Layout {
  private long total, done;
  private int elemSize; // size of each element

  private long[] segPos;    // bytes
  private long[] segMax, segMin; // bytes

  // outer chunk
  private IndexChunker chunker;
  private IndexChunker.Chunk chunkOuter;

  // inner chunk = deal with segmentation
  private IndexChunker.Chunk chunkInner = new IndexChunker.Chunk(0,0,0);

  private static final boolean debugNext = false;

  /**
   * Constructor.
   *
   * @param segPos      starting address of each segment.
   * @param segSize     number of bytes in each segment. Assume multiple of elemSize
   * @param elemSize    size of an element in bytes.
   * @param srcShape    shape of the entire data array.
   * @param wantSection the wanted section of data
   * @throws ucar.ma2.InvalidRangeException if ranges are misformed
   */
  public LayoutSegmented(long[] segPos, int[] segSize, int elemSize, int[] srcShape, Section wantSection) throws InvalidRangeException {
    assert segPos.length == segSize.length;
    this.segPos = segPos;

    int nsegs = segPos.length;
    segMin = new long[nsegs];
    segMax = new long[nsegs];
    long totalElems = 0;
    for (int i = 0; i < nsegs; i++) {
      assert segPos[i] >= 0;
      assert segSize[i] > 0;
      assert (segSize[i] % elemSize) == 0;

      segMin[i] = totalElems;
      totalElems += segSize[i];
      segMax[i] = totalElems;
    }
    assert totalElems >=  Index.computeSize(srcShape) * elemSize;

    chunker = new IndexChunker(srcShape, wantSection);
    this.total = chunker.getTotalNelems();
    this.done = 0;
    this.elemSize = elemSize;
  }

  public long getTotalNelems() {
    return total;
  }

  public int getElemSize() {
    return elemSize;
  }

  public boolean hasNext() {
    return done < total;
  }

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

  private long getFilePos(long elem) {
    int segno = 0;
    while (elem >= segMax[segno]) segno++;
    return segPos[segno] + elem - segMin[segno];
  }

  // how many more bytes are in this segment ?
  private int getMaxBytes(long start) {
    int segno = 0;
    while (start >= segMax[segno]) segno++;
    return (int) (segMax[segno] - start);
  }

  private int needInner = 0;
  private int doneInner = 0;

  public Chunk next() {
    Chunk result;

    if (needInner > 0) {
      result = nextInner(false, 0);

    } else {
      result = nextOuter();
      int nbytes = getMaxBytes( chunkOuter.getSrcElem() * elemSize);
      if (nbytes < result.getNelems() * elemSize)
        result = nextInner(true, nbytes);
    }

    done += result.getNelems();
    doneInner += result.getNelems();
    needInner -= result.getNelems();

    if (debugNext)
      System.out.println(" next chunk: " + result);

    return result;
  }

  private Chunk nextInner(boolean first, int nbytes) {
    if (first) {
      chunkInner.setNelems(nbytes / elemSize);
      chunkInner.setDestElem( chunkOuter.getDestElem());
      needInner = chunkOuter.getNelems();
      doneInner = 0;

    } else {
      chunkInner.incrDestElem( chunkInner.getNelems()); // increment using last chunks' value
      nbytes = getMaxBytes( (chunkOuter.getSrcElem() + doneInner) * elemSize);
      nbytes = Math.min(nbytes, needInner * elemSize);
      chunkInner.setNelems(nbytes / elemSize); // set this chunk's value
    }

    chunkInner.setSrcPos( getFilePos( (chunkOuter.getSrcElem() + doneInner) * elemSize));
    return chunkInner;
  }

  public Chunk nextOuter() {
    chunkOuter = chunker.next();
    long srcPos = getFilePos( chunkOuter.getSrcElem() * elemSize);
    chunkOuter.setSrcPos( srcPos);
    return chunkOuter;
  }

}


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

package ucar.nc2.grib;

import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarPeriod;
import ucar.unidata.util.StringUtil2;

/**
 * General Utilities used by GRIB code.
 *
 * @author caron
 * @since 11/16/11
 */
public class GribUtils {
  public static final String CENTER = "Originating_or_generating_Center";
  public static final String SUBCENTER = "Originating_or_generating_Subcenter";
  public static final String GEN_PROCESS = "Generating_process_or_model";
  public static final String TABLE_VERSION = "GRIB_table_version";

  /**
   * Convert a time unit to a CalendarPeriod
   * GRIB1 and GRIB2 are the same (!)
   *
   * @param timeUnit (GRIB1 table 4) (GRIB2   Code table 4.4 : Indicator of unit of time range)
   * @return equivalent CalendarPeriod
   */
  public static CalendarPeriod getCalendarPeriod(int timeUnit) {
    // LOOK - some way to intern these ? put in hash table  ?
    switch (timeUnit) { // code table 4.4
      case 0:
        return CalendarPeriod.of(1, CalendarPeriod.Field.Minute);
      case 1:
        return CalendarPeriod.of(1, CalendarPeriod.Field.Hour);
      case 2:
        return CalendarPeriod.of(1, CalendarPeriod.Field.Day);
      case 3:
        return CalendarPeriod.of(1, CalendarPeriod.Field.Month);
      case 4:
        return CalendarPeriod.of(1, CalendarPeriod.Field.Year);
      case 5:
        return CalendarPeriod.of(10, CalendarPeriod.Field.Year);
      case 6:
        return CalendarPeriod.of(30, CalendarPeriod.Field.Year);
      case 7:
        return CalendarPeriod.of(100, CalendarPeriod.Field.Year);
      case 10:
        return CalendarPeriod.of(3, CalendarPeriod.Field.Hour);
      case 11:
        return CalendarPeriod.of(6, CalendarPeriod.Field.Hour);
      case 12:
        return CalendarPeriod.of(12, CalendarPeriod.Field.Hour);
      case 13:
        return CalendarPeriod.of(15, CalendarPeriod.Field.Minute);
      case 14:
        return CalendarPeriod.of(30, CalendarPeriod.Field.Minute);
      default:
        throw new UnsupportedOperationException("Unknown time unit = " + timeUnit);
    }
  }

  public static CalendarDate getValidTime(CalendarDate refDate, int timeUnit, int offset) {
    CalendarPeriod period = GribUtils.getCalendarPeriod(timeUnit);
    return refDate.add(period.multiply(offset));
  }

  public static String cleanupUnits(String unit) {
    if (unit == null) return "";
    if (unit.equalsIgnoreCase("-")) unit = "";
    else {
      if (unit.startsWith("/")) unit = "1" + unit;
      unit = unit.trim();
      unit = StringUtil2.remove(unit, "**");
      StringBuilder sb = new StringBuilder(unit);
      StringUtil2.remove(sb, "^[]");
      StringUtil2.substitute(sb, " / ", "/");
      StringUtil2.replace(sb, ' ', ".");
      StringUtil2.replace(sb, '*', ".");
      unit = sb.toString();
    }
    return unit;
  }

  public static String cleanupDescription(String desc) {
    if (desc == null) return "";
    int pos = desc.indexOf("(see");
    if (pos < 0) pos = desc.indexOf("(See");
    if (pos > 0) desc = desc.substring(0, pos);

    StringBuilder sb = new StringBuilder(desc.trim());
    StringUtil2.replace(sb, '+', "and");
    StringUtil2.remove(sb, ".;,=[]()/");
    return sb.toString().trim();
  }

  public static String makeNameFromDescription(String desc) {
    if (desc == null) return "";
    int pos = desc.indexOf("(see");
    if (pos < 0) pos = desc.indexOf("(See");
    if (pos > 0) desc = desc.substring(0, pos);

    StringBuilder sb = new StringBuilder(desc.trim());
    StringUtil2.replace(sb, '+', "plus");
    StringUtil2.replace(sb, "/. ", "-p_");
    StringUtil2.remove(sb, ";,=[]()");
    return sb.toString();
  }

  /*
  scanMode
  Grib1
  Flag/Code table 8 – Scanning mode
  Bit No. Value Meaning
  1 0 Points scan in +i direction
    1 Points scan in –i direction
  2 0 Points scan in –j direction
    1 Points scan in +j direction
  3 0 Adjacent points in i direction are consecutive
    1 Adjacent points in j direction are consecutive
  Notes:
  (1) i direction: west to east along a parallel, or left to right along an X-axis.
  (2) j direction: south to north along a meridian, or bottom to top along a Y-axis.

  Grin2
    Flag table 3.4 – Scanning mode
       Bit No. Value Meaning
 128    1 0 Points of first row or column scan in the +i (+x) direction
          1 Points of first row or column scan in the –i (–x) direction
 64     2 0 Points of first row or column scan in the –j (–y) direction
          1 Points of first row or column scan in the +j (+y) direction
 32     3 0 Adjacent points in i (x) direction are consecutive
          1 Adjacent points in j (y) direction is consecutive
 16     4 0 All rows scan in the same direction
          1 Adjacent rows scans in the opposite direction
        5–8 Reserved
        Notes:
        (1) i direction: west to east along a parallel or left to right along an x-axis.
        (2) j direction: south to north along a meridian, or bottom to top along a y-axis.
        (3) If bit number 4 is set, the first row scan is as defined by previous flags.
   */

  /**
    * X Points scan in +/- direction. Grib 1 or 2.
    * Positive means west to east along a parallel, or left to right along an X-axis..
    * @param scanMode scanMode byte
    * @return true: x points scan in positive direction, false: x points scan in negetive direction
    */
  public static boolean scanModeXisPositive(int scanMode) {
    return !GribNumbers.testGribBitIsSet(scanMode, 1);
  }

  /**
    * Y Points scan in +/- direction. Grib 1 or 2.
   * Positive means south to north along a meridian, or bottom to top along a Y-axis.
    * @param scanMode scanMode byte
    * @return true: y points scan in positive direction, false: y points scan in negetive direction
    */
  public static boolean scanModeYisPositive(int scanMode) {
    return GribNumbers.testGribBitIsSet(scanMode, 2);
  }

  /**
   * Adjacent points in x/y direction are consecutive. Grib 1 or 2.
   * @param scanMode scanMode byte
   * @return true: x points are consecutive (row) false: y points are consecutive (col)
   */
  public static boolean scanModeXisConsecutive(int scanMode) {
    return !GribNumbers.testGribBitIsSet(scanMode, 3);
  }

  /**
   * All rows scan in the same/opposite direction. Grib 2 only.
   * @param scanMode scanMode byte
   * @return  true: All rows scan in the same direction, false: Adjacent rows scan in the opposite direction, the first row scan is as defined by previous flags
   */
  public static boolean scanModeSameDirection(int scanMode) {
    return !GribNumbers.testGribBitIsSet(scanMode, 4);
  }

}


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

package opendap.dap;

import java.io.*;

/**
 * A vector of unsigned ints.
 *
 * @author ndp
 * @version $Revision: 15901 $
 * @see PrimitiveVector
 */
public class UInt16PrimitiveVector extends Int16PrimitiveVector {
    /**
     * Constructs a new UInt16PrimitiveVector.
     */
    public UInt16PrimitiveVector(BaseType var) {
        super(var);
    }

    /**
     * Prints the value of all variables in this vector.  This
     * method is primarily intended for debugging OPeNDAP applications and
     * text-based clients such as geturl.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param space this value is passed to the printDecl method,
     *              and controls the leading spaces of the output.
     * @see BaseType#printVal(PrintWriter, String, boolean)
     */
    public void printVal(PrintWriter os, String space) {
        int len = getLength();
        for (int i = 0; i < len - 1; i++) {
            // to print properly, cast to long and convert to unsigned
            os.print(((long) getValue(i)) & 0xFFFFL);
            os.print(", ");
        }
        // print last value, if any, without trailing comma
        if (len > 0)
            os.print(((long) getValue(len - 1)) & 0xFFFFL);
    }

    /**
     * Prints the value of a single variable in this vector.
     * method is used by DArray's printVal method.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param index the index of the variable to print.
     * @see DArray#printVal(PrintWriter, String, boolean)
     */
    public void printSingleVal(PrintWriter os, int index) {
        // to print properly, cast to long and convert to unsigned
        os.print(((long) getValue(index)) & 0xFFFFL);
    }
}


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

package opendap.dap;

import java.io.*;

/**
 * A vector of bytes.
 *
 * @author jehamby
 * @version $Revision: 15901 $
 * @see PrimitiveVector
 */
public class BytePrimitiveVector extends PrimitiveVector
{
    /**
     * the array of byte values.
     */
    private byte vals[];

    /**
     * Constructs a new BytePrimitiveVector.
     *
     * @param var the template BaseType to use.
     */
    public BytePrimitiveVector(BaseType var) {
        super(var);
    }

    /**
     * Returns the number of elements in the array.
     *
     * @return the number of elements in the array.
     */
    public int getLength() {
        return vals.length;
    }

    /**
     * Sets the number of elements in the array.  Allocates a new primitive
     * array of the desired size.  Note that if this is called multiple times,
     * the old array and its contents will be lost.
     *

     * Only called inside of deserialize method or in derived
     * classes on server.
     *
     * @param len the number of elements in the array.
     */
    public void setLength(int len) {
        vals = new byte[len];
    }

    /**
     * Return the i'th value as a byte.
     *
     * @param i the index of the value to return.
     * @return the i'th value.
     */
    public final byte getValue(int i) {
        return vals[i];
    }

    /**
     * Set the i'th value of the array.
     *
     * @param i      the index of the value to set.
     * @param newVal the new value.
     */
    public final void setValue(int i, byte newVal) {
        vals[i] = newVal;
    }

    /**
     * Prints the value of all variables in this vector.  This
     * method is primarily intended for debugging OPeNDAP applications and
     * text-based clients such as geturl.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param space this value is passed to the printDecl method,
     *              and controls the leading spaces of the output.
     * @see BaseType#printVal(PrintWriter, String, boolean)
     */
    public final void printVal(PrintWriter os, String space) {
        int len = vals.length;
        for (int i = 0; i < len - 1; i++) {
            // convert to unsigned and print
            os.print(((int) vals[i]) & 0xFF);
            os.print(", ");
        }
        // print last value, if any, without trailing comma
        if (len > 0)
            os.print(((int) vals[len - 1]) & 0xFF);
    }

    /**
     * Prints the value of a single variable in this vector.
     * method is used by DArray's printVal method.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param index the index of the variable to print.
     * @see DArray#printVal(PrintWriter, String, boolean)
     */
    public void printSingleVal(PrintWriter os, int index) {
        // convert to unsigned and print
        os.print(((int) vals[index]) & 0xFF);
    }

    /**
     * Reads data from a DataInputStream. This method is only used
     * on the client side of the OPeNDAP client/server connection.
     *
     * @param source   a DataInputStream to read from.
     * @param sv       The ServerVersion returned by the server.
     *                 (used by DSequence to determine which protocol version was
     *                 used).
     * @param statusUI The StatusUI object to use for GUI updates
     *                 and user cancellation notification (may be null).
     * @throws DataReadException when invalid data is read, or if the user
     *                           cancels the download.
     * @throws EOFException      if EOF is found before the variable is completely
     *                           deserialized.
     * @throws IOException       thrown on any other InputStream exception.
     * @see ClientIO#deserialize(DataInputStream, ServerVersion, StatusUI)
     */
    public synchronized void deserialize(DataInputStream source,
                                         ServerVersion sv,
                                         StatusUI statusUI)
            throws IOException, EOFException, DataReadException {

        int modFour = vals.length % 4;
        // number of bytes to pad
        int pad = (modFour != 0) ? (4 - modFour) : 0;

        for (int i = 0; i < vals.length; i++) {
            vals[i] = source.readByte();
            if (statusUI != null) {
                statusUI.incrementByteCount(1);
                if (statusUI.userCancelled())
                    throw new DataReadException("User cancelled");
            }
        }
        // pad out to a multiple of four bytes
        byte unused;
        for (int i = 0; i < pad; i++)
            unused = source.readByte();
        if (statusUI != null)
            statusUI.incrementByteCount(pad);
    }

    /**
     * Writes data to a DataOutputStream. This method is used
     * primarily by GUI clients which need to download OPeNDAP data, manipulate
     * it, and then re-save it as a binary file.
     *
     * @param sink a DataOutputStream to write to.
     * @throws IOException thrown on any OutputStream
     *                     exception.
     */
    public void externalize(DataOutputStream sink) throws IOException {
        int modFour = vals.length % 4;
        // number of bytes to pad
        int pad = (modFour != 0) ? (4 - modFour) : 0;

        for (int i = 0; i < vals.length; i++) {
            sink.writeByte(vals[i]);
        }
        // pad out to a multiple of four bytes
        for (int i = 0; i < pad; i++)
            sink.writeByte(0);
    }

    /**
     * Write a subset of the data to a DataOutputStream.
     *
     * @param sink    a DataOutputStream to write to.
     * @param start  starting index (i=start)
     * @param stop   ending index (i<=stop)
     * @param stride index stride (i+=stride)
     * @throws IOException thrown on any OutputStream exception.
     */
    public void externalize(DataOutputStream sink, int start, int stop, int stride) throws IOException {
        int count = 0;
        for (int i = start; i <= stop; i += stride) {
            sink.writeByte(vals[i]);
            count++;
        }

        // pad out to a multiple of four bytes
        int modFour = count % 4;
        int pad = (modFour != 0) ? (4 - modFour) : 0;
        for (int i = 0; i < pad; i++)
            sink.writeByte(0);
    }

    /**
     * Create a new primitive vector using a subset of the data.
     *
     * @param start  starting index (i=start)
     * @param stop   ending index (i<=stop)
     * @param stride index stride (i+=stride)
     * @return new primitive vector, of type BytePrimitiveVector.
     */
    public PrimitiveVector subset(int start, int stop, int stride) {
        BytePrimitiveVector n = new BytePrimitiveVector(getTemplate());
        stride = Math.max(stride, 1);
        stop = Math.max(start, stop);
        int length = 1 + (stop - start) / stride;
        n.setLength(length);

        int count = 0;
        for (int i = start; i <= stop; i += stride) {
            n.setValue(count, vals[i]);
            count++;
        }
        return n;
    }

    /**
     * Returns (a reference to) the internal storage for this PrimitiveVector
     * object.
     *
WARNING:

     * Because this method breaks encapsulation rules the user must beware!
     * If we (the OPeNDAP prgramming team) choose to change the internal
     * representation(s) of these types your code will probably break.
     *

     * This method is provided as an optimization to eliminate massive
     * copying of data.
     *
     * @return The internal array of bytes.
     */
    public Object getInternalStorage() {
        return (vals);
    }

    /**
     * Set the internal storage for PrimitiveVector.
     *
WARNING:

     * Because this method breaks encapsulation rules the user must beware!
     * If we (the OPeNDAP prgramming team) choose to change the internal
     * representation(s) of these types your code will probably break.
     *

     * This method is provided as an optimization to eliminate massive
     * copying of data.
     */
    public void setInternalStorage(Object o) {
        vals = (byte []) o;
    }

    /**
     * Returns a clone of this Attribute.
     * See DAPNode.cloneDag()
     *
     * @param map track previously cloned nodes
     * @return a clone of this Attribute.
     */
    public DAPNode cloneDAG(CloneMap map)
        throws CloneNotSupportedException
    {
        BytePrimitiveVector v = (BytePrimitiveVector) super.cloneDAG(map);
        if (vals != null) {
            v.vals = new byte[vals.length];
            System.arraycopy(vals, 0, v.vals, 0, vals.length);
        }
        return v;
    }

}


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

import java.io.*;

/*
 * Static methods to read resource files.
 *
 * @author caron 07/28/14
 * @version 2.0
 */
public class GribResourceReader {

  /**
   * Get the input stream to the given resource
   *
   * @param resourceName The resource name. May be a resource on the class path or a file
   * @return The input stream to the resource
   */
  public static InputStream getInputStream(String resourceName) throws FileNotFoundException {

    // Try class loader to get resource
    ClassLoader cl = GribResourceReader.class.getClassLoader();
    InputStream s = cl.getResourceAsStream(resourceName);
    if (s != null) {
      return s;
    }

    // Try the file system
    File f = new File(resourceName);
    if (f.exists())
      return new FileInputStream(f);

    // give up
    throw new FileNotFoundException("Cant find resource "+resourceName);
  }

}


/*
 * 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.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.AxisType;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.ma2.DataType;
import ucar.ma2.Array;

import java.io.IOException;

/**
 * NsslRadarMosaicConvention Convention.
 *
 * @see "http://www.nmq.nssl.noaa.gov/~qpeverif/blog/?page_id=12"
 *
 * @author caron
 */

public class NsslRadarMosaicConvention extends CoordSysBuilder {

  /**
   * Is this my file?
   *
   * @param ncfile test this NetcdfFile
   * @return true if we think this is a NsslRadarMosaicConvention file.
   */
  public static boolean isMine(NetcdfFile ncfile) {
    String cs = ncfile.findAttValueIgnoreCase(null, CDM.CONVENTIONS, null);
    if (cs != null) return false;

    String s = ncfile.findAttValueIgnoreCase(null, "DataType", null);
    if ((s == null) || !(s.equalsIgnoreCase("LatLonGrid") || s.equalsIgnoreCase("LatLonHeightGrid")))
      return false;

    if ((null == ncfile.findGlobalAttribute("Latitude")) ||
        (null == ncfile.findGlobalAttribute("Longitude")) ||
        (null == ncfile.findGlobalAttribute("LatGridSpacing")) ||
        (null == ncfile.findGlobalAttribute("LonGridSpacing")) ||
        (null == ncfile.findGlobalAttribute("Time")))
      return false;

    return !(null == ncfile.findDimension("Lat") ||
            null == ncfile.findDimension("Lon"));

  }

  public NsslRadarMosaicConvention() {
    this.conventionName = "NSSL National Reflectivity Mosaic";
  }

  public void augmentDataset(NetcdfDataset ds, CancelTask cancelTask) throws IOException {
    if (null != ds.findVariable("Lat")) return; // check if its already been done - aggregating enhanced datasets.
    String s = ds.findAttValueIgnoreCase(null, "DataType", null);
    if (s == null) return;

    if (s.equalsIgnoreCase("LatLonGrid"))
      augment2D(ds, cancelTask);
    else
      augment3D(ds, cancelTask);

    ds.finish();
  }

  private void augment3D(NetcdfDataset ds, CancelTask cancelTask) throws IOException {
    ds.addAttribute(null, new Attribute(CDM.CONVENTIONS, "NSSL National Reflectivity Mosaic"));

    addLongName(ds, "mrefl_mosaic", "3-D reflectivity mosaic grid");
    addCoordinateAxisType(ds, "Height", AxisType.Height);
    addCoordSystem(ds);

    Variable var = ds.findVariable("mrefl_mosaic");
    assert var != null;

    float scale_factor = Float.NaN;
    Attribute att = var.findAttributeIgnoreCase("Scale");
    if (att != null) {
      scale_factor = att.getNumericValue().floatValue();
      var.addAttribute(new Attribute(CDM.SCALE_FACTOR, 1.0f / scale_factor));
    }
    att = ds.findGlobalAttributeIgnoreCase("MissingData");
    if (null != att) {
      float val = att.getNumericValue().floatValue();
      if (!Float.isNaN(scale_factor)) val *= scale_factor;
      var.addAttribute(new Attribute(CDM.MISSING_VALUE, (short) val));
    }
    // hack
    Array missingData = Array.factory(DataType.SHORT, new int[] {2}, new short[] {-990, -9990});
    var.addAttribute(new Attribute(CDM.MISSING_VALUE, missingData));
    var.addAttribute(new Attribute(_Coordinate.Axes, "Height Lat Lon"));
  }

  private void augment2D(NetcdfDataset ds, CancelTask cancelTask) throws IOException {
    ds.addAttribute(null, new Attribute(CDM.CONVENTIONS, "NSSL National Reflectivity Mosaic"));

    addLongName(ds, "cref", "composite reflectivity");
    addLongName(ds, "hgt_cref", "height associated with the composite reflectivity");
    addLongName(ds, "etp18", "scho top");
    addLongName(ds, "shi", "csevere hail index");
    addLongName(ds, "posh", "probability of severe hail");
    addLongName(ds, "mehs", "maximum estimated hail size");
    addLongName(ds, "hsr", "hybrid scan reflectivity");
    addLongName(ds, "hsrh", "height associated with the hybrid scan reflectivity");
    addLongName(ds, "vil", "vertically integrated liquid");
    addLongName(ds, "vilD", "vertically integrated liquid density");
    addLongName(ds, "pcp_flag", "Radar precipitation flag");
    addLongName(ds, "pcp_type", "Surface precipitation type");

    addCoordSystem(ds);

    // fix the variable attributes
    for (Variable var : ds.getVariables()) {
      //boolean need_enhance = false;
      float scale_factor = Float.NaN;
      Attribute att = var.findAttributeIgnoreCase("Scale");
      if (att != null) {
        scale_factor = att.getNumericValue().floatValue();
        var.addAttribute(new Attribute(CDM.SCALE_FACTOR, 1.0f / scale_factor));
        //need_enhance = true;
      }
      att = var.findAttributeIgnoreCase("MissingData");
      if (null != att) {
        float val = att.getNumericValue().floatValue();
        if (!Float.isNaN(scale_factor)) val *= scale_factor;
        var.addAttribute(new Attribute(CDM.MISSING_VALUE, (short) val));
        //need_enhance = true;
      }
      //if (need_enhance)
      //  ((VariableDS)var).enhance();
    }

    ds.finish();
  }

  private void addLongName(NetcdfDataset ds, String varName, String longName) {
    Variable v = ds.findVariable(varName);
    if (v != null)
      v.addAttribute(new Attribute(CDM.LONG_NAME, longName));
  }

    private void addCoordinateAxisType(NetcdfDataset ds, String varName, AxisType type) {
    Variable v = ds.findVariable(varName);
    if (v != null)
      v.addAttribute(new Attribute(_Coordinate.AxisType, type.name()));
  }

  private void addCoordSystem(NetcdfDataset ds) throws IOException {

    double lat = ds.findGlobalAttributeIgnoreCase("Latitude").getNumericValue().doubleValue();
    double lon = ds.findGlobalAttributeIgnoreCase("Longitude").getNumericValue().doubleValue();
    double dlat = ds.findGlobalAttributeIgnoreCase("LatGridSpacing").getNumericValue().doubleValue();
    double dlon = ds.findGlobalAttributeIgnoreCase("LonGridSpacing").getNumericValue().doubleValue();
    int time = ds.findGlobalAttributeIgnoreCase("Time").getNumericValue().intValue();

    if (debug) System.out.println(ds.getLocation() + " Lat/Lon=" + lat + "/" + lon);

    int nlat = ds.findDimension("Lat").getLength();
    int nlon = ds.findDimension("Lon").getLength();

    // add lat
    CoordinateAxis v = new CoordinateAxis1D(ds, null, "Lat", DataType.FLOAT, "Lat", CDM.LAT_UNITS, "latitude coordinate");
    v.setValues(nlat, lat, -dlat);
    v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));
    ds.addCoordinateAxis( v);

    // add lon
    v = new CoordinateAxis1D(ds, null, "Lon", DataType.FLOAT, "Lon", CDM.LON_UNITS, "longitude coordinate");
    v.setValues(nlon, lon, dlon);
    v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
    ds.addCoordinateAxis( v);

    // add time
    ds.addDimension(null, new Dimension("Time", 1));
    v = new CoordinateAxis1D(ds, null, "Time", DataType.INT, "Time", "seconds since 1970-1-1 00:00:00", "time coordinate");
    v.setValues(1, time, 1);
    v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
    ds.addCoordinateAxis( v);
  }

}


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

/**
 * Convenience routines for constructing one-off StructureData objects
 *
 * @author caron
 * @since Jan 19, 2009
 */
public class StructureDataFactory {

  /* static public StructureData make(String name, String value) {
    StructureMembers members = new StructureMembers("");
    StructureMembers.Member m = members.addMember(name, null, null, DataType.STRING, new int[]{1});
    StructureDataW sw = new StructureDataW(members);
    Array dataArray = Array.factory(DataType.STRING, new int[]{1});
    dataArray.setObject(dataArray.getIndex(), value);
    sw.setMemberData(m, dataArray);
    return sw;
  } */

  static public StructureData make(String name, Object value) {
    StructureMembers members = new StructureMembers("");
    DataType dtype = DataType.getType(value.getClass(), false);  // LOOK unsigned
    StructureMembers.Member m = members.addMember(name, null, null, dtype, new int[]{1});
    StructureDataW sw = new StructureDataW(members);
    Array dataArray = Array.factory(dtype, new int[]{1});
    dataArray.setObject(dataArray.getIndex(), value);
    sw.setMemberData(m, dataArray);
    return sw;
  }

  static public StructureData make(StructureData s1, StructureData s2) {
    return make(new StructureData[]{s1, s2});
  }

  static public StructureData make(StructureData[] sdatas) {
    if (sdatas.length == 1) return sdatas[0];

    // look for sole
    int count = 0;
    StructureData result = null;
    for (StructureData sdata : sdatas) {
      if (sdata != null) {
        count++;
        result = sdata;
      }
    }
    if (count == 1) return result;

    // combine
    StructureDataComposite result2 = new StructureDataComposite();
    for (StructureData sdata : sdatas) {
      if (sdata != null)
        result2.add(sdata);
    }
    return result2;
  }

}


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

package thredds.inventory;

import thredds.client.catalog.Access;
import thredds.client.catalog.Dataset;
import thredds.client.catalog.tools.CatalogCrawler;
import thredds.client.catalog.tools.DataFactory;
import ucar.nc2.units.DateType;

import javax.annotation.concurrent.ThreadSafe;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.List;

/**
 * CollectionManager of datasets from a catalog.
 *
 * @author caron
 * @since Jan 14, 2010
 */
@ThreadSafe
public class CollectionManagerCatalog extends CollectionManagerAbstract implements CatalogCrawler.Listener {
  private final String catalogUrl;
  private long lastScanned;
  private boolean debug = false;
  private List<MFile> mfiles;

  public CollectionManagerCatalog(String collectionName, String collectionSpec, String olderThan, Formatter errlog) {
    super(collectionName, null);

    if (collectionSpec.startsWith(MFileCollectionManager.CATALOG))
      collectionSpec = collectionSpec.substring(MFileCollectionManager.CATALOG.length());

    int pos = collectionSpec.indexOf('?');
    if (pos > 0) {
      this.dateExtractor = new DateExtractorFromName(collectionSpec.substring(pos + 1), true);  // WTF ?
      collectionSpec = collectionSpec.substring(0, pos);
    }

    this.catalogUrl = collectionSpec;
    this.root = System.getProperty("user.dir");
  }

  @Override
  public String getRoot() {
    return null;
  }

  @Override
  public long getLastScanned() {
    return lastScanned;
  }

  @Override
  public long getLastChanged() {
    return 0;
  }

  @Override
  public boolean isScanNeeded() {
    return (mfiles == null); // LOOK
  }

  @Override
  public boolean scan(boolean sendEvent) throws IOException {
    mfiles = new ArrayList<>(100);

    CatalogCrawler crawler = new CatalogCrawler(CatalogCrawler.Type.all_direct, 0, null, this, null, null, null);
    long start = System.currentTimeMillis();
    try {
      crawler.crawl(catalogUrl);
    } finally {
      long took = (System.currentTimeMillis() - start);
      if (debug) System.out.format("***Done " + catalogUrl + " took = " + took + " msecs%n");
    }

    lastScanned = System.currentTimeMillis();
    return true;
  }

  @Override
  public Iterable<MFile> getFilesSorted() {
    return mfiles == null ? new ArrayList<>() : mfiles;
  }

  private static class MFileRemote implements MFile {
    private Object info;
    private final Access access;
    private Date lastModified;

    MFileRemote(Access access) {
      this.access = access;
      for (DateType dateType : access.getDataset().getDates()) {
        if (dateType.getType().equals("modified"))
          lastModified = dateType.getDate();
      }
    }

    @Override
    public long getLastModified() {
      return lastModified == null ? -1 : lastModified.getTime();
    }

    @Override
    public long getLength() {
      return access.getDataSize();
    }

    @Override
    public boolean isDirectory() {
      return false;
    }

    @Override
    public String getPath() {
      return access.getWrappedUrlName();
    }

    @Override
    public String getName() {
      return access.getDataset().getName();
    }

    @Override
    public MFile getParent() throws IOException {
      return null;
    }

    @Override
    public int compareTo(MFile o) {
      return getPath().compareTo(o.getPath());
    }

    @Override
    public Object getAuxInfo() {
      return info;
    }

    @Override
    public void setAuxInfo(Object info) {
      this.info = info;
    }
  }

  ///////////////////////////////
  // CatalogCrawler.Listener

  @Override
  public void getDataset(Dataset ds, Object context) {
    if (ds.hasAccess()) {
      DataFactory tdataFactory = new DataFactory();
      Access access = tdataFactory.chooseDatasetAccess(ds.getAccess());
      if (access == null) throw new IllegalStateException();
      MFileRemote mfile = new MFileRemote(access);
      if (mfile.getPath().endsWith(".xml")) return; // eliminate latest.xml  LOOK kludge-o-rama
      mfiles.add(mfile);
      if (debug) System.out.format("add %s %n", mfile.getPath());
    }
  }

  /* public static void main(String arg[]) throws IOException {
    Formatter errlog = new Formatter();
    String catUrl = "http://thredds.ucar.edu/thredds/catalog/fmrc/NCEP/NDFD/CONUS_5km/files/catalog.xml";
    CollectionManagerCatalog man = new CollectionManagerCatalog("test", catUrl, null, errlog);
    man.debug = true;
    man.scan(true);
    Fmrc fmrc = Fmrc.open(MFileCollectionManager.CATALOG+catUrl, errlog);
    System.out.printf("errlog = %s %n", errlog);
  }  */

}