/* * 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.ncss.view.gridaspoint.netcdf; import java.io.IOException; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ucar.ma2.Array; import ucar.ma2.ArrayDouble; import ucar.ma2.ArrayInt; import ucar.ma2.ArrayObject; import ucar.ma2.ArrayStructureW; import ucar.ma2.DataType; import ucar.ma2.InvalidRangeException; import ucar.ma2.StructureData; import ucar.ma2.StructureMembers; import ucar.ma2.StructureMembers.Member; import ucar.nc2.Attribute; import ucar.nc2.Dimension; import ucar.nc2.NetcdfFileWriter; import ucar.nc2.Variable; import ucar.nc2.VariableSimpleIF; import ucar.nc2.constants.CDM; import ucar.nc2.constants.CF; import ucar.nc2.dataset.CoordinateAxis1D; import ucar.nc2.dt.GridDataset; import ucar.nc2.ft.PointFeature; import ucar.nc2.ft.point.writer.CFPointWriter; import ucar.nc2.ft.point.writer.CFPointWriterUtils; import ucar.nc2.time.CalendarDate; import ucar.unidata.geoloc.Station; /** * Write a CF "Discrete Sample" station file for the NetCDF Subset Service. It uses * GridAsPoint features and must be kept package private. * Adds support for ensemble dimension and in that case files are not CF-1.0 complaint. * * Example H.7. Timeseries of station data in the indexed ragged array representation. * * <p/> * <pre> * writeHeader() * iterate { writeRecord() } * finish() * </pre> * * @see "http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.6/cf-conventions.html#idp8340320" * @author mhermida * @since Nov 09, 2012 */ class WriterCFStationCollection extends CFPointWriter { static private Logger log = LoggerFactory.getLogger(WriterCFStationCollection.class); private static final String stationDimName = "station"; private static final String idName = "station_id"; private static final String descName = "station_description"; private static final String wmoName = "wmo_id"; private static final String stationIndexName = "stationIndex"; private static final boolean debug = false; private GridDataset gds=null; ////////////////////////////////////////////////////////// private int name_strlen = 1, desc_strlen = 1, wmo_strlen = 1; private Variable lat, lon, alt, time, id, wmoId, desc, stationIndex, ensVar; private double currentTimeCoordValue = -1; //Keeps track of the time value we are writing (when writing data with ensemble dimension it does not change until all members are done) private List<Dimension> stationDims = new ArrayList<>(1); private boolean useAlt = false; private boolean useWmoId = false; //WriterCFStationCollection(String fileOut, String title) throws IOException { // this(fileOut, Arrays.asList(new Attribute[]{new Attribute(CDM.TITLE, title)})); //} WriterCFStationCollection(NetcdfFileWriter.Version version, String fileOut, List<Attribute> atts) throws IOException { super(fileOut, atts, version); writer.addGroupAttribute(null, new Attribute(CF.FEATURE_TYPE, CF.FeatureType.timeSeries.name())); } // LOOK fake protected void makeFeatureVariables(StructureData featureData, boolean isExtended) throws IOException {} void writeHeader(List<Station> stns, List<VariableSimpleIF> vars, GridDataset gds, List<Attribute> timeDimAtts, String altUnits) throws IOException { this.gds =gds; this.altUnits = altUnits; createStations(stns); createObsVariables(timeDimAtts); addDataVariablesClassic(vars); writer.create(); // done with define mode //record = writer.addRecordStructure(); writeStationData(stns); // write out the station info } private void createStations(List<ucar.unidata.geoloc.Station> stnList) throws IOException { int nstns = stnList.size(); // see if there's altitude, wmoId for any stations for (Station stn : stnList) { if (!Double.isNaN(stn.getAltitude())) useAlt = true; if ((stn.getWmoId() != null) && (stn.getWmoId().trim().length() > 0)) useWmoId = true; } /* if (useAlt) ncfile.addGlobalAttribute("altitude_coordinate", altName); */ // find string lengths for (Station station : stnList) { name_strlen = Math.max(name_strlen, station.getName().length()); desc_strlen = Math.max(desc_strlen, station.getDescription().length()); if (useWmoId) wmo_strlen = Math.max(wmo_strlen, station.getWmoId().length()); } llbb = CFPointWriterUtils.getBoundingBox(stnList); // gets written in super.finish(); // add the dimensions writer.addUnlimitedDimension(recordDimName); Dimension stationDim = writer.addDimension(null, stationDimName, nstns); stationDims.add(stationDim); // add the station Variables using the station dimension lat = writer.addVariable(null, latName, DataType.DOUBLE, stationDimName); writer.addVariableAttribute(lat, new Attribute(CDM.UNITS, CDM.LAT_UNITS)); writer.addVariableAttribute(lat, new Attribute(CDM.LONG_NAME, "station latitude")); lon = writer.addVariable(null, lonName, DataType.DOUBLE, stationDimName); writer.addVariableAttribute(lon, new Attribute(CDM.UNITS, CDM.LON_UNITS)); writer.addVariableAttribute(lon, new Attribute(CDM.LONG_NAME, "station longitude")); if (useAlt) { alt = writer.addVariable(null, altName, DataType.DOUBLE, stationDimName); writer.addVariableAttribute(alt, new Attribute(CDM.UNITS, "meters")); writer.addVariableAttribute(alt, new Attribute(CF.POSITIVE, CF.POSITIVE_UP)); writer.addVariableAttribute(alt, new Attribute(CDM.LONG_NAME, "station altitude")); writer.addVariableAttribute(alt, new Attribute(CF.STANDARD_NAME, CF.SURFACE_ALTITUDE)); } id = writer.addStringVariable(null, idName, stationDims, name_strlen); writer.addVariableAttribute(id, new Attribute(CDM.LONG_NAME, "station identifier")); writer.addVariableAttribute(id, new Attribute(CF.CF_ROLE, CF.TIMESERIES_ID)); // station_id:cf_role = "timeseries_id"; desc = writer.addStringVariable(null, descName, stationDims, desc_strlen); writer.addVariableAttribute(desc, new Attribute(CDM.LONG_NAME, "station description")); writer.addVariableAttribute(desc, new Attribute(CF.STANDARD_NAME, CF.PLATFORM_NAME)); if (useWmoId) { wmoId = writer.addStringVariable(null, wmoName, stationDims, wmo_strlen); writer.addVariableAttribute(wmoId, new Attribute(CDM.LONG_NAME, "station WMO id")); writer.addVariableAttribute(wmoId, new Attribute(CF.STANDARD_NAME, CF.PLATFORM_ID)); } } private void createObsVariables(List<Attribute> timeDimAtts) throws IOException { // time variable time = writer.addVariable(null, timeName, DataType.DOUBLE, recordDimName); for(Attribute att : timeDimAtts){ writer.addVariableAttribute(time, att); } //writer.addVariableAttribute(time, new Attribute(CDM.UNITS, timeUnit.getUnitsString())); //writer.addVariableAttribute(time, new Attribute(CDM.LONG_NAME, "time of measurement")); stationIndex = writer.addVariable(null, stationIndexName, DataType.INT, recordDimName); writer.addVariableAttribute(stationIndex, new Attribute(CDM.LONG_NAME, "station index for this observation record")); writer.addVariableAttribute(stationIndex, new Attribute(CF.INSTANCE_DIMENSION, stationDimName)); } protected void addDataVariablesClassic(List<? extends VariableSimpleIF> dataVars) throws IOException { Set<Dimension> dimSet = new HashSet<>(20); String coordNames = latName + " " + lonName + " " + altName + " " + timeName; if(!useAlt){ coordNames = latName + " " + lonName + " " + timeName; } // find all dimensions needed by the data variables for (VariableSimpleIF var : dataVars) { List<Dimension> dims = var.getDimensions(); dimSet.addAll(dims); } // add them for (Dimension d : dimSet) { if (!d.isUnlimited()) writer.addDimension(null, d.getShortName(), d.getLength(), d.isShared(), false, d.isVariableLength()); } //see if dataset has ensemble dimension CoordinateAxis1D ensAxis = gds.findGridDatatype(dataVars.get(0).getShortName()).getCoordinateSystem().getEnsembleAxis(); Dimension ens = null; if(ensAxis != null){ ens = writer.addDimension(null, ensAxis.getShortName(), ensAxis.getCoordValues().length); dimSet.add(ens); List<Dimension> ensDim = new ArrayList<>(); ensDim.add(ens); //ensVar = writer.addVariable(null, ensAxis.getShortName() , ensAxis.getDataType() , ensDim ); //We'll write the double values to the new array //DataType is int but the coordinates are stored as double ???? ensVar = writer.addVariable(null, ensAxis.getShortName() , DataType.DOUBLE , ensDim ); } // find all variables already in use List<VariableSimpleIF> useDataVars = new ArrayList<>(dataVars.size()); for (VariableSimpleIF var : dataVars) { if (writer.findVariable(var.getShortName()) == null) useDataVars.add(var); } // add the data variables all using the record dimension for (VariableSimpleIF oldVar : useDataVars) { //List<Dimension> dims = oldVar.getDimensions(); StringBuilder dimNames = new StringBuilder(recordDimName); //for (Dimension d : dims) { for (Dimension d : dimSet) { if (!d.isUnlimited()) dimNames.append(" ").append(d.getShortName()); } Variable newVar = writer.addVariable(null, oldVar.getShortName(), oldVar.getDataType(), dimNames.toString()); List<Attribute> atts = oldVar.getAttributes(); for (Attribute att : atts) { newVar.addAttribute(att); } newVar.addAttribute(new Attribute(CF.COORDINATES, coordNames)); } } private HashMap<String, Integer> stationMap; private void writeStationData(List<ucar.unidata.geoloc.Station> stnList) throws IOException { int nstns = stnList.size(); stationMap = new HashMap<>(2 * nstns); if (debug) System.out.println("stationMap created"); // now write the station data ArrayDouble.D1 latArray = new ArrayDouble.D1(nstns); ArrayDouble.D1 lonArray = new ArrayDouble.D1(nstns); ArrayDouble.D1 altArray = new ArrayDouble.D1(nstns); ArrayObject.D1 idArray = new ArrayObject.D1(String.class, nstns); ArrayObject.D1 descArray = new ArrayObject.D1(String.class, nstns); ArrayObject.D1 wmoArray = new ArrayObject.D1(String.class, nstns); for (int i = 0; i < stnList.size(); i++) { ucar.unidata.geoloc.Station stn = stnList.get(i); stationMap.put(stn.getName(), i); latArray.set(i, stn.getLatitude()); lonArray.set(i, stn.getLongitude()); if (useAlt) altArray.set(i, stn.getAltitude()); idArray.set(i, stn.getName()); descArray.set(i, stn.getDescription()); if (useWmoId) wmoArray.set(i, stn.getWmoId()); } try { writer.write(lat, latArray); writer.write(lon, lonArray); if (useAlt) writer.write(alt, altArray); writer.writeStringData(id, idArray); writer.writeStringData(desc, descArray); if (useWmoId) writer.writeStringData(wmoId, wmoArray); } catch (InvalidRangeException e) { e.printStackTrace(); throw new IllegalStateException(e); } } private int recno = -1; private ArrayDouble.D1 timeArray = new ArrayDouble.D1(1); //private ArrayInt.D1 prevArray = new ArrayInt.D1(1); private ArrayInt.D1 parentArray = new ArrayInt.D1(1); private int[] origin = new int[1]; void writeRecord(Station s, PointFeature sobs, StructureData sdata) throws IOException { writeRecord(s.getName(), sobs.getObservationTime(), sobs.getObservationTimeAsCalendarDate(), sdata); } void writeRecord(String stnName, double timeCoordValue, CalendarDate obsDate, StructureData sdata) throws IOException { trackBB(null, obsDate); if(recno < 0){ recno =0; } Integer parentIndex = stationMap.get(stnName); if (parentIndex == null) throw new RuntimeException("Cant find station " + stnName); // needs to be wrapped as an ArrayStructure, even though we are only writing one at a time. ArrayStructureW sArray = new ArrayStructureW(sdata.getStructureMembers(), new int[]{1}); sArray.setStructureData(sdata, 0); timeArray.set(0, timeCoordValue); parentArray.set(0, parentIndex); // write the recno record origin[0] = recno; try { //writer.write(record, twoDIdx, sArray); writer.write(time, origin, timeArray); writer.write(stationIndex, origin, parentArray); StructureMembers sm = sdata.getStructureMembers(); for( Member m : sm.getMembers() ){ Variable v = writer.findVariable(m.getName()); if( v != null && !v.getShortName().equals(lonName) && !v.getShortName().equals(latName) && !v.getShortName().equals("time")){ Array arr = CFPointWriterUtils.getArrayFromMember(v, m); writer.write( v , origin, arr ); } } } catch (InvalidRangeException e) { e.printStackTrace(); throw new IllegalStateException(e); } recno++; } void writeRecord(String stnName, double timeCoordValue, CalendarDate obsDate, double ensCoord, StructureData sdata) throws IOException { trackBB(null, obsDate); if(currentTimeCoordValue != timeCoordValue){ recno++; currentTimeCoordValue = timeCoordValue; } Integer parentIndex = stationMap.get(stnName); if (parentIndex == null) throw new RuntimeException("Cant find station " + stnName); // needs to be wrapped as an ArrayStructure, even though we are only writing one at a time. ArrayStructureW sArray = new ArrayStructureW(sdata.getStructureMembers(), new int[]{1}); sArray.setStructureData(sdata, 0); timeArray.set(0, timeCoordValue); parentArray.set(0, parentIndex); // write the recno record origin[0] = recno; int[] twoDIdx = new int[]{recno, (int)ensCoord}; try { //writer.write(record, twoDIdx, sArray); writer.write(time, origin, timeArray); writer.write(stationIndex, origin, parentArray); StructureMembers sm = sdata.getStructureMembers(); for( Member m : sm.getMembers() ){ Variable v = writer.findVariable(m.getName()); if( v != null && !v.getShortName().equals(lonName) && !v.getShortName().equals(latName) && !v.getShortName().equals("time")){ Array arr = CFPointWriterUtils.getArrayFromMember(v, m); writer.write( v , twoDIdx, arr ); } } } catch (InvalidRangeException e) { e.printStackTrace(); throw new IllegalStateException(e); } } void writeEnsCoord(int ensIdx, double ensCoord) throws IOException{ ArrayDouble.D1 tmpArray = new ArrayDouble.D1(1); tmpArray.setDouble(0, ensCoord); int[] idx = new int[]{ensIdx}; try { writer.write( ensVar , idx, tmpArray ); }catch(InvalidRangeException ire){ log.error("Error writing data: "+ire); } } }