/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.dataset;

import ucar.nc2.*;

import java.util.List;
import java.util.ArrayList;

/**
 * Helper methods for constructing NetcdfDatasets.
 * @author caron
 * @since Jul 6, 2007
 */
public class DatasetConstructor {

  /**
   * Copy contents of "src" to "target". skip ones that already exist (by name).
   * Dimensions and Variables are replaced with equivalent elements, but unlimited dimensions are turned into regular dimensions.
   * Attribute doesnt have to be replaced because its immutable, so its copied by reference.
   *
   * @param src transfer from here. If src is a NetcdfDataset, transferred variables get reparented to target group.
   * @param target transfer to this NetcdfDataset.
   * @param replaceCheck if null, add if a Variable of the same name doesnt already exist, otherwise
   *   replace if replaceCheck.replace( Variable v) is true
   */
  static public void transferDataset(NetcdfFile src, NetcdfDataset target, ReplaceVariableCheck replaceCheck) {
    transferGroup(src, target, src.getRootGroup(), target.getRootGroup(), replaceCheck);
  }

  // transfer the objects in src group to the target group
  static private void transferGroup(NetcdfFile ds, NetcdfDataset targetDs, Group src, Group targetGroup, ReplaceVariableCheck replaceCheck) {
    boolean unlimitedOK = true; // LOOK why not allowed?

    // group attributes
    transferGroupAttributes(src, targetGroup);

    // dimensions
    for (Dimension d : src.getDimensions()) {
      if (null == targetGroup.findDimensionLocal(d.getShortName())) {
        Dimension newd = new Dimension(d.getShortName(), d.getLength(), d.isShared(), unlimitedOK && d.isUnlimited(), d.isVariableLength());
        targetGroup.addDimension(newd);
      }
    }

    // variables
    for (Variable v : src.getVariables()) {
      Variable targetV = targetGroup.findVariable(v.getShortName());
      VariableEnhanced targetVe = (VariableEnhanced) targetV;
      boolean replace = (replaceCheck != null) && replaceCheck.replace(v); // replaceCheck not currently used

      if (replace || (null == targetV)) { // replace it
        if ((v instanceof Structure) && !(v instanceof StructureDS)) {
           v = new StructureDS(targetGroup, (Structure) v);

          // else if (!(v instanceof VariableDS) && !(v instanceof StructureDS)) Doug Lindolm
        } else if (!(v instanceof VariableDS)) {
          v = new VariableDS(targetGroup, v, false);  // enhancement done by original variable, this is just to reparent to target dataset.
        }

        if (null != targetV) targetGroup.remove(targetV);
        targetGroup.addVariable(v); // reparent group
        v.resetDimensions(); // dimensions will be different

      } else if (!targetV.hasCachedData() && (targetVe.getOriginalVariable() == null)) {
        // this is the case where we defined the variable, but didnt set its data. we now set it with the first nested
        // dataset that has a variable with the same name
        targetVe.setOriginalVariable(v);
      }
    }

    // nested groups - check if target already has it
    for (Group srcNested : src.getGroups()) {
      Group nested = targetGroup.findGroup(srcNested.getShortName());
      if (null == nested) {
        nested = new Group(ds, targetGroup, srcNested.getShortName());
        targetGroup.addGroup(nested);
      }
      transferGroup(ds, targetDs, srcNested, nested, replaceCheck);
    }
  }

  /**
   * Copy attributes from src to target, skip ones that already exist (by name)
   * @param src copy from here
   * @param target copy to here
   */
  static public void transferVariableAttributes(Variable src, Variable target) {
    for (Attribute a : src.getAttributes()) {
      if (null == target.findAttribute(a.getShortName()))
        target.addAttribute(a);
    }
  }

  /**
   * Copy attributes from src to target, skip ones that already exist (by name)
   * @param src copy from here
   * @param target copy to here
   */
  static public void transferGroupAttributes(Group src, Group target) {
    for (Attribute a : src.getAttributes()) {
      if (null == target.findAttribute(a.getShortName()))
        target.addAttribute(a);
    }
  }

  /**
   * Find the Group in newFile that corresponds (by name) with oldGroup
   *
   * @param newFile look in this NetcdfFile
   * @param oldGroup corresponding (by name) with oldGroup
   * @return corresponding Group, or null if no match.
   */
  static public Group findGroup(NetcdfFile newFile, Group oldGroup) {
    List<Group> chain = new ArrayList<>(5);
    Group g = oldGroup;
    while ( g.getParentGroup() != null) { // skip the root
      chain.add(0, g); // put in front
      g = g.getParentGroup();
    }

    Group newg = newFile.getRootGroup();
    for (Group oldg : chain) {
      newg = newg.findGroup( oldg.getShortName());
      if (newg == null) return null;
    }
    return newg;
  }

  static private final String boundsDimName = "bounds_dim";
  static public Dimension getBoundsDimension(NetcdfFile ncfile) {
    Group g = ncfile.getRootGroup();
    Dimension d = g.findDimension(boundsDimName);
    if (d == null)
      d = ncfile.addDimension(g, new Dimension(boundsDimName, 2, true));
    return d;
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.iosp.noaa;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.iosp.AbstractIOServiceProvider;
import ucar.nc2.ncml.NcmlConstructor;
import ucar.nc2.util.CancelTask;
import ucar.unidata.io.RandomAccessFile;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Nomads IGRA files.
 * Can open all data by opening "igra-stations.txt", with data files in subdir "igra-por".
 * Can open single station data by opening <stationId>.dat with igra-stations.txt in same or parent directory.
 *  -	IGRA - Integrated Global Radiosonde Archive
 *  LOOK probably file leaks
 * @author caron
 * @see "http://www.ncdc.noaa.gov/oa/climate/igra/"
 * @see "ftp://ftp.ncdc.noaa.gov/pub/data/igra"
 * @since 3/3/11
 */
public class IgraPor extends AbstractIOServiceProvider {
  private static final String dataPatternRegexp =
          "(\\d{2})([ \\-\\d]{6})(.)([ \\-\\d]{5})(.)([ \\-\\d]{5})(.)([ \\-\\d]{5})([ \\-\\d]{5})([ \\-\\d]{5})$";

  private static final String dataHeaderPatternRegexp =
          "#(\\d{5})(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{4})([ \\d]{4})$";

  private static final String stnPatternRegexp =
          "([A-Z]{2})  (\\d{5})  (.{35}) ([ \\.\\-\\d]{6}) ([ \\.\\-\\d]{7}) ([ \\-\\d]{4}) (.)(.)(.)  ([ \\d]{4}) ([ \\d]{4})$";

  private static final Pattern dataPattern = Pattern.compile(dataPatternRegexp);
  private static final Pattern dataHeaderPattern = Pattern.compile(dataHeaderPatternRegexp);
  private static final Pattern stnPattern = Pattern.compile(stnPatternRegexp);

  private static final String STNID = "stnid";

  private static final String STN_FILE = "igra-stations.txt";
  private static final String DAT_EXT = ".dat";
  private static final String DAT_DIR = "igra-por";
  private static final String IDX_EXT = ".ncx";
  private static final String MAGIC_START_IDX = "IgraPorIndex";
  private static final int version = 1;

  @Override
  public boolean isValidFile(RandomAccessFile raf) throws IOException {
    String location = raf.getLocation();
    File file = new File(location);

    int pos = location.lastIndexOf(".");
    if (pos <= 0) return false;
    String base = location.substring(0, pos);
    String ext = location.substring(pos);

    // must be data file or station file or index file
    if (!ext.equals(DAT_EXT) && !ext.equals(IDX_EXT) && !file.getName().equals(STN_FILE))
      return false;

    if (ext.equals(IDX_EXT)) {
      // data, stn files must be in the same directory
      File datFile = new File(base + DAT_EXT);
      if (!datFile.exists())
        return false;
      File stnFile = getStnFile(location);
      if (stnFile == null || !stnFile.exists())
        return false;

      raf.seek(0);
      String test = raf.readString(MAGIC_START_IDX.length());
      return test.equals(MAGIC_START_IDX);

    } else if (ext.equals(DAT_EXT)) {
      File stnFile = getStnFile(location);
      return stnFile != null && isValidFile(raf, dataHeaderPattern);

    } else {
      // data directory must exist
      File dataDir = new File(file.getParentFile(), DAT_DIR);
      return dataDir.exists() && dataDir.isDirectory() && isValidFile(raf, stnPattern);
    }
  }

  // stn file must be in the same directory or one up

  private File getStnFile(String location) {
    File file = new File(location);
    File stnFile = new File(file.getParentFile(), STN_FILE);
    if (!stnFile.exists()) {
      if (file.getParentFile() == null) return null;
      stnFile = new File(file.getParentFile().getParentFile(), STN_FILE);
      if (!stnFile.exists()) return null;
    }
    return stnFile;
  }

  private boolean isValidFile(RandomAccessFile raf, Pattern p) throws IOException {
    raf.seek(0);
    String line;
    while (true) {
      line = raf.readLine();
      if (line == null) break;
      if (line.trim().length() == 0) continue;
      Matcher matcher = p.matcher(line);
      return matcher.matches();
    }
    return false;
  }

  @Override
  public String getFileTypeId() {
    return "IGRA-POR";
  }

  @Override
  public String getFileTypeDescription() {
    return "Integrated Global Radiosonde Archive";
  }

  @Override
  public String getFileTypeVersion() {
    return Integer.toString(version);
  }

  /////////////////////////////////////////////////////////////////////////
  private RandomAccessFile stnRaf, dataRaf;
  private File dataDir;
  //private HashMap<Long, StationIndex> map = new HashMap<Long, StationIndex>(10000);
  private int stn_fldno;
  private StructureDataRegexp.Vinfo stnVinfo, seriesVinfo, profileVinfo;
  private String stationId; // if a DAT file

  @Override
  public void open(RandomAccessFile raff, NetcdfFile ncfile, CancelTask cancelTask) throws IOException {
    super.open(raff, ncfile, cancelTask);
    int pos = location.lastIndexOf(".");
    String ext = location.substring(pos);

    File file = new File(location);
    File stnFile = getStnFile(location);
    if (stnFile == null)
      throw new FileNotFoundException("Station File does not exist="+location);

    if (ext.equals(IDX_EXT)) {
      stnRaf = RandomAccessFile.acquire(stnFile.getPath());

    } else if (ext.equals(DAT_EXT)) {
      stnRaf = RandomAccessFile.acquire(stnFile.getPath());
      dataRaf = raff;

      //extract the station id
      String name = file.getName();
      stationId = name.substring(0, name.length() - DAT_EXT.length());

    } else { // pointed to the station file
      stnRaf = raff;
      dataDir = new File(file.getParentFile(), DAT_DIR);
    }

    NcmlConstructor ncmlc = new NcmlConstructor();
    if (!ncmlc.populateFromResource("resources/nj22/iosp/igra-por.ncml", ncfile)) {
      throw new IllegalStateException(ncmlc.getErrlog().toString());
    }
    ncfile.finish();

    //dataVinfo = setVinfo(dataRaf, ncfile, dataPattern, "all_data");
    stnVinfo = setVinfo(stnRaf, ncfile, stnPattern, "station");
    seriesVinfo = setVinfo(stnRaf, ncfile, dataHeaderPattern, "station.time_series");
    profileVinfo = setVinfo(stnRaf, ncfile, dataPattern, "station.time_series.levels");

    StructureMembers.Member m = stnVinfo.sm.findMember(STNID);
    StructureDataRegexp.VinfoField f = (StructureDataRegexp.VinfoField) m.getDataObject();
    stn_fldno = f.fldno;

    /* make index file if needed
    File idxFile = new File(base + IDX_EXT);
    if (!idxFile.exists())
      makeIndex(stnVinfo, dataVinfo, idxFile);
    else
      readIndex(idxFile.getPath());  */
  }

  private StructureDataRegexp.Vinfo setVinfo(RandomAccessFile raff, NetcdfFile ncfile, Pattern p, String seqName) {
    Sequence seq = (Sequence) ncfile.findVariable(seqName);
    StructureMembers sm = seq.makeStructureMembers();
    StructureDataRegexp.Vinfo result = new StructureDataRegexp.Vinfo(raff, sm, p);
    seq.setSPobject(result);

    int fldno = 1;
    for (StructureMembers.Member m : sm.getMembers()) {
      StructureDataRegexp.VinfoField vf = new StructureDataRegexp.VinfoField(fldno++);
      Variable v = seq.findVariable(m.getName());
      Attribute att = v.findAttribute("iosp_scale");
      if (att != null) {
        vf.hasScale = true;
        vf.scale = att.getNumericValue().floatValue();
        //v.remove(att);
      }
      m.setDataObject(vf);
    }

    return result;
  }

  public void close() throws java.io.IOException {
    if (stnRaf != null) stnRaf.close();
    if (dataRaf != null) dataRaf.close();
    stnRaf = null;
    dataRaf = null;
  }

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

  @Override
  public Array readData(Variable v2, Section section) throws IOException, InvalidRangeException {
    StructureDataRegexp.Vinfo vinfo = (StructureDataRegexp.Vinfo) v2.getSPobject();
    if (stationId != null)
      return new ArraySequence(vinfo.sm, new SingleStationSeqIter(vinfo), vinfo.nelems);
    else
      return new ArraySequence(vinfo.sm, new StationSeqIter(vinfo), vinfo.nelems);
  }

  @Override
  public StructureDataIterator getStructureIterator(Structure s, int bufferSize) throws java.io.IOException {
    StructureDataRegexp.Vinfo vinfo = (StructureDataRegexp.Vinfo) s.getSPobject();
    if (stationId != null)
      return new SingleStationSeqIter(vinfo);
    else
      return new StationSeqIter(vinfo);
  }

  // when theres only one station

  private class SingleStationSeqIter implements StructureDataIterator {
    private StructureDataRegexp.Vinfo vinfo;
    private int recno = 0;

    SingleStationSeqIter(StructureDataRegexp.Vinfo vinfo) throws IOException {
      this.vinfo = vinfo;
      vinfo.rafile.seek(0);
    }

    @Override
    public StructureDataIterator reset() {
      recno = 0;
      return this;
    }

    @Override
    public boolean hasNext() throws IOException {
      return recno == 0;
    }

    @Override
    public StructureData next() throws IOException {
      Matcher matcher;
      while (true) {
        String line = vinfo.rafile.readLine();
        if (line == null) return null;
        if (line.startsWith("#")) continue;
        if (line.trim().length() == 0) continue;
        //System.out.printf("line %s%n", line);
        matcher = vinfo.p.matcher(line);
        if (matcher.matches()) {
          String stnid = matcher.group(stn_fldno).trim();
          if (stnid.equals(stationId)) break;
        }
      }
      recno++;
      return new StationData(vinfo.sm, matcher);
    }

    @Override
    public int getCurrentRecno() {
      return recno - 1;
    }

  }

  // sequence of stations

  private class StationSeqIter implements StructureDataIterator {
    private StructureDataRegexp.Vinfo vinfo;
    private long totalBytes;
    private int recno;
    private StructureData curr;

    StationSeqIter(StructureDataRegexp.Vinfo vinfo) throws IOException {
      this.vinfo = vinfo;
      totalBytes = (int) vinfo.rafile.length();
      vinfo.rafile.seek(0);
    }

    @Override
    public StructureDataIterator reset() {
      recno = 0;

      try {
        vinfo.rafile.seek(0);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return this;
    }

    @Override
    public boolean hasNext() throws IOException {
      boolean more = (vinfo.rafile.getFilePointer() < totalBytes); // && (recno < 10);
      if (!more) {
        vinfo.nelems = recno;
        //System.out.printf("nelems=%d%n", recno);
        return false;
      }
      curr = reallyNext();
      more = (curr != null);
      if (!more) {
        vinfo.nelems = recno;
        //System.out.printf("nelems=%d%n", recno);
        return false;
      }
      return more;
    }

    @Override
    public StructureData next() throws IOException {
      return curr;
    }

    private StructureData reallyNext() throws IOException {
      Matcher matcher;
      while (true) {
        String line = vinfo.rafile.readLine();
        if (line == null) return null;
        if (line.startsWith("#")) continue;
        if (line.trim().length() == 0) continue;
        //System.out.printf("line %s%n", line);
        matcher = vinfo.p.matcher(line);
        if (matcher.matches())
          break;
        System.out.printf("FAIL %s%n", line);
      }
      recno++;
      return new StationData(vinfo.sm, matcher);
    }

    @Override
    public void setBufferSize(int bytes) {
    }

    @Override
    public int getCurrentRecno() {
      return recno - 1;
    }

    @Override
    public void close() {
      // ignored
    }
  }

  private class StationData extends StructureDataRegexp {

    StationData(StructureMembers members, Matcher matcher) {
      super(members, matcher);
    }

    @Override
    // nested array sequence must be the stn_data
    public ArraySequence getArraySequence(StructureMembers.Member m) {
      String stnid = matcher.group(stn_fldno).trim();
      return new ArraySequence(seriesVinfo.sm, new TimeSeriesIter(stnid), -1);
    }
  }

  //////////////////////////////////////////////////////
  // sequence of time series for one station

  private class TimeSeriesIter implements StructureDataIterator {
    private int countRead = 0;
    private long totalBytes;
    private File file;
    private RandomAccessFile timeSeriesRaf = null;
    private boolean exists;

    TimeSeriesIter(String stnid) {
      if (dataRaf != null)
        exists = true;
      else {
        this.file = new File(dataDir, stnid + DAT_EXT);
        exists = file.exists();
      }
    }

    private void init() {
      if (!exists) return;
      try {
        if (dataRaf != null)
          this.timeSeriesRaf = dataRaf; // single station case - data file already open
        else
          this.timeSeriesRaf = RandomAccessFile.acquire(file.getPath());

        totalBytes = timeSeriesRaf.length();
        timeSeriesRaf.seek(0);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public StructureDataIterator reset() {
      if (!exists) return this;

      if (timeSeriesRaf == null) init();

      countRead = 0;
      try {
        timeSeriesRaf.seek(0);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return this;
    }

    @Override
    public boolean hasNext() throws IOException {
      if (!exists) return false;
      if (timeSeriesRaf == null) init();
      assert timeSeriesRaf != null;
      return (timeSeriesRaf.getFilePointer() < totalBytes); // && (recno < 10);   LOOK not perfect, eg trailing blanks
    }

    @Override
    public StructureData next() throws IOException {
      Matcher matcher;
      String line;
      while (true) {
        line = timeSeriesRaf.readLine();
        if (line == null) return null;  // only on EOF
        if (line.trim().length() == 0) continue;
        matcher = seriesVinfo.p.matcher(line);
        if (matcher.matches())
          break;
        System.out.printf("FAIL TimeSeriesIter <%s>%n", line);
      }
      countRead++;
      return new TimeSeriesData(matcher);
    }

    @Override
    public int getCurrentRecno() {
      return countRead - 1;
    }

    @Override
    public void close() {
      try {
        if (this.timeSeriesRaf != null && this.timeSeriesRaf != dataRaf) {
          timeSeriesRaf.close();
          timeSeriesRaf = null;
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    private class TimeSeriesData extends StructureDataRegexp {
      // Matcher matcher;          // matcher on the station ascii
      List<String> lines = new ArrayList<>(30);

      TimeSeriesData(Matcher matcher) throws IOException {
        super(seriesVinfo.sm, matcher);

        String line;
        long pos;
        while (true) {
          pos = timeSeriesRaf.getFilePointer();
          line = timeSeriesRaf.readLine();
          if (line == null) break;
          if (line.trim().length() == 0) continue;
          matcher = profileVinfo.p.matcher(line);
          if (matcher.matches())
            lines.add(line);
          else {
            timeSeriesRaf.seek(pos); // put the line back
            break;
          }
        }
      }

      @Override
      // nested array sequence must be the stn_data
      public ArraySequence getArraySequence(StructureMembers.Member m) {
        return new ArraySequence(profileVinfo.sm, new ProfileIter(), -1);
      }

      //////////////////////////////////////////////////////
      // sequence of levels for one profile = station-timeSeries

      private class ProfileIter implements StructureDataIterator {
        private int countRead;

        ProfileIter() {
          countRead = 0;
        }

        @Override
        public StructureDataIterator reset() {
          countRead = 0;
          return this;
        }

        @Override
        public boolean hasNext() throws IOException {
          return countRead < lines.size();
        }

        @Override
        public StructureData next() throws IOException {
          if (!hasNext()) return null;
          Matcher matcher = profileVinfo.p.matcher(lines.get(countRead));
          StructureData sd;
          if (matcher.matches())
            sd = new StructureDataRegexp(profileVinfo.sm, matcher);
          else
            throw new IllegalStateException("line = " + lines.get(countRead) + "pattern = " + profileVinfo.p);
          countRead++;
          return sd;
        }

        @Override
        public int getCurrentRecno() {
          return countRead - 1;
        }

      }
    }

  }

  ///////////////////////////////////////////
  /*
  private void readIndex(String indexFilename) throws IOException {
    FileInputStream fin = new FileInputStream(indexFilename);

    if (!NcStream.readAndTest(fin, MAGIC_START_IDX.getBytes("UTF-8")))
      throw new IllegalStateException("bad index file");
    int version = fin.read();
    if (version != 1)
      throw new IllegalStateException("Bad version = " + version);

    int count = NcStream.readVInt(fin);

    for (int i = 0; i < count; i++) {
      int size = NcStream.readVInt(fin);
      byte[] pb = new byte[size];
      NcStream.readFully(fin, pb);
      StationIndex si = decodeStationIndex(pb);
      map.put(si.stnId, si);
    }
    fin.close();

    System.out.println(" read index map size=" + map.values().size());
  }

  private void makeIndex(StructureDataRegexp.Vinfo stnInfo, StructureDataRegexp.Vinfo dataInfo, File indexFile) throws IOException {
    // get map of Stations
    StructureMembers.Member m = stnInfo.sm.findMember(STNID);
    StructureDataRegexp.VinfoField f = (StructureDataRegexp.VinfoField) m.getDataObject();
    int stnCount = 0;

    // read through entire file LOOK: could use SeqIter
    stnInfo.rafile.seek(0);
    while (true) {
      long stnPos = stnInfo.rafile.getFilePointer();
      String line = stnInfo.rafile.readLine();
      if (line == null) break;

      Matcher matcher = stnInfo.p.matcher(line);
      if (!matcher.matches()) {
        System.out.printf("FAIL %s%n", line);
        continue;
      }
      String svalue = matcher.group(f.fldno);
      Long id = Long.parseLong(svalue.trim());

      StationIndex s = new StationIndex();
      s.stnId = id;
      s.stnPos = stnPos;
      map.put(id, s);
      stnCount++;
    }

    // assumes that the stn data is in order by stnId
    m = dataInfo.sm.findMember(STNID);
    f = (StructureDataRegexp.VinfoField) m.getDataObject();
    StationIndex currStn = null;
    int totalCount = 0;

    // read through entire data file
    dataInfo.rafile.seek(0);
    while (true) {
      long dataPos = dataInfo.rafile.getFilePointer();
      String line = dataInfo.rafile.readLine();
      if (line == null) break;

      Matcher matcher = dataInfo.p.matcher(line);
      if (!matcher.matches()) {
        System.out.printf("FAIL %s%n", line);
        continue;
      }

      String svalue = matcher.group(f.fldno).trim();
      Long id = Long.parseLong(svalue);

      if ((currStn == null) || (currStn.stnId != id)) {
        StationIndex s = map.get(id);
        if (s == null)
          System.out.printf("Cant find %d%n", id);
        else if (s.dataCount != 0)
          System.out.printf("Not in order %d at pos %d %n", id, dataPos);
        else {
          s.dataPos = dataPos;
          totalCount++;
        }
        currStn = s;
      }
      currStn.dataCount++;
    }
    //System.out.printf("ok stns=%s data=%d%n", stnCount, totalCount);

    //////////////////////////////
    // write the index file
    FileOutputStream fout = new FileOutputStream(indexFile); // LOOK need DiskCache for non-writeable directories
    long size = 0;

    //// header message
    fout.write(MAGIC_START_IDX.getBytes("UTF-8"));
    fout.write(version);
    size += NcStream.writeVInt(fout, stnCount);

    /* byte[] pb = encodeStationListProto( map.values());
   size += NcStream.writeVInt(fout, pb.length);
   size += pb.length;
   fout.write(pb);

    for (StationIndex s : map.values()) {
      byte[] pb = s.encodeStationProto();
      size += NcStream.writeVInt(fout, pb.length);
      size += pb.length;
      fout.write(pb);
    }
    fout.close();

    //System.out.println(" index size=" + size);
  }

  private StationIndex decodeStationIndex(byte[] data) throws InvalidProtocolBufferException {
    ucar.nc2.iosp.noaa.GhcnmProto.StationIndex proto = GhcnmProto.StationIndex.parseFrom(data);
    return new StationIndex(proto);
  }

  private class StationIndex {
    long stnId;
    long stnPos; // file pos in inv file
    long dataPos; // file pos of first data line in the data file
    int dataCount; // number of data records

    StationIndex() {
    }

    StationIndex(ucar.nc2.iosp.noaa.GhcnmProto.StationIndex proto) {
      this.stnId = proto.getStnid();
      this.stnPos = proto.getStnPos();
      this.dataPos = proto.getDataPos();
      this.dataCount = proto.getDataCount();
    }

    private byte[] encodeStationProto() {
      GhcnmProto.StationIndex.Builder builder = GhcnmProto.StationIndex.newBuilder();
      builder.setStnid(stnId);
      builder.setStnPos(stnPos);
      builder.setDataPos(dataPos);
      builder.setDataCount(dataCount);
      ucar.nc2.iosp.noaa.GhcnmProto.StationIndex proto = builder.build();
      return proto.toByteArray();
    }
  } */

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package thredds.server.ncss.params;

import thredds.server.ncss.validation.NcssGridRequestConstraint;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft2.coverage.CoverageCoordSys;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.time.Calendar;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateFormatter;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.ProjectionRect;

/**
 * Parameters specific to ncss grid
 *
 * @author caron
 * @since 4/29/2015
 */
@NcssGridRequestConstraint
public class NcssGridParamsBean extends NcssParamsBean {

  //// projection rectangle
  private Double minx;
  private Double maxx;
  private Double miny;
  private Double maxy;

  private boolean addLatLon;
  private Integer horizStride = 1;

  private Integer timeStride = 1;
  private Double vertCoord;
  private Double ensCoord;

  private String runtime;
  private String timeOffset;

  public Double getMinx() {
    return minx;
  }

  public void setMinx(Double minx) {
    this.minx = minx;
  }

  public Double getMaxx() {
    return maxx;
  }

  public void setMaxx(Double maxx) {
    this.maxx = maxx;
  }

  public Double getMiny() {
    return miny;
  }

  public void setMiny(Double miny) {
    this.miny = miny;
  }

  public Double getMaxy() {
    return maxy;
  }

  public void setMaxy(Double maxy) {
    this.maxy = maxy;
  }

  public boolean isAddLatLon() {
    return addLatLon;
  }

  public void setAddLatLon(boolean addLatLon) {
    this.addLatLon = addLatLon;
  }

  public Integer getHorizStride() {
    return horizStride;
  }

  public void setHorizStride(Integer horizStride) {
    this.horizStride = horizStride;
  }

  public Integer getTimeStride() {
    return timeStride;
  }

  public void setTimeStride(Integer timeStride) {
    this.timeStride = timeStride;
  }

  public boolean hasProjectionBB() { // need to validate
    return minx != null && miny != null && maxx != null && maxy != null;
  }

  public ProjectionRect getProjectionBB() {
    return new ProjectionRect(minx, miny, maxx, maxy);
  }

  public Double getVertCoord() {
    return vertCoord;
  }

  public void setVertCoord(Double vertCoord) {
    this.vertCoord = vertCoord;
  }

  public Double getEnsCoord() {
    return ensCoord;
  }

  public void setEnsCoord(Double ensCoord) {
    this.ensCoord = ensCoord;
  }

  public String getRuntime() {
    return runtime;
  }

  public void setRuntime(String runtime) {
    this.runtime = runtime;
  }

  public String getTimeOffset() {
    return timeOffset;
  }

  public void setTimeOffset(String timeOffset) {
    this.timeOffset = timeOffset;
  }

  ////////////////////////////
  protected Double timeOffsetVal;
  protected boolean firstTimeOffset;

  public void setTimeOffsetVal(Double timeOffsetVal) {
    this.timeOffsetVal = timeOffsetVal;
  }

  public void setFirstTimeOffset(boolean firstTimeOffset) {
    this.firstTimeOffset = firstTimeOffset;
  }

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

  protected CalendarDate runtimeDate;
  protected boolean latestRuntime;
  protected boolean allRuntime;

  public void setLatestRuntime(boolean b) {
    this.latestRuntime = b;
  }

  public void setAllRuntime(boolean b) {
    this.allRuntime = b;
  }

  public void setRuntimeDate(CalendarDate runtimeDate) {
    this.runtimeDate = runtimeDate;
  }

  public CalendarDate getRuntimeDate(Calendar cal) {
    if (runtimeDate == null) return null;
    if (cal.equals(Calendar.getDefault())) return runtimeDate;

    // otherwise must reparse
    if (getTime().equalsIgnoreCase("present")) {
      return CalendarDate.present(cal);
    }

    return CalendarDateFormatter.isoStringToCalendarDate(cal, getRuntime());
  }

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

  public SubsetParams makeSubset(CoverageCollection gcd) {
    Calendar cal = gcd.getCalendar();
    boolean isFmrc = gcd.getCoverageType() == FeatureType.FMRC;

    SubsetParams subset = new SubsetParams();

    // vars
    subset.set(SubsetParams.variables, var);

    // other coords
    if (vertCoord != null)
      subset.set(SubsetParams.vertCoord, vertCoord);

    if (ensCoord != null)
      subset.set(SubsetParams.ensCoord, ensCoord);

    // horiz subset
    if (hasProjectionBB())
      subset.set(SubsetParams.projBB, getProjectionBB());
    else if (hasLatLonBB())
      subset.set(SubsetParams.latlonBB, getLatLonBoundingBox());
    if (horizStride != null && horizStride != 1)
      subset.set(SubsetParams.horizStride, horizStride);
    if (hasLatLonPoint())
      subset.set(SubsetParams.latlonPoint, new LatLonPointImpl(getLatitude(), getLongitude()));

    if (isFmrc) { // 2D Time subsetting

      // runtime
      CalendarDate rundate = getRuntimeDate(cal);
      if (rundate != null)
        subset.set(SubsetParams.runtime, rundate);
      else if (allRuntime)
        subset.set(SubsetParams.runtimeAll, true);
      else
        subset.set(SubsetParams.runtimeLatest, true); // default

      // timeOffset
      if (timeOffsetVal != null)
        subset.set(SubsetParams.timeOffset, timeOffsetVal);
      else if (firstTimeOffset)
        subset.set(SubsetParams.timeOffsetFirst, true);

      else { // if no timeOffset, will allow some time values
        CalendarDate date = getRequestedDate(cal);
        CalendarDateRange dateRange = getCalendarDateRange(cal);

        if (isPresentTime())
          subset.setTimePresent();

        else if (isAllTimes() && !allRuntime) {
          subset.set(SubsetParams.timeAll, true);
          if (timeStride != null && timeStride != 1)
            subset.set(SubsetParams.timeStride, timeStride);

        } else if (date != null) {              // for allRuntimes, only a date is allowed
          subset.set(SubsetParams.time, date);

        } else if (dateRange != null  && !allRuntime) {
          subset.set(SubsetParams.timeRange, dateRange);
          if (timeStride != null && timeStride != 1)
            subset.set(SubsetParams.timeStride, timeStride);
        }
      }

    } else { // not an FMRC

      // time
      CalendarDate date = getRequestedDate(cal);
      CalendarDateRange dateRange = getCalendarDateRange(cal);
      if (isPresentTime())
        subset.setTimePresent();
      else if (isAllTimes()) {
        subset.set(SubsetParams.timeAll, true);
        if (timeStride != null && timeStride != 1)
          subset.set(SubsetParams.timeStride, timeStride);

      } else if (date != null) {
        subset.set(SubsetParams.time, date);

      } else if (dateRange != null) {
        subset.set(SubsetParams.timeRange, dateRange);
        if (timeStride != null && timeStride != 1)
          subset.set(SubsetParams.timeStride, timeStride);

      } else {
        subset.set(SubsetParams.timePresent, true);
      }
    }

    return subset;
  }

}


/* This file is Copyright (c) 2005 Robert Alten Simons (info@cohort.com).
 * See the MIT/X-like license in LICENSE.txt.
 * For more information visit www.cohort.com or contact info@cohort.com.
 */
package ucar.nc2.ogc.erddap.util;

/**
 * A class with static String methods that add to native String methods.
 * All are static methods.
 */
public class ErddapString2 {
    /**
     * ERROR is a constant so that it will be consistent, so that one can
     * search for it in output files.
     * This is NOT final, so EDStatic can change it.
     * This is the original definition, referenced by many other classes.
     */
    public static String ERROR = "ERROR";

    /**
     * Finds the first instance of c at or after fromIndex (0.. ) in cArray.
     *
     * @param cArray
     * @param c the char you want to find
     * @param fromIndex the index number of the position to start the search
     * @return The first instance of c. If not found, it returns -1.
     */
    public static int indexOf(char[] cArray, char c, int fromIndex) {
        int cArrayLength = cArray.length;
        for (int index = Math.max(fromIndex, 0); index < cArrayLength; index++) {
            if (cArray[index] == c)
                return index;
        }
        return -1;
    }

    /**
     * This includes hiASCII/ISO Latin 1/ISO 8859-1, but not extensive unicode characters.
     * Letters are A..Z, a..z, and #192..#255 (except #215 and #247).
     * For unicode characters, see Java Lang Spec pg 14.
     *
     * @param c a char
     * @return true if c is a letter
     */
    public static boolean isLetter(int c) {
        //return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'))
        //|| ((c >= '\u00c0') && (c <= '\u00FF') && (c != '\u00d7')
        //&& (c != '\u00f7')));
        if (c <  'A') return false;
        if (c <= 'Z') return true;
        if (c <  'a') return false;
        if (c <= 'z') return true;
        if (c <  '\u00c0') return false;
        if (c == '\u00d7') return false;
        if (c <= '\u00FF') return true;
        return false;
    }

    /**
     * 0..9.
     * Non-Latin numeric characters are not included (see Java Lang Spec pg 14).
     *
     * @param c a char
     * @return true if c is a digit
     */
    public static boolean isDigit(int c) {
        return ((c >= '0') && (c <= '9'));
    }

    /**
     * Returns a string where all occurences of <TT>oldCh</TT> have
     *   been replaced with <TT>newCh</TT>.
     * This doesn't throw exceptions if bad values.
     */
    public static String replaceAll(String s, char oldCh, char newCh) {
        int po = s.indexOf(oldCh);
        if (po < 0)
            return s;

        StringBuilder buffer = new StringBuilder(s);
        while (po >= 0) {
            buffer.setCharAt(po, newCh);
            po = s.indexOf(oldCh, po + 1);
        }
        return buffer.toString();
    }

    /**
     * Convert a string to an int.
     * Leading or trailing spaces are automatically removed.
     * This accepts hexadecimal integers starting with "0x".
     * Leading 0's (e.g., 0012) are ignored; number is treated as decimal (not octal as Java would).
     * Floating point numbers are rounded.
     * This won't throw an exception if the number isn't formatted right.
     * To make a string from an int, use ""+i, Integer.toHexString, or Integer.toString(i,radix).
     *
     * @param s is the String representation of a number.
     * @return the int value from the String
     *    (or Integer.MAX_VALUE if error).
     */
    public static int parseInt(String s) {
        //*** XML.decodeEntities relies on leading 0's being ignored
        //    and number treated as decimal (not octal)

        //quickly reject most non-numbers
        //This is a huge speed improvement when parsing ASCII data files
        //  because Java is very slow at filling in the stack trace when an exception is thrown.
        if (s == null)
            return Integer.MAX_VALUE;
        s = s.trim();
        if (s.length() == 0)
            return Integer.MAX_VALUE;
        char ch = s.charAt(0);
        if ((ch < '0' || ch > '9') && ch != '-' && ch != '+' && ch != '.')
            return Integer.MAX_VALUE;

        //try to parse hex or regular int
        try {
            if (s.startsWith("0x"))
                return Integer.parseInt(s.substring(2), 16);
            return Integer.parseInt(s);
        } catch (Exception e) {
            //falls through
        }

        //round from double?
        try {
            //2011-02-09 Bob Simons added to avoid Java hang bug.
            //But now, latest version of Java is fixed.
            //if (isDoubleTrouble(s)) return 0;

            return ErddapMath2.roundToInt(Double.parseDouble(s));
        } catch (Exception e) {
            return Integer.MAX_VALUE;
        }
    }

    /**
     * Convert a string to a double.
     * Leading or trailing spaces are automatically removed.
     * This accepts hexadecimal integers starting with "0x".
     * Whole number starting with '0' (e.g., 012) is treated as decimal (not octal as Java would).
     * This won't throw an exception if the number isn't formatted right.
     *
     * @param s is the String representation of a number.
     * @return the double value from the String (a finite value,
     *   Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY,
     *   or Double.NaN if error).
     */
    public static double parseDouble(String s) {
        //quickly reject most non-numbers
        //This is a huge speed improvement when parsing ASCII data files
        //  because Java is very slow at filling in the stack trace when an exception is thrown.
        if (s == null)
            return Double.NaN;
        s = s.trim();
        if (s.length() == 0)
            return Double.NaN;
        char ch = s.charAt(0);
        if ((ch < '0' || ch > '9') && ch != '-' && ch != '+' && ch != '.')
            return Double.NaN;

        try {
            if (s.startsWith("0x"))
                return Integer.parseInt(s.substring(2), 16);

            //2011-02-09 Bob Simons added to avoid Java hang bug.
            //But now, latest version of Java is fixed.
            //if (isDoubleTrouble(s)) return 0;

            return Double.parseDouble(s);
        } catch (Exception e) {
            return Double.NaN;
        }
    }

    /**
     * This converts String representation of a long.
     * Leading or trailing spaces are automatically removed.
     * This *doesn't* round. So floating point values lead to Long.MAX_VALUE.
     *
     * @param s a valid String representation of a long value
     * @return a long (or Long.MAX_VALUE if trouble).
     */
    public static long parseLong(String s) {
        //quickly reject most non-numbers
        //This is a huge speed improvement when parsing ASCII data files
        //  because Java is very slow at filling in the stack trace when an exception is thrown.
        if (s == null)
            return Long.MAX_VALUE;
        s = s.trim();
        if (s.length() == 0)
            return Long.MAX_VALUE;
        char ch = s.charAt(0);
        if ((ch < '0' || ch > '9') && ch != '-' && ch != '+')
            return Long.MAX_VALUE;

        try {
            if (s.startsWith("0x"))
                return Long.parseLong(s.substring(2), 16);
            return Long.parseLong(s);
        } catch (Exception e) {
            return Long.MAX_VALUE;
        }
    }

    /**
     * This converts a double to a rational number (m * 10^t).
     * This is similar to Math2.mantissa and Math2.intExponent, but works via string manipulation
     * to avoid roundoff problems (e.g., with 6.6260755e-24).
     *
     * @param d
     * @return int[2]: [0]=m, [1]=t.
     *  (or {0, 0} if d=0, or {1, Integer.MAX_VALUE} if !finite(d))
     */
    public static int[] toRational(double d) {
        if (d == 0)
            return new int[]{0, 0};

        if (!ErddapMath2.isFinite(d))
            return new int[]{1, Integer.MAX_VALUE};

        String s = "" + d; //-12.0 or 6.6260755E-24
        //String2.log("\nd=" + d + "\ns=" + s);
        int ten = 0;

        //remove the e
        int epo = s.indexOf('E');
        if (epo > 0) {
            ten = parseInt(s.substring(epo + 1));
            s = s.substring(0, epo);
            //String2.log("remove E s=" + s + " ten=" + ten);
        }

        //remove .0; remove decimal point
        if (s.endsWith(".0"))
            s = s.substring(0, s.length() - 2);
        int dpo = s.indexOf('.');
        if (dpo > 0) {
            ten -= s.length() - dpo - 1;
            s = s.substring(0, dpo) + s.substring(dpo + 1);
            //String2.log("remove . s=" + s + " ten=" + ten);
        }

        //convert s to long
        //need to lose some precision?
        long tl = parseLong(s);
        //String2.log("tl=" + tl + " s=" + s);
        while (Math.abs(tl) > 1000000000) {
            tl = Math.round(tl / 10.0);
            ten++;
            //String2.log("tl=" + tl + " ten=" + ten);
        }
        //remove trailing 0's
        while (tl != 0 && tl / 10 == tl / 10.0) {
            tl /= 10;
            ten++;
            //String2.log("remove 0 tl=" + tl + " ten=" + ten);
        }
        //add up to 3 0's?
        if (tl < 100000 && ten >= 1 && ten <= 3) {
            while (ten > 0) {
                tl *= 10;
                ten--;
            }
        }

        return new int[]{(int)tl, ten}; //safe since large values handled above
    }
}


package ucar.nc2.ft2.simpgeometry;

import java.io.IOException;

import ucar.ma2.Array;
import ucar.nc2.Variable;

/**
 * A Simple Geometry Index Finder can go find the beginning and end indicies of
 * a simple geometry X and Y within a variable. But first the Index Finder needs a few variables to find it in.
 * If the indexer is tasked to find a Simple Geometry close to one before, it will find it faster.
 *
 * @author wchen@usgs.gov
 *
 */
public class SimpleGeometryIndexFinder {

	private final int INVALID_INDEX = -10;	// default invalid index before any lookups have been done
	private Array nodeCount = null;
	private int pastIndex;
	private int previousEnd;
	private int previousBegin;

	private int getNodeCount(int index) {
		return nodeCount.getInt(index);
	}

	/**
	 * Gets the beginning index of a geometry's points given the index of the geometry within the array.
	 *
	 * @param index
	 * @return beginning of the range
	 */
	public int getBeginning(int index) {

		//Test if the last end is the new beginning
		if(index == (pastIndex + 1 ))
		{
			return previousEnd + 1;
		}

		// Otherwise, find it!
		int newBeginning = 0;
		for(int i = 0; i < index; i++) {
			newBeginning += getNodeCount(i);
		}

		pastIndex = index;
		previousBegin = newBeginning;
		return newBeginning;
	}

	/**
	 * Gets the ending index of a geometry's points given the index of the geometry within the array.
	 *
	 * @param index of the geometry within the array
	 * @return end of the range
	 */
	public int getEnd(int index) {

		// Test if the last beginning is the new end
		if(index == (pastIndex - 1))
		{
			return previousBegin - 1;
		}

		// Otherwise find it!
		int new_end = 0;
		for(int i = 0; i < index + 1; i++) {
			new_end += getNodeCount(i);
		}

		pastIndex = index;
		previousEnd = new_end;
		return new_end - 1;
	}

	/**
	 * Create a new indexer, the indexer must be given a variable to look through.
	 *
	 * @param node_count Amount of nodes per geometry
	 */
	public SimpleGeometryIndexFinder(Variable node_count) {

		try {
			this.nodeCount = node_count.read();
		} catch (IOException e) {

			this.nodeCount = null;
			e.printStackTrace();
		}

		pastIndex = INVALID_INDEX;
		previousEnd = INVALID_INDEX;
		previousBegin = INVALID_INDEX;
	}
}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.servlet;

import java.io.*;
import java.net.MalformedURLException;
import java.util.*;

import opendap.dap.*;
import opendap.servlet.ascii.asciiFactory;
import opendap.servlet.ascii.toASCII;
import opendap.util.Debug;
import opendap.dap.parsers.ParseException;

/**
 * Default handler for OPeNDAP ascii requests. This class is used
 * by AbstractServlet. This code exists as a seperate class in order to alleviate
 * code bloat in the AbstractServlet class. As such, it contains virtually no
 * state, just behaviors.
 *
 * @author Nathan David Potter
 */

public class GetAsciiHandler {

    private static final boolean _Debug = false;

    /**
     * ************************************************************************
     * Default handler for OPeNDAP ascii requests. Returns OPeNDAP DAP2 data in
     * comma delimited ascii columns for ingestion into some not so
     * OPeNDAP enabled application such as MS-Excel. Accepts constraint
     * expressions in exactly the same way as the regular OPeNDAP dataserver.
     *
     * @param rs
     * @throws opendap.dap.DAP2Exception
     * @throws ParseException
     */
    public void sendASCII(ReqState rs,
                          String dataSet)
            throws DAP2Exception, ParseException {
        if (Debug.isSet("showResponse"))
            System.out.println("Sending OPeNDAP ASCII Data For: " + dataSet +
                    "    CE: '" + rs.getConstraintExpression() + "'");

        String requestURL, ce;
        DataDDS dds;

        if (rs.getConstraintExpression() == null) {
            ce = "";
        } else {
            ce = "?" + rs.getConstraintExpression();
        }

        int suffixIndex = rs.getRequestURL().toString().lastIndexOf(".");

        requestURL = rs.getRequestURL().substring(0, suffixIndex);

        if (Debug.isSet("showResponse")) {
            System.out.println("New Request URL Resource: '" + requestURL + "'");
            System.out.println("New Request Constraint Expression: '" + ce + "'");
        }

        if (_Debug) System.out.println("Making connection to .dods service...");

        try (DConnect2 url = new DConnect2(requestURL, true)) {

            if (_Debug) System.out.println("Requesting data...");
            dds = url.getData(ce, null, new asciiFactory());

            if (_Debug) System.out.println(" ASC DDS: ");
            if (_Debug) dds.print(System.out);

            PrintWriter pw = new PrintWriter(new OutputStreamWriter(rs.getResponse().getOutputStream(), Util.UTF8));
            PrintWriter pwDebug = new PrintWriter(new OutputStreamWriter(System.out,Util.UTF8));

            if(dds!=null){
                dds.print(pw);
                pw.println("---------------------------------------------");

                String s = "";
                Enumeration e = dds.getVariables();

                while (e.hasMoreElements()) {
                    BaseType bt = (BaseType) e.nextElement();
                    if (_Debug) ((toASCII) bt).toASCII(pwDebug, true, null, true);
                    //bt.toASCII(pw,addName,getNAme(),true);
                    ((toASCII) bt).toASCII(pw, true, null, true);
                }
            }
            else {

                String betterURL = rs.getRequestURL().substring(0, rs.getRequestURL().lastIndexOf(".")) +
                        ".dods?"+ rs.getConstraintExpression();

                pw.println("-- ASCII RESPONSE HANDLER PROBLEM --");
                pw.println("");
                pw.println("The ASCII response handler was unable to obtain requested data set.");
                pw.println("");
                pw.println("Because this handler calls it's own OPeNDAP server to get the requested");
                pw.println("data the source error is obscured.");
                pw.println("");
                pw.println("To get a better idea of what is going wrong, try requesting the URL:");
                pw.println("");
                pw.println("    "+betterURL);
                pw.println("");
                pw.println("And then look carefully at the returned document. Note that if you");
                pw.println("are using a browser to access the URL the returned document will");
                pw.println("more than likely be treated as a download and written to your");
                pw.println("local disk. It should be a file with the extension \".dods\"");
                pw.println("");
                pw.println("Locate it, open it with a text editor, and find your");
                pw.println("way to happiness and inner peace.");
                pw.println("");
            }

            //pw.println("</pre>");
            pw.flush();
            if (_Debug) pwDebug.flush();

        }
        catch (FileNotFoundException fnfe) {
            System.out.println("OUCH! FileNotFoundException: " + fnfe.getMessage());
            fnfe.printStackTrace(System.out);
        }
        catch (MalformedURLException mue) {
            System.out.println("OUCH! MalformedURLException: " + mue.getMessage());
            mue.printStackTrace(System.out);
        }
        catch (IOException ioe) {
            System.out.println("OUCH! IOException: " + ioe.getMessage());
            ioe.printStackTrace(System.out);
        }
        catch (Throwable t) {
            System.out.println("OUCH! Throwable: " + t.getMessage());
            t.printStackTrace(System.out);
        }

        if (_Debug) System.out.println(" GetAsciiHandler done");
    }
    /***************************************************************************/

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.grib.collection;

import jdk.nashorn.internal.ir.annotations.Immutable;
import thredds.featurecollection.FeatureCollectionConfig;
import thredds.inventory.CollectionManager;
import thredds.inventory.CollectionUpdateType;
import thredds.inventory.MCollection;
import thredds.inventory.MFile;
import thredds.inventory.partition.PartitionManager;
import thredds.inventory.partition.PartitionManagerFromIndexList;
import ucar.nc2.grib.coord.Coordinate;
import ucar.nc2.grib.coord.CoordinateRuntime;
import ucar.nc2.grib.coord.CoordinateTime2D;
import ucar.nc2.grib.coord.CoordinateTimeAbstract;
import ucar.nc2.grib.GribIndex;
import ucar.nc2.grib.GribIndexCache;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.util.CloseableIterator;
import ucar.unidata.util.StringUtil2;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Superclass to build indexes for collections of Grib files.
 *
 * @author caron
 * @since 2/19/14
 */
abstract class GribCollectionBuilder {

  protected final MCollection dcm;
  protected final org.slf4j.Logger logger;
  protected final boolean isGrib1;
  protected GribCollectionImmutable.Type type;

  protected final String name;            // collection name
  protected final File directory;         // top directory

  protected abstract List<? extends Group> makeGroups(List<MFile> allFiles, boolean singleRuntime, Formatter errlog) throws IOException;

  protected abstract boolean writeIndex(String name, String indexFilepath, CoordinateRuntime masterRuntime,
                                        List<? extends Group> groups, List<MFile> files, CalendarDateRange dateRange) throws IOException;

  GribCollectionBuilder(boolean isGrib1, String name, MCollection dcm, org.slf4j.Logger logger) {
    this.dcm = dcm;
    this.logger = logger;
    this.isGrib1 = isGrib1;

    this.name = StringUtil2.replace(name, ' ', "_");
    this.directory = new File(dcm.getRoot());
  }

  boolean updateNeeded(CollectionUpdateType ff) throws IOException {
    if (ff == CollectionUpdateType.never) return false;
    if (ff == CollectionUpdateType.always) return true;

    File collectionIndexFile = GribIndexCache.getExistingFileOrCache(dcm.getIndexFilename(GribCdmIndex.NCX_SUFFIX));
    if (collectionIndexFile == null) return true;

    if (ff == CollectionUpdateType.nocheck) return false;

    return needsUpdate(ff, collectionIndexFile);
  }

  private boolean needsUpdate(CollectionUpdateType ff, File collectionIndexFile) throws IOException {
    long collectionLastModified = collectionIndexFile.lastModified();
    Set<String> newFileSet = new HashSet<>();

    CollectionManager.ChangeChecker cc = GribIndex.getChangeChecker();
    try (CloseableIterator<MFile> iter = dcm.getFileIterator()) {
      while (iter != null && iter.hasNext()) {
        MFile memberOfCollection = iter.next();
        if (cc.hasChangedSince(memberOfCollection, collectionLastModified)) return true;   // checks both data and gbx9 file
        newFileSet.add(memberOfCollection.getPath());
      }
    }
    if (ff == CollectionUpdateType.testIndexOnly) return false;

    // now see if any files were deleted, by reading the index and comparing to the files there
    GribCdmIndex reader = new GribCdmIndex(logger);
    List<MFile> oldFiles = new ArrayList<>();
    reader.readMFiles(collectionIndexFile.toPath(), oldFiles);
    Set<String> oldFileSet = new HashSet<>();
    for (MFile oldFile : oldFiles) {
      if (!newFileSet.contains(oldFile.getPath()))
        return true;              // got deleted - must recreate the index
      oldFileSet.add(oldFile.getPath());
    }

    // now see if any files were added
    for (String newFilename : newFileSet) {
      if (!oldFileSet.contains(newFilename))
        return true;              // got added - must recreate the index
    }

    return false;
  }

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

   // Throw exception if failure
   boolean createIndex(FeatureCollectionConfig.PartitionType ptype, Formatter errlog) throws IOException {
    if (ptype == FeatureCollectionConfig.PartitionType.all)
      return createAllRuntimeCollections(errlog);
    else
      return createMultipleRuntimeCollections(errlog);
  }

   // Throw exception if failure
  private boolean createMultipleRuntimeCollections(Formatter errlog) throws IOException {
    long start = System.currentTimeMillis();

    List<MFile> files = new ArrayList<>();
    List<? extends Group> groups = makeGroups(files, false, errlog);
    List<MFile> allFiles = Collections.unmodifiableList(files);
    if (allFiles.size() == 0) {
      throw new IllegalStateException("No files in this collection =" + name + " topdir=" + dcm.getRoot());
    }
    if (groups.size() == 0) {
      throw new IllegalStateException("No records in this collection =" + name + " topdir=" + dcm.getRoot());
    }

    // Create the master runtimes, classify the result
    CalendarDateRange calendarDateRangeAll = null;
    //boolean allTimesAreOne = true;
    boolean allTimesAreUnique = true;
    Set<Long> allRuntimes = new HashSet<>();
    for (Group g : groups) {
      allRuntimes.addAll(g.getCoordinateRuntimes());
      for (Coordinate coord : g.getCoordinates()) {
        if (coord instanceof CoordinateTime2D) {
          CoordinateTime2D coord2D = (CoordinateTime2D) coord;
          if (allTimesAreUnique) {
            allTimesAreUnique = coord2D.hasUniqueTimes();
          }
        }
        if (coord instanceof CoordinateTimeAbstract) {
          CalendarDateRange calendarDateRange = ((CoordinateTimeAbstract) coord).makeCalendarDateRange(null);
          if (calendarDateRangeAll == null) calendarDateRangeAll = calendarDateRange;
          else calendarDateRangeAll = calendarDateRangeAll.extend(calendarDateRange);
        }
      }
    }
    List<Long> sortedList = new ArrayList<>(allRuntimes);
    Collections.sort(sortedList);
    if (sortedList.size() == 0)
      throw new IllegalArgumentException("No runtimes in this collection ="+name);

    else if (sortedList.size() == 1)
      this.type = GribCollectionImmutable.Type.SRC;
    else if (allTimesAreUnique)
      this.type =  GribCollectionImmutable.Type.MRUTC;
    else
      this.type =  GribCollectionImmutable.Type.MRC;

    CoordinateRuntime masterRuntimes = new CoordinateRuntime(sortedList, null);
    MFile indexFileForRuntime = GribCollectionMutable.makeIndexMFile(this.name, directory);
    boolean ok = writeIndex(this.name, indexFileForRuntime.getPath(), masterRuntimes, groups, allFiles, calendarDateRangeAll);

    /* if (this.type ==  GribCollectionImmutable.Type.MRC) {
      GribBestDatasetBuilder.makeDatasetBest();
    } */

    long took = System.currentTimeMillis() - start;
    logger.debug("That took {} msecs", took);
    return ok;
  }

  // PartitionType = all; not currently used but leave it here in case it needs to be revived
  // creates seperate collection and index for each runtime.
  private boolean createAllRuntimeCollections(Formatter errlog) throws IOException {
    long start = System.currentTimeMillis();
    this.type =  GribCollectionImmutable.Type.SRC;
    boolean ok = true;

    List<MFile> files = new ArrayList<>();
    List<? extends Group> groups = makeGroups(files, true, errlog);
    List<MFile> allFiles = Collections.unmodifiableList(files);

    // gather into collections with a single runtime
    Map<Long, List<Group>> runGroups = new HashMap<>();
    for (Group g : groups) {
      List<Group> runGroup = runGroups
          .computeIfAbsent(g.getRuntime().getMillis(), k -> new ArrayList<>());
      runGroup.add(g);
    }

    // write each rungroup separately
    boolean multipleRuntimes = runGroups.values().size() > 1;
    List<MFile> partitions = new ArrayList<>();
    for (List<Group> runGroupList : runGroups.values()) {
      Group g = runGroupList.get(0);
      // if multiple Runtimes, we will write a partition. otherwise, we need to use the standard name (without runtime) so we know the filename from the collection
      String gcname = multipleRuntimes ? GribCollectionMutable.makeName(this.name, g.getRuntime()) : this.name;
      MFile indexFileForRuntime = GribCollectionMutable.makeIndexMFile(gcname, directory); // not using disk cache LOOK why ?
      partitions.add(indexFileForRuntime);

     // create the master runtimes, consisting of the single runtime
      List<Long> runtimes = new ArrayList<>(1);
      runtimes.add(g.getRuntime().getMillis());
      CoordinateRuntime masterRuntimes = new CoordinateRuntime(runtimes, null);

      CalendarDateRange calendarDateRangeAll = null;
      for (Coordinate coord : g.getCoordinates()) {
        if (coord instanceof CoordinateTimeAbstract) {
          CalendarDateRange calendarDateRange = ((CoordinateTimeAbstract) coord).makeCalendarDateRange(null);
          if (calendarDateRangeAll == null) calendarDateRangeAll = calendarDateRange;
          else calendarDateRangeAll = calendarDateRangeAll.extend(calendarDateRange);
        }
      }
      assert calendarDateRangeAll != null;

      // for each Group write an index file
      ok &= writeIndex(gcname, indexFileForRuntime.getPath(), masterRuntimes, runGroupList, allFiles, calendarDateRangeAll);
      logger.info("GribCollectionBuilder write {} ok={}", indexFileForRuntime.getPath(), ok);
    }

    // if theres more than one runtime, create a partition collection to collect all the runtimes together
    if (multipleRuntimes) {
      Collections.sort(partitions); // ??
      PartitionManager part = new PartitionManagerFromIndexList(dcm, partitions, logger);
      part.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, dcm.getAuxInfo(FeatureCollectionConfig.AUX_CONFIG));
      ok &= GribCdmIndex.updateGribCollectionFromPCollection(isGrib1, part, CollectionUpdateType.always, errlog, logger);
    }

    long took = System.currentTimeMillis() - start;
    logger.debug("That took {} msecs", took);
    return ok;
  }

  public interface Group {
    CalendarDate getRuntime();
    List<Coordinate> getCoordinates();
    Set<Long> getCoordinateRuntimes();
  }

  @Immutable
  protected static class GroupAndRuntime {
    private final int hashCode;
    private final long runtime;

    GroupAndRuntime(int hashCode, long runtime) {
      this.hashCode = hashCode;
      this.runtime = runtime;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      GroupAndRuntime that = (GroupAndRuntime) o;
      if (hashCode != that.hashCode) return false;
      return runtime == that.runtime;
    }

    @Override
    public int hashCode() {
      int result = hashCode;
      result = 31 * result + (int) (runtime ^ (runtime >>> 32));
      return result;
    }
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.ui.datatype.prefs;

import ucar.nc2.ui.widget.MultilineTooltip;
import ucar.nc2.units.*;

import ucar.util.prefs.PersistenceManager;
import ucar.util.prefs.ui.FldInputVerifier;

import javax.swing.*;
import javax.swing.text.*;

/**
 * Input field for thredds.datatype.TimeDuration, part of ucar.util.prefs.ui.
 *
 * @author John Caron
 * @see ucar.util.prefs.ui.Field
 */

public class DurationField extends ucar.util.prefs.ui.Field {

  protected JTextComponent tf;

  /** Constructor.
   *  @param name of the field; must be unique within the store
   *  @param label to display to the user
   *  @param defValue default value to start with.
   *  @param storeData store/fetch data from here, may be null.
   */
  public DurationField(String name, String label, TimeDuration defValue, PersistenceManager storeData) {
    super(name, label, storeData);
    validValue = getStoreValue( defValue);
    tf = new JTextField() {
      public JToolTip createToolTip() { return new MultilineTooltip(); }
    };
    tf.setToolTipText("Formats:\n udunits time duration string");
    tf.setInputVerifier( new FldInputVerifier(tf, this));

    if (validValue != null)
      tf.setText(validValue.toString());

    finish();
  }

  // return the editing JComponent
  public JComponent getEditComponent() { return tf; }

  public TimeDuration getTimeDuration() { return (TimeDuration) getValue(); }

 /** See if edit value is valid, put error message in buff. */
  protected boolean _validate( StringBuffer buff) {
    String editValue = tf.getText().trim();
    if (editValue.length() == 0) return true; // empty ok

    try {
      new TimeDuration( tf.getText());
      return true;
    } catch (java.text.ParseException e) {
      buff.append(label).append(": ").append(e.getMessage());
      return false;
    }
  }

  // get current value from editComponent
  protected Object getEditValue() {
    String editValue = tf.getText().trim();
    if (editValue.length() == 0) return null; // empty ok

    try {
      return new TimeDuration( editValue);
    } catch (java.text.ParseException e) {
      return null;
    }
 }

  // set current value of editComponent
  protected void setEditValue(Object value) {
    if (value == null)
      tf.setText("");
    else
      tf.setText(value.toString());
    // tf.repaint();
  }

  protected void setStoreValue(Object value) {
    if (storeData != null)
      storeData.putObject(name, value); // TimeDurations are immutable
  }

  protected Object getStoreValue(Object defValue) {
    Object value = defValue;
    if (storeData != null) {
      Object value2 = storeData.getObject(name);
      if (value2 != null)
        value = value2;
    }
    return (TimeDuration) value;
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

// $Id:Doradeheader.java 63 2006-07-12 21:50:51Z edavis $

package ucar.nc2.iosp.dorade;

import ucar.nc2.*;

import ucar.nc2.constants.*;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.MAMath;

import ucar.atd.dorade.*;

import java.io.*;
import java.util.*;

public class Doradeheader {
  private boolean debug = false;
  private ucar.nc2.NetcdfFile ncfile;
  private float[] lat_min, lat_max, lon_min, lon_max, hi_max, hi_min;

  static public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) {
    try {
      java.io.RandomAccessFile file = raf.getRandomAccessFile();
      if (file == null) return false;
      boolean t = DoradeSweep.isDoradeSweep(file);
      if (!t) return false;
    } catch (DoradeSweep.DoradeSweepException ex) {
      return false;
    }
    return true;
  }

  void read(DoradeSweep mySweep, ucar.nc2.NetcdfFile ncfile, PrintStream out) throws IOException {

    this.ncfile = ncfile;
    DoradePARM[] parms = mySweep.getParamList();
    int nRays = mySweep.getNRays();

    if (debug) System.out.println(parms.length + " params in file");

    int numSensor = mySweep.getNSensors();
    int[] ncells = new int[numSensor];
    Dimension[] gateDim = new Dimension[numSensor];
    for (int i = 0; i < numSensor; i++) {
      try {
        int j = i + 1;
        ncells[i] = mySweep.getNCells(i);
        gateDim[i] = new Dimension("gate_" + j, ncells[i]);
        ncfile.addDimension(null, gateDim[i]);
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }

    ArrayList[] dims = new ArrayList[numSensor];
    ArrayList dims1 = new ArrayList();
    ArrayList[] dims2 = new ArrayList[numSensor];
    int nCells = mySweep.getNCells(0);

    //  Dimension sensorDim = new Dimension("sensor", numSensor, true);
    //  ncfile.addDimension( null, sensorDim);
    Dimension radialDim = new Dimension("radial", nRays);
    ncfile.addDimension(null, radialDim);

    for (int i = 0; i < numSensor; i++) {
      dims[i] = new ArrayList();
      dims2[i] = new ArrayList();
      dims[i].add(radialDim);
      dims[i].add(gateDim[i]);
      dims2[i].add(gateDim[i]);
    }

    // dims1.add( sensorDim);
    dims1.add(radialDim);

    float[][] altitudes = new float[numSensor][];
    float[][] latitudes = new float[numSensor][];
    float[][] longitudes = new float[numSensor][];
    lat_min = new float[numSensor];
    lat_max = new float[numSensor];
    lon_min = new float[numSensor];
    lon_max = new float[numSensor];
    hi_min = new float[numSensor];
    hi_max = new float[numSensor];
    MAMath.MinMax[] latMinMax = new MAMath.MinMax[numSensor];
    MAMath.MinMax[] lonMinMax = new MAMath.MinMax[numSensor];
    MAMath.MinMax[] hiMinMax = new MAMath.MinMax[numSensor];
    boolean[] isMoving = new boolean[numSensor];
    for (int i = 0; i < numSensor; i++) {
      try {
        int nc = mySweep.getNCells(i);
        altitudes[i] = new float[nRays];
        latitudes[i] = new float[nRays];
        longitudes[i] = new float[nRays];
        isMoving[i] = mySweep.sensorIsMoving(i);
        altitudes[i] = mySweep.getAltitudes(i);
        latitudes[i] = mySweep.getLatitudes(i);
        longitudes[i] = mySweep.getLongitudes(i);
      } catch (Exception ex) {
        ex.printStackTrace();
      }

      latMinMax[i] = getMinMaxData(latitudes[i]);
      lonMinMax[i] = getMinMaxData(longitudes[i]);
      hiMinMax[i] = getMinMaxData(altitudes[i]);
      float dis = (float) ((mySweep.getRangeToFirstCell(i) + (mySweep.getNCells(i) - 1) * mySweep.getCellSpacing(i)) * 1.853 / 111.26 / 1000);
      lat_min[i] = (float) (latMinMax[i].min - dis);
      lat_max[i] = (float) (latMinMax[i].max + dis);
      lon_min[i] = (float) (lonMinMax[i].min + dis * Math.cos(latitudes[i][0]));
      lon_max[i] = (float) (lonMinMax[i].max - dis * Math.cos(latitudes[i][0]));
      hi_min[i] = (float) hiMinMax[i].min;
      hi_max[i] = (float) hiMinMax[i].max;
    }

    //adding the global nc attribute
    addNCAttributes(ncfile, mySweep);

    // add elevation coordinate variable
    String vName = "elevation";
    String lName = "elevation angle in degres: 0 = parallel to pedestal base, 90 = perpendicular";
    Attribute att = new Attribute(_Coordinate.AxisType, AxisType.RadialElevation.toString());
    addParameter(vName, lName, ncfile, dims1, att, DataType.FLOAT, "degrees");

    // add azimuth coordinate variable
    vName = "azimuth";
    lName = "azimuth angle in degrees: 0 = true north, 90 = east";
    att = new Attribute(_Coordinate.AxisType, AxisType.RadialAzimuth.toString());
    addParameter(vName, lName, ncfile, dims1, att, DataType.FLOAT, "degrees");

    // add gate coordinate variable
    for (int i = 0; i < numSensor; i++) {
      int j = i + 1;
      vName = "distance_" + j;
      lName = "Radial distance to the start of gate";
      att = new Attribute(_Coordinate.AxisType, AxisType.RadialDistance.toString());
      addParameter(vName, lName, ncfile, dims2[i], att, DataType.FLOAT, "meters");
    }

    // add radial coordinate variable

    for (int i = 0; i < numSensor; i++) {
      int j = i + 1;
      vName = "latitudes_" + j;
      lName = "Latitude of the instrument " + j;
      att = new Attribute(_Coordinate.AxisType, AxisType.Lat.toString());
      addParameter(vName, lName, ncfile, dims1, att, DataType.FLOAT, "degrees");

      vName = "longitudes_" + j;
      lName = "Longitude of the instrument " + j;
      att = new Attribute(_Coordinate.AxisType, AxisType.Lon.toString());
      addParameter(vName, lName, ncfile, dims1, att, DataType.FLOAT, "degrees");

      vName = "altitudes_" + j;
      lName = "Altitude in meters (asl) of the instrument " + j;
      att = new Attribute(_Coordinate.AxisType, AxisType.Height.toString());
      addParameter(vName, lName, ncfile, dims1, att, DataType.FLOAT, "meters");

      vName = "rays_time";
      lName = "rays time";
      att = new Attribute(_Coordinate.AxisType, AxisType.Time.toString());
      addParameter(vName, lName, ncfile, dims1, att, DataType.DOUBLE, "milliseconds since 1970-01-01 00:00 UTC");

    }

    vName = "Range_to_First_Cell";
    lName = "Range to the center of the first cell";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "meters");

    vName = "Cell_Spacing";
    lName = "Distance between cells";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "meters");

    vName = "Fixed_Angle";
    lName = "Targeted fixed angle for this scan";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "degrees");

    vName = "Nyquist_Velocity";
    lName = "Effective unambigous velocity";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "m/s");

    vName = "Unambiguous_Range";
    lName = "Effective unambigous range";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "meters");

    vName = "Radar_Constant";
    lName = "Radar constant";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "c");

    vName = "rcvr_gain";
    lName = "Receiver Gain";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "db");

    vName = "ant_gain";
    lName = "Antenna Gain";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "db");

    vName = "sys_gain";
    lName = "System Gain";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "db");

    vName = "bm_width";
    lName = "Beam Width";
    addParameter(vName, lName, ncfile, null, null, DataType.FLOAT, "degrees");

    /* Variable ct = new Variable(ncfile, null, null, "radialCoordinateTransform");
      ct.setDataType(DataType.CHAR);
      ct.setDimensions(""); // scalar
      ct.addAttribute( new Attribute("transform_type", "Radial"));
      ct.addAttribute( new Attribute("_CoordinateTransformType", "Radial"));
      ct.addAttribute( new Attribute("_CoordinateAxes", "elevation azimuth distance_1"));
      ncfile.addVariable(null, ct);   */

    try {
      for (int p = 0; p < parms.length; p++) {
        String pval = parms[p].getDescription();
        nCells = parms[p].getNCells();
        int ii = getGateDimsIndex(nCells, gateDim, numSensor);

        if (debug) System.out.println("Param " + p + " name " + pval + " and ncel " + nCells);
        addVariable(ncfile, dims[ii], parms[p]);
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }

    // finish
    ncfile.finish();

  }

  public MAMath.MinMax getMinMaxData(float[] data) {
    int[] shape = new int[1];
    shape[0] = data.length;
    Array a = Array.factory(DataType.FLOAT, shape, data);
    return MAMath.getMinMax(a);
  }

  int getGateDimsIndex(int cell, Dimension[] dList, int numSensor) {
    int j = 0;
    for (int i = 0; i < numSensor; i++) {
      Dimension d = new Dimension("gate_" + i, cell);
      if (dList[i].equals(d)) {
        j = i;
        break;
      }
    }
    return j;
  }

  private void makeCoordinateData(Variable elev, Variable azim, DoradeSweep mySweep) {
    Object ele =  mySweep.getElevations();
    Object azi =  mySweep.getAzimuths();

    Array elevData = Array.factory(elev.getDataType(), elev.getShape(), ele);
    Array aziData = Array.factory(azim.getDataType(), azim.getShape(), azi);

    elev.setCachedData(elevData, false);
    azim.setCachedData(aziData, false);
  }

  void addParameter(String pName, String longName, NetcdfFile nc, ArrayList dims, Attribute att, DataType dtype, String ut) {
    Variable vVar = new Variable(nc, null, null, pName);
    vVar.setDataType(dtype);
    if (dims != null) vVar.setDimensions(dims);
    else vVar.setDimensions("");
    if (att != null) vVar.addAttribute(att);
    vVar.addAttribute(new Attribute(CDM.UNITS, ut));
    vVar.addAttribute(new Attribute(CDM.LONG_NAME, longName));
    nc.addVariable(null, vVar);
  }

  void addVariable(NetcdfFile nc, ArrayList dims, DoradePARM dparm) {

    Variable v = new Variable(nc, null, null, dparm.getName());
    v.setDataType(DataType.FLOAT);
    v.setDimensions(dims);
    ncfile.addVariable(null, v);

    v.addAttribute(new Attribute(CDM.LONG_NAME, dparm.getDescription()));
    v.addAttribute(new Attribute(CDM.UNITS, dparm.getUnits()));
    String coordinates = "elevation azimuth distance_1 " + "latitudes_1 longitudes_1 altitudes_1";
    v.addAttribute(new Attribute(_Coordinate.Axes, coordinates));
      /*
      v.addAttribute( new Attribute(CDM.MISSING_VALUE, new Float(dparm.getBadDataFlag())));
      v.addAttribute( new Attribute("_FillValue", new Float(dparm.getBadDataFlag())));
      v.addAttribute( new Attribute("scale_factor", dparm.getUnits()));
      v.addAttribute( new Attribute("polarization", dparm.getUnits()));
      v.addAttribute( new Attribute("Frequencies_GHz", dparm.getUnits()));
      v.addAttribute( new Attribute("InterPulsePeriods_secs", dparm.getUnits()));
      v.addAttribute( new Attribute("ThresholdValue", new Float(dparm.getThresholdValue())));
      v.addAttribute( new Attribute("ThresholdParamName", dparm.getthresholdParamName()));
      v.addAttribute( new Attribute("usedPRTs", new Integer(dparm.getusedPRTs())));
      v.addAttribute( new Attribute("usedFrequencies", new Integer(dparm.getusedFrequencies())));
       */
  }

  void addNCAttributes(NetcdfFile nc, DoradeSweep mySweep) throws DoradeSweep.DoradeSweepException {
    nc.addAttribute(null, new Attribute("summary", "Dorade radar data " +
            "from radar " + mySweep.getSensorName(0) + " in the project " + mySweep.getProjectName()));
    nc.addAttribute(null, new Attribute("radar_name", mySweep.getSensorName(0)));
    nc.addAttribute(null, new Attribute("project_name", mySweep.getProjectName()));
    nc.addAttribute(null, new Attribute("keywords_vocabulary", "dorade"));
    nc.addAttribute(null, new Attribute("geospatial_lat_min", lat_min[0]));
    nc.addAttribute(null, new Attribute("geospatial_lat_max", lat_max[0]));
    nc.addAttribute(null, new Attribute("geospatial_lon_min", lon_min[0]));
    nc.addAttribute(null, new Attribute("geospatial_lon_max", lon_max[0]));
    nc.addAttribute(null, new Attribute("geospatial_vertical_min", lon_min[0]));
    nc.addAttribute(null, new Attribute("geospatial_vertical_max", lon_max[0]));

    Date[] dd = mySweep.getTimes();
    if (dd != null) {
      nc.addAttribute(null, new Attribute("time_coverage_start", dd[0].toString()));
      nc.addAttribute(null, new Attribute("time_coverage_end", dd[dd.length - 1].toString()));
    }
    nc.addAttribute(null, new Attribute("Content", "This file contains one scan of remotely sensed data"));
    nc.addAttribute(null, new Attribute(CDM.CONVENTIONS, _Coordinate.Convention));
    nc.addAttribute(null, new Attribute(CF.FEATURE_TYPE, FeatureType.RADIAL.toString()));
    nc.addAttribute(null, new Attribute("format", "Unidata/netCDF/Dorade"));
    nc.addAttribute(null, new Attribute("Radar_Name", mySweep.getSensorName(0)));
    nc.addAttribute(null, new Attribute("Project_name", "" + mySweep.getProjectName()));
    nc.addAttribute(null, new Attribute("VolumeCoveragePatternName", mySweep.getScanMode(0).getName()));
    nc.addAttribute(null, new Attribute("Volume_Number", "" + mySweep.getVolumnNumber()));
    nc.addAttribute(null, new Attribute("Sweep_Number", "" + mySweep.getSweepNumber()));
    nc.addAttribute(null, new Attribute("Sweep_Date", DoradeSweep.formatDate(mySweep.getTime())));
    if (mySweep.sensorIsMoving(0))
      nc.addAttribute(null, new Attribute("IsStationary", "0"));
    else
      nc.addAttribute(null, new Attribute("IsStationary", "1"));
  }

  // Return the string of entity ID for the Dorade image file
  DataType getDataType(int format) {
    DataType p;

    switch (format) {
      case 1:    // 8-bit signed integer format.
        p = DataType.SHORT;
        break;
      case 2:  // 16-bit signed integer format.
        p = DataType.FLOAT;
        break;
      case 3:    // 32-bit signed integer format.
        p = DataType.LONG;
        break;
      case 4:   // 32-bit IEEE float format.
        p = DataType.FLOAT;
        break;
      case 5:   //  16-bit IEEE float format.
        p = DataType.DOUBLE;
        break;
      default:
        p = null;
        break;
    } //end of switch
    return p;
  }
  ////////////////////////////////////////////////////////////////////////////////////////////////////////
}
/* Change History:
   $Log: Doradeheader.java,v $
   Revision 1.5  2006/04/19 20:24:09  yuanho
   radial dataset sweep for all radar dataset

   Revision 1.4  2005/08/08 22:45:32  yuanho
   spelling bug fix

   Revision 1.3  2005/08/03 21:50:45  yuanho
   called IsDoradeSweep to check input file, adding global atts.

   Revision 1.2  2005/05/11 00:10:03  caron
   refactor StuctureData, dt.point

   Revision 1.1  2005/04/26 19:39:06  yuanho
   iosp for dorade format radar data

   Revision 1.8  2004/12/15 22:35:25  caron
   add _unsigned

   Revision 1.7  2004/12/07 22:13:28  yuanho
   add phyElem for 1hour and total precipitation

   Revision 1.6  2004/12/07 22:13:15  yuanho
   add phyElem for 1hour and total precipitation

   Revision 1.5  2004/12/07 01:29:31  caron
   redo convention parsing, use _Coordinate encoding.

   Revision 1.4  2004/10/29 00:14:11  caron
   no message

   Revision 1.3  2004/10/19 15:17:22  yuanho
   Dorade header DxKm update

   Revision 1.2  2004/10/15 23:18:34  yuanho
   Dorade projection update

   Revision 1.1  2004/10/13 22:57:57  yuanho
   no message

   Revision 1.4  2004/08/16 20:53:45  caron
   2.2 alpha (2)

   Revision 1.3  2004/07/12 23:40:17  caron
   2.2 alpha 1.0 checkin

   Revision 1.2  2004/07/06 19:28:10  caron
   pre-alpha checkin

   Revision 1.1.1.1  2003/12/04 21:05:27  caron
   checkin 2.2

 */