cdm/src/main/java/thredds/client/catalog/tools/DatasetHtmlWriter.java
public void writeHtmlDescription(Formatter out, Dataset ds,
                                   boolean complete, boolean isServer,
                                   boolean datasetEvents, boolean catrefEvents,
                                   boolean resolveRelativeUrls) {

    if (ds == null) return;

    if (complete) {
      out.format("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"%n");
      out.format("        \"http://www.w3.org/TR/html4/loose.dtd\">%n");
      out.format("<html>%n");
      out.format("<head>%n");
      out.format("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">%n");
      out.format("</head>%n");
      out.format("<body>%n");
    }

    out.format("<h2>Dataset: %s</h2>%n<ul>", ds.getName());
    if (ds.getDataFormatName() != null)
      out.format(" <li><em>Data format: </em>%s</li>%n", htmlEscaper.escape(ds.getDataFormatName()));

    if ((ds.getDataSize() > 0))
      out.format(" <li><em>Data size: </em>%s</li>%n", Format.formatByteSize(ds.getDataSize()));

    if (ds.getFeatureTypeName() != null)
      out.format(" <li><em>Feature type: </em>%s</li>%n", htmlEscaper.escape(ds.getFeatureTypeName()));

    if (ds.getCollectionType() != null)
      out.format(" <li><em>Collection type: </em>%s</li>%n", htmlEscaper.escape(ds.getCollectionType()));

    if (ds.isHarvest())
      out.format(" <li><em>Harvest:</em> true</li>%n");

    if (ds.getAuthority() != null)
      out.format(" <li><em>Naming Authority: </em>%s</li>%n%n", htmlEscaper.escape(ds.getAuthority()));

    if (ds.getId() != null)
      out.format(" <li><em>ID: </em>%s</li>%n", htmlEscaper.escape(ds.getId()));

    if (ds.getRestrictAccess() != null)
      out.format(" <li><em>RestrictAccess: </em>%s</li>%n", htmlEscaper.escape(ds.getRestrictAccess()));

    if (ds instanceof CatalogRef) {
      CatalogRef catref = (CatalogRef) ds;
      String href = resolveRelativeUrls | catrefEvents ? resolve(ds, catref.getXlinkHref()) : catref.getXlinkHref();
      if (catrefEvents) href = "catref:" + href;
      out.format(" <li><em>CatalogRef: </em>%s</li>%n", makeHref(href, null, null));
    }

    out.format("</ul>%n");

    java.util.List<Documentation> docs = ds.getDocumentation();
    if (docs.size() > 0) {
      out.format("<h3>Documentation:</h3>%n<ul>%n");
      for (Documentation doc : docs) {
        String type = (doc.getType() == null) ? "" : "<strong>" + htmlEscaper.escape(doc.getType()) + ":</strong> ";
        String inline = doc.getInlineContent();
        if ((inline != null) && (inline.length() > 0))
          out.format(" <li>%s %s</li>%n", type, htmlEscaper.escape(inline));
        if (doc.hasXlink()) {
          out.format(" <li>%s %s</li>%n", type, makeHref(doc.getXlinkHref(), null, doc.getXlinkTitle()));
        }
      }
      out.format("</ul>%n");
    }

    java.util.List<Access> access = ds.getAccess();
    if (access.size() > 0) {
      out.format("<h3>Access:</h3>%n<ol>%n");
      for (Access a : access) {
        Service s = a.getService();
        String urlString = resolveRelativeUrls || datasetEvents ? a.getStandardUrlName() : a.getUnresolvedUrlName();
        String queryString = null;
        // String fullUrlString = urlString;
        if (datasetEvents) urlString = "dataset:" + urlString;

        ServiceType stype = s.getType();
        if (isServer && stype != null)
          switch (stype) {
            case OPENDAP:
            case DODS:
              urlString = urlString + ".html";
              break;

            case DAP4:
              urlString = urlString + ".dmr.xml";
              break;

            case WCS:
              queryString = "service=WCS&version=1.0.0&request=GetCapabilities";
              break;

            case WMS:
              queryString = "service=WMS&version=1.3.0&request=GetCapabilities";
              break;

            case NCML:
            case UDDC:
            case ISO:
              String catalogUrl = ds.getCatalogUrl();
              String datasetId = ds.getId();
              if (catalogUrl != null && datasetId != null) {
                if (catalogUrl.indexOf('#') > 0)
                  catalogUrl = catalogUrl.substring(0, catalogUrl.lastIndexOf('#'));
                /* try {
                  catalogUrl = URLEncoder.encode(catalogUrl, "UTF-8");
                  datasetId = URLEncoder.encode(datasetId, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
                } */
                queryString = "catalog=" + urlParamEscaper.escape(catalogUrl) + "&dataset=" + urlParamEscaper.escape(datasetId);
              }
              break;

            case NetcdfSubset:
              urlString = urlString + "/dataset.html";
              break;

            case CdmRemote:
              queryString = "req=cdl";
              break;
            case CdmrFeature:
              queryString = "req=form";
          }
        out.format(" <li> <b>%s: </b>%s</li>%n", s.getServiceTypeName(), makeHref(urlString, queryString, null));
      }
      out.format("</ol>%n");
    }

    java.util.List<ThreddsMetadata.Contributor> contributors = ds.getContributors();
    if (contributors.size() > 0) {
      out.format("<h3>Contributors:</h3>%n<ul>%n");
      for (ThreddsMetadata.Contributor t : contributors) {
        String role = (t.getRole() == null) ? "" : "<strong> (" + htmlEscaper.escape(t.getRole()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getName()), role);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Vocab> keywords = ds.getKeywords();
    if (keywords.size() > 0) {
      out.format("<h3>Keywords:</h3>%n<ul>%n");
      for (ThreddsMetadata.Vocab t : keywords) {
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>(" + htmlEscaper.escape(t.getVocabulary()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("</ul>%n");
    }

    java.util.List<DateType> dates = ds.getDates();
    if (dates.size() > 0) {
      out.format("<h3>Dates:</h3>%n<ul>%n");
      for (DateType d : dates) {
        String type = (d.getType() == null) ? "" : " <strong>(" + htmlEscaper.escape(d.getType()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(d.getText()), type);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Vocab> projects = ds.getProjects();
    if (projects.size() > 0) {
      out.format("<h3>Projects:</h3>%n<ul>%n");
      for (ThreddsMetadata.Vocab t : projects) {
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>(" + htmlEscaper.escape(t.getVocabulary()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Source> creators = ds.getCreators();
    if (creators.size() > 0) {
      out.format("<h3>Creators:</h3>%n<ul>%n");
      for (ThreddsMetadata.Source t : creators) {
        out.format(" <li><strong>%s</strong><ul>%n", htmlEscaper.escape(t.getName()));
        out.format(" <li><em>email: </em>%s</li>%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String newUrl = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format(" <li> <em>%s</em></li>%n", newUrl);
        }
        out.format(" </ul></li>%n");
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Source> publishers = ds.getPublishers();
    if (publishers.size() > 0) {
      out.format("<h3>Publishers:</h3>%n<ul>%n");
      for (ThreddsMetadata.Source t : publishers) {
        out.format(" <li><strong>%s</strong><ul>%n", htmlEscaper.escape(t.getName()));
        out.format(" <li><em>email: </em>%s%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String urlLink = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format(" <li> <em>%s</em></li>%n", urlLink);
        }
        out.format(" </ul>%n");
      }
      out.format("</ul>%n");
    }
 
     /*
     4.2:
     <h3>Variables:</h3>
     <ul>
     <li><em>Vocabulary</em> [DIF]:
     <ul>
      <li><strong>Reflectivity</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Radar Reflectivity (db)
      <li><strong>Velocity</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Doppler Velocity (m/s)
      <li><strong>SpectrumWidth</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Doppler Spectrum Width (m/s)
      </ul>
      </ul>
     </ul>
 
     4.3:
     <h3>Variables:</h3>
     <ul>
     <li><em>Vocabulary</em> [CF-1.0]:
     <ul>
      <li><strong>d3d (meters) </strong> =  <i>3D Depth at Nodes
     <p>        </i> = depth_at_nodes
      <li><strong>depth (meters) </strong> =  <i>Bathymetry</i> = depth
      <li><strong>eta (m) </strong> =  <i></i> =
      <li><strong>temp (Celsius) </strong> =  <i>Temperature
     <p>        </i> = sea_water_temperature
      <li><strong>u (m/s) </strong> =  <i>Eastward Water
     <p>          Velocity
     <p>        </i> = eastward_sea_water_velocity
      <li><strong>v (m/s) </strong> =  <i>Northward Water
     <p>          Velocity
     <p>        </i> = northward_sea_water_velocity
     </ul>
     </ul>
      */
    java.util.List<ThreddsMetadata.VariableGroup> vars = ds.getVariables();
    if (vars.size() > 0) {
      out.format("<h3>Variables:</h3>%n<ul>%n");
      for (ThreddsMetadata.VariableGroup t : vars) {

        out.format("<li><em>Vocabulary</em> [");
        if (t.getVocabUri() != null) {
          ThreddsMetadata.UriResolved uri = t.getVocabUri();
          String vocabLink = resolveRelativeUrls ? makeHref(uri.resolved.toString(), null, t.getVocabulary()) : makeHref(uri.href, null, t.getVocabulary());
          out.format(vocabLink);
        } else {
          out.format(htmlEscaper.escape(t.getVocabulary()));
        }
        out.format("]:%n<ul>%n");

        java.util.List<ThreddsMetadata.Variable> vlist = t.getVariableList();
        if (vlist.size() > 0) {
          for (ThreddsMetadata.Variable v : vlist) {
            String units = (v.getUnits() == null || v.getUnits().length() == 0) ? "" : " (" + v.getUnits() + ") ";
            out.format(" <li><strong>%s</strong> = ", htmlEscaper.escape(v.getName() + units));
            if (v.getDescription() != null)
              out.format(" <i>%s</i> = ", htmlEscaper.escape(v.getDescription()));
            if (v.getVocabularyName() != null)
              out.format("%s", htmlEscaper.escape(v.getVocabularyName()));
            out.format("%n");
          }
        }
        out.format("</ul>%n");
      }
      out.format("</ul>%n");
    }

    // LOOK what about VariableMapLink string ??
    if (ds.getVariableMapLink() != null) {
      out.format("<h3>Variables:</h3>%n");
      ThreddsMetadata.UriResolved uri = ds.getVariableMapLink();
      out.format("<ul><li>%s</li></ul>%n", makeHref(uri.resolved.toASCIIString(), null, "VariableMap"));
    }

    ThreddsMetadata.GeospatialCoverage gc = ds.getGeospatialCoverage();
    if (gc != null) {
      out.format("<h3>GeospatialCoverage:</h3>%n<ul>%n");
      // if (gc.isGlobal()) out.format(" <li><em> Global </em>%n");

      out.format(" <li><em> Longitude: </em> %s</li>%n", rangeString(gc.getEastWestRange()));
      out.format(" <li><em> Latitude: </em> %s</li>%n", rangeString(gc.getNorthSouthRange()));
      if (gc.getUpDownRange() != null) {
        out.format(" <li><em> Altitude: </em> %s (positive is <strong>%s)</strong></li>%n", rangeString(gc.getUpDownRange()), gc.getZPositive());
      }

      java.util.List<ThreddsMetadata.Vocab> nlist = gc.getNames();
      if ((nlist != null) && (nlist.size() > 0)) {
        out.format(" <li><em>  Names: </em> <ul>%n");
        for (ThreddsMetadata.Vocab elem : nlist) {
          out.format(" <li>%s</li>%n", htmlEscaper.escape(elem.getText()));
        }
        out.format(" </ul>%n");
      }
      out.format(" </ul>%n");
    }

    DateRange tc = ds.getTimeCoverage();
    if (tc != null) {
      out.format("<h3>TimeCoverage:</h3>%n<ul>%n");
      DateType start = tc.getStart();
      if (start != null)
        out.format(" <li><em>  Start: </em> %s</li>%n", start.toString());
      DateType end = tc.getEnd();
      if (end != null) {
        out.format(" <li><em>  End: </em> %s</li>%n", end.toString());
      }
      TimeDuration duration = tc.getDuration();
      if (duration != null)
        out.format(" <li><em>  Duration: </em> %s</li>%n", htmlEscaper.escape(duration.toString()));
      TimeDuration resolution = tc.getResolution();
      if (resolution != null) {
        out.format(" <li><em>  Resolution: </em> %s</li>%n", htmlEscaper.escape(resolution.toString()));
      }
      out.format(" </ul>%n");
    }

    java.util.List<ThreddsMetadata.MetadataOther> metadata = ds.getMetadataOther();
    boolean gotSomeMetadata = false;
    for (ThreddsMetadata.MetadataOther m : metadata) {
      if (m.getXlinkHref() != null) gotSomeMetadata = true;
    }

    if (gotSomeMetadata) {
      out.format("<h3>Metadata:</h3>%n<ul>%n");
      for (ThreddsMetadata.MetadataOther m : metadata) {
        String type = (m.getType() == null) ? "" : m.getType();
        if (m.getXlinkHref() != null) {
          String title = (m.getTitle() == null) ? "Type " + type : m.getTitle();
          String mdLink = resolveRelativeUrls ? makeHrefResolve(ds, m.getXlinkHref(), title) : makeHref(m.getXlinkHref(), null, title);
          out.format(" <li> %s</li>%n", mdLink);
        } //else {
        //out.format(" <li> <pre>"+m.getMetadataType()+" "+m.getContentObject()+"</pre>%n");
        //}
      }
      out.format("</ul>%n");
    }

    java.util.List<Property> propsOrg = ds.getProperties();
    java.util.List<Property> props = new ArrayList<>(ds.getProperties().size());
    for (Property p : propsOrg) {
      if (!p.getName().startsWith("viewer"))  // eliminate the viewer properties from the html view
        props.add(p);
    }
    if (props.size() > 0) {
      out.format("<h3>Properties:</h3>%n<ul>%n");
      for (Property p : props) {
        if (p.getName().equals("attachments")) { // LOOK whats this ?
          String attachLink = resolveRelativeUrls ? makeHrefResolve(ds, p.getValue(), p.getName()) : makeHref(p.getValue(), null, p.getName());
          out.format(" <li>%s</li>%n", attachLink);
        } else {
          out.format(" <li>%s = \"%s\"</li>%n", htmlEscaper.escape(p.getName()), htmlEscaper.escape(p.getValue()));
        }
      }
      out.format("</ul>%n");
    }

    if (complete) out.format("</body></html>");
  }
public void writeHtmlDescription(Formatter out, Dataset ds,
                                   boolean complete, boolean isServer,
                                   boolean datasetEvents, boolean catrefEvents,
                                   boolean resolveRelativeUrls) {

    if (ds == null) return;

    if (complete) {
      out.format("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"%n");
      out.format("        \"http://www.w3.org/TR/html4/loose.dtd\">%n");
      out.format("<html>%n");
      out.format("<head>%n");
      out.format("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">%n");
      out.format("</head>%n");
      out.format("<body>%n");
    }

    out.format("<h2>Dataset: %s</h2>%n<ul>", ds.getName());
    if (ds.getDataFormatName() != null)
      out.format(" <li><em>Data format: </em>%s</li>%n", htmlEscaper.escape(ds.getDataFormatName()));

    if ((ds.getDataSize() > 0))
      out.format(" <li><em>Data size: </em>%s</li>%n", Format.formatByteSize(ds.getDataSize()));

    if (ds.getFeatureTypeName() != null)
      out.format(" <li><em>Feature type: </em>%s</li>%n", htmlEscaper.escape(ds.getFeatureTypeName()));

    if (ds.getCollectionType() != null)
      out.format(" <li><em>Collection type: </em>%s</li>%n", htmlEscaper.escape(ds.getCollectionType()));

    if (ds.isHarvest())
      out.format(" <li><em>Harvest:</em> true</li>%n");

    if (ds.getAuthority() != null)
      out.format(" <li><em>Naming Authority: </em>%s</li>%n%n", htmlEscaper.escape(ds.getAuthority()));

    if (ds.getId() != null)
      out.format(" <li><em>ID: </em>%s</li>%n", htmlEscaper.escape(ds.getId()));

    if (ds.getRestrictAccess() != null)
      out.format(" <li><em>RestrictAccess: </em>%s</li>%n", htmlEscaper.escape(ds.getRestrictAccess()));

    if (ds instanceof CatalogRef) {
      CatalogRef catref = (CatalogRef) ds;
      String href = resolveRelativeUrls | catrefEvents ? resolve(ds, catref.getXlinkHref()) : catref.getXlinkHref();
      if (catrefEvents) href = "catref:" + href;
      out.format(" <li><em>CatalogRef: </em>%s</li>%n", makeHref(href, null, null));
    }

    out.format("</ul>%n");

    java.util.List<Documentation> docs = ds.getDocumentation();
    if (docs.size() > 0) {
      out.format("<h3>Documentation:</h3>%n<ul>%n");
      for (Documentation doc : docs) {
        String type = (doc.getType() == null) ? "" : "<strong>" + htmlEscaper.escape(doc.getType()) + ":</strong> ";
        String inline = doc.getInlineContent();
        if ((inline != null) && (inline.length() > 0))
          out.format(" <li>%s %s</li>%n", type, htmlEscaper.escape(inline));
        if (doc.hasXlink()) {
          out.format(" <li>%s %s</li>%n", type, makeHref(doc.getXlinkHref(), null, doc.getXlinkTitle()));
        }
      }
      out.format("</ul>%n");
    }

    java.util.List<Access> access = ds.getAccess();
    if (access.size() > 0) {
      out.format("<h3>Access:</h3>%n<ol>%n");
      for (Access a : access) {
        Service s = a.getService();
        String urlString = resolveRelativeUrls || datasetEvents ? a.getStandardUrlName() : a.getUnresolvedUrlName();
        String queryString = null;
        // String fullUrlString = urlString;
        if (datasetEvents) urlString = "dataset:" + urlString;

        ServiceType stype = s.getType();
        if (isServer && stype != null)
          switch (stype) {
            case OPENDAP:
            case DODS:
              urlString = urlString + ".html";
              break;

            case DAP4:
              urlString = urlString + ".dmr.xml";
              break;

            case WCS:
              queryString = "service=WCS&version=1.0.0&request=GetCapabilities";
              break;

            case WMS:
              queryString = "service=WMS&version=1.3.0&request=GetCapabilities";
              break;

            case NCML:
            case UDDC:
            case ISO:
              String catalogUrl = ds.getCatalogUrl();
              String datasetId = ds.getId();
              if (catalogUrl != null && datasetId != null) {
                if (catalogUrl.indexOf('#') > 0)
                  catalogUrl = catalogUrl.substring(0, catalogUrl.lastIndexOf('#'));
                /* try {
                  catalogUrl = URLEncoder.encode(catalogUrl, "UTF-8");
                  datasetId = URLEncoder.encode(datasetId, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
                } */
                queryString = "catalog=" + urlParamEscaper.escape(catalogUrl) + "&dataset=" + urlParamEscaper.escape(datasetId);
              }
              break;

            case NetcdfSubset:
              urlString = urlString + "/dataset.html";
              break;

            case CdmRemote:
              queryString = "req=cdl";
              break;
            case CdmrFeature:
              queryString = "req=form";
          }
        out.format(" <li> <b>%s: </b>%s</li>%n", s.getServiceTypeName(), makeHref(urlString, queryString, null));
      }
      out.format("</ol>%n");
    }

    java.util.List<ThreddsMetadata.Contributor> contributors = ds.getContributors();
    if (contributors.size() > 0) {
      out.format("<h3>Contributors:</h3>%n<ul>%n");
      for (ThreddsMetadata.Contributor t : contributors) {
        String role = (t.getRole() == null) ? "" : "<strong> (" + htmlEscaper.escape(t.getRole()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getName()), role);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Vocab> keywords = ds.getKeywords();
    if (keywords.size() > 0) {
      out.format("<h3>Keywords:</h3>%n<ul>%n");
      for (ThreddsMetadata.Vocab t : keywords) {
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>(" + htmlEscaper.escape(t.getVocabulary()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("</ul>%n");
    }

    java.util.List<DateType> dates = ds.getDates();
    if (dates.size() > 0) {
      out.format("<h3>Dates:</h3>%n<ul>%n");
      for (DateType d : dates) {
        String type = (d.getType() == null) ? "" : " <strong>(" + htmlEscaper.escape(d.getType()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(d.getText()), type);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Vocab> projects = ds.getProjects();
    if (projects.size() > 0) {
      out.format("<h3>Projects:</h3>%n<ul>%n");
      for (ThreddsMetadata.Vocab t : projects) {
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>(" + htmlEscaper.escape(t.getVocabulary()) + ")</strong> ";
        out.format(" <li>%s %s</li>%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Source> creators = ds.getCreators();
    if (creators.size() > 0) {
      out.format("<h3>Creators:</h3>%n<ul>%n");
      for (ThreddsMetadata.Source t : creators) {
        out.format(" <li><strong>%s</strong><ul>%n", htmlEscaper.escape(t.getName()));
        out.format(" <li><em>email: </em>%s</li>%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String newUrl = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format(" <li> <em>%s</em></li>%n", newUrl);
        }
        out.format(" </ul></li>%n");
      }
      out.format("</ul>%n");
    }

    java.util.List<ThreddsMetadata.Source> publishers = ds.getPublishers();
    if (publishers.size() > 0) {
      out.format("<h3>Publishers:</h3>%n<ul>%n");
      for (ThreddsMetadata.Source t : publishers) {
        out.format(" <li><strong>%s</strong><ul>%n", htmlEscaper.escape(t.getName()));
        out.format(" <li><em>email: </em>%s%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String urlLink = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format(" <li> <em>%s</em></li>%n", urlLink);
        }
        out.format(" </ul>%n");
      }
      out.format("</ul>%n");
    }
 
     /*
     4.2:
     <h3>Variables:</h3>
     <ul>
     <li><em>Vocabulary</em> [DIF]:
     <ul>
      <li><strong>Reflectivity</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Radar Reflectivity (db)
      <li><strong>Velocity</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Doppler Velocity (m/s)
      <li><strong>SpectrumWidth</strong> =  <i></i> = EARTH SCIENCE &gt; Spectral/Engineering &gt; Radar &gt; Doppler Spectrum Width (m/s)
      </ul>
      </ul>
     </ul>
 
     4.3:
     <h3>Variables:</h3>
     <ul>
     <li><em>Vocabulary</em> [CF-1.0]:
     <ul>
      <li><strong>d3d (meters) </strong> =  <i>3D Depth at Nodes
     <p>        </i> = depth_at_nodes
      <li><strong>depth (meters) </strong> =  <i>Bathymetry</i> = depth
      <li><strong>eta (m) </strong> =  <i></i> =
      <li><strong>temp (Celsius) </strong> =  <i>Temperature
     <p>        </i> = sea_water_temperature
      <li><strong>u (m/s) </strong> =  <i>Eastward Water
     <p>          Velocity
     <p>        </i> = eastward_sea_water_velocity
      <li><strong>v (m/s) </strong> =  <i>Northward Water
     <p>          Velocity
     <p>        </i> = northward_sea_water_velocity
     </ul>
     </ul>
      */
    java.util.List<ThreddsMetadata.VariableGroup> vars = ds.getVariables();
    if (vars.size() > 0) {
      out.format("<h3>Variables:</h3>%n<ul>%n");
      for (ThreddsMetadata.VariableGroup t : vars) {

        out.format("<li><em>Vocabulary</em> [");
        if (t.getVocabUri() != null) {
          ThreddsMetadata.UriResolved uri = t.getVocabUri();
          String vocabLink = resolveRelativeUrls ? makeHref(uri.resolved.toString(), null, t.getVocabulary()) : makeHref(uri.href, null, t.getVocabulary());
          out.format(vocabLink);
        } else {
          out.format(htmlEscaper.escape(t.getVocabulary()));
        }
        out.format("]:%n<ul>%n");

        java.util.List<ThreddsMetadata.Variable> vlist = t.getVariableList();
        if (vlist.size() > 0) {
          for (ThreddsMetadata.Variable v : vlist) {
            String units = (v.getUnits() == null || v.getUnits().length() == 0) ? "" : " (" + v.getUnits() + ") ";
            out.format(" <li><strong>%s</strong> = ", htmlEscaper.escape(v.getName() + units));
            if (v.getDescription() != null)
              out.format(" <i>%s</i> = ", htmlEscaper.escape(v.getDescription()));
            if (v.getVocabularyName() != null)
              out.format("%s", htmlEscaper.escape(v.getVocabularyName()));
            out.format("%n");
          }
        }
        out.format("</ul>%n");
      }
      out.format("</ul>%n");
    }

    // LOOK what about VariableMapLink string ??
    if (ds.getVariableMapLink() != null) {
      out.format("<h3>Variables:</h3>%n");
      ThreddsMetadata.UriResolved uri = ds.getVariableMapLink();
      out.format("<ul><li>%s</li></ul>%n", makeHref(uri.resolved.toASCIIString(), null, "VariableMap"));
    }

    ThreddsMetadata.GeospatialCoverage gc = ds.getGeospatialCoverage();
    if (gc != null) {
      out.format("<h3>GeospatialCoverage:</h3>%n<ul>%n");
      // if (gc.isGlobal()) out.format(" <li><em> Global </em>%n");

      out.format(" <li><em> Longitude: </em> %s</li>%n", rangeString(gc.getEastWestRange()));
      out.format(" <li><em> Latitude: </em> %s</li>%n", rangeString(gc.getNorthSouthRange()));
      if (gc.getUpDownRange() != null) {
        out.format(" <li><em> Altitude: </em> %s (positive is <strong>%s)</strong></li>%n", rangeString(gc.getUpDownRange()), gc.getZPositive());
      }

      java.util.List<ThreddsMetadata.Vocab> nlist = gc.getNames();
      if ((nlist != null) && (nlist.size() > 0)) {
        out.format(" <li><em>  Names: </em> <ul>%n");
        for (ThreddsMetadata.Vocab elem : nlist) {
          out.format(" <li>%s</li>%n", htmlEscaper.escape(elem.getText()));
        }
        out.format(" </ul>%n");
      }
      out.format(" </ul>%n");
    }

    DateRange tc = ds.getTimeCoverage();
    if (tc != null) {
      out.format("<h3>TimeCoverage:</h3>%n<ul>%n");
      DateType start = tc.getStart();
      if (start != null)
        out.format(" <li><em>  Start: </em> %s</li>%n", start.toString());
      DateType end = tc.getEnd();
      if (end != null) {
        out.format(" <li><em>  End: </em> %s</li>%n", end.toString());
      }
      TimeDuration duration = tc.getDuration();
      if (duration != null)
        out.format(" <li><em>  Duration: </em> %s</li>%n", htmlEscaper.escape(duration.toString()));
      TimeDuration resolution = tc.getResolution();
      if (resolution != null) {
        out.format(" <li><em>  Resolution: </em> %s</li>%n", htmlEscaper.escape(resolution.toString()));
      }
      out.format(" </ul>%n");
    }

    java.util.List<ThreddsMetadata.MetadataOther> metadata = ds.getMetadataOther();
    boolean gotSomeMetadata = false;
    for (ThreddsMetadata.MetadataOther m : metadata) {
      if (m.getXlinkHref() != null) gotSomeMetadata = true;
    }

    if (gotSomeMetadata) {
      out.format("<h3>Metadata:</h3>%n<ul>%n");
      for (ThreddsMetadata.MetadataOther m : metadata) {
        String type = (m.getType() == null) ? "" : m.getType();
        if (m.getXlinkHref() != null) {
          String title = (m.getTitle() == null) ? "Type " + type : m.getTitle();
          String mdLink = resolveRelativeUrls ? makeHrefResolve(ds, m.getXlinkHref(), title) : makeHref(m.getXlinkHref(), null, title);
          out.format(" <li> %s</li>%n", mdLink);
        } //else {
        //out.format(" <li> <pre>"+m.getMetadataType()+" "+m.getContentObject()+"</pre>%n");
        //}
      }
      out.format("</ul>%n");
    }

    java.util.List<Property> propsOrg = ds.getProperties();
    java.util.List<Property> props = new ArrayList<>(ds.getProperties().size());
    for (Property p : propsOrg) {
      if (!p.getName().startsWith("viewer"))  // eliminate the viewer properties from the html view
        props.add(p);
    }
    if (props.size() > 0) {
      out.format("<h3>Properties:</h3>%n<ul>%n");
      for (Property p : props) {
        if (p.getName().equals("attachments")) { // LOOK whats this ?
          String attachLink = resolveRelativeUrls ? makeHrefResolve(ds, p.getValue(), p.getName()) : makeHref(p.getValue(), null, p.getName());
          out.format(" <li>%s</li>%n", attachLink);
        } else {
          out.format(" <li>%s = \"%s\"</li>%n", htmlEscaper.escape(p.getName()), htmlEscaper.escape(p.getValue()));
        }
      }
      out.format("</ul>%n");
    }

    if (complete) out.format("</body></html>");
  }
Write an Html representation of the given dataset.
<p> With datasetEvents, catrefEvents = true, this is used to construct an HTML page on the client
(eg using HtmlPage); the client then detects URL clicks and processes.
<p> With datasetEvents, catrefEvents = false, this is used to construct an HTML page on the server.
(eg using HtmlPage); the client then detects URL clicks and processes.

@param out           put HTML here.
@param ds            the dataset.
@param complete      if true, add HTML header and ender so its a complete, valid HTML page.
@param isServer      if true, then we are in the thredds data server, so do the following: <ul>
<li> append "html" to DODS Access URLs
</ul>
@param datasetEvents if true, prepend "dataset:" to any dataset access URLS
@param catrefEvents  if true, prepend "catref:" to any catref URLS