/* * Copyright (c) 1998-2020 John Caron and University Corporation for Atmospheric Research/Unidata * See LICENSE for license information. */ package ucar.nc2.ft.point.writer2; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; import ucar.ma2.DataType; import ucar.nc2.Attribute; import ucar.nc2.AttributeContainer; import ucar.nc2.AttributeContainerMutable; import ucar.nc2.Dimension; import ucar.nc2.VariableSimpleIF; import ucar.nc2.constants.CDM; import ucar.nc2.constants.FeatureType; import ucar.nc2.ft.DsgFeatureCollection; import ucar.nc2.ft.FeatureDatasetPoint; import ucar.nc2.ft.StationTimeSeriesFeatureCollection; import ucar.nc2.ft.point.StationFeature; import ucar.nc2.ncml.NcMLReader; import ucar.nc2.time.CalendarDate; import ucar.nc2.time.CalendarDateFormatter; import ucar.nc2.time.CalendarDateRange; import ucar.nc2.time.CalendarDateUnit; import ucar.nc2.write.NcmlWriter; import ucar.unidata.geoloc.LatLonPoint; import ucar.unidata.geoloc.LatLonRect; import ucar.unidata.geoloc.Station; /** generate capabilities XML for a FeatureDatasetPoint / StationTimeSeriesFeatureCollection */ public class FeatureDatasetCapabilitiesWriter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FeatureDatasetCapabilitiesWriter.class); private FeatureDatasetPoint fdp; private String path; public FeatureDatasetCapabilitiesWriter(FeatureDatasetPoint fdp, String path) { this.fdp = fdp; this.path = path; } public String getCapabilities() { XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat()); return fmt.outputString(getCapabilitiesDocument()); } public void getCapabilities(OutputStream os) throws IOException { XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat()); fmt.output(getCapabilitiesDocument(), os); } /** * Create an XML document for the stations in this dataset, possible subsetted by bb. * Must be a station dataset. * * @param bb restrict stations to this bounding box, may be null * @param names restrict stations to these names, may be null * @return XML document for the stations */ public Document makeStationCollectionDocument(LatLonRect bb, String[] names) { List<DsgFeatureCollection> list = fdp.getPointFeatureCollectionList(); DsgFeatureCollection fc = list.get(0); // LOOK maybe should pass in the dsg? if (!(fc instanceof StationTimeSeriesFeatureCollection)) { throw new UnsupportedOperationException(fc.getClass().getName() + " not a StationTimeSeriesFeatureCollection"); } StationTimeSeriesFeatureCollection sobs = (StationTimeSeriesFeatureCollection) fc; Element rootElem = new Element("stationCollection"); Document doc = new Document(rootElem); List<StationFeature> stations; if (bb != null) stations = sobs.getStationFeatures(bb); else if (names != null) stations = sobs.getStationFeatures(Arrays.asList(names)); else stations = sobs.getStationFeatures(); for (Station s : stations) { Element sElem = new Element("station"); sElem.setAttribute("name", s.getName()); if (s.getWmoId() != null) sElem.setAttribute("wmo_id", s.getWmoId()); if ((s.getDescription() != null) && (!s.getDescription().isEmpty())) sElem.addContent(new Element("description").addContent(s.getDescription())); sElem.addContent(new Element("longitude").addContent(Double.toString(s.getLongitude()))); sElem.addContent(new Element("latitide").addContent(Double.toString(s.getLatitude()))); if (!Double.isNaN(s.getAltitude())) sElem.addContent(new Element("altitude").addContent(Double.toString(s.getAltitude()))); rootElem.addContent(sElem); } return doc; } /** * Create the capabilities XML document for this dataset * * @return capabilities XML document */ public Document getCapabilitiesDocument() { Element rootElem = new Element("capabilities"); Document doc = new Document(rootElem); if (null != path) { rootElem.setAttribute("location", path); Element elem = new Element("featureDataset"); FeatureType ft = fdp.getFeatureType(); elem.setAttribute("type", ft.toString().toLowerCase()); String url = path.replace("dataset.xml", ft.toString().toLowerCase() + ".xml"); elem.setAttribute("url", url); rootElem.addContent(elem); } List<DsgFeatureCollection> list = fdp.getPointFeatureCollectionList(); DsgFeatureCollection fc = list.get(0); // LOOK maybe should pass in the dsg? rootElem.addContent(writeTimeUnit(fc.getTimeUnit())); rootElem.addContent(new Element("AltitudeUnits").addContent(fc.getAltUnits())); // data variables List<? extends VariableSimpleIF> vars = fdp.getDataVariables(); Collections.sort(vars); for (VariableSimpleIF v : vars) { rootElem.addContent(writeVariable(v)); } LatLonRect bb = fc.getBoundingBox(); if (bb != null) rootElem.addContent(writeBoundingBox(bb)); // add date range CalendarDateRange dateRange = fc.getCalendarDateRange(); if (dateRange != null) { Element drElem = new Element("TimeSpan"); // from KML drElem.addContent(new Element("begin").addContent(dateRange.getStart().toString())); drElem.addContent(new Element("end").addContent(dateRange.getEnd().toString())); if (dateRange.getResolution() != null) drElem.addContent(new Element("resolution").addContent(dateRange.getResolution().toString())); rootElem.addContent(drElem); } return doc; } private Element writeTimeUnit(CalendarDateUnit dateUnit) { Element elem = new Element("TimeUnit"); elem.addContent(dateUnit.getUdUnit()); elem.setAttribute("calendar", dateUnit.getCalendar().toString()); return elem; } private Element writeBoundingBox(LatLonRect bb) { int decToKeep = 6; double bbExpand = Math.pow(10, -decToKeep); // extend the bbox to make sure the implicit rounding does not result in a bbox that does not contain // any points (can happen when you have a single station with very precise lat/lon values) // See https://github.com/Unidata/thredds/issues/470 // This accounts for the implicit rounding errors that result from the use of // ucar.unidata.util.Format.dfrac when writing out the lat/lon box on the NCSS for Points dataset.html // page LatLonPoint extendNorthEast = LatLonPoint.create(bb.getLatMax() + bbExpand, bb.getLonMax() + bbExpand); LatLonPoint extendSouthWest = LatLonPoint.create(bb.getLatMin() - bbExpand, bb.getLonMin() - bbExpand); bb.extend(extendNorthEast); bb.extend(extendSouthWest); Element bbElem = new Element("LatLonBox"); // from KML bbElem.addContent(new Element("west").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMin(), decToKeep))); bbElem.addContent(new Element("east").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMax(), decToKeep))); bbElem.addContent(new Element("south").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMin(), decToKeep))); bbElem.addContent(new Element("north").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMax(), decToKeep))); return bbElem; } private Element writeVariable(VariableSimpleIF v) { NcmlWriter ncMLWriter = new NcmlWriter(); Element varElem = new Element("variable"); varElem.setAttribute("name", v.getShortName()); DataType dt = v.getDataType(); if (dt != null) varElem.setAttribute("type", dt.toString()); // attributes for (Attribute att : v.attributes()) { varElem.addContent(ncMLWriter.makeAttributeElement(att)); } return varElem; } ///////////////////////////////////////////// // public Document readCapabilitiesDocument(InputStream in) throws JDOMException, IOException { SAXBuilder builder = new SAXBuilder(); return builder.build(in); } public static LatLonRect getSpatialExtent(Document doc) { Element root = doc.getRootElement(); Element latlonBox = root.getChild("LatLonBox"); if (latlonBox == null) return null; String westS = latlonBox.getChildText("west"); String eastS = latlonBox.getChildText("east"); String northS = latlonBox.getChildText("north"); String southS = latlonBox.getChildText("south"); if ((westS == null) || (eastS == null) || (northS == null) || (southS == null)) return null; try { double west = Double.parseDouble(westS); double east = Double.parseDouble(eastS); double south = Double.parseDouble(southS); double north = Double.parseDouble(northS); return new LatLonRect(LatLonPoint.create(south, east), LatLonPoint.create(north, west)); } catch (Exception e) { return null; } } public static CalendarDateRange getTimeSpan(Document doc) { Element root = doc.getRootElement(); Element timeSpan = root.getChild("TimeSpan"); if (timeSpan == null) return null; String beginS = timeSpan.getChildText("begin"); String endS = timeSpan.getChildText("end"); // String resS = timeSpan.getChildText("resolution"); if ((beginS == null) || (endS == null)) return null; try { CalendarDate start = CalendarDateFormatter.isoStringToCalendarDate(null, beginS); CalendarDate end = CalendarDateFormatter.isoStringToCalendarDate(null, endS); if ((start == null) || (end == null)) { return null; } CalendarDateRange dr = CalendarDateRange.of(start, end); // LOOK if (resS != null) // dr.setResolution(new TimeDuration(resS)); return dr; } catch (Exception e) { return null; } } public static CalendarDateUnit getTimeUnit(Document doc) { Element root = doc.getRootElement(); Element timeUnitE = root.getChild("TimeUnit"); if (timeUnitE == null) return null; String cal = timeUnitE.getAttributeValue("calendar"); String timeUnitS = timeUnitE.getTextNormalize(); try { return CalendarDateUnit.of(cal, timeUnitS); } catch (Exception e) { log.error("Illegal date unit {} in FeatureDatasetCapabilitiesXML", timeUnitS); return null; } } public static String getAltUnits(Document doc) { Element root = doc.getRootElement(); String altUnits = root.getChildText("AltitudeUnits"); if (altUnits == null || altUnits.isEmpty()) return null; return altUnits; } public static List<VariableSimpleIF> getDataVariables(Document doc) { Element root = doc.getRootElement(); List<VariableSimpleIF> dataVars = new ArrayList<>(); List<Element> varElems = root.getChildren("variable"); for (Element varElem : varElems) { dataVars.add(new VariableSimpleAdapter(varElem)); } return dataVars; } private static class VariableSimpleAdapter implements VariableSimpleIF { String name, desc, units; DataType dt; List<Attribute> atts; VariableSimpleAdapter(Element velem) { name = velem.getAttributeValue("name"); String type = velem.getAttributeValue("type"); dt = DataType.getType(type); atts = new ArrayList<>(); List<Element> attElems = velem.getChildren("attribute"); for (Element attElem : attElems) { String attName = attElem.getAttributeValue("name"); ucar.ma2.Array values = NcMLReader.readAttributeValues(attElem); atts.add(new Attribute(attName, values)); } for (Attribute att : atts) { if (att.getShortName().equals(CDM.UNITS)) units = att.getStringValue(); if (att.getShortName().equals(CDM.LONG_NAME)) desc = att.getStringValue(); if ((desc == null) && att.getShortName().equals("description")) desc = att.getStringValue(); if ((desc == null) && att.getShortName().equals("standard_name")) desc = att.getStringValue(); } } @Override public String getName() { return name; } @Override public String getFullName() { return name; } @Override public String getShortName() { return name; } @Override public String getDescription() { return desc; } @Override public String getUnitsString() { return units; } @Override public int getRank() { return 0; } @Override public int[] getShape() { return new int[0]; } @Override public List<Dimension> getDimensions() { return null; } @Override public DataType getDataType() { return dt; } @Override public List<Attribute> getAttributes() { return atts; } @Override public Attribute findAttributeIgnoreCase(String name) { for (Attribute att : atts) { if (att.getShortName().equalsIgnoreCase(name)) return att; } return null; } @Override public AttributeContainer attributes() { return new AttributeContainerMutable(name, atts).toImmutable(); } @Override public int compareTo(VariableSimpleIF o) { return name.compareTo(o.getShortName()); // ?? } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } VariableSimpleAdapter that = (VariableSimpleAdapter) o; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } } }