/**
 * Helper class for using the netcdf-3 record dimension.
 *
 * @author caron
 * @since Feb 29, 2008
 */
public class RecordDatasetHelper {
  private static org.slf4j.Logger log =
      org.slf4j.LoggerFactory.getLogger(RecordDatasetHelper.class);

  protected NetcdfDataset ncfile;
  protected String obsTimeVName, nomTimeVName;
  protected String latVName, lonVName, zcoordVName, zcoordUnits;
  protected String stnIdVName, stnIndexVName, stnDescVName;
  protected StationHelper stationHelper;
  protected DataType stationIdType;

  protected StructureDS recordVar;
  protected Dimension obsDim;

  protected LatLonRect boundingBox;
  protected double minDate, maxDate;
  protected DateUnit timeUnit;

  protected double altScaleFactor = 1.0;

  protected Formatter errs = null;
  protected boolean showErrors = true;

  /**
   * Constructor.
   *
   * @param ncfile the netccdf file
   * @param typedDataVariables list of data variables; all record variables will be added to this
   *     list, except . You can remove extra
   * @param obsTimeVName observation time variable name (required)
   * @param nomTimeVName nominal time variable name (may be null)
   * @throws IllegalArgumentException if ncfile has no unlimited dimension and recDimName is null.
   */
  public RecordDatasetHelper(
      NetcdfDataset ncfile,
      String obsTimeVName,
      String nomTimeVName,
      List<VariableSimpleIF> typedDataVariables,
      String recDimName,
      Formatter errBuffer) {
    this.ncfile = ncfile;
    this.obsTimeVName = obsTimeVName;
    this.nomTimeVName = nomTimeVName;
    this.errs = errBuffer;

    // check if we already have a structure vs if we have to add it.

    if (this.ncfile.hasUnlimitedDimension()) {
      this.ncfile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
      this.recordVar = (StructureDS) this.ncfile.getRootGroup().findVariable("record");
      this.obsDim = ncfile.getUnlimitedDimension();

    } else {
      if (recDimName == null)
        throw new IllegalArgumentException(
            "File <"
                + this.ncfile.getLocation()
                + "> has no unlimited dimension, specify psuedo record dimension with observationDimension global attribute.");
      this.obsDim = this.ncfile.getRootGroup().findDimension(recDimName);
      this.recordVar = new StructurePseudoDS(this.ncfile, null, "record", null, obsDim);
    }

    // create member variables
    List<Variable> recordMembers = ncfile.getVariables();
    for (Variable v : recordMembers) {
      if (v == recordVar) continue;
      if (v.isScalar()) continue;
      if (v.getDimension(0) == this.obsDim) typedDataVariables.add(v);
    }

    // need the time units
    Variable timeVar = ncfile.findVariable(obsTimeVName);
    String timeUnitString =
        ncfile.findAttValueIgnoreCase(timeVar, CDM.UNITS, "seconds since 1970-01-01");
    try {
      timeUnit = new DateUnit(timeUnitString);
    } catch (Exception e) {
      if (null != errs) errs.format("Error on string = %s == %s%n", timeUnitString, e.getMessage());
      try {
        timeUnit = new DateUnit("seconds since 1970-01-01");
      } catch (Exception e1) {
        // cant happen
      }
    }
  }

  /**
   * Set extra information used by station obs datasets. Use stnIdVName or stnIndexVName.
   *
   * @param stnIdVName the obs variable that is used to find the station in the stnHash; may be type
   *     int or a String (char).
   * @param stnDescVName optional station var containing station description
   */
  public void setStationInfo(
      String stnIdVName, String stnDescVName, String stnIndexVName, StationHelper stationHelper) {
    this.stnIdVName = stnIdVName;
    this.stnDescVName = stnDescVName;
    this.stnIndexVName = stnIndexVName;
    this.stationHelper = stationHelper;

    if (stnIdVName != null) {
      Variable stationVar = ncfile.findVariable(stnIdVName);
      stationIdType = stationVar.getDataType();
    }
  }

  public void setLocationInfo(String latVName, String lonVName, String zcoordVName) {
    this.latVName = latVName;
    this.lonVName = lonVName;
    this.zcoordVName = zcoordVName;

    // check for meter conversion
    if (zcoordVName != null) {
      Variable v = ncfile.findVariable(zcoordVName);
      zcoordUnits = ncfile.findAttValueIgnoreCase(v, CDM.UNITS, null);
      if (zcoordUnits != null)
        try {
          altScaleFactor = getMetersConversionFactor(zcoordUnits);
        } catch (Exception e) {
          if (errs != null) errs.format("%s", e.getMessage());
        }
    }
  }

  // make structure variable names to shortNames so StructureData sdata can
  // access it members
  public void setShortNames(
      String latVName, String lonVName, String altVName, String obsTimeVName, String nomTimeVName) {
    this.latVName = latVName;
    this.lonVName = lonVName;
    this.zcoordVName = altVName;
    this.obsTimeVName = obsTimeVName;
    this.nomTimeVName = nomTimeVName;
  }

  protected static double getMetersConversionFactor(String unitsString) throws Exception {
    SimpleUnit unit = SimpleUnit.factoryWithExceptions(unitsString);
    return unit.convertTo(1.0, SimpleUnit.meterUnit);
  }

  public Structure getRecordVar() {
    return (this.recordVar);
  }

  public int getRecordCount() {
    Dimension unlimitedDim = ncfile.getUnlimitedDimension();
    return unlimitedDim.getLength();
  }

  public void setTimeUnit(DateUnit timeUnit) {
    this.timeUnit = timeUnit;
  }

  public DateUnit getTimeUnit() {
    return this.timeUnit;
  }

  public LatLonPoint getLocation(StructureData sdata) {
    StructureMembers members = sdata.getStructureMembers();
    double lat = sdata.convertScalarDouble(members.findMember(latVName));
    double lon = sdata.convertScalarDouble(members.findMember(lonVName));
    return new LatLonPointImpl(lat, lon);
  }

  public double getLatitude(StructureData sdata) {
    StructureMembers members = sdata.getStructureMembers();
    return sdata.convertScalarDouble(members.findMember(latVName));
  }

  public double getLongitude(StructureData sdata) {
    StructureMembers members = sdata.getStructureMembers();
    return sdata.convertScalarDouble(members.findMember(lonVName));
  }

  public double getZcoordinate(StructureData sdata) {
    StructureMembers members = sdata.getStructureMembers();
    return (zcoordVName == null)
        ? Double.NaN
        : sdata.convertScalarDouble(members.findMember(zcoordVName));
  }

  public String getZcoordUnits() {
    return zcoordUnits;
  }

  public Date getObservationTimeAsDate(StructureData sdata) {
    return timeUnit.makeDate(getObservationTime(sdata));
  }

  public double getObservationTime(StructureData sdata) {
    return getTime(sdata.findMember(obsTimeVName), sdata);
  }

  private double getTime(StructureMembers.Member timeVar, StructureData sdata) {
    if (timeVar == null) return 0.0;

    if ((timeVar.getDataType() == DataType.CHAR) || (timeVar.getDataType() == DataType.STRING)) {
      String time = sdata.getScalarString(timeVar);
      CalendarDate date = CalendarDateFormatter.isoStringToCalendarDate(null, time);
      if (date == null) {
        log.error("Cant parse date - not ISO formatted, = " + time);
        return 0.0;
      }
      return date.getMillis() / 1000.0;

    } else {
      return sdata.convertScalarDouble(timeVar);
    }
  }

  /*
   * This reads through all the records in the dataset, and constructs a list of
   * RecordPointObs or RecordStationObs. It does not cache the data.
   * <p>If stnIdVName is not null, its a StationDataset, then construct a Station HashMap of StationImpl
   * objects. Add the RecordStationObs into the list of obs for that station.
   *
   * @param cancel allow user to cancel
   * @return List of RecordPointObs or RecordStationObs
   * @throws IOException on read error
   *
  public List<RecordPointObs> readAllCreateObs(CancelTask cancel) throws IOException {

    // see if its a station or point dataset
    boolean hasStations = stnIdVName != null;
    if (hasStations)
      stnHash = new HashMap<Object, Station>();

    // get min and max date and lat,lon
    double minDate = Double.MAX_VALUE;
    double maxDate = -Double.MAX_VALUE;

    double minLat = Double.MAX_VALUE;
    double maxLat = -Double.MAX_VALUE;

    double minLon = Double.MAX_VALUE;
    double maxLon = -Double.MAX_VALUE;

    // read all the data, create a RecordObs
    StructureMembers members = null;
    List<RecordPointObs> records = new ArrayList<RecordPointObs>();
    int recno = 0;
    Structure.Iterator ii = recordVar.getStructureIterator();
    while (ii.hasNext()) {
      StructureData sdata = ii.next();
      if (members == null)
        members = sdata.getStructureMembers();

      Object stationId = null;
      if (hasStations) {
        if (stationIdType == DataType.INT) {
          stationId = sdata.getScalarInt(stnIdVName);
        } else
          stationId = sdata.getScalarString(stnIdVName).trim();
      }

      String desc = (stnDescVName == null) ? null : sdata.getScalarString(stnDescVName);
      double lat = sdata.getScalarDouble(latVName);
      double lon = sdata.getScalarDouble(lonVName);
      double alt = (altVName == null) ? 0.0 : altScaleFactor * sdata.getScalarDouble(altVName);
      double obsTime = sdata.convertScalarDouble(members.findMember(obsTimeVName));
      double nomTime = (nomTimeVName == null) ? obsTime : sdata.convertScalarDouble(members.findMember(nomTimeVName));

      //double obsTime = sdata.convertScalarDouble( members.findMember( obsTimeVName) );
      //double nomTime = (nomTimeVName == null) ? obsTime : sdata.convertScalarDouble( members.findMember( nomTimeVName));

      if (hasStations) {
        Station stn = stnHash.get(stationId);
        if (stn == null) {
          stn = new Station(stationId.toString(), desc, lat, lon, alt);
          stnHash.put(stationId, stn);
        }
        RecordStationObs stnObs = new RecordStationObs(stn, obsTime, nomTime, timeUnit, recno);
        records.add(stnObs);
        //stn.addObs( stnObs);

      } else {
        records.add(new RecordPointObs(new EarthLocation(lat, lon, alt), obsTime, nomTime, timeUnit, recno));
      }

      // track date range and bounding box
      minDate = Math.min(minDate, obsTime);
      maxDate = Math.max(maxDate, obsTime);

      minLat = Math.min(minLat, lat);
      maxLat = Math.max(maxLat, lat);
      minLon = Math.min(minLon, lon);
      maxLon = Math.max(maxLon, lon);

      recno++;
      if ((cancel != null) && cancel.isCancel()) return null;
    }
    boundingBox = new LatLonRect(new LatLonPointImpl(minLat, minLon), new LatLonPointImpl(maxLat, maxLon));

    return records;
  }

  /* private boolean debugBB = false;
  public List getData(ArrayList records, LatLonRect boundingBox, CancelTask cancel) throws IOException {
    if (debugBB) System.out.println("Want bb= "+boundingBox);
    ArrayList result = new ArrayList();
    for (int i = 0; i < records.size(); i++) {
      RecordDatasetHelper.RecordPointObs r =  (RecordDatasetHelper.RecordPointObs) records.get(i);
      if (boundingBox.contains(r.getLatLon())) {
        if (debugBB) System.out.println(" ok latlon= "+r.getLatLon());
        result.add( r);
      }
      if ((cancel != null) && cancel.isCancel()) return null;
    }
    return result;
  }

  // return List<PointObsDatatype>
  public List getData(ArrayList records, LatLonRect boundingBox, double startTime, double endTime, CancelTask cancel) throws IOException {
    if (debugBB) System.out.println("Want bb= "+boundingBox);
    ArrayList result = new ArrayList();
    for (int i = 0; i < records.size(); i++) {
      RecordDatasetHelper.RecordPointObs r =  (RecordDatasetHelper.RecordPointObs) records.get(i);
      if (boundingBox.contains(r.getLatLon())) {
        if (debugBB) System.out.println(" ok latlon= "+r.getLatLon());
        double timeValue = r.getObservationTime();
        if ((timeValue >= startTime) && (timeValue <= endTime))
          result.add( r);
      }
      if ((cancel != null) && cancel.isCancel()) return null;
    }
    return result;
  }  */

  //////////////////////////////////////////////////////////////////////////////////////
  public PointFeature factory(StationImpl s, StructureData sdata, int recno) {
    if (s == null) return new RecordPointObs(sdata, recno);
    else return new RecordStationObs(s, sdata, recno);
  }

  class RecordPointObs extends PointFeatureImpl {
    protected int recno;
    protected StructureData sdata;

    RecordPointObs(int recno) {
      super(RecordDatasetHelper.this.timeUnit);
      this.recno = recno;
    }

    // Constructor for the case where you keep track of the location, time of each record, but not
    // the data.
    protected RecordPointObs(
        EarthLocation location, double obsTime, double nomTime, DateUnit timeUnit, int recno) {
      super(location, obsTime, nomTime, timeUnit);
      this.recno = recno;
    }

    // Constructor for when you already have the StructureData and want to wrap it in a
    // StationObsDatatype
    protected RecordPointObs(StructureData sdata, int recno) {
      super(RecordDatasetHelper.this.timeUnit);
      this.sdata = sdata;
      this.recno = recno;

      StructureMembers members = sdata.getStructureMembers();
      obsTime = getTime(members.findMember(obsTimeVName), sdata);
      nomTime = (nomTimeVName == null) ? obsTime : getTime(members.findMember(nomTimeVName), sdata);

      // this assumes the lat/lon/alt is stored in the obs record
      double lat = sdata.convertScalarDouble(members.findMember(latVName));
      double lon = sdata.convertScalarDouble(members.findMember(lonVName));
      double alt =
          (zcoordVName == null)
              ? 0.0
              : altScaleFactor * sdata.convertScalarDouble(members.findMember(zcoordVName));
      location = new EarthLocationImpl(lat, lon, alt);
    }

    public String getId() {
      return Integer.toString(recno);
    }

    public LatLonPoint getLatLon() {
      return new LatLonPointImpl(location.getLatitude(), location.getLongitude());
    }

    public StructureData getFeatureData() throws IOException {
      if (null == sdata) {
        try {
          // deal with files that are updating // LOOK kludge?
          if (recno > getRecordCount()) {
            int n = getRecordCount();
            ncfile.syncExtend();
            log.info(
                "RecordPointObs.getData recno="
                    + recno
                    + " > "
                    + n
                    + "; after sync= "
                    + getRecordCount());
          }

          sdata = recordVar.readStructure(recno);
        } catch (ucar.ma2.InvalidRangeException e) {
          e.printStackTrace();
          throw new IOException(e.getMessage());
        }
      }
      return sdata;
    }

    public ucar.ma2.StructureData getDataAll() throws java.io.IOException {
      return getFeatureData();
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////

  // a PointObs with the location info stored as a Station
  class RecordStationObs extends RecordPointObs {
    private Station station;

    /**
     * Constructor for the case where you keep track of the station, time of each record, but the
     * data reading is deferred.
     *
     * @param station data is for this Station
     * @param obsTime observation time
     * @param nomTime nominal time (may be NaN)
     * @param recno data is at this record number
     */
    protected RecordStationObs(
        Station station, double obsTime, double nomTime, DateUnit timeUnit, int recno) {
      super(station, obsTime, nomTime, timeUnit, recno);
      this.station = station;
    }

    // Constructor for when you have everything
    protected RecordStationObs(
        Station station, double obsTime, double nomTime, StructureData sdata, int recno) {
      super(recno);
      this.station = station;
      this.location = station;
      this.obsTime = obsTime;
      this.nomTime = nomTime;
      this.sdata = sdata;
    }

    // Constructor for when you already have the StructureData and Station, and calculate times
    protected RecordStationObs(Station station, StructureData sdata, int recno) {
      super(recno);
      this.station = station;
      this.location = station;
      this.sdata = sdata;

      StructureMembers members = sdata.getStructureMembers();
      obsTime = getTime(members.findMember(obsTimeVName), sdata);
      nomTime = (nomTimeVName == null) ? obsTime : getTime(members.findMember(nomTimeVName), sdata);
    }

    // Constructor for when you already have the StructureData, and need to find Station and times
    protected RecordStationObs(StructureData sdata, int recno, boolean useId) {
      super(recno);
      this.recno = recno;
      this.sdata = sdata;
      this.timeUnit = RecordDatasetHelper.this.timeUnit;

      StructureMembers members = sdata.getStructureMembers();
      obsTime = getTime(members.findMember(obsTimeVName), sdata);
      nomTime = (nomTimeVName == null) ? obsTime : getTime(members.findMember(nomTimeVName), sdata);

      if (useId) {
        // this assumes the station id/name is stored in the obs record
        String stationId;
        if (stationIdType == DataType.INT) {
          stationId = Integer.toString(sdata.getScalarInt(stnIdVName));
        } else stationId = sdata.getScalarString(stnIdVName).trim();
        station = stationHelper.getStation(stationId);
        if (null != errs)
          errs.format(" cant find station id = <%s> when reading record %d%n", stationId, recno);
        log.error(" cant find station id = <" + stationId + "> when reading record " + recno);

      } else {
        // use a station index
        List<Station> stations = stationHelper.getStations();
        int stationIndex = sdata.getScalarInt(stnIndexVName);
        if (stationIndex < 0 || stationIndex >= stations.size()) {
          if (null != errs)
            errs.format(
                " cant find station at index =%d when reading record %d%n", stationIndex, recno);
          log.error(
              "cant find station at index = " + stationIndex + " when reading record " + recno);
        } else station = stations.get(stationIndex);
      }

      location = station;
    }
  }
}
Пример #2
0
/**
 * _more_
 *
 * @author edavis
 * @since 4.0
 */
public class WcsCoverage {
  private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WcsCoverage.class);

  private GridDataset.Gridset coverage;
  private WcsDataset dataset;

  private String name;
  private String label;
  private String description;

  private GridCoordSystem coordSys;
  private String nativeCRS;

  private String defaultRequestCrs;

  private List<WcsRequest.Format> supportedCoverageFormatList;

  private HashMap<String, WcsRangeField> range;

  public WcsCoverage(GridDataset.Gridset coverage, WcsDataset dataset) {
    this.dataset = dataset;
    if (this.dataset == null) {
      log.error("WcsCoverage(): non-null dataset required.");
      throw new IllegalArgumentException("Non-null dataset required.");
    }

    this.coverage = coverage;
    if (this.coverage == null) {
      log.error("WcsCoverage(): non-null coverage required.");
      throw new IllegalArgumentException("Non-null coverage required.");
    }
    this.coordSys = coverage.getGeoCoordSystem();
    if (this.coordSys == null) {
      log.error("WcsCoverage(): Coverage must have non-null coordinate system.");
      throw new IllegalArgumentException("Non-null coordinate system required.");
    }

    this.name = this.coordSys.getName();
    this.label = this.coordSys.getName();

    this.range = new HashMap<String, WcsRangeField>();
    StringBuilder descripSB =
        new StringBuilder("All parameters on the \"")
            .append(this.name)
            .append("\" coordinate system: ");
    for (GridDatatype curField : this.coverage.getGrids()) {
      String stdName = curField.findAttValueIgnoreCase("standard_name", "");
      descripSB.append(stdName.length() == 0 ? curField.getFullName() : stdName).append(",");

      WcsRangeField field = new WcsRangeField(curField);
      range.put(field.getName(), field);
    }
    descripSB.setCharAt(descripSB.length() - 1, '.');
    this.description = descripSB.toString();

    this.nativeCRS = EPSG_OGC_CF_Helper.getWcs1_0CrsId(this.coordSys.getProjection());

    this.defaultRequestCrs = "OGC:CRS84";

    this.supportedCoverageFormatList = new ArrayList<WcsRequest.Format>();
    // this.supportedCoverageFormatList = "application/x-netcdf";
    this.supportedCoverageFormatList.add(WcsRequest.Format.GeoTIFF);
    this.supportedCoverageFormatList.add(WcsRequest.Format.GeoTIFF_Float);
    this.supportedCoverageFormatList.add(WcsRequest.Format.NetCDF3);
  }

  GridDataset.Gridset getGridset() {
    return coverage;
  }

  public String getName() {
    return this.name;
  }

  public String getLabel() {
    return this.label;
  }

  public String getDescription() {
    return this.description;
  }

  public GridCoordSystem getCoordinateSystem() {
    return coordSys;
  }

  public String getDefaultRequestCrs() {
    return defaultRequestCrs;
  }

  public String getNativeCrs() {
    return nativeCRS;
  }

  public List<WcsRequest.Format> getSupportedCoverageFormatList() {
    return Collections.unmodifiableList(supportedCoverageFormatList);
  }

  public boolean isSupportedCoverageFormat(WcsRequest.Format covFormat) {
    return this.supportedCoverageFormatList.contains(covFormat);
  }

  public boolean isRangeFieldName(String fieldName) {
    return range.containsKey(fieldName);
  }

  public Set<String> getRangeFieldNames() {
    return range.keySet();
  }

  public Collection<WcsRangeField> getRange() {
    return range.values();
  }

  private static DiskCache2 diskCache = null;

  public static void setDiskCache(DiskCache2 _diskCache) {
    diskCache = _diskCache;
  }

  private static DiskCache2 getDiskCache() {
    if (diskCache == null) {
      log.error("getDiskCache(): Disk cache has not been set.");
      throw new IllegalStateException(
          "Disk cache must be set before calling GetCoverage.getDiskCache().");
    }
    return diskCache;
  }

  public File writeCoverageDataToFile(
      WcsRequest.Format format,
      LatLonRect bboxLatLonRect,
      AxisSubset vertSubset,
      List<String> rangeSubset,
      CalendarDateRange timeRange)
      throws WcsException {
    boolean zRangeDone = false;
    boolean tRangeDone = false;

    try {
      // Get the height range.
      Range zRange = vertSubset != null ? vertSubset.getRange() : null;
      zRangeDone = true;

      // Get the time range.
      Range tRange = null;
      if (timeRange != null) {
        CoordinateAxis1DTime timeAxis = this.coordSys.getTimeAxis1D();
        int startIndex = timeAxis.findTimeIndexFromCalendarDate(timeRange.getStart());
        int endIndex = timeAxis.findTimeIndexFromCalendarDate(timeRange.getEnd());
        tRange = new Range(startIndex, endIndex);
        tRangeDone = true;
      }

      if (format == WcsRequest.Format.GeoTIFF || format == WcsRequest.Format.GeoTIFF_Float) {
        if (rangeSubset.size() != 1) {
          String msg =
              "GeoTIFF response encoding only available for single range field selection ["
                  + rangeSubset
                  + "].";
          log.error("writeCoverageDataToFile(): " + msg);
          throw new WcsException(WcsException.Code.InvalidParameterValue, "RangeSubset", msg);
        }
        String reqRangeFieldName = rangeSubset.get(0);

        File dir = new File(getDiskCache().getRootDirectory());
        File tifFile = File.createTempFile("WCS", ".tif", dir);
        if (log.isDebugEnabled())
          log.debug("writeCoverageDataToFile(): tifFile=" + tifFile.getPath());

        WcsRangeField rangeField = this.range.get(reqRangeFieldName);
        GridDatatype subset =
            rangeField.getGridDatatype().makeSubset(tRange, zRange, bboxLatLonRect, 1, 1, 1);
        Array data = subset.readDataSlice(0, 0, -1, -1);

        GeotiffWriter writer = new GeotiffWriter(tifFile.getPath());
        writer.writeGrid(
            this.dataset.getDataset(), subset, data, format == WcsRequest.Format.GeoTIFF);

        writer.close();

        return tifFile;
      } else if (format == WcsRequest.Format.NetCDF3) {
        File dir = new File(getDiskCache().getRootDirectory());
        File outFile = File.createTempFile("WCS", ".nc", dir);
        if (log.isDebugEnabled())
          log.debug("writeCoverageDataToFile(): ncFile=" + outFile.getPath());
        // WTF ?? this.coordSys.getVerticalAxis().isNumeric();

        NetcdfFileWriter writer =
            NetcdfFileWriter.createNew(NetcdfFileWriter.Version.netcdf3, outFile.getAbsolutePath());
        CFGridWriter2.writeFile(
            this.dataset.getDataset(),
            rangeSubset,
            bboxLatLonRect,
            null,
            1,
            zRange,
            timeRange,
            1,
            true,
            writer);
        return outFile;
      } else {
        log.error(
            "writeCoverageDataToFile(): Unsupported response encoding format [" + format + "].");
        throw new WcsException(
            WcsException.Code.InvalidFormat,
            "Format",
            "Unsupported response encoding format [" + format + "].");
      }
    } catch (InvalidRangeException e) {
      String msg = "Failed to subset coverage [" + this.getName();
      if (!zRangeDone) msg += "] along vertical axis [" + vertSubset + "]. ";
      else if (!tRangeDone) msg += "] along time axis [" + timeRange + "]. ";
      else msg += "] in horizontal plane [" + bboxLatLonRect + "]. ";
      log.error("writeCoverageDataToFile(): " + msg + e.getMessage());
      throw new WcsException(WcsException.Code.CoverageNotDefined, "", msg);
    } catch (IOException e) {
      log.error(
          "writeCoverageDataToFile(): Failed to write file for requested coverage <"
              + this.getName()
              + ">: "
              + e.getMessage());
      throw new WcsException(
          WcsException.Code.UNKNOWN, "", "Problem creating coverage [" + this.getName() + "].");
    }
  }
}
Пример #3
0
/**
 * A georeferencing "gridded" VariableEnhanced, that has a GridCoordSys. In VisAD data model, it is
 * a sampled Field. The dimension are put into canonical order: (rt, e, t, z, y, x).
 *
 * <p>
 *
 * <p>Implementation note: If the Horizontal axes are 2D, the x and y dimensions are arbitrarily
 * chosen to be gcs.getXHorizAxis().getDimension(1), gcs.getXHorizAxis().getDimension(0)
 * respectively.
 *
 * <p>
 *
 * @author caron
 */
public class GeoGrid implements NamedObject, ucar.nc2.dt.GridDatatype {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GeoGrid.class);
  private static final boolean debugArrayShape = false;

  private final GridDataset dataset;
  private final GridCoordSys gcs;
  private final VariableDS vs;
  private int xDimOrgIndex = -1,
      yDimOrgIndex = -1,
      zDimOrgIndex = -1,
      tDimOrgIndex = -1,
      eDimOrgIndex = -1,
      rtDimOrgIndex = -1;
  private int xDimNewIndex = -1,
      yDimNewIndex = -1,
      zDimNewIndex = -1,
      tDimNewIndex = -1,
      eDimNewIndex = -1,
      rtDimNewIndex = -1;
  private final List<Dimension> mydims;

  /**
   * Constructor.
   *
   * @param dataset belongs to this dataset
   * @param dsvar wraps this Variable
   * @param gcs has this grid coordinate system
   */
  public GeoGrid(GridDataset dataset, VariableDS dsvar, GridCoordSys gcs) {
    this.dataset = dataset;
    this.vs = dsvar;
    this.gcs = gcs;

    CoordinateAxis xaxis = gcs.getXHorizAxis();
    if (xaxis instanceof CoordinateAxis1D) {
      xDimOrgIndex = findDimension(gcs.getXHorizAxis().getDimension(0));
      yDimOrgIndex = findDimension(gcs.getYHorizAxis().getDimension(0));

    } else { // 2D case
      yDimOrgIndex = findDimension(gcs.getXHorizAxis().getDimension(0));
      xDimOrgIndex = findDimension(gcs.getXHorizAxis().getDimension(1));
    }

    if (gcs.getVerticalAxis() != null)
      zDimOrgIndex = findDimension(gcs.getVerticalAxis().getDimension(0));
    if (gcs.getTimeAxis() != null) {
      if (gcs.getTimeAxis1D() != null)
        tDimOrgIndex = findDimension(gcs.getTimeAxis1D().getDimension(0));
      else tDimOrgIndex = findDimension(gcs.getTimeAxis().getDimension(1));
    }
    if (gcs.getEnsembleAxis() != null)
      eDimOrgIndex = findDimension(gcs.getEnsembleAxis().getDimension(0));
    if (gcs.getRunTimeAxis() != null)
      rtDimOrgIndex = findDimension(gcs.getRunTimeAxis().getDimension(0));

    // construct canonical dimension list
    int count = 0;
    this.mydims = new ArrayList<Dimension>();
    if ((rtDimOrgIndex >= 0) && (rtDimOrgIndex != tDimOrgIndex)) {
      mydims.add(dsvar.getDimension(rtDimOrgIndex));
      rtDimNewIndex = count++;
    }
    if (eDimOrgIndex >= 0) {
      mydims.add(dsvar.getDimension(eDimOrgIndex));
      eDimNewIndex = count++;
    }
    if (tDimOrgIndex >= 0) {
      mydims.add(dsvar.getDimension(tDimOrgIndex));
      tDimNewIndex = count++;
    }
    if (zDimOrgIndex >= 0) {
      mydims.add(dsvar.getDimension(zDimOrgIndex));
      zDimNewIndex = count++;
    }
    if (yDimOrgIndex >= 0) {
      mydims.add(dsvar.getDimension(yDimOrgIndex));
      yDimNewIndex = count++;
    }
    if (xDimOrgIndex >= 0) {
      mydims.add(dsvar.getDimension(xDimOrgIndex));
      xDimNewIndex = count;
    }
  }

  private int findDimension(Dimension want) {
    java.util.List dims = vs.getDimensions();
    for (int i = 0; i < dims.size(); i++) {
      Dimension d = (Dimension) dims.get(i);
      if (d.equals(want)) return i;
    }
    return -1;
  }

  /**
   * Returns an ArrayList containing the dimensions used by this geoGrid. The dimension are put into
   * canonical order: (rt, e, t, z, y, x). Note that the z and t dimensions are optional. If the
   * Horizontal axes are 2D, the x and y dimensions are arbitrarily chosen to be
   * gcs.getXHorizAxis().getDimension(1), gcs.getXHorizAxis().getDimension(0), respectively.
   *
   * @return List with objects of type Dimension, in canonical order.
   */
  public java.util.List<Dimension> getDimensions() {
    return new ArrayList<Dimension>(mydims);
  }

  /**
   * get the ith dimension
   *
   * @param i : which dimension
   * @return ith Dimension
   */
  public Dimension getDimension(int i) {
    if ((i < 0) || (i >= mydims.size())) return null;
    return mydims.get(i);
  }

  /** get the time Dimension, if it exists */
  public Dimension getTimeDimension() {
    return tDimNewIndex < 0 ? null : getDimension(tDimNewIndex);
  }

  /** get the z Dimension, if it exists */
  public Dimension getZDimension() {
    return zDimNewIndex < 0 ? null : getDimension(zDimNewIndex);
  }

  /** get the y Dimension, if it exists */
  public Dimension getYDimension() {
    return yDimNewIndex < 0 ? null : getDimension(yDimNewIndex);
  }

  /** get the x Dimension, if it exists */
  public Dimension getXDimension() {
    return xDimNewIndex < 0 ? null : getDimension(xDimNewIndex);
  }

  /** get the ensemble Dimension, if it exists */
  public Dimension getEnsembleDimension() {
    return eDimNewIndex < 0 ? null : getDimension(eDimNewIndex);
  }

  /** get the run time Dimension, if it exists */
  public Dimension getRunTimeDimension() {
    return rtDimNewIndex < 0 ? null : getDimension(rtDimNewIndex);
  }

  /** get the time Dimension index in the geogrid (canonical order), or -1 if none */
  public int getTimeDimensionIndex() {
    return tDimNewIndex;
  }

  /** get the z Dimension index in the geogrid (canonical order), or -1 if none */
  public int getZDimensionIndex() {
    return zDimNewIndex;
  }

  /** get the y Dimension index in the geogrid (canonical order) */
  public int getYDimensionIndex() {
    return yDimNewIndex;
  }

  /** get the x Dimension index in the geogrid (canonical order) */
  public int getXDimensionIndex() {
    return xDimNewIndex;
  }

  /** get the ensemble Dimension index in the geogrid (canonical order) */
  public int getEnsembleDimensionIndex() {
    return eDimNewIndex;
  }

  /** get the runtime Dimension index in the geogrid (canonical order) */
  public int getRunTimeDimensionIndex() {
    return rtDimNewIndex;
  }

  /**
   * Convenience function; lookup Attribute by name.
   *
   * @param name the name of the attribute
   * @return the attribute, or null if not found
   */
  public Attribute findAttributeIgnoreCase(String name) {
    return vs.findAttributeIgnoreCase(name);
  }

  /**
   * Convenience function; lookup Attribute value by name. Must be String valued
   *
   * @param attName name of the attribute
   * @param defaultValue if not found, use this as the default
   * @return Attribute string value, or default if not found.
   */
  public String findAttValueIgnoreCase(String attName, String defaultValue) {
    return dataset.getNetcdfDataset().findAttValueIgnoreCase((Variable) vs, attName, defaultValue);
  }

  // implementation of GridDatatype interface

  /** get the rank */
  public int getRank() {
    return mydims.size();
  }

  /** get the shape */
  public int[] getShape() {
    int[] shape = new int[mydims.size()];
    for (int i = 0; i < mydims.size(); i++) {
      Dimension d = mydims.get(i);
      shape[i] = d.getLength();
    }
    return shape;
  }

  /** get the data type */
  public DataType getDataType() {
    return vs.getDataType();
  }

  public List<Attribute> getAttributes() {
    return vs.getAttributes();
  }

  public VariableDS getVariable() {
    return vs;
  }

  public String getFullName() {
    return vs.getFullName();
  }

  public String getName() {
    return vs.getFullName();
  }

  public String getShortName() {
    return vs.getShortName();
  }

  /** get the GridCoordSys for this GeoGrid. */
  public GridCoordSystem getCoordinateSystem() {
    return gcs;
  }

  /** get the Projection. */
  public ProjectionImpl getProjection() {
    return gcs.getProjection();
  }

  /** @return ArrayList of thredds.util.NamedObject, from the GridCoordSys. */
  public List<NamedObject> getLevels() {
    return gcs.getLevels();
  }

  /** @return ArrayList of thredds.util.NamedObject, from the GridCoordSys. */
  public List<NamedObject> getTimes() {
    return gcs.getTimes();
  }

  /** get the standardized description */
  public String getDescription() {
    return vs.getDescription();
  }

  /** get the unit as a string */
  public String getUnitsString() {
    String units = vs.getUnitsString();
    return (units == null) ? "" : units;
  }

  /**
   * @return getUnitsString()
   * @deprecated use getUnitsString()
   */
  public java.lang.String getUnitString() {
    return getUnitsString();
  }

  // public ucar.unidata.geoloc.ProjectionImpl getProjection() { return gcs.getProjection(); }

  /** true if there may be missing data, see VariableDS.hasMissing() */
  public boolean hasMissingData() {
    return vs.hasMissing();
  }

  /** if val is missing data, see VariableDS.isMissingData() */
  public boolean isMissingData(double val) {
    return vs.isMissing(val);
  }

  /**
   * Convert (in place) all values in the given array that are considered as "missing" to Float.NaN,
   * according to isMissingData(val).
   *
   * @param values input array
   * @return input array, with missing values converted to NaNs.
   */
  public float[] setMissingToNaN(float[] values) {
    if (!vs.hasMissing()) return values;
    final int length = values.length;
    for (int i = 0; i < length; i++) {
      double value = values[i];
      if (vs.isMissing(value)) values[i] = Float.NaN;
    }
    return values;
  }

  /**
   * Get the minimum and the maximum data value of the previously read Array, skipping missing
   * values as defined by isMissingData(double val).
   *
   * @param a Array to get min/max values
   * @return both min and max value.
   */
  public MAMath.MinMax getMinMaxSkipMissingData(Array a) {
    if (!hasMissingData()) return MAMath.getMinMax(a);

    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (isMissingData(val)) continue;
      if (val > max) max = val;
      if (val < min) min = val;
    }
    return new MAMath.MinMax(min, max);
  }

  /**
   * Reads in the data "volume" at the given time index. If its a product set, put into canonical
   * order (z-y-x). If not a product set, reorder to (z,i,j), where i, j are from the original
   *
   * @param t time index; ignored if no time axis.
   * @return data[z,y,x] or data[y,x] if no z axis.
   */
  public Array readVolumeData(int t) throws java.io.IOException {
    // if (gcs.isProductSet())
    return readDataSlice(t, -1, -1, -1);
    /* else { // 2D XY
      int rank = vs.getRank();
      int[] shape = vs.getShape();
      int [] start = new int[rank];

      CoordinateAxis taxis = gcs.getTimeAxis();
      if (taxis != null) {
        if ((t >= 0) && (t < taxis.getSize()))
          shape[ tDim] = 1;  // fix t
          start[ tDim] = t;
      }

      if (debugArrayShape) {
        System.out.println("getDataVolume shape = ");
        for (int i=0; i<rank; i++)
          System.out.println("   start = "+start[i]+" shape = "+ shape[i]);
      }

      Array dataVolume;
      try {
        dataVolume = vs.read( start, shape);
      } catch (Exception e) {
        System.out.println("Exception: GeoGridImpl.getdataSlice() on dataset "+getName());
        e.printStackTrace();
        throw new java.io.IOException(e.getMessage());
      }

      // no reordering FIX
      return dataVolume.reduce();
    } */
  }

  /**
   * Reads a Y-X "horizontal slice" at the given time and vertical index. If its a product set, put
   * into canonical order (y-x).
   *
   * @param t time index; ignored if no time axis.
   * @param z vertical index; ignored if no z axis.
   * @return data[y,x]
   * @throws java.io.IOException on read error
   */
  public Array readYXData(int t, int z) throws java.io.IOException {
    return readDataSlice(t, z, -1, -1);
  }

  /**
   * Reads a Z-Y "vertical slice" at the given time and x index. If its a product set, put into
   * canonical order (z-y).
   *
   * @param t time index; ignored if no time axis.
   * @param x x index; ignored if no x axis.
   * @return data[z,y]
   * @throws java.io.IOException on read error
   */
  public Array readZYData(int t, int x) throws java.io.IOException {
    return readDataSlice(t, -1, -1, x);
  }

  /**
   * @throws java.io.IOException on read error
   * @deprecated use readDataSlice
   */
  public Array getDataSlice(int t, int z, int y, int x) throws java.io.IOException {
    return readDataSlice(t, z, y, x);
  }

  /**
   * This reads an arbitrary data slice, returning the data in canonical order (t-z-y-x). If any
   * dimension does not exist, ignore it.
   *
   * @param t if < 0, get all of time dim; if valid index, fix slice to that value.
   * @param z if < 0, get all of z dim; if valid index, fix slice to that value.
   * @param y if < 0, get all of y dim; if valid index, fix slice to that value.
   * @param x if < 0, get all of x dim; if valid index, fix slice to that value.
   * @return data[t,z,y,x], eliminating missing or fixed dimension.
   */
  public Array readDataSlice(int t, int z, int y, int x) throws java.io.IOException {
    return readDataSlice(0, 0, t, z, y, x);
  }

  /**
   * This reads an arbitrary data slice, returning the data in canonical order (rt-e-t-z-y-x). If
   * any dimension does not exist, ignore it.
   *
   * @param rt if < 0, get all of runtime dim; if valid index, fix slice to that value.
   * @param e if < 0, get all of ensemble dim; if valid index, fix slice to that value.
   * @param t if < 0, get all of time dim; if valid index, fix slice to that value.
   * @param z if < 0, get all of z dim; if valid index, fix slice to that value.
   * @param y if < 0, get all of y dim; if valid index, fix slice to that value.
   * @param x if < 0, get all of x dim; if valid index, fix slice to that value.
   * @return data[rt,e,t,z,y,x], eliminating missing or fixed dimension.
   */
  public Array readDataSlice(int rt, int e, int t, int z, int y, int x) throws java.io.IOException {

    int rank = vs.getRank();
    int[] start = new int[rank];
    int[] shape = new int[rank];
    for (int i = 0; i < rank; i++) {
      start[i] = 0;
      shape[i] = 1;
    }
    Dimension xdim = getXDimension();
    Dimension ydim = getYDimension();
    Dimension zdim = getZDimension();
    Dimension tdim = getTimeDimension();
    Dimension edim = getEnsembleDimension();
    Dimension rtdim = getRunTimeDimension();

    // construct the shape of the data volume to be read
    if (rtdim != null) {
      if ((rt >= 0) && (rt < rtdim.getLength())) start[rtDimOrgIndex] = rt; // fix rt
      else {
        shape[rtDimOrgIndex] = rtdim.getLength(); // all of rt
      }
    }

    if (edim != null) {
      if ((e >= 0) && (e < edim.getLength())) start[eDimOrgIndex] = e; // fix e
      else {
        shape[eDimOrgIndex] = edim.getLength(); // all of e
      }
    }

    if (tdim != null) {
      if ((t >= 0) && (t < tdim.getLength())) start[tDimOrgIndex] = t; // fix t
      else {
        shape[tDimOrgIndex] = tdim.getLength(); // all of t
      }
    }

    if (zdim != null) {
      if ((z >= 0) && (z < zdim.getLength())) start[zDimOrgIndex] = z; // fix z
      else {
        shape[zDimOrgIndex] = zdim.getLength(); // all of z
      }
    }

    if (ydim != null) {
      if ((y >= 0) && (y < ydim.getLength())) start[yDimOrgIndex] = y; // fix y
      else {
        shape[yDimOrgIndex] = ydim.getLength(); // all of y
      }
    }

    if (xdim != null) {
      if ((x >= 0) && (x < xdim.getLength())) // all of x
      start[xDimOrgIndex] = x; // fix x
      else {
        shape[xDimOrgIndex] = xdim.getLength(); // all of x
      }
    }

    if (debugArrayShape) {
      System.out.println("read shape from org variable = ");
      for (int i = 0; i < rank; i++)
        System.out.println(
            "   start = "
                + start[i]
                + " shape = "
                + shape[i]
                + " name = "
                + vs.getDimension(i).getName());
    }

    // read it
    Array dataVolume;
    try {
      dataVolume = vs.read(start, shape);
    } catch (Exception ex) {
      log.error(
          "GeoGrid.getdataSlice() on dataset " + getFullName() + " " + dataset.getLocation(), ex);
      throw new java.io.IOException(ex.getMessage());
    }

    // LOOK: the real problem is the lack of named dimensions in the Array object
    // figure out correct permutation for canonical ordering for permute
    List<Dimension> oldDims = new ArrayList<Dimension>(vs.getDimensions());
    int[] permuteIndex = new int[dataVolume.getRank()];
    int count = 0;
    if (oldDims.contains(rtdim)) permuteIndex[count++] = oldDims.indexOf(rtdim);
    if (oldDims.contains(edim)) permuteIndex[count++] = oldDims.indexOf(edim);
    if (oldDims.contains(tdim)) permuteIndex[count++] = oldDims.indexOf(tdim);
    if (oldDims.contains(zdim)) permuteIndex[count++] = oldDims.indexOf(zdim);
    if (oldDims.contains(ydim)) permuteIndex[count++] = oldDims.indexOf(ydim);
    if (oldDims.contains(xdim)) permuteIndex[count] = oldDims.indexOf(xdim);

    if (debugArrayShape) {
      System.out.println("oldDims = ");
      for (Dimension oldDim : oldDims) System.out.println("   oldDim = " + oldDim.getName());
      System.out.println("permute dims = ");
      for (int aPermuteIndex : permuteIndex)
        System.out.println("   oldDim index = " + aPermuteIndex);
    }

    // check to see if we need to permute
    boolean needPermute = false;
    for (int i = 0; i < permuteIndex.length; i++) {
      if (i != permuteIndex[i]) needPermute = true;
    }

    // permute to the order rt,e,t,z,y,x
    if (needPermute) dataVolume = dataVolume.permute(permuteIndex);

    // eliminate fixed dimensions, but not all dimensions of length 1.
    count = 0;
    if (rtdim != null) {
      if (rt >= 0) dataVolume = dataVolume.reduce(count);
      else count++;
    }
    if (edim != null) {
      if (e >= 0) dataVolume = dataVolume.reduce(count);
      else count++;
    }
    if (tdim != null) {
      if (t >= 0) dataVolume = dataVolume.reduce(count);
      else count++;
    }
    if (zdim != null) {
      if (z >= 0) dataVolume = dataVolume.reduce(count);
      else count++;
    }
    if (ydim != null) {
      if (y >= 0) dataVolume = dataVolume.reduce(count);
      else count++;
    }
    if (xdim != null) {
      if (x >= 0) dataVolume = dataVolume.reduce(count);
    }

    return dataVolume;
  }

  //////////////////////////////////

  /**
   * Create a new GeoGrid that is a logical subset of this GeoGrid.
   *
   * @param t_range subset the time dimension, or null if you want all of it
   * @param z_range subset the vertical dimension, or null if you want all of it
   * @param bbox a lat/lon bounding box, or null if you want all x,y
   * @param z_stride use only if z_range is null, then take all z with this stride (1 means all)
   * @param y_stride use this stride on the y coordinate (1 means all)
   * @param x_stride use this stride on the x coordinate (1 means all)
   * @return subsetted GeoGrid
   * @throws InvalidRangeException if bbox does not intersect GeoGrid
   */
  public GeoGrid subset(
      Range t_range, Range z_range, LatLonRect bbox, int z_stride, int y_stride, int x_stride)
      throws InvalidRangeException {

    if ((z_range == null) && (z_stride > 1)) {
      Dimension zdim = getZDimension();
      if (zdim != null) z_range = new Range(0, zdim.getLength() - 1, z_stride);
    }

    Range y_range = null, x_range = null;
    if (bbox != null) {
      List yx_ranges = gcs.getRangesFromLatLonRect(bbox);
      y_range = (Range) yx_ranges.get(0);
      x_range = (Range) yx_ranges.get(1);
    }

    if (y_stride > 1) {
      if (y_range == null) {
        Dimension ydim = getYDimension();
        y_range = new Range(0, ydim.getLength() - 1, y_stride);
      } else {
        y_range = new Range(y_range.first(), y_range.last(), y_stride);
      }
    }

    if (x_stride > 1) {
      if (x_range == null) {
        Dimension xdim = getXDimension();
        x_range = new Range(0, xdim.getLength() - 1, x_stride);
      } else {
        x_range = new Range(x_range.first(), x_range.last(), x_stride);
      }
    }

    return subset(t_range, z_range, y_range, x_range);
  }

  public GridDatatype makeSubset(
      Range t_range, Range z_range, LatLonRect bbox, int z_stride, int y_stride, int x_stride)
      throws InvalidRangeException {
    return subset(t_range, z_range, bbox, z_stride, y_stride, x_stride);
  }

  /**
   * Create a new GeoGrid that is a logical subset of this GeoGrid.
   *
   * @param t_range subset the time dimension, or null if you want all of it
   * @param z_range subset the vertical dimension, or null if you want all of it
   * @param y_range subset the y dimension, or null if you want all of it
   * @param x_range subset the x dimension, or null if you want all of it
   * @return subsetted GeoGrid
   * @throws InvalidRangeException if any of the ranges are invalid
   */
  public GeoGrid subset(Range t_range, Range z_range, Range y_range, Range x_range)
      throws InvalidRangeException {
    return (GeoGrid) makeSubset(null, null, t_range, z_range, y_range, x_range);
  }

  public GridDatatype makeSubset(
      Range rt_range, Range e_range, Range t_range, Range z_range, Range y_range, Range x_range)
      throws InvalidRangeException {
    // get the ranges list
    int rank = getRank();
    Range[] ranges = new Range[rank];
    if (null != getXDimension()) ranges[xDimOrgIndex] = x_range;
    if (null != getYDimension()) ranges[yDimOrgIndex] = y_range;
    if (null != getZDimension()) ranges[zDimOrgIndex] = z_range;
    if (null != getTimeDimension()) ranges[tDimOrgIndex] = t_range;
    if (null != getRunTimeDimension()) ranges[rtDimOrgIndex] = rt_range;
    if (null != getEnsembleDimension()) ranges[eDimOrgIndex] = e_range;
    List<Range> rangesList = Arrays.asList(ranges);

    // subset the variable
    VariableDS v_section = (VariableDS) vs.section(new Section(rangesList));
    List<Dimension> dims = v_section.getDimensions();
    for (Dimension dim : dims) {
      dim.setShared(true); // make them shared (section will make them unshared)
    }

    // subset the axes in the GridCoordSys
    GridCoordSys gcs_section =
        new GridCoordSys(gcs, rt_range, e_range, t_range, z_range, y_range, x_range);

    // now we can make the geogrid
    return new GeoGrid(dataset, v_section, gcs_section);
  }

  /////////////////////////////////////////////////////////////////////////////////
  /** Instances which have same name and coordinate system are equal. */
  public boolean equals(Object oo) {
    if (this == oo) return true;
    if (!(oo instanceof GeoGrid)) return false;

    GeoGrid d = (GeoGrid) oo;
    if (!getFullName().equals(d.getFullName())) return false;
    if (!getCoordinateSystem().equals(d.getCoordinateSystem())) return false;

    return true;
  }

  /** Override Object.hashCode() to be consistent with equals. */
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      // result = 37*result + dataset.getName().hashCode();
      result = 37 * result + getFullName().hashCode();
      result = 37 * result + getCoordinateSystem().hashCode();
      hashCode = result;
    }
    return hashCode;
  }

  private int hashCode = 0; // Bloch, item 8

  /** string representation */
  public String toString() {
    return getFullName();
  }

  /** nicely formatted information */
  public String getInfo() {
    StringBuilder buf = new StringBuilder(200);
    buf.setLength(0);
    buf.append(getFullName());
    Format.tab(buf, 30, true);
    buf.append(getUnitsString());
    Format.tab(buf, 60, true);
    buf.append(hasMissingData());
    Format.tab(buf, 66, true);
    buf.append(getDescription());
    return buf.toString();
  }

  public int compareTo(GridDatatype g) {
    return getFullName().compareTo(g.getFullName());
  }
}