/* * Copyright 1998-2014 University Corporation for Atmospheric Research/Unidata * * Portions of this software were developed by the Unidata Program at the * University Corporation for Atmospheric Research. * * Access and use of this software shall impose the following obligations * and understandings on the user. The user is granted the right, without * any fee or cost, to use, copy, modify, alter, enhance and distribute * this software, and any derivative works thereof, and its supporting * documentation for any purpose whatsoever, provided that this entire * notice appears in all copies of the software, derivative works and * supporting documentation. Further, UCAR requests that the user credit * UCAR/Unidata in any publications that result from the use of this * software or in any product that includes this software. The names UCAR * and/or Unidata, however, may not be used in any advertising or publicity * to endorse or promote any products or commercial entity unless specific * written permission is obtained from UCAR/Unidata. The user also * understands that UCAR/Unidata is not obligated to provide the user with * any support, consulting, training or assistance of any kind with regard * to the use, operation and performance of this software nor to provide * the user with any updates, revisions, new versions or "bug fixes." * * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL, * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE. */ package thredds.server.cdmremote; import thredds.server.cdmremote.params.CdmrfQueryBean; import ucar.ma2.Array; import ucar.ma2.StructureData; import ucar.nc2.Attribute; import ucar.nc2.VariableSimpleIF; import ucar.nc2.constants.CDM; import ucar.nc2.ft.*; import ucar.nc2.ft.point.StationFeature; import ucar.nc2.ft.point.StationPointFeature; import ucar.nc2.ft.point.remote.PointStream; import ucar.nc2.ft.point.remote.PointStreamProto; import ucar.nc2.ft.point.writer.WriterCFStationCollection; import ucar.nc2.stream.NcStream; import ucar.nc2.stream.NcStreamProto; import ucar.nc2.time.CalendarDateFormatter; import ucar.nc2.time.CalendarDateRange; import ucar.nc2.units.DateRange; import ucar.nc2.units.DateType; import ucar.nc2.units.TimeUnit; import ucar.unidata.geoloc.LatLonPoint; import ucar.unidata.geoloc.LatLonPointImpl; import ucar.unidata.geoloc.LatLonRect; import ucar.unidata.geoloc.Station; import ucar.unidata.util.Format; import javax.servlet.http.HttpServletResponse; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.*; /** * CdmrFeature subsetting for station data. * thread safety: new object for each request * * @author caron * @since Aug 19, 2009 */ public class StationWriter { static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StationWriter.class); private static final boolean debug = false, debugDetail = false; private final FeatureDatasetPoint fd; private final StationTimeSeriesFeatureCollection sfc; //private final CdmRemoteQueryBean qb; private final CdmrfQueryBean qb; private final Date start, end; private List<VariableSimpleIF> wantVars; private DateRange wantRange; private ucar.nc2.util.DiskCache2 diskCache; public StationWriter(FeatureDatasetPoint fd, StationTimeSeriesFeatureCollection sfc, CdmrfQueryBean qb, ucar.nc2.util.DiskCache2 diskCache) throws IOException { this.fd = fd; this.sfc = sfc; this.qb = qb; this.diskCache = diskCache; start = fd.getStartDate(); end = fd.getEndDate(); } public boolean validate(HttpServletResponse res) throws IOException { // verify TemporalSelection intersects if (qb.getTemporalSelection() == CdmrfQueryBean.TemporalSelection.range) { wantRange = qb.getDateRange(); DateRange haveRange = fd.getDateRange(); if ((haveRange != null) && !haveRange.intersects(wantRange)) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "ERROR: This dataset does not include the requested time range= " + wantRange + "\ndataset time range = " + haveRange); return false; } } // restrict to these variables List<? extends VariableSimpleIF> dataVars = fd.getDataVariables(); String[] vars = qb.getVarNames(); List<String> varNames = (vars == null) ? null : Arrays.asList(vars); if ((varNames == null) || (varNames.size() == 0)) { wantVars = new ArrayList<>(dataVars); } else { wantVars = new ArrayList<>(); for (VariableSimpleIF v : dataVars) { if (varNames.contains(v.getShortName())) // LOOK N**2 wantVars.add(v); } } // verify SpatialSelection has some stations if (qb.getSpatialSelection() == CdmrfQueryBean.SpatialSelection.bb) { LatLonRect bb = sfc.getBoundingBox(); if ((bb != null) && (bb.intersect(qb.getLatLonRect()) == null)) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "ERROR: Bounding Box contains no stations; bb= " + qb.getLatLonRect()); return false; } //System.out.printf("sfc.flatten0 wantRange= %s on %s %n", wantRange, fd.getLocation()); // pfc = sfc.flatten(qb.getLatLonRect(), wantRange); } else if (qb.getSpatialSelection() == CdmrfQueryBean.SpatialSelection.stns) { if (!contains(sfc, qb.getStnNames())) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "ERROR: No valid stations specified = " + qb.getStn()); return false; } //System.out.printf("sfc.flatten1 wantRange= %s on %s %n", wantRange, fd.getLocation()); // List<String> wantStns = Arrays.asList(qb.getStnNames()); //pfc = sfc.flatten(wantStns, wantRange, null); } else { //System.out.printf("sfc.flatten2 wantRange= %s on %s %n", wantRange, fd.getLocation()); //pfc = sfc.flatten(null, wantRange, null); } return true; } private boolean contains(StationTimeSeriesFeatureCollection sfc, String[] stnNames) { for (String name : stnNames) { if (sfc.getStation(name) != null) return true; } return false; } //////////////////////////////////////////////////////////////// // writing public File writeNetcdf() throws IOException { WriterNetcdf w = (WriterNetcdf) write(null); return w.netcdfResult; } public Writer write(HttpServletResponse res) throws IOException { long start = System.currentTimeMillis(); Limit counter = new Limit(); //counter.limit = 150; // which writer, based on desired response CdmrfQueryBean.ResponseType resType = qb.getResponseType(); Writer w; if (resType == CdmrfQueryBean.ResponseType.xml) { w = new WriterXML(res.getWriter()); } else if (resType == CdmrfQueryBean.ResponseType.csv) { w = new WriterCSV(res.getWriter()); } else if (resType == CdmrfQueryBean.ResponseType.netcdf) { w = new WriterNetcdf(); } else if (resType == CdmrfQueryBean.ResponseType.ncstream) { w = new WriterNcstream(res.getOutputStream()); } else { log.error("Unknown result type = " + resType); return null; } // for closet time, set wantRange to the time LOOK - do we need +- increment ?? if (qb.getTemporalSelection() == CdmrfQueryBean.TemporalSelection.point) { TimeUnit hour = null; try { hour = new TimeUnit(1, "hour"); } catch (Exception e) { e.printStackTrace(); log.error("bad time unit", e); return null; } DateType startR = qb.getTimePoint().subtract(hour); DateType endR = qb.getTimePoint().add(hour); wantRange = new DateRange(startR.getDate(), endR.getDate()); } // time: all, range, point // spatial: all, bb, point, stns //StationTimeSeriesFeatureCollection useFc = sfc; PointFeatureCollection pfc = null; switch (qb.getSpatialSelection()) { case all: pfc = sfc.flatten(null, CalendarDateRange.of(wantRange)); break; case bb: //useFc = sfc.subset(qb.getLatLonRect()); pfc = sfc.flatten(qb.getLatLonRect(), CalendarDateRange.of(wantRange)); break; case point: Station closestStation = findClosestStation(qb.getLatlonPoint()); List<String> stn = new ArrayList<String>(); stn.add(closestStation.getName()); //useFc = sfc.subset(stn); pfc = sfc.flatten(stn, CalendarDateRange.of(wantRange), null); break; case stns: //List<Station> wantStns = getStationList(qb.getStnNames()); //useFc = sfc.subset(stns); List<String> wantStns = Arrays.asList(qb.getStnNames()); pfc = sfc.flatten(wantStns, CalendarDateRange.of(wantRange), null); break; } Action act = w.getAction(); w.header(); if (qb.getTemporalSelection() == CdmrfQueryBean.TemporalSelection.point) { scanForClosestTime(pfc, qb.getTimePoint(), null, act, counter); } else { scan(pfc, wantRange, null, act, counter); } w.trailer(); if (debug) { long took = System.currentTimeMillis() - start; System.out.println("\nread " + counter.count + " records; match and write " + counter.matches + " raw records"); System.out.println("that took = " + took + " msecs"); long timeToScan = 0; // ?? if (timeToScan > 0) { long writeTime = took - timeToScan; double mps = 1000.0 * counter.matches / writeTime; System.out.println(" writeTime = " + writeTime + " msecs; write messages/sec = " + mps); } } return w; } /////////////////////////////////////// // station handling private HashMap<String, Station> stationMap; /* * Determine if any of the given station names are actually in the dataset. * * @param stns List of station names * @return true if list is empty, ie no names are in the actual station list * @throws IOException if read error */ private boolean isStationListEmpty(List<String> stns) throws IOException { makeStationMap(); for (String stn : stns) { if (stationMap.get(stn) != null) return false; } return true; } private List<Station> getStationList(String[] stnNames) throws IOException { makeStationMap(); List<Station> result = new ArrayList<>(stnNames.length); for (String s : stnNames) { Station stn = stationMap.get(s); if (stn != null) result.add(stn); } return result; } private void makeStationMap() throws IOException { if (null == stationMap) { stationMap = new HashMap<>(); for (Station station : sfc.getStations()) { stationMap.put(station.getName(), station); } } } /* * Get the list of station names that are contained within the bounding box. * * @param boundingBox lat/lon bounding box * @return list of station names contained within the bounding box * @throws IOException if read error */ public List<String> getStationNames(LatLonRect boundingBox) throws IOException { LatLonPointImpl latlonPt = new LatLonPointImpl(); ArrayList<String> result = new ArrayList<>(); for (Station s : sfc.getStations()) { latlonPt.set(s.getLatitude(), s.getLongitude()); if (boundingBox.contains(latlonPt)) { result.add(s.getName()); // boundingBox.contains(latlonPt); debugging } } return result; } /* * Find the station closest to the specified point. * The metric is (lat-lat0)**2 + (cos(lat0)*(lon-lon0))**2 * * @param lat latitude value * @param lon longitude value * @return name of station closest to the specified point * @throws IOException if read error */ public Station findClosestStation(LatLonPoint pt) throws IOException { double lat = pt.getLatitude(); double lon = pt.getLongitude(); double cos = Math.cos(Math.toRadians(lat)); List<Station> stations = sfc.getStations(); Station min_station = stations.get(0); double min_dist = Double.MAX_VALUE; for (Station s : stations) { double lat1 = s.getLatitude(); double lon1 = LatLonPointImpl.lonNormal(s.getLongitude(), lon); double dy = Math.toRadians(lat - lat1); double dx = cos * Math.toRadians(lon - lon1); double dist = dy * dy + dx * dx; if (dist < min_dist) { min_dist = dist; min_station = s; } } return min_station; } ///////////////////// public boolean intersect(DateRange dr) throws IOException { return dr.intersects(start, end); } //////////////////////////////////////////////////////// // scanning flattened collection // scan PointFeatureCollection, records that pass the predicate match are acted on, within limits private void scan(PointFeatureCollection collection, DateRange range, Predicate p, Action a, Limit limit) throws IOException { collection.resetIteration(); while (collection.hasNext()) { PointFeature pf = collection.next(); if (range != null) { Date obsDate = pf.getObservationTimeAsDate(); // LOOK: needed? if (!range.contains(obsDate)) continue; } limit.count++; StructureData sdata = pf.getData(); if ((p == null) || p.match(sdata)) { a.act(pf, sdata); limit.matches++; } if (limit.matches > limit.limit) { collection.finish(); break; } if (debugDetail && (limit.matches % 50 == 0)) System.out.println(" matches " + limit.matches); } collection.finish(); } // scan all data in the file, first eliminate any that dont pass the predicate // for each station, track the closest record to the given time // then act on those private void scanForClosestTime(PointFeatureCollection collection, DateType time, Predicate p, Action a, Limit limit) throws IOException { HashMap<String, StationDataTracker> map = new HashMap<>(); long wantTime = time.getDate().getTime(); collection.resetIteration(); while (collection.hasNext()) { PointFeature pf = collection.next(); //System.out.printf("%s%n", pf); // general predicate filter if (p != null) { StructureData sdata = pf.getData(); if (!p.match(sdata)) continue; } // find closest time for this station long obsTime = pf.getObservationTimeAsDate().getTime(); long diff = Math.abs(obsTime - wantTime); Station s = ((StationPointFeature)pf).getStation(); StationDataTracker track = map.get(s.getName()); if (track == null) { map.put(s.getName(), new StationDataTracker(pf, diff)); } else { if (diff < track.timeDiff) { track.sobs = pf; track.timeDiff = diff; } } } for (Map.Entry<String, StationDataTracker> entry : map.entrySet()) { StationDataTracker track = entry.getValue(); a.act(track.sobs, track.sobs.getData()); limit.matches++; limit.count++; if (limit.count > limit.limit) break; } } private static class StationDataTracker { PointFeature sobs; long timeDiff = Long.MAX_VALUE; StationDataTracker(PointFeature sobs, long timeDiff) { this.sobs = sobs; this.timeDiff = timeDiff; } } // scan StationTimeSeriesFeatureCollection, records that pass the DateRange and Predicate match are acted on, within limits private void scan(StationTimeSeriesFeatureCollection collection, DateRange range, Predicate p, Action a, Limit limit) throws IOException { while (collection.hasNext()) { StationTimeSeriesFeature sf = collection.next(); while (sf.hasNext()) { PointFeature pf = sf.next(); if (range != null) { Date obsDate = pf.getObservationTimeAsDate(); if (!range.contains(obsDate)) continue; } limit.count++; StructureData sdata = pf.getData(); if ((p == null) || p.match(sdata)) { a.act(pf, sdata); limit.matches++; } if (limit.matches > limit.limit) { sf.finish(); break; } if (debugDetail && (limit.matches % 50 == 0)) System.out.println(" matches " + limit.matches); } if (limit.matches > limit.limit) { collection.finish(); break; } } } //////////////////////////////////////////////////////// // scanning station collection /* scan data for the list of stations, in order // records that pass the dateRange and predicate match are acted on private void scanStations(Dataset ds, List<String> stns, DateRange range, Predicate p, Action a, Limit limit) throws IOException { StringBuilder sbuff = new StringBuilder(); StationObsDataset sod = ds.get(); if (debug) System.out.println("scanStations open " + ds.filename); if (null == sod) { log.info("Cant open " + ds.filename + "; " + sbuff); return; } for (String stn : stns) { Station s = sod.getStation(stn); if (s == null) { log.warn("Cant find station " + s); continue; } if (debugDetail) System.out.println("stn " + s.getName()); DataIterator iter = sod.getDataIterator(s); while (iter.hasNext()) { StationObsDatatype sobs = (StationObsDatatype) iter.nextData(); // date filter if (null != range) { Date obs = sobs.getObservationTimeAsDate(); if (!range.included(obs)) continue; } // general predicate filter StructureData sdata = sobs.getData(); if ((p == null) || p.match(sdata)) { a.act(sod, sobs, sdata); limit.matches++; } limit.count++; if (limit.count > limit.limit) break; } } } */ // scan all data in the file, first eliminate any that dont pass the predicate // for each station, track the closest record to the given time /* then act on those private void scanAll(Dataset ds, DateType time, Predicate p, Action a, Limit limit) throws IOException { StringBuilder sbuff = new StringBuilder(); HashMap<Station, StationDataTracker> map = new HashMap<Station, StationDataTracker>(); long wantTime = time.getDate().getTime(); StationObsDataset sod = ds.get(); if (debug) System.out.println("scanAll open " + ds.filename); if (null == sod) { log.info("Cant open " + ds.filename + "; " + sbuff); return; } DataIterator iter = sod.getDataIterator(0); while (iter.hasNext()) { StationObsDatatype sobs = (StationObsDatatype) iter.nextData(); // general predicate filter if (p != null) { StructureData sdata = sobs.getData(); if (!p.match(sdata)) continue; } // find closest time for this station long obsTime = sobs.getObservationTimeAsDate().getTime(); long diff = Math.abs(obsTime - wantTime); Station s = sobs.getStation(); StationDataTracker track = map.get(s); if (track == null) { map.put(s, new StationDataTracker(sobs, diff)); } else { if (diff < track.timeDiff) { track.sobs = sobs; track.timeDiff = diff; } } } for (Station s : map.keySet()) { StationDataTracker track = map.get(s); a.act(sod, track.sobs, track.sobs.getData()); limit.matches++; limit.count++; if (limit.count > limit.limit) break; } } private class StationDataTracker { StationObsDatatype sobs; long timeDiff = Long.MAX_VALUE; StationDataTracker(StationObsDatatype sobs, long timeDiff) { this.sobs = sobs; this.timeDiff = timeDiff; } } // scan data for the list of stations, in order // eliminate records that dont pass the predicate // for each station, track the closest record to the given time, then act on those private void scanStations(Dataset ds, List<String> stns, DateType time, Predicate p, Action a, Limit limit) throws IOException { StringBuilder sbuff = new StringBuilder(); StationObsDataset sod = ds.get(); if (null == sod) { log.info("Cant open " + ds.filename + "; " + sbuff); return; } long wantTime = time.getDate().getTime(); for (String stn : stns) { Station s = sod.getStation(stn); if (s == null) { log.warn("Cant find station " + s); continue; } StationObsDatatype sobsBest = null; long timeDiff = Long.MAX_VALUE; // loop through all data for this station, take the obs with time closest DataIterator iter = sod.getDataIterator(s); while (iter.hasNext()) { StationObsDatatype sobs = (StationObsDatatype) iter.nextData(); // general predicate filter if (p != null) { StructureData sdata = sobs.getData(); if (!p.match(sdata)) continue; } long obsTime = sobs.getObservationTimeAsDate().getTime(); long diff = Math.abs(obsTime - wantTime); if (diff < timeDiff) { sobsBest = sobs; timeDiff = diff; } } if (sobsBest != null) { a.act(sod, sobsBest, sobsBest.getData()); limit.matches++; } limit.count++; if (limit.count > limit.limit) break; } } */ //////////////////////////////////////////////////////////////// /* date filter private List<Dataset> filterDataset(DateRange range) { if (range == null) return datasetList; List<Dataset> result = new ArrayList<Dataset>(); for (Dataset ds : datasetList) { if (range.intersects(ds.time_start, ds.time_end)) result.add(ds); } return result; } Dataset filterDataset(DateType want) { if (want.isPresent()) return datasetList.get(datasetList.size() - 1); Date time = want.getDate(); for (Dataset ds : datasetList) { if (time.before(ds.time_end) && time.after(ds.time_start)) { return ds; } if (time.equals(ds.time_end) || time.equals(ds.time_start)) { return ds; } } return null; } */ private interface Predicate { boolean match(StructureData sdata); } private interface Action { void act(PointFeature pf, StructureData sdata) throws IOException; } static private class Limit { int count; // how many scanned int limit = Integer.MAX_VALUE; // max matches int matches; // how want matched } public abstract class Writer { abstract void header() throws IOException; abstract Action getAction(); abstract void trailer() throws IOException; java.io.PrintWriter writer; int count = 0; Writer(final java.io.PrintWriter writer) { this.writer = writer; // LOOK what about buffering? } } class WriterNetcdf extends Writer { File netcdfResult; WriterCFStationCollection cfWriter; boolean headerWritten = false; private List<StationFeature> wantStations; WriterNetcdf() throws IOException { super(null); netcdfResult = diskCache.createUniqueFile("cdmSW", ".nc"); List<Attribute> atts = new ArrayList<>(); atts.add(new Attribute(CDM.TITLE, "Extracted data from TDS using CDM remote subsetting")); cfWriter = null; // new WriterCFStationCollection(null, netcdfResult.getAbsolutePath(), atts); // verify SpatialSelection has some stations if (qb.getSpatialSelection() == CdmrfQueryBean.SpatialSelection.bb) { wantStations = sfc.getStationFeatures(qb.getLatLonRect()); } else if (qb.getSpatialSelection() == CdmrfQueryBean.SpatialSelection.stns) { List<String> stnNames = Arrays.asList(qb.getStnNames()); wantStations = sfc.getStationFeatures(stnNames); } else { wantStations = sfc.getStationFeatures(); } } public void header() { } public void trailer() throws IOException { cfWriter.finish(); } Action getAction() { return new Action() { public void act(PointFeature pf, StructureData sdata) throws IOException { if (!headerWritten) { try { // LOOK cfWriter.writeHeader(wantStations, wantVars, sfc.getTimeUnit(), sfc.getAltUnits(), null); cfWriter.writeHeader(wantStations, (StationPointFeature) pf); headerWritten = true; } catch (IOException e) { log.error("WriterNetcdf.header", e); } } cfWriter.writeRecord(sfc.getStation(pf), pf, sdata); count++; } }; } } class WriterNcstream extends Writer { OutputStream out; WriterNcstream(OutputStream os) throws IOException { super(null); out = os; } public void header() throws IOException { // PointStream.writeMagic(out, PointStream.MessageType.Start); // LOOK - not ncstream protocol } public void trailer() throws IOException { PointStream.writeMagic(out, PointStream.MessageType.End); out.flush(); } Action getAction() { return new Action() { public void act(PointFeature pf, StructureData sdata) throws IOException { try { if (count == 0) { // first time : need a point feature so cant do it in header PointStreamProto.PointFeatureCollection proto = PointStream.encodePointFeatureCollection(fd.getLocation(), sfc.getTimeUnit().getTimeUnitString(), pf); byte[] b = proto.toByteArray(); PointStream.writeMagic(out, PointStream.MessageType.PointFeatureCollection); NcStream.writeVInt(out, b.length); out.write(b); } PointStreamProto.PointFeature pfp = PointStream.encodePointFeature(pf); byte[] b = pfp.toByteArray(); PointStream.writeMagic(out, PointStream.MessageType.PointFeature); NcStream.writeVInt(out, b.length); out.write(b); count++; } catch (Throwable t) { String mess = t.getMessage(); if (mess == null) mess = t.getClass().getName(); NcStreamProto.Error err = NcStream.encodeErrorMessage(t.getMessage()); byte[] b = err.toByteArray(); PointStream.writeMagic(out, PointStream.MessageType.Error); NcStream.writeVInt(out, b.length); out.write(b); throw new IOException(t); } } }; } } class WriterRaw extends Writer { WriterRaw(final java.io.PrintWriter writer) { super(writer); } public void header() { } public void trailer() { writer.flush(); } Action getAction() { return new Action() { public void act(PointFeature pf, StructureData sdata) throws IOException { writer.print(CalendarDateFormatter.toDateTimeString(pf.getObservationTimeAsCalendarDate())); writer.print("= "); String report = sdata.getScalarString("report"); writer.println(report); count++; } }; } } class WriterXML extends Writer { XMLStreamWriter staxWriter; WriterXML(final java.io.PrintWriter writer) { super(writer); XMLOutputFactory f = XMLOutputFactory.newInstance(); try { staxWriter = f.createXMLStreamWriter(writer); } catch (XMLStreamException e) { throw new RuntimeException(e.getMessage()); } } public void header() { try { staxWriter.writeStartDocument("UTF-8", "1.0"); staxWriter.writeCharacters("\n"); staxWriter.writeStartElement("stationFeatureCollection"); //staxWriter.writeAttribute("dataset", datasetName); staxWriter.writeCharacters("\n "); } catch (XMLStreamException e) { throw new RuntimeException(e.getMessage()); } //writer.println("<?xml version='1.0' encoding='UTF-8'?>"); //writer.println("<metarCollection dataset='"+datasetName+"'>\n"); } public void trailer() { try { staxWriter.writeEndElement(); staxWriter.writeCharacters("\n"); staxWriter.writeEndDocument(); staxWriter.close(); } catch (XMLStreamException e) { throw new RuntimeException(e.getMessage()); } writer.flush(); } Action getAction() { return new Action() { public void act(PointFeature pf, StructureData sdata) throws IOException { Station s = sfc.getStation(pf); try { staxWriter.writeStartElement("pointFeature"); staxWriter.writeAttribute("date", CalendarDateFormatter.toDateTimeString(pf.getObservationTimeAsCalendarDate())); staxWriter.writeCharacters("\n "); staxWriter.writeStartElement("station"); staxWriter.writeAttribute("name", s.getName()); staxWriter.writeAttribute("latitude", Format.dfrac(s.getLatitude(), 3)); staxWriter.writeAttribute("longitude", Format.dfrac(s.getLongitude(), 3)); if (!Double.isNaN(s.getAltitude())) staxWriter.writeAttribute("altitude", Format.dfrac(s.getAltitude(), 0)); if (s.getDescription() != null) staxWriter.writeCharacters(s.getDescription()); staxWriter.writeEndElement(); staxWriter.writeCharacters("\n "); for (VariableSimpleIF var : wantVars) { staxWriter.writeCharacters(" "); staxWriter.writeStartElement("data"); staxWriter.writeAttribute("name", var.getShortName()); if (var.getUnitsString() != null) staxWriter.writeAttribute(CDM.UNITS, var.getUnitsString()); Array sdataArray = sdata.getArray(var.getShortName()); String ss = sdataArray.toString(); Class elemType = sdataArray.getElementType(); if ((elemType == String.class) || (elemType == char.class) || (elemType == StructureData.class)) ss = ucar.nc2.util.xml.Parse.cleanCharacterData(ss); // make sure no bad chars staxWriter.writeCharacters(ss); staxWriter.writeEndElement(); staxWriter.writeCharacters("\n "); } staxWriter.writeEndElement(); staxWriter.writeCharacters("\n"); count++; } catch (XMLStreamException e) { throw new RuntimeException(e.getMessage()); } } }; } } class WriterCSV extends Writer { WriterCSV(final java.io.PrintWriter writer) { super(writer); } public void header() { writer.print("time,station,latitude[unit=\"degrees_north\"],longitude[unit=\"degrees_east\"]"); for (VariableSimpleIF var : wantVars) { writer.print(","); writer.print(var.getShortName()); if (var.getUnitsString() != null) writer.print("[unit=\"" + var.getUnitsString() + "\"]"); } writer.println(); } public void trailer() { writer.flush(); } Action getAction() { return new Action() { public void act(PointFeature pf, StructureData sdata) throws IOException { Station s = sfc.getStation(pf); writer.print(CalendarDateFormatter.toDateTimeString(pf.getObservationTimeAsCalendarDate())); writer.print(','); writer.print(s.getName()); writer.print(','); writer.print(Format.dfrac(s.getLatitude(), 3)); writer.print(','); writer.print(Format.dfrac(s.getLongitude(), 3)); for (VariableSimpleIF var : wantVars) { writer.print(','); Array sdataArray = sdata.getArray(var.getShortName()); writer.print(sdataArray.toString()); } writer.println(); count++; } }; } } static public void main(String args[]) throws IOException { //getFiles("R:/testdata2/station/ldm/metar/"); // StationObsCollection soc = new StationObsCollection("C:/data/metars/", false); } }