/*
 * 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.
   */
  public static 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
    public static 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.
   */
  public static 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
    public static String hiddenProperties() {
      return "nameVocab";
    }

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

  //////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Implements Vocab type, just text with an optional "vocabulary" attribute.
   */
  public static 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
    public static 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.
   */
  public static class GeospatialCoverage {
    private static Range defaultEastwest = new Range(0.0, 0.0, Double.NaN, CDM.LON_UNITS);
    private static Range defaultNorthsouth = new Range(0.0, 0.0, Double.NaN, CDM.LAT_UNITS);
    private static Range defaultUpdown = new Range(0.0, 0.0, Double.NaN, "km");
    private static 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.
   */
  public static 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.
   */
  public static 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
    public static String hiddenProperties() {
      return "nameVocab";
    }

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

  /**
   * Implements Variables type.
   */
  public static 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));
   * }
   */
  public static void main(String[] args) throws IOException {
    GeospatialCoverage gc = new GeospatialCoverage();
    LatLonRect bb = new LatLonRect();
    gc.setBoundingBox(bb);
    gc.toXML(System.out);
  }

}

