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

package ucar.nc2.grib.grib2;

import com.google.common.base.MoreObjects;
import javax.annotation.Nullable;
import ucar.nc2.grib.GribData;
import ucar.nc2.grib.QuasiRegular;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.util.StringUtil2;

import java.io.IOException;
import java.util.Formatter;

/**
 * Read one Record from a GRIB-2 files
 *
 * @author caron
 * @since 3/28/11
 */
public class Grib2Record {

  //////////////////////////////////////////////////////////////
  private final Grib2SectionIndicator is;
  private final Grib2SectionIdentification id;
  private Grib2SectionLocalUse lus; // local use section
  private Grib2SectionGridDefinition gdss;
  private Grib2SectionProductDefinition pdss;
  private Grib2SectionDataRepresentation drss;
  private Grib2SectionBitMap bms;
  private Grib2SectionData dataSection;

  private Grib2Pds pds2 = null;

  private final byte[] header; // anything in between the records - eg idd header
  private int file; // for multiple files in same dataset
  private boolean bmsReplaced;
  private int scanMode;

  public int repeat; // debug = see Grib2Report.doDrsSummary

  /**
   * Construction for Grib2Record.
   *
   * @param header      Grib header
   * @param is          Grib2IndicatorSection
   * @param id          Grib2IdentificationSection
   * @param lus         raw bytes of local use section
   * @param gdss        Grib2GridDefinitionSection
   * @param pdss        Grib2ProductDefinitionSection
   * @param drs         Grib2SectionDataRepresentation
   * @param bms         Grib2SectionBitMap
   * @param dataSection Grib2SectionData
   * @param bmsReplaced Grib2SectionData
   * @param scanMode    from GDS of this record
   */
  public Grib2Record(byte[] header, Grib2SectionIndicator is,
                     Grib2SectionIdentification id,
                     Grib2SectionLocalUse lus,
                     Grib2SectionGridDefinition gdss,
                     Grib2SectionProductDefinition pdss,
                     Grib2SectionDataRepresentation drs,
                     Grib2SectionBitMap bms,
                     Grib2SectionData dataSection,
                     boolean bmsReplaced,
                     int scanMode) {

    this.header = header;
    this.is = is;
    this.id = id;
    this.lus = lus;
    this.gdss = gdss;
    this.pdss = pdss;
    this.drss = drs;
    this.bms = bms;
    this.dataSection = dataSection;
    this.bmsReplaced = bmsReplaced;

    // stored in index file after 4.5 2/6/2014, otherwise equals Grib2Index.ScanModeMissing, so get it from the GDS, which may have wrong one
    this.scanMode = scanMode;
    if (scanMode == Grib2Index.ScanModeMissing && gdss != null) {
      this.scanMode = gdss.getGDS().getScanMode();
    }
  }

  // copy constructor
  Grib2Record(Grib2Record from) {
    this.header = from.header;
    this.is = from.is;
    this.id = from.id;
    this.lus = from.lus;
    this.gdss = from.gdss;
    this.pdss = from.pdss;
    this.drss = from.drss;
    this.bms = from.bms;
    this.dataSection = from.dataSection;
    this.repeat = from.repeat;
    this.bmsReplaced = from.bmsReplaced;
    this.scanMode = from.scanMode;
  }

  public byte[] getHeader() {
    return header;
  }

  public Grib2SectionIndicator getIs() {
    return is;
  }

  public Grib2SectionIdentification getId() {
    return id;
  }

  public boolean hasLocalUseSection() {
    return lus != null && lus.getRawBytes() != null;
  }

  public Grib2SectionLocalUse getLocalUseSection() {
    return lus;
  }

  public Grib2SectionGridDefinition getGDSsection() {
    return gdss;
  }

  public Grib2SectionProductDefinition getPDSsection() {
    return pdss;
  }

  public Grib2SectionDataRepresentation getDataRepresentationSection() {
    return drss;
  }

  public Grib2SectionBitMap getBitmapSection() {
    return bms;
  }

  public Grib2SectionData getDataSection() {
    return dataSection;
  }

  public int getDiscipline() {
    return is.getDiscipline();
  }

  public CalendarDate getReferenceDate() {
    return id.getReferenceDate();
  }

  public Grib2Pds getPDS() {
    if (pds2 == null)
      pds2 = pdss.getPDS();
    return pds2;
  }

  public synchronized Grib2Gds getGDS() {
    return gdss.getGDS();
  }

  public int getScanMode() {
    return scanMode;
  }

  public void show(Formatter f) {
    f.format("discipline=%d ", is.getDiscipline());
    Grib2Pds pds = getPDS();
    pds.show(f);
  }

  //////////////////////////////////////////
  // setters used by repeating records

  public void setLus(Grib2SectionLocalUse lus) {
    this.lus = lus;
  }

  public void setGdss(Grib2SectionGridDefinition gdss) {
    this.gdss = gdss;
  }

  public void setPdss(Grib2SectionProductDefinition pdss) {
    this.pdss = pdss;
  }

  public void setDrs(Grib2SectionDataRepresentation drs) {
    this.drss = drs;
  }

  public void setBms(Grib2SectionBitMap bms, boolean replaced) {
    this.bms = bms;
    this.bmsReplaced = replaced;
  }

  public void setDataSection(Grib2SectionData dataSection) {
    this.dataSection = dataSection;
  }

  public int getFile() {
    return file;
  }

  public void setFile(int file) {
    this.file = file;
  }

  public boolean isBmsReplaced() {
    return bmsReplaced;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("is", is)
        .add("id", id)
        .add("lus", lus)
        .add("gdss", gdss)
        .add("pdss", pdss)
        .add("drss", drss)
        .add("bms", bms)
        .add("dataSection", dataSection)
        .add("pds2", pds2)
        .add("header", header)
        .add("file", file)
        .add("bmsReplaced", bmsReplaced)
        .add("scanMode", scanMode)
        .add("repeat", repeat)
        .toString();
  }

  // isolate dependencies here - in case we have a "minimal I/O" mode where not all fields are available
  public float[] readData(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
            getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (gds.isThin())
      data = QuasiRegular.convertQuasiGrid(data, gds.getNptsInLine(), gds.getNxRaw(), gds.getNyRaw(), GribData.getInterpolationMethod());

    lastRecordRead = this;
    return data;
  }

  // debugging - do not use
  @Nullable
  public int[] readRawData(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
            getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);

    return reader.getRawData(raf, bms, gdrs);
  }

  // debugging - do not use
  @Nullable
  public Grib2Drs.Type40 readDataTest(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
            getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);
    if (gdrs instanceof Grib2Drs.Type40) {
      reader.getData(raf, bms, gdrs);
      return (Grib2Drs.Type40) gdrs;
    }
    return null;
  }

  /**
   * Read data array
   *
   * @param raf    from this RandomAccessFile
   * @param drsPos Grib2SectionDataRepresentation starts here
   * @return data as float[] array
   * @throws IOException on read error
   */
  public float[] readData(RandomAccessFile raf, long drsPos) throws IOException {
    raf.seek(drsPos);
    Grib2SectionDataRepresentation drs = new Grib2SectionDataRepresentation(raf);
    Grib2SectionBitMap bms = new Grib2SectionBitMap(raf);
    Grib2SectionData dataSection = new Grib2SectionData(raf);

    Grib2Gds gds = getGDS();
    Grib2DataReader reader = new Grib2DataReader(drs.getDataTemplate(), gdss.getNumberPoints(), drs.getDataPoints(),
            getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drs.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (gds.isThin())
      data = QuasiRegular.convertQuasiGrid(data, gds.getNptsInLine(), gds.getNxRaw(), gds.getNyRaw(), GribData.getInterpolationMethod());

    lastRecordRead = this;
    return data;
  }

  /**
   * Read data array: use when you want to be independent of the GribRecord
   *
   * @param raf             from this RandomAccessFile
   * @param drsPos          Grib2SectionDataRepresentation starts here
   * @param bmsPos          if non-zero, use the bms that starts here
   * @param gdsNumberPoints gdss.getNumberPoints()
   * @param scanMode        gds.scanMode
   * @param nx              gds.nx
   * @return data as float[] array
   * @throws IOException on read error
   */
  public static float[] readData(RandomAccessFile raf, long drsPos, long bmsPos, int gdsNumberPoints, int scanMode, int nx, int ny, int[] nptsInLine) throws IOException {
    raf.seek(drsPos);
    Grib2SectionDataRepresentation drs = new Grib2SectionDataRepresentation(raf);
    Grib2SectionBitMap bms = new Grib2SectionBitMap(raf);
    Grib2SectionData dataSection = new Grib2SectionData(raf);

    if (bmsPos > 0)
      bms = Grib2SectionBitMap.factory(raf, bmsPos);

    Grib2DataReader reader = new Grib2DataReader(drs.getDataTemplate(), gdsNumberPoints, drs.getDataPoints(),
            scanMode, nx, dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drs.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (nptsInLine != null)
      data = QuasiRegular.convertQuasiGrid(data, nptsInLine, nx, ny, GribData.getInterpolationMethod());

    if (getlastRecordRead)
      lastRecordRead = Grib2RecordScanner.findRecordByDrspos(raf, drsPos);
    return data;
  }

  public void check(RandomAccessFile raf, Formatter f) throws IOException {
    long messLen = is.getMessageLength();
    long startPos = is.getStartPos();
    long endPos = is.getEndPos();

    if (endPos > raf.length()) {
      f.format("End of GRIB message (start=%d len=%d) end=%d > file.length=%d for %s%n", startPos, messLen , endPos, raf.length(), raf.getLocation());
      return;
    }

    raf.seek(endPos-4);
    for (int i = 0; i < 4; i++) {
      if (raf.read() != 55) {
        String clean = StringUtil2.cleanup(header);
        if (clean.length() > 40) clean = clean.substring(0,40) + "...";
        f.format("Missing End of GRIB message (start=%d len=%d) end=%d header= %s for %s (len=%d)%n", startPos, messLen, endPos, clean, raf.getLocation(), raf.length());
        break;
      }
    }

    long dataLen = dataSection.getMsgLength();
    long dataStart = dataSection.getStartingPosition();
    long dataEnd = dataStart + dataLen;

    if (dataEnd > raf.length()) {
      f.format("GRIB data section (start=%d len=%d) end=%d > file.length=%d for %s%n", dataStart, dataLen, dataEnd, raf.length(), raf.getLocation());
      return;
    }

    if (dataEnd > endPos) {
      f.format("GRIB data section (start=%d len=%d) end=%d > message end=%d for %s%n", dataStart, dataLen, dataEnd, endPos, raf.getLocation());
    }

  }

  public GribData.Info getBinaryDataInfo(RandomAccessFile raf) throws IOException {
    GribData.Info info = this.drss.getDrs(raf).getBinaryDataInfo(raf);
    info.bitmapLength = (bms == null) ? 0 : bms.getLength(raf);
    info.msgLength = is.getMessageLength();
    info.dataLength = dataSection.getMsgLength();
    info.ndataPoints = drss.getDataPoints();
    Grib2Gds gds = getGDS();
    info.nPoints = gds.getNx() * gds.getNy();
    return info;
  }

  // debugging do not use
  public static boolean getlastRecordRead;
  public static Grib2Record lastRecordRead;

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information.
*/
package dap4.dap4lib;

import dap4.core.ce.CEConstraint;
import dap4.core.data.DSP;
import dap4.core.data.DataCursor;
import dap4.core.dmr.*;
import dap4.core.util.*;

import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.List;

/**
 * DAP DSP Printer.
 * Given a constraint and a DSP print the constrained
 * subset of the data in text form.
 */

public class DSPPrinter
{
    //////////////////////////////////////////////////
    // Printer Control flags

    public enum Flags
    {
        CONTROLCHAR;
    }

    //////////////////////////////////////////////////
    // Constants

    static protected final int COLUMNS = 8;

    static protected final char LPAREN = '(';
    static protected final char RPAREN = ')';
    static protected final char LBRACE = '{';
    static protected final char RBRACE = '}';
    static protected final char LBRACKET = '[';
    static protected final char RBRACKET = ']';

    // Could use enumset, but it is so ugly,
    // so use good old OR'able flags
    static protected final int NILFLAGS = 0;
    static protected final int PERLINE = 1; // print xml attributes 1 per line
    static protected final int NONAME = 2; // do not print name xml attribute
    static protected final int NONNIL = 4; // print empty xml attributes

    //////////////////////////////////////////////////
    // Instance variables

    protected PrintWriter writer = null;
    protected IndentWriter printer = null;

    protected DSP dsp = null;
    protected CEConstraint ce = null;

    protected EnumSet<Flags> flags = EnumSet.noneOf(Flags.class);

    //////////////////////////////////////////////////
    //Constructor(s)

    protected DSPPrinter()
    {
    }

    public DSPPrinter(DSP dsp, Writer writer)
    {
        this(dsp, null, writer);
    }

    public DSPPrinter(DSP dsp, CEConstraint ce, Writer writer)
    {
        this.dsp = dsp;
        this.ce = ce;
        this.writer = new PrintWriter(writer);
        this.printer = new IndentWriter(this.writer);
    }

    //////////////////////////////////////////////////
    // External API

    public DSPPrinter flag(Flags flag)
    {
        this.flags.add(flag);
        return this;
    }

    public DSPPrinter flush()
    {
        this.printer.flush();
        return this;
    }

    public DSPPrinter close()
    {
        this.flush();
        return this;
    }

    /**
     * Print data from a DSP
     * - optionally constrained
     *
     * @throws DapException
     */

    public DSPPrinter
    print()
            throws DapException
    {
        DapDataset dmr = this.dsp.getDMR();
        if(this.ce == null)
            this.ce = CEConstraint.getUniversal(dmr);
        this.printer.setIndent(0);
        List<DapVariable> topvars = dmr.getTopVariables();
        for(int i = 0; i < topvars.size(); i++) {
            DapVariable top = topvars.get(i);
            List<Slice> slices = this.ce.getConstrainedSlices(top);
            if(this.ce.references(top)) {
                DataCursor data = dsp.getVariableData(top);
                printVariable(data,slices);
            }
        }
        printer.eol();
        return this;
    }

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

    /**
     * Print an arbitrary DataVariable using a constraint.
     * <p>
     * Handling newlines is a bit tricky
     * so the rule is that the
     * last newline is elided and left
     * for the caller to print.
     * Exceptions: ?
     *
     * @param data - the cursor to print
     * @throws DapException Note that the PrintWriter and CEConstraint are global.
     */

    protected void
    printVariable(DataCursor data, List<Slice> slices)
            throws DapException
    {
        DapVariable dapv = (DapVariable) data.getTemplate();
        if(data.isScalar()) {
            assert slices == Slice.SCALARSLICES;
            printScalar(data);
        } else {// not scalar
            printArray(data, slices);
        }
    }

    protected void
    printScalar(DataCursor data)
            throws DapException
    {
        DapVariable dapv = (DapVariable) data.getTemplate();
        printer.marginPrint(dapv.getFQN() + " = ");
        switch (data.getScheme()) {
        case ATOMIC:
            printAtomicInstance(data, Index.SCALAR);
            break;
        case STRUCTARRAY:
        case SEQARRAY:  // remember that scalars are treated as 1-element arrays
            DataCursor[] scalar = (DataCursor[]) data.read(Index.SCALAR);
            assert scalar.length == 1;
            data = scalar[0]; // fall thru
        case STRUCTURE:
        case SEQUENCE:
        case RECORD:
            printer.marginPrint("{");
            printer.eol();
            printer.indent();
            printCompoundInstance(data);
            printer.outdent();
            printer.marginPrint("}");
            printer.eol();
            break;
        default:
            throw new DapException("Unexpected data cursor type: " + data.getScheme());
        }
    }

    protected void
    printArray(DataCursor data, List<Slice> slices)
            throws DapException
    {
        DapVariable dapv = (DapVariable) data.getTemplate();
        Odometer odom = Odometer.factory(slices);
        switch (data.getScheme()) {
        case ATOMIC:
            if(DapUtil.isContiguous(slices))
                printAtomicVector(data, slices, odom);
            else {
                while(odom.hasNext()) {
                    Index pos = odom.next();
                    String s = indicesToString(pos);
                    printer.marginPrint(dapv.getFQN() + s + " = ");
                    printAtomicInstance(data, pos);
                }
            }
            break;
        case STRUCTARRAY:
        case SEQARRAY:
            DapStructure ds = (DapStructure) ((DapVariable) data.getTemplate()).getBaseType();
            DataCursor[] instances = (DataCursor[]) data.read(slices);
            while(odom.hasNext()) {
                Index pos = odom.next();
                String s = indicesToString(pos);
                printer.marginPrint(ds.getFQN() + s + " = {");
                printer.eol();
                printer.indent();
                printCompoundInstance(instances[(int)pos.index()]);
                printer.outdent();
                printer.marginPrint("}");
                printer.eol();
            }
            break;
        default:
            throw new DapException("Unexpected data cursor type: " + data.getScheme());
        }
    }

    protected void
    printAtomicVector(DataCursor data, List<Slice> slices, Odometer odom)
            throws DapException
    {
        assert data.getScheme() == DataCursor.Scheme.ATOMIC;
        Object values = data.read(slices);
        DapVariable atom = (DapVariable) data.getTemplate();
        String name = atom.getFQN();
        if(Array.getLength(values) == 0) {// zero length case
            printer.marginPrint(name + " = <empty>");
            printer.eol();
        } else for(int i = 0; odom.hasNext(); i++) {
            Index index = odom.next();
            String prefix = (odom.rank() == 0 ? name : name + indicesToString(index));
            printer.marginPrint(prefix + " = ");
            printer.print(valueString(values, i, atom.getBaseType()));
            printer.eol();
        }
    }

    protected void
    printAtomicInstance(DataCursor datav, Index pos)
            throws DapException
    {
        assert datav.getScheme() == DataCursor.Scheme.ATOMIC;
        Object value = datav.read(pos);
        DapVariable av = (DapVariable) datav.getTemplate();
        printer.print(valueString(value, 0, av.getBaseType()));
        printer.eol();
    }

    /**
     * Print a single structure or sequence or record instance
     *
     * @param datav
     * @throws DapException
     */
    protected void
    printCompoundInstance(DataCursor datav)
            throws DapException
    {
        //Index index = datav.getIndex();
        DapStructure dstruct = (DapStructure) ((DapVariable) datav.getTemplate()).getBaseType();
        switch (datav.getScheme()) {
        case STRUCTURE:
        case RECORD:
            List<DapVariable> dfields = dstruct.getFields();
            for(int f = 0; f < dfields.size(); f++) {
                DapVariable field = dfields.get(f);
                List<Slice> fieldslices = this.ce.getConstrainedSlices(field);
                DataCursor fdata = datav.readField(f);
                printVariable(fdata,fieldslices);
            }
            break;

        case SEQUENCE:
            DapSequence dseq = (DapSequence)dstruct;
            long count = datav.getRecordCount();
            for(long r = 0; r < count; r++) {
                DataCursor dr = datav.readRecord(r);
                printer.marginPrint("[");
                printer.eol();
                printer.indent();
                printCompoundInstance(dr);
                printer.outdent();
                printer.marginPrint("]");
            }
            break;
        default:
            throw new DapException("Unexpected data cursor scheme:" + datav.getScheme());
        }
    }

    protected String
    indicesToString(Index indices)
            throws DapException
    {
        StringBuilder buf = new StringBuilder();
        if(indices != null && indices.getRank() > 0) {
            for(int i = 0; i < indices.getRank(); i++) {
                buf.append(i == 0 ? LBRACKET : ",");
                buf.append(String.format("%d", indices.get(i)));
            }
            buf.append(RBRACKET);
        }
        return buf.toString();
    }

    protected String
    valueString(Object vector, long pos, DapType basetype)
            throws DapException
    {
        if(vector == null) return "null";
        TypeSort atype = basetype.getTypeSort();
        boolean unsigned = atype.isUnsigned();
        int ipos = (int) pos;
        switch (atype) {
        case Int8:
        case UInt8:
            long lvalue = ((byte[]) vector)[ipos];
            if(unsigned) lvalue &= 0xFFL;
            return String.format("%d", lvalue);
        case Int16:
        case UInt16:
            lvalue = ((short[]) vector)[ipos];
            if(unsigned) lvalue &= 0xFFFFL;
            return String.format("%d", lvalue);
        case Int32:
        case UInt32:
            lvalue = ((int[]) vector)[ipos];
            if(unsigned) lvalue &= 0xFFFFFFFFL;
            return String.format("%d", lvalue);
        case Int64:
        case UInt64:
            lvalue = ((long[]) vector)[ipos];
            if(unsigned) {
                BigInteger b = BigInteger.valueOf(lvalue);
                b = b.and(DapUtil.BIG_UMASK64);
                return b.toString();
            } else
                return String.format("%d", lvalue);
        case Float32:
            return String.format("%f", ((float[]) vector)[ipos]);
        case Float64:
            return String.format("%f", ((double[]) vector)[ipos]);
        case Char:
            return String.format("'%c'", ((char[]) vector)[ipos]);
        case String:
        case URL:
            String s = (((String[]) vector)[ipos]);
            if(flags.contains(Flags.CONTROLCHAR)) {
                s = s.replace("\r", "\\r");
                s = s.replace("\n", "\\n");
                s = s.replace("\t", "\\t");
            }
            return "\"" + s + "\"";
        case Opaque:
            ByteBuffer opaque = ((ByteBuffer[]) vector)[ipos];
            StringBuilder buf = new StringBuilder();
            buf.append("0x");
            for(int i = 0; i < opaque.limit(); i++) {
                byte b = opaque.get(i);
                char c = hexchar((b >> 4) & 0xF);
                buf.append(c);
                c = hexchar((b) & 0xF);
                buf.append(c);
            }
            return buf.toString();
        case Enum:
            DapEnumeration de = (DapEnumeration) basetype;
            Object newvec = CoreTypeFcns.createVector(de.getBaseType().getTypeSort(),1);
            Object v = java.lang.reflect.Array.get(vector, ipos);
            java.lang.reflect.Array.set(newvec,0,v);
            long[] lv  = (long[]) Convert.convert(DapType.INT64, de, newvec);
            DapEnumConst dec = de.lookup(lv[0]);
            return dec.getShortName();
        default:
            break;
        }
        throw new DapException("Unknown type: " + basetype);
    }

    //////////////////////////////////////////////////
    // Misc. Utilities

    static protected char
    hexchar(int i)
    {
        return "0123456789ABCDEF".charAt((i & 0xF));
    }

    static protected String
    getPrintValue(Object value)
    {
        if(value instanceof String) {
            return Escape.entityEscape((String) value,null);
        } else
            return value.toString();
    }

}


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

package ucar.unidata.geoloc.projection.sat;

import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.ProjectionPoint;
import ucar.unidata.geoloc.ProjectionPointImpl;
import ucar.unidata.geoloc.ProjectionRect;

/**
 * Describe: https://cf-pcmdi.llnl.gov/trac/ticket/72
 * Accepted for CF-1.7
 *
 * grid_mapping_name = geostationary
   Map parameters:
     latitude_of_projection_origin
     longitude_of_projection_origin
     perspective_point_height
     semi_minor_axis
     semi_major_axis
     inverse_flattening
     sweep_angle_axis
     fixed_angle_axis

 Map coordinates:
  The x (abscissa) and y (ordinate) rectangular coordinates are identified by the standard_name attribute value projection_x_coordinate and projection_y_coordinate
 respectively. In the case of this projection, the projection coordinates in this projection are directly related to the scanning angle of the satellite instrument,
 and their units are radians.

 Notes:

 The algorithm for computing the mapping may be found at http://www.eumetsat.int/idcplg?IdcService=GET_FILE&dDocName=PDF_CGMS_03&RevisionSelectionMethod=LatestReleased.
 This document assumes the point of observation is directly over the equator, and that the sweep_angle_axis is y.

 Notes on using the PROJ.4 software packages for computing the mapping may be found at http://trac.osgeo.org/proj/wiki/proj%3Dgeos and
 http://remotesensing.org/geotiff/proj_list/geos.html .

 The "perspective_point_height" is the distance to the surface of the ellipsoid. Adding the earth major axis gives the distance from the centre of the earth.

 The "sweep_angle_axis" attribute indicates which axis the instrument sweeps. The value = "y" corresponds to the spin-stabilized Meteosat satellites,
 the value = "x" to the GOES-R satellite.

 The "fixed_angle_axis" attribute indicates which axis the instrument is fixed. The values are opposite to "sweep_angle_axis". Only one of those two attributes are
 mandatory.

 latitude_of_projection_origin will be taken as zero (at the Equator).

 inverse_flattening may be specified independent of the semi_minor/major axes (GRS80). If left unspecified it will be computed
 from semi_minor/major_axis values.
 *
 * @author caron
 * @since 12/5/13
 */

public class Geostationary extends ProjectionImpl {
  private static final String NAME = CF.GEOSTATIONARY;
  private boolean isGeoCoordinateScaled = false;
  private double geoCoordinateScaleFactor;

  GEOSTransform navigation = null;

  public Geostationary(double subLonDegrees, double perspective_point_height, double semi_minor_axis,
                       double semi_major_axis, double inv_flattening, boolean isSweepX) {

    // scale factors (last two doubles in the sig) less than zero indicate no scaling of map x, y coordinates
    this(subLonDegrees, perspective_point_height, semi_minor_axis, semi_major_axis, inv_flattening, isSweepX,
            -1.0);
  }

  public Geostationary(double subLonDegrees, double perspective_point_height, double semi_minor_axis,
            double semi_major_axis, double inv_flattening, boolean isSweepX,
                       double geoCoordinateScaleFactor) {
    super(NAME, false);

    String sweepAngleAxis = "y";
    if (isSweepX) {
      sweepAngleAxis = "x";
    }

    /* Must assume incoming distances are SI units, so convert 'm' -> 'km' for GEOSTransform */
    perspective_point_height /= 1000.0;
    semi_minor_axis /= 1000.0;
    semi_major_axis /= 1000.0;

    navigation = new GEOSTransform(subLonDegrees, perspective_point_height, semi_minor_axis, semi_major_axis, inv_flattening, sweepAngleAxis);
    makePP();

    if (geoCoordinateScaleFactor > 0) {
      isGeoCoordinateScaled = true;
      this.geoCoordinateScaleFactor = geoCoordinateScaleFactor;
    }
  }

  public Geostationary() {
    super(NAME, false);
    navigation = new GEOSTransform();
    makePP();
  }

  public Geostationary(double subLonDegrees) {
    super(NAME, false);
    navigation = new GEOSTransform(subLonDegrees, GEOSTransform.GOES);
    makePP();
  }

  public Geostationary(double subLonDegrees, boolean isSweepX) {
    super(NAME, false);

    String sweepAngleAxis = "y";
    if (isSweepX) {
      sweepAngleAxis = "x";
    }

    String scanGeometry = GEOSTransform.sweepAngleAxisToScanGeom(sweepAngleAxis);

    navigation = new GEOSTransform(subLonDegrees, scanGeometry);
    makePP();
  }

  public Geostationary(double subLonDegrees, String sweepAngleAxis, double geoCoordinateScaleFactor) {
    super(NAME, false);

    String scanGeometry = GEOSTransform.sweepAngleAxisToScanGeom(sweepAngleAxis);

    navigation = new GEOSTransform(subLonDegrees, scanGeometry);

    if (geoCoordinateScaleFactor > 0) {
      isGeoCoordinateScaled = true;
      this.geoCoordinateScaleFactor = geoCoordinateScaleFactor;
    }

    makePP();
  }

  private void makePP() {
    addParameter(CF.GRID_MAPPING_NAME, NAME);
    addParameter(CF.LONGITUDE_OF_PROJECTION_ORIGIN, navigation.sub_lon_degrees);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, 0.0);
    addParameter(CF.PERSPECTIVE_POINT_HEIGHT, navigation.sat_height * 1000.0);
    addParameter(CF.SWEEP_ANGLE_AXIS, GEOSTransform.scanGeomToSweepAngleAxis(navigation.scan_geom));
    addParameter(CF.SEMI_MAJOR_AXIS, navigation.r_eq * 1000.0);
    addParameter(CF.SEMI_MINOR_AXIS, navigation.r_pol * 1000.0);
  }

  /**
   * copy constructor - avoid clone !!
   */
  @Override
  public ProjectionImpl constructCopy() {
    // constructor takes sweep_angle_axis, so need to translate between
    // scan geometry and sweep_angle_axis first
    // GOES: x
    // GEOS: y
    String sweepAxisAngle = GEOSTransform.scanGeomToSweepAngleAxis(navigation.scan_geom);

    return new Geostationary(navigation.sub_lon_degrees, sweepAxisAngle, geoCoordinateScaleFactor);
  }

  @Override
  public String paramsToString() {
    return "";
  }

  @Override
  public ProjectionPoint latLonToProj(LatLonPoint latlon, ProjectionPointImpl destPoint) {
    double[] satCoords = navigation.earthToSat(latlon.getLongitude(), latlon.getLatitude());
    double x = satCoords[0];
    double y = satCoords[1];

    // scale back to required units of x, y (we need them in radians)
    if (isGeoCoordinateScaled) x = x * geoCoordinateScaleFactor;

    destPoint.setLocation(satCoords[0], satCoords[1]);
    return destPoint;
  }

  @Override
  public LatLonPoint projToLatLon(ProjectionPoint ppt, LatLonPointImpl destPoint) {
    double x = ppt.getX();
    double y = ppt.getY();
    if (isGeoCoordinateScaled) x = x * geoCoordinateScaleFactor;
    double[] lonlat = navigation.satToEarth(x, y);
    destPoint.setLongitude(lonlat[0]);
    destPoint.setLatitude(lonlat[1]);
    return destPoint;
  }

  @Override
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2))
      return true;

    double x1 = pt1.getX();
    double x2 = pt2.getX();

    if (isGeoCoordinateScaled) {
      x1 = x1 * geoCoordinateScaleFactor;
      x2 = x2 * geoCoordinateScaleFactor;
    }

    // opposite signed X values, larger then 100 km
    return (x1 * x2 < 0) && (Math.abs(x1 - x2) > 100);
  }

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

    Geostationary that = (Geostationary) o;

    if (!navigation.equals(that.navigation)) return false;

    if (!(geoCoordinateScaleFactor == that.geoCoordinateScaleFactor)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    return navigation.hashCode();
  }

  /**
   * Create a ProjectionRect from the given LatLonRect.
   * Handles lat/lon points that do not intersect the projection panel.
   * LOOK NEEDS OVERRIDDING
   * @param rect the LatLonRect
   * @return ProjectionRect, or null if no part of the LatLonRect intersects the projection plane
   */
  @Override
  public ProjectionRect latLonToProjBB(LatLonRect rect) {
    return super.latLonToProjBB(rect);
  }

}


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

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Sets;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.Format;
import org.jdom2.output.LineSeparator;
import org.jdom2.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thredds.client.catalog.Catalog;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.Index;
import ucar.ma2.IndexIterator;
import ucar.nc2.*;
import ucar.nc2.util.URLnaming;
import ucar.nc2.util.xml.Parse;

import java.io.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Helper class to write NcML.
 *
 * @author caron
 * @author cwardgar
 * @see ucar.nc2.NetcdfFile
 * @see http://www.unidata.ucar.edu/software/netcdf/ncml/
 */
public class NcMLWriter {
  /**
   * A default namespace constructed from the NcML URI: {@code http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2}.
   */
  // A default namespace means that we can use it without having to prepend the "ncml:" prefix to every element name.
  // thredds.client.catalog.Catalog.ncmlNS is *not* default and therefore *does* require the prefix.
  public static final Namespace ncmlDefaultNamespace = Namespace.getNamespace(Catalog.NJ22_NAMESPACE);

  private static final Logger log = LoggerFactory.getLogger(NcMLWriter.class);

  //////////////////////////////////////// Variable-writing predicates ////////////////////////////////////////

  /** Predicate that always returns {@code false}. */
  public static final Predicate<Variable> writeNoVariablesPredicate = Predicates.alwaysFalse();

  /**
   * Predicate that returns {@code true} for variables that are {@link Variable#isMetadata() metadata variables}.
   * For such variables, the data is not actually present in the file, so we must include it in the NcML.
   * It could be synthesized (i.e. generated by an IOSP) or specified in a <values> element of the input NcML.
   * */
  public static final Predicate<Variable> writeMetadataVariablesPredicate = Variable::isMetadata;

  /**
   * Predicate that returns {@code true} for variables that are
   * {@link Variable#isCoordinateVariable() coordinate variables}.
   **/
  public static final Predicate<Variable> writeCoordinateVariablesPredicate = Variable::isCoordinateVariable;

  /** Predicate that always returns {@code true}. */
  public static final Predicate<Variable> writeAllVariablesPredicate = Predicates.alwaysTrue();

  /** Predicate that returns {@code true} for variables whose names are specified to the constructor. */
  public static class WriteVariablesWithNamesPredicate implements Predicate<Variable> {
    private final Set<String> variableNames;

    public WriteVariablesWithNamesPredicate(Iterable<String> variableNames) {
      this.variableNames = Sets.newHashSet(variableNames);
    }

    @Override
    public boolean apply(Variable var) {
      return variableNames.contains(var.getFullName());
    }
  }

  //////////////////////////////////////// Instance variables ////////////////////////////////////////

  private Namespace namespace;
  private Format xmlFormat;
  private Predicate<Variable> writeVariablesPredicate;

  private final XMLOutputter xmlOutputter = new XMLOutputter();

  //////////////////////////////////////// Constructors ////////////////////////////////////////

  public NcMLWriter() {
    this.namespace = ncmlDefaultNamespace;
    this.xmlFormat = Format.getPrettyFormat().setLineSeparator(LineSeparator.UNIX);
    this.writeVariablesPredicate = writeMetadataVariablesPredicate;
  }

  //////////////////////////////////////// Getters and setters ////////////////////////////////////////

  /**
   * Gets the XML namespace for the elements in the NcML. By default, it is {@link #ncmlDefaultNamespace}.
   *
   * @return  the XML namespace.
   */
  public Namespace getNamespace() {
    return namespace;
  }

  /**
   * Sets the XML namespace.
   *
   * @param namespace  the new namespace. {@code null} implies {@link Namespace#NO_NAMESPACE}.
   */
  public void setNamespace(Namespace namespace) {
    this.namespace = namespace == null ? Namespace.NO_NAMESPACE : namespace;
  }

  /**
   * Gets the object that encapsulates XML formatting options. By default, the format is
   * {@link Format#getPrettyFormat() pretty} with {@link LineSeparator#UNIX UNIX line separators}.
   *
   * @return  the XML formatting options.
   */
  public Format getXmlFormat() {
    return xmlFormat;
  }

  public void setXmlFormat(Format xmlFormat) {
    this.xmlFormat = Preconditions.checkNotNull(xmlFormat);
  }

  /**
   * Gets the predicate that will be applied to variables to determine wither their values should be written in
   * addition to their metadata. The values will be contained within a {@code <values>} element.
   *
   * @return the predicate.
   */
  public Predicate<Variable> getWriteVariablesPredicate() {
    return writeVariablesPredicate;
  }

  /**
   * Sets the predicate that will be applied to variables to determine whether their values should be written in
   * addition to their metadata. The values will be contained within a {@code <values>} element.
   *

   * By default, the predicate will be {@link #writeMetadataVariablesPredicate}. Their could be data loss if the values
   * of metadata variables aren't included in the NcML, so we recommend that you always use it, possibly as part of a
   * compound predicate. For example, suppose you wanted to print the values of metadata and coordinate
   * variables. Just do:
   *

   * Predicate<Variable> compoundPred = Predicates.or(
   *         writeMetadataVariablesPredicate,
   *         writeCoordinateVariablesPredicate);
   * ncmlWriter.setWriteVariablesPredicate(compoundPred);
   *

   *
   * @param predicate  the predicate to apply.
   */
  public void setWriteVariablesPredicate(Predicate<Variable> predicate) {
    this.writeVariablesPredicate = Preconditions.checkNotNull(predicate);
  }

  //////////////////////////////////////// Writing ////////////////////////////////////////

  /**
   * Writes an NcML element to a string.
   *
   * @param elem an NcML element.
   * @return the string that represents the NcML document.
   */
  public String writeToString(Element elem) {
    try (StringWriter writer = new StringWriter()) {
      writeToWriter(elem, writer);
      return writer.toString();
    } catch (IOException e) {
      throw new AssertionError("CAN'T HAPPEN: StringWriter.close() is a no-op.", e);
    }
  }

  /**
   * Writes an NcML element to an output file.
   *
   * @param elem  an NcML element.
   * @param outFile  the file to write the NcML document to.
   * @throws IOException  if there's any problem writing.
   */
  public void writeToFile(Element elem, File outFile) throws IOException {
    try (OutputStream outStream = new BufferedOutputStream(new FileOutputStream(outFile, false))) {
      writeToStream(elem, outStream);
    }
  }

  /**
   * Writes an NcML element to an output stream.
   *
   * @param elem  an NcML element.
   * @param outStream  the stream to write the NcML document to. Will be closed at end of the method.
   * @throws IOException  if there's any problem writing.
   */
  public void writeToStream(Element elem, OutputStream outStream) throws IOException {
    try (Writer writer = new BufferedWriter(new OutputStreamWriter(
            new BufferedOutputStream(outStream), xmlFormat.getEncoding()))) {
      writeToWriter(elem, writer);
    }
  }

  /**
   * Writes an NcML element to a Writer.
   *
   * @param elem  an NcML element.
   * @param writer  the Writer to write the NcML document to. Will be closed at end of the method.
   * @throws IOException  if there's any problem writing.
   */
  public void writeToWriter(Element elem, Writer writer) throws IOException {
    xmlOutputter.setFormat(xmlFormat);
    elem.detach();  // In case this element had previously been added to a Document.
    xmlOutputter.output(new Document(elem), writer);
  }

  //////////////////////////////////////// Element creation ////////////////////////////////////////

  public Element makeExplicitNetcdfElement(NetcdfFile ncFile, String location) {
    Element netcdfElem = makeNetcdfElement(ncFile, location);
    netcdfElem.addContent(0, new Element("explicit", namespace));
    return netcdfElem;
  }

  public Element makeNetcdfElement(NetcdfFile ncFile, String location) {
    Element rootElem = makeGroupElement(ncFile.getRootGroup());

    // rootElem isn't just like any other group element; we must undo some of the changes made to it in writeGroup().
    rootElem.setName("netcdf");         // Was "group".
    rootElem.removeAttribute("name");   // This attribute is not defined on the root "netcdf" element.

    rootElem.addNamespaceDeclaration(namespace);

    if (null == location)
      location = ncFile.getLocation();

    if (null != location) {
      rootElem.setAttribute("location", URLnaming.canonicalizeWrite(location));
    }

    if (null != ncFile.getId())
      rootElem.setAttribute("id", ncFile.getId());

    if (null != ncFile.getTitle())
      rootElem.setAttribute("title", ncFile.getTitle());

    return rootElem;
  }

  public Element makeGroupElement(Group group) {
    Element elem = new Element("group", namespace);
    elem.setAttribute("name", group.getShortName());

    // enumTypeDef
    for (EnumTypedef etd : group.getEnumTypedefs()) {
      elem.addContent(makeEnumTypedefElement(etd));
    }

    // dimensions
    for (Dimension dim : group.getDimensions()) {
      elem.addContent(makeDimensionElement(dim));
    }

    // regular variables
    for (Variable var : group.getVariables()) {
      boolean showValues = writeVariablesPredicate.apply(var);
      elem.addContent(makeVariableElement(var, showValues));
    }

    // nested groups
    for (Group g : group.getGroups()) {
      Element groupElem = new Element("group", namespace);
      groupElem.setAttribute("name", g.getShortName());
      elem.addContent(makeGroupElement(g));
    }

    // attributes
    for (Attribute att : group.getAttributes()) {
      elem.addContent(makeAttributeElement(att));
    }

    return elem;
  }

  // enum Typedef
  public Element makeEnumTypedefElement(EnumTypedef etd) {
    Element typeElem = new Element("enumTypedef", namespace);
    typeElem.setAttribute("name", etd.getShortName());
    typeElem.setAttribute("type", etd.getBaseType().toString());

    // Use a TreeMap so that the key-value pairs are emitted in a consistent order.
    TreeMap<Integer, String> map = new TreeMap<>(etd.getMap());

    for (Map.Entry<Integer, String> entry : map.entrySet()) {
      typeElem.addContent(new Element("enum", namespace)
              .setAttribute("key", Integer.toString(entry.getKey()))
              .addContent(entry.getValue()));
    }

    return typeElem;
  }

  // Only for shared dimensions.
  public Element makeDimensionElement(Dimension dim) throws IllegalArgumentException {
    if (!dim.isShared()) {
      throw new IllegalArgumentException("Cannot create private dimension: " +
              "in NcML, <dimension> elements are always shared.");
    }

    Element dimElem = new Element("dimension", namespace);
    dimElem.setAttribute("name", dim.getShortName());
    dimElem.setAttribute("length", Integer.toString(dim.getLength()));

    if (dim.isUnlimited())
      dimElem.setAttribute("isUnlimited", "true");

    return dimElem;
  }

  public Element makeVariableElement(Variable var, boolean showValues) {
    boolean isStructure = var instanceof Structure;

    Element varElem = new Element("variable", namespace);
    varElem.setAttribute("name", var.getShortName());

    StringBuilder buff = new StringBuilder();
    List dims = var.getDimensions();
    for (int i = 0; i < dims.size(); i++) {
      Dimension dim = (Dimension) dims.get(i);
      if (i > 0) buff.append(" ");
      if (dim.isShared())
        buff.append(dim.getShortName());
      else if (dim.isVariableLength())
        buff.append("*");
      else
        buff.append(dim.getLength());
    }
    //if (buff.length() > 0)
    varElem.setAttribute("shape", buff.toString());

    DataType dt = var.getDataType();
    if (dt != null) {
      varElem.setAttribute("type", dt.toString());
      if (dt.isEnum())
        varElem.setAttribute("typedef", var.getEnumTypedef().getShortName());
    }

    // attributes
    for (Attribute att : var.getAttributes()) {
      varElem.addContent(makeAttributeElement(att));
    }

    if (isStructure) {
      Structure s = (Structure) var;
      for (Variable variable : s.getVariables()) {
        varElem.addContent(makeVariableElement(variable, showValues));
      }
    } else if (showValues) {
      try {
        varElem.addContent(makeValuesElement(var, true));
      } catch (IOException e) {
        String message = String.format("Couldn't read values for %s. Omitting <values> element.%n\t%s",
                                       var.getFullName(), e.getMessage());
        log.warn(message);
      }
    }

    return varElem;
  }

  public Element makeAttributeElement(Attribute attribute) {
    Element attElem = new Element("attribute", namespace);
    attElem.setAttribute("name", attribute.getShortName());

    DataType dt = attribute.getDataType();
    if ((dt != null) && (dt != DataType.STRING))
      attElem.setAttribute("type", dt.toString());

    if (attribute.getLength() == 0) {
      //if (attribute.isUnsigned())
      //  attElem.setAttribute("isUnsigned", "true");
      return attElem;
    }

    if (attribute.isString()) {
      StringBuilder buff = new StringBuilder();
      for (int i = 0; i < attribute.getLength(); i++) {
        String sval = attribute.getStringValue(i);
        if (i > 0) buff.append("|");
        buff.append(sval);
      }

      attElem.setAttribute("value", Parse.cleanCharacterData(buff.toString()));
      if (attribute.getLength() > 1)
        attElem.setAttribute("separator", "|");

    } else {
      StringBuilder buff = new StringBuilder();
      for (int i = 0; i < attribute.getLength(); i++) {
        Number val = attribute.getNumericValue(i);
        if (i > 0) buff.append(" ");
        buff.append(val.toString());
      }
      attElem.setAttribute("value", buff.toString());

      //if (attribute.isUnsigned())
      //  attElem.setAttribute("isUnsigned", "true");
    }
    return attElem;
  }

  /**
   * Creates a {@code <values>} element from the variable's data.
   *
   * @param variable  the variable to read values from
   * @param allowRegular  {@code true} if regular values should be represented with {@code start}, {@code increment},
   *     and {@code npts} attributes instead of space-separated Element text. Has no effect if the data isn't regular.
   * @return  the {@code <values>} element.
   * @throws IOException  if there was an I/O error when reading the variable's data.
   */
  public Element makeValuesElement(Variable variable, boolean allowRegular) throws IOException {
    Element elem = new Element("values", namespace);

    StringBuilder buff = new StringBuilder();
    Array a = variable.read();

    if (variable.getDataType() == DataType.CHAR) {
      char[] data = (char[]) a.getStorage();
      elem.setText(new String(data));
    } else if (variable.getDataType() == DataType.STRING) {
      elem.setAttribute("separator", "|");
      int count = 0;

      for (IndexIterator iter = a.getIndexIterator(); iter.hasNext(); ) {
        if (count++ > 0) {
          buff.append("|");
        }
        buff.append(iter.getObjectNext());
      }

      elem.setText(buff.toString());
    } else {
      //check to see if regular
      if (allowRegular && (a.getRank() == 1) && (a.getSize() > 2)) {
        Index ima = a.getIndex();
        double start = a.getDouble(ima.set(0));
        double incr = a.getDouble(ima.set(1)) - start;
        boolean isRegular = true;
        for (int i = 2; i < a.getSize(); i++) {
          double v1 = a.getDouble(ima.set(i));
          double v0 = a.getDouble(ima.set(i - 1));
          if (!ucar.nc2.util.Misc.nearlyEquals(v1 - v0, incr))
            isRegular = false;
        }

        if (isRegular) {
          elem.setAttribute("start", Double.toString(start));
          elem.setAttribute("increment", Double.toString(incr));
          elem.setAttribute("npts", Long.toString(variable.getSize()));
          return elem;
        }
      }

      // not regular
      boolean isRealType = (variable.getDataType() == DataType.DOUBLE) || (variable.getDataType() == DataType.FLOAT);
      IndexIterator iter = a.getIndexIterator();
      buff.append(isRealType ? iter.getDoubleNext() : iter.getIntNext());
      while (iter.hasNext()) {
        buff.append(" ");
        buff.append(isRealType ? iter.getDoubleNext() : iter.getIntNext());
      }
      elem.setText(buff.toString());

    } // not string

    return elem;
  }
}


package ucar.nc2.ogc.waterml;

import java.io.IOException;
import java.util.Arrays;
import net.opengis.waterml.x20.MeasureType;
import ucar.ma2.Array;
import ucar.ma2.StructureMembers;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.ft.PointFeature;

/**
 * Created by cwardgar on 2014/03/06.
 */
public abstract class NcMeasureType {
    // wml2:Collection/wml2:observationMember/om:OM_Observation/om:result/wml2:MeasurementTimeseries/wml2:point/
    //         wml2:MeasurementTVP/wml2:value
    public static MeasureType initValue(MeasureType value, PointFeature pointFeat, VariableSimpleIF dataVar)
            throws IOException {
        // TEXT
        StructureMembers.Member firstDataMember = pointFeat.getDataAll().findMember(dataVar.getShortName());
        assert firstDataMember != null : String.format(
                "%s appeared in the list of data variables but not in the StructureData.", dataVar.getShortName());

        Array dataArray = pointFeat.getDataAll().getArray(firstDataMember);
        assert dataArray.getSize() == 1 : String.format("Expected array to be scalar, but its shape was %s.",
                Arrays.toString(dataArray.getShape()));

        double dataVal = dataArray.getDouble(0);
        value.setDoubleValue(dataVal);

        return value;
    }

    private NcMeasureType() { }
}


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

package ucar.nc2.iosp.mcidas;

import ucar.ma2.Array;

import ucar.nc2.Attribute;
import ucar.nc2.AttributeContainer;
import ucar.nc2.dataset.ProjectionCT;

import ucar.nc2.dataset.transform.AbstractTransformBuilder;
import ucar.nc2.dataset.transform.HorizTransformBuilderIF;

/**
 * Projection based on Mcidas Area files.
 *
 * @author caron
 */
public class McIDASAreaTransformBuilder extends AbstractTransformBuilder implements HorizTransformBuilderIF {

  /**
   * Get the transform name
   *
   * @return the transform name
   */
  public String getTransformName() {
    return McIDASAreaProjection.GRID_MAPPING_NAME;
  }

  /**
   * Make the coordinate transform
   *
   * @param ctv the coordinate transform variable
   * @return the coordinate transform
   */
  public ProjectionCT makeCoordinateTransform(AttributeContainer ctv, String units) {

    int[] area = getIntArray(ctv, McIDASAreaProjection.ATTR_AREADIR);
    int[] nav = getIntArray(ctv, McIDASAreaProjection.ATTR_NAVBLOCK);
    int[] aux = null;
    if (ctv.findAttributeIgnoreCase(McIDASAreaProjection.ATTR_AUXBLOCK) != null) {
      aux = getIntArray(ctv, McIDASAreaProjection.ATTR_AUXBLOCK);
    }
    // not clear if its ok if aux is null, coverity is complaining

    McIDASAreaProjection proj = new McIDASAreaProjection(area, nav, aux);
    return new ProjectionCT(ctv.getName(), "FGDC", proj);
  }

  /**
   * get the int array from the variable attribute
   *
   * @param ctv     coordinate transform variable
   * @param attName the attribute name
   * @return the int array
   */
  private int[] getIntArray(AttributeContainer ctv, String attName) {
    Attribute att = ctv.findAttribute(attName);
    if (att == null) {
      throw new IllegalArgumentException("McIDASArea coordTransformVariable " + ctv.getName() + " must have " + attName + " attribute");
    }

    Array arr = att.getValues();
    return (int[]) arr.get1DJavaArray(int.class);
  }
}


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

package ucar.nc2.grib.grib2.table;

import javax.annotation.Nullable;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import ucar.nc2.grib.*;
import ucar.nc2.grib.coord.VertCoordType;
import ucar.nc2.grib.grib1.tables.NcepTables;
import ucar.nc2.grib.grib2.Grib2Parameter;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.net.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * NCEP local tables
 *
 * @author caron
 * @since 4/3/11
 */
class NcepLocalTables extends LocalTables {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(NcepLocalTables.class);

  protected final NcepLocalParams localParams;

  NcepLocalTables(Grib2TableConfig config) {
    super(config);
    this.localParams = new NcepLocalParams(config.getPath());
    initCodes();
  }

  @Override
  public String getTablePath(int discipline, int category, int number) {
    if ((category <= 191) && (number <= 191)) return super.getTablePath(discipline, category, number);
    return localParams.getTablePath(discipline, category);
  }

  private String[] getResourceListing(String path) throws URISyntaxException, IOException {
    Class clazz = this.getClass();
    URL dirURL = clazz.getClassLoader().getResource(path);
    if (dirURL != null && dirURL.getProtocol().equals("file")) {
      return new File(dirURL.toURI()).list();
    }

    if (dirURL == null) {
      //In case of a jar file, we can't actually find a directory.
      //Have to assume the same jar as clazz.
      String me = clazz.getName().replace(".", "/") + ".class";
      dirURL = clazz.getClassLoader().getResource(me);
    }
    if (dirURL == null) {
      throw new UnsupportedOperationException("Cannot list files for path "+path);
    }

    if (dirURL.getProtocol().equals("jar")) {
            /* A JAR path */
      String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
      Enumeration<JarEntry> entries;
      try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"))) {
        //gives ALL entries in jar
        entries = jar.entries();
      }
      Set<String> result = new HashSet<>(); //avoid duplicates in case it is a subdirectory
      while (entries.hasMoreElements()) {
        String name = entries.nextElement().getName();
        if (name.startsWith(path)) { //filter according to the path
          String entry = name.substring(path.length());
          int checkSubdir = entry.indexOf("/");
          if (checkSubdir >= 0) {
            // if it is a subdirectory, we just return the directory name
            entry = entry.substring(0, checkSubdir);
          }
          result.add(entry);
        }
      }
      return result.toArray(new String[0]);
    }

    throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
  }

  @Override
  public List<GribTables.Parameter> getParameters() {
    List<GribTables.Parameter> allParams = new ArrayList<>(3000);
    try {
      String[] fileNames = getResourceListing(config.getPath());
      if (fileNames == null) return allParams;
      for (String fileName : fileNames) {
        File f = new File(fileName);
        if (f.isDirectory()) continue;
        if (!f.getName().contains("Table4.2.")) continue;
        if (!f.getName().endsWith(".xml")) continue;
        try {
          List<GribTables.Parameter> params = localParams.getParameters(config.getPath() + f.getPath());
          if (params != null)
            allParams.addAll(params);
        } catch (Exception e) {
          logger.error("Error reading wmo tables", e);
        }
      }
    } catch (URISyntaxException | IOException e) {
      logger.error("NcepLocalTables failed", e);
    }
    return allParams;
  }

  @Override
  public String getVariableName(int discipline, int category, int parameter) {
    if ((category <= 191) && (parameter <= 191))
      return super.getVariableName(discipline, category, parameter);

    GribTables.Parameter te = getParameter(discipline, category, parameter);
    if (te == null)
      return super.getVariableName(discipline, category, parameter);
    else
      return te.getName();
  }

  @Override
  public GribTables.Parameter getParameter(int discipline, int category, int number) {
    /* email from boi.vuong@noaa.gov 1/19/2012
     "I find that the parameter 2-4-3 (Haines Index) now is parameter 2 in WMO version 8.
      The NAM fire weather nested  will take change in next implementation of cnvgrib (NCEP conversion program)."  */
    //if (makeHash(discipline, category, number) == makeHash(2,4,3))
    //  return getParameter(2,4,2);

    /* email from boi.vuong@noaa.gov 1/26/2012
     The parameter 0-19-242 (Relative Humidity with Respect to Precipitable Water)  was in http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table4-2-0-1.shtml
     It was a mistake in table conversion (from grib1 to grib2) in cnvgrib. It will be fixed in next implementation of cnvgrib in June or July, 2012.
     RHPW  in grib1 in table 129 parameter 230  and in grib2 in 0-1-242  */
    // if (makeHash(discipline, category, number) == makeHash(0, 19, 242))
    //   return getParameter(0, 1, 242);

    Grib2Parameter plocal = localParams.getParameter(discipline, category, number);

    if ((category <= 191) && (number <= 191)) {
      GribTables.Parameter pwmo = WmoParamTable.getParameter(discipline, category, number);
      if (plocal == null) return pwmo;

      // allow local table to override all but name, units
      if (pwmo != null) {
        return new Grib2Parameter(plocal, pwmo.getName(), pwmo.getUnit());
      }
    }

    return plocal;
  }

  @Override
  public GribTables.Parameter getParameterRaw(int discipline, int category, int number) {
     return localParams.getParameter(discipline, category, number);
   }

  @Override
  public String getTableValue(String tableName, int code) {
    if (tableName.equals("ProcessId")) {
      return getGeneratingProcessName(code);
    }

    if ((code < 192) || (code > 254) || tableName.equals("4.0"))
      return super.getTableValue(tableName, code);

    return codeMap.get(tableName + "." + code);
  }

  ////////////////////////////////////////////////////////////////////
  // Vert

  @Override
  public VertCoordType getVertUnit(int code) {

    switch (code) {
      case 235:
        return new VertCoordType(code, "0.1 C", null, true);

      case 237:
        return new VertCoordType(code, "m", null, true);

      case 238:
        return new VertCoordType(code, "m", null, true);

      case 241:
        return new VertCoordType(code, "count", null, true);   // eg see NCEP World Watch datasets

      default:
        return super.getVertUnit(code);
    }
  }

  @Override
  public String getLevelNameShort(int id) {
    switch (id) {
      case 200:
        return "entire_atmosphere_single_layer";
      case 201:
        return "entire_ocean_single_layer";
      case 204:
        return "highest_tropospheric_freezing";
      case 206:
        return "grid_scale_cloud_bottom";
      case 207:
        return "grid_scale_cloud_top";
      case 209:
        return "boundary_layer_cloud_bottom";
      case 210:
        return "boundary_layer_cloud_top";
      case 211:
        return "boundary_layer_cloud";
      case 212:
        return "low_cloud_bottom";
      case 213:
        return "low_cloud_top";
      case 214:
        return "low_cloud";
      case 215:
        return "cloud_ceiling";
      case 220:
        return "planetary_boundary";
      case 221:
        return "between_two_hybrids";
      case 222:
        return "middle_cloud_bottom";
      case 223:
        return "middle_cloud_top";
      case 224:
        return "middle_cloud";
      case 232:
        return "high_cloud_bottom";
      case 233:
        return "high_cloud_top";
      case 234:
        return "high_cloud";
      case 235:
        return "ocean_isotherm";
      case 236:
        return "layer_between_two_depths_below_ocean";
      case 237:
        return "bottom_of_ocean_mixed";
      case 238:
        return "bottom_of_ocean_isothermal";
      case 239:
        return "ocean_surface_and_26C_isothermal";
      case 240:
        return "ocean_mixed";
      case 241:
        return "ordered_sequence_of_data";
      case 242:
        return "convective_cloud_bottom";
      case 243:
        return "convective_cloud_top";
      case 244:
        return "convective_cloud";
      case 245:
        return "lowest_level_of_the_wet_bulb_zero";
      case 246:
        return "maximum_equivalent_potential_temperature";
      case 247:
        return "equilibrium";
      case 248:
        return "shallow_convective_cloud_bottom";
      case 249:
        return "shallow_convective_cloud_top";
      case 251:
        return "deep_convective_cloud_bottom";
      case 252:
        return "deep_convective_cloud_top";
      case 253:
        return "lowest_level_water_layer";
      case 254:
        return "highest_level_water_layer";
      default:
        return super.getLevelNameShort(id);
    }
  }

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

  @Override
  public String getStatisticNameShort(int id) {
    switch (id) {
      case 192:
        return "ClimatologicalMeanValue";
      case 193:
        return "AverageNforecasts";
      case 194:
        return "AverageNanalysis";
      case 195:
        // Average of forecast accumulations. P1 = start of accumulation period. P2 = end of accumulation period. Reference time is the start time of the
        // first forecast, other forecasts at 24-hour intervals. Number in Ave = number of forecasts used.
        return "AverageAccum-24hourIntv";
      case 196:
        return "AverageForecastSuccessiveAccumulations";
      case 197:
        // Average of forecast averages. P1 = start of averaging period. P2 = end of averaging period. Reference time is the start time of the first forecast,
        // other forecasts at 24-hour intervals. Number in Ave = number of forecast used
        return "AverageAvg-24hourIntv";
      case 198:
        return "AverageForecastSuccessiveAverages";
      case 199:
        return "ClimatologicalAverageNanalysis";
      case 200:
        return "ClimatologicalAverageNforecasts";
      case 201:
        return "ClimatologicalRMSdiffNforecasts";
      case 202:
        return "ClimatologicalStandardDeviationNforecasts";
      case 203:
        return "ClimatologicalStandardDeviationNanalyses";
      case 204:
        return "AverageForecastAccumulations-204";
      case 205:
        // Average of forecast averages. P1 = start of averaging period. P2 = end of averaging period. Reference time is the start time of the
        // first forecast, other forecasts at 6-hour intervals. Number in Ave = number of forecast used
        return "AverageAvg-6hourIntv";
      case 206:
        return "AverageForecastAccumulations-206";
      case 207:
        return "AverageForecastAverages-207";
      case 255:
        return "Interval";
      default:
        return super.getStatisticNameShort(id);
    }
  }

  @Override
  @Nullable
  public GribStatType getStatType(int id) {
    if (id < 192) return super.getStatType(id);
    switch (id) {  // LOOK not correct
      case 192:
      case 193:
      case 194:
      case 195:
      case 196:
      case 197:
      case 198:
      case 199:
      case 200:
      case 204:
      case 205:
      case 206:
      case 207:
        return GribStatType.Average;
      case 201:
        return GribStatType.RootMeanSquare;
      case 202:
      case 203:
        return GribStatType.StandardDeviation;
    }

    return null;
  }

  private static Map<Integer, String> statName;  // shared by all instances

  @Override
  @Nullable
  public String getStatisticName(int id) {
    if (id < 192) return super.getStatisticName(id);
    if (statName == null) statName = initTable410();
    if (statName == null) return null;
    return statName.get(id);
  }

  @Nullable
  private Map<Integer, String> initTable410() {
    String path = config.getPath() + "Table4.10.xml";
    try (InputStream is = GribResourceReader.getInputStream(path)) {
      SAXBuilder builder = new SAXBuilder();
      org.jdom2.Document doc = builder.build(is);
      Element root = doc.getRootElement();

      HashMap<Integer, String> result = new HashMap<>(200);
      List<Element> params = root.getChildren("parameter");
      for (Element elem1 : params) {
        int code = Integer.parseInt(elem1.getAttributeValue("code"));
        String desc = elem1.getChildText("description");
        result.put(code, desc);
      }
      return result;  // all at once - thread safe

    } catch (IOException | JDOMException ioe) {
      logger.error("Cant read  " + path, ioe);
      return null;
    }
  }

  /////////////////////////////////////////////////////////////////
  // generating process ids for NCEP
  // GRIB1 TableA - can share (?)
  private static Map<Integer, String> genProcessMap;  // shared by all instances

  @Override
  @Nullable
  public String getGeneratingProcessName(int genProcess) {
    if (genProcessMap == null) genProcessMap = NcepTables.getNcepGenProcess();
    if (genProcessMap == null) return null;
    return genProcessMap.get(genProcess);
  }

  @Override
  @Nullable
  public String getCategory(int discipline, int category) {
    String catName = localParams.getCategory(discipline, category);
    if (catName != null) return catName;
    return super.getCategory(discipline, category);
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  // NCEP local tables taken from degrib code
  // validated against /resources/grib2/tablesOld/grib2StdQuantities.xml

    /* from degrib:
Updated this table last on 12/29/2005
Based on:
http://www.nco.ncep.noaa.gov/pmb/docs/grib2/GRIB2_parmeter_conversion_table.html
Better source is:
http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table4-1.shtml
http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table4-2.shtml
For abreviations see:
http://www.nco.ncep.noaa.gov/pmb/docs/on388/table2.html

Updated again on 2/14/2006
Updated again on 3/15/2006
Updated again on 3/26/2008
*/

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  private final Map<String, String> codeMap = new HashMap<>(400);

  // see http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc.shtml
  private Map<String, String> initCodes() {
    codeMap.put("3.1.204", "Curvilinear_Orthogonal");

    codeMap.put("4.3.192", "Forecast Confidence Indicator");
    codeMap.put("4.3.193", "Bias Corrected Ensemble Forecast");

    codeMap.put("4.5.200", "Entire atmosphere layer");
    codeMap.put("4.5.201", "Entire ocean layer");
    codeMap.put("4.5.204", "Highest tropospheric freezing level");
    codeMap.put("4.5.206", "Grid scale cloud bottom level");
    codeMap.put("4.5.207", "Grid scale cloud top level");
    codeMap.put("4.5.209", "Boundary layer cloud bottom level");
    codeMap.put("4.5.210", "Boundary layer cloud top level");
    codeMap.put("4.5.211", "Boundary layer cloud layer");
    codeMap.put("4.5.212", "Low cloud bottom level");
    codeMap.put("4.5.213", "Low cloud top level");
    codeMap.put("4.5.214", "Low cloud layer");
    codeMap.put("4.5.215", "Cloud ceiling");
    codeMap.put("4.5.220", "Planetary Boundary Layer");
    codeMap.put("4.5.221", "Layer Between Two Hybrid Levels");
    codeMap.put("4.5.222", "Middle cloud bottom level");
    codeMap.put("4.5.223", "Middle cloud top level");
    codeMap.put("4.5.224", "Middle cloud layer");
    codeMap.put("4.5.232", "High cloud bottom level");
    codeMap.put("4.5.233", "High cloud top level");
    codeMap.put("4.5.234", "High cloud layer");
    codeMap.put("4.5.235", "Ocean isotherm level");
    codeMap.put("4.5.236", "Layer between two depths below ocean surface");
    codeMap.put("4.5.237", "Bottom of ocean mixed layer");
    codeMap.put("4.5.238", "Bottom of ocean isothermal layer");
    codeMap.put("4.5.239", "Layer Ocean Surface and 26C Ocean Isothermal Level");
    codeMap.put("4.5.240", "Ocean Mixed Layer");
    codeMap.put("4.5.241", "Ordered Sequence of Data");
    codeMap.put("4.5.242", "Convective cloud bottom level");
    codeMap.put("4.5.243", "Convective cloud top level");
    codeMap.put("4.5.244", "Convective cloud layer");
    codeMap.put("4.5.245", "Lowest level of the wet bulb zero");
    codeMap.put("4.5.246", "Maximum equivalent potential temperature level");
    codeMap.put("4.5.247", "Equilibrium level");
    codeMap.put("4.5.248", "Shallow convective cloud bottom level");
    codeMap.put("4.5.249", "Shallow convective cloud top level");
    codeMap.put("4.5.251", "Deep convective cloud bottom level");
    codeMap.put("4.5.252", "Deep convective cloud top level");
    codeMap.put("4.5.253", "Lowest bottom level of supercooled liquid water layer");
    codeMap.put("4.5.254", "Highest top level of supercooled liquid water layer");

    codeMap.put("4.6.192", "Perturbed Ensemble Member");

    codeMap.put("4.7.192", "Unweighted Mode of All Members");
    codeMap.put("4.7.193", "Percentile value (10%) of All Members");
    codeMap.put("4.7.194", "Percentile value (50%) of All Members");
    codeMap.put("4.7.195", "Percentile value (90%) of All Members");

    codeMap.put("4.10.192", "Climatological Mean Value");
    codeMap.put("4.10.193", "Average of N forecasts");
    codeMap.put("4.10.194", "Average of N uninitialized analyses");
    codeMap.put("4.10.195", "Average of forecast accumulations");
    codeMap.put("4.10.196", "Average of successive forecast accumulations");
    codeMap.put("4.10.197", "Average of forecast averages");
    codeMap.put("4.10.198", "Average of successive forecast averages");
    codeMap.put("4.10.199", "Climatological Average of N analyses, each a year apart");
    codeMap.put("4.10.200", "Climatological Average of N forecasts, each a year apart");
    codeMap.put("4.10.201", "Climatological Root Mean Square difference between N forecasts and their verifying analyses, each a year apart");
    codeMap.put("4.10.202", "Climatological Standard Deviation of N forecasts from the mean of the same N forecasts, for forecasts one year apart");
    codeMap.put("4.10.203", "Climatological Standard Deviation of N analyses from the mean of the same N analyses, for analyses one year apart");
    codeMap.put("4.10.204", "Average of forecast accumulations");
    codeMap.put("4.10.205", "Average of forecast averages");
    codeMap.put("4.10.206", "Average of forecast accumulations");
    codeMap.put("4.10.207", "Average of forecast averages");

    return codeMap;
  }

}


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

import thredds.crawlabledataset.CrawlableDataset;
import thredds.crawlabledataset.CrawlableDatasetFile;
import thredds.crawlabledataset.CrawlableDatasetFilter;
import thredds.catalog.InvService;
import thredds.catalog.InvCatalogFactory;
import thredds.catalog.InvCatalogImpl;
import thredds.catalog.InvDatasetImpl;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Iterator;

/**
 * _more_
 *
 * @author edavis
 * @since Jan 29, 2007 4:48:08 PM
 */
public class CatGenAndWrite
{
  private static org.slf4j.Logger log =
          org.slf4j.LoggerFactory.getLogger( CatGenAndWrite.class );

  private CatalogBuilder catBuilder;
  private CrawlableDataset collectionCrDs;
  private CrawlableDataset topCatCrDs;

  private File topCatWriteDir;

  private CrawlableDatasetFilter collectionOnlyFilter;
  private InvCatalogFactory factory;

  public CatGenAndWrite( String collectionId, String collectionTitle,
                         String collectionUrlId, InvService service,
                         CrawlableDataset collectionCrDs, CrawlableDataset topCatCrDs,
                         CrawlableDatasetFilter filter,
                         InvDatasetImpl topLevelMetadataContainer,
                         File catWriteDir )
  {
    String topCatWritePath = topCatCrDs.getPath().substring( collectionCrDs.getPath().length() );
    File topCatWriteDir = new File( catWriteDir, topCatWritePath );
    if ( ! topCatWriteDir.exists() )
    {
      if ( ! topCatWriteDir.mkdirs() )
      {
        log.error( "CatGenAndWrite(): could not create directory(s) for " + topCatWriteDir.getPath() );
        throw new IllegalArgumentException( "Could not create directory(s) for " + topCatWriteDir.getPath() );
      }
    }
    this.topCatWriteDir = topCatWriteDir;

    catBuilder = new StandardCatalogBuilder( collectionUrlId, collectionTitle,
                                             collectionCrDs, filter, service, collectionId,
                                             null, null, true, null, null,null, topLevelMetadataContainer, null );

    this.collectionCrDs = collectionCrDs;
    this.topCatCrDs = topCatCrDs;

    collectionOnlyFilter = new CollectionOnlyCrDsFilter( filter );
    factory = InvCatalogFactory.getDefaultFactory( false );

  }

  /**
   * Need a few test cases:
   * 1) Anne's TDR case - catalog a collection of TDS served data.
   * 2) Catalog a collection of local data files.
   * 3)
   *
   * @param args
   */
  public static void main1( String[] args )
  {
    // Test case 2: local data files.
    String collectionPath = "C:/Ethan/data/mlode";
    String startPath = "grid/NCEP";
    String catWriteDirPath = "C:/Ethan/data/tmpTest";

    if ( args.length == 3 )
    {
      collectionPath = args[0];
      startPath = args[1];
      catWriteDirPath = args[2];
    }

    File catWriteDir = new File( catWriteDirPath );

    File collectionFile = new File( collectionPath );
    CrawlableDataset collectionCrDs = new CrawlableDatasetFile( collectionFile );
    InvService service = new InvService( "myServer", "File", collectionCrDs.getPath() + "/", null, null);
    CrawlableDatasetFilter filter = null;
    CrawlableDataset topCatCrDs = collectionCrDs.getDescendant( startPath );

    CatGenAndWrite cgaw = new CatGenAndWrite( "DATA", "My data", "", service,
                                              collectionCrDs, topCatCrDs, filter, null, catWriteDir );

    try
    {
      cgaw.genCatAndSubCats( topCatCrDs );
    }
    catch ( IOException e )
    {
      log.error( "I/O error generating and writing catalogs at and under \"" + topCatCrDs.getPath() + "\": " + e.getMessage());
      return;
    }

  }
  public static void main( String[] args )
  {
    // Test case 1: local data files served by TDS.
    String collectionPath = "C:/Ethan/data/mlode";
    String startPath = "grid/NCEP";
    String catWriteDirPath = "C:/Ethan/code/svnThredds/tds/content/thredds/catGenAndWrite";
    //String catWriteDirPath = "C:/Ethan/data/tmpTest2";

    if ( args.length == 3 )
    {
      collectionPath = args[0];
      startPath = args[1];
      catWriteDirPath = args[2];
    }

    File catWriteDir = new File( catWriteDirPath );

    File collectionFile = new File( collectionPath );
    CrawlableDataset collectionCrDs = new CrawlableDatasetFile( collectionFile );
    InvService service = new InvService( "myServer", "OPENDAP", "/thredds/dodsC/", null, null );
    CrawlableDatasetFilter filter = null;
    CrawlableDataset topCatCrDs = collectionCrDs.getDescendant( startPath );

    CatGenAndWrite cgaw = new CatGenAndWrite( "DATA", "My data", "mlode", service,
                                              collectionCrDs, topCatCrDs, filter, null, catWriteDir );

    try
    {
      cgaw.genCatAndSubCats( topCatCrDs );
    }
    catch ( IOException e )
    {
      log.error( "I/O error generating and writing catalogs at and under \"" + topCatCrDs.getPath() + "\": " + e.getMessage() );
      return;
    }
  }

  public void genAndWriteCatalogTree()
          throws IOException
  {
    this.genCatAndSubCats( topCatCrDs);
  }

  private void genCatAndSubCats( CrawlableDataset catCrDs )
          throws IOException
  {

    String catWritePath = catCrDs.getPath().substring( topCatCrDs.getPath().length() );
    File catWriteDir = new File( topCatWriteDir, catWritePath );
    if ( ! catWriteDir.exists() )
    {
      if ( ! catWriteDir.mkdirs() )
      {
        log.error( "genCatAndSubCats(): could not create directory(s) for " + catWriteDir.getPath() );
        throw new IOException( "Could not create directory(s) for " + catWriteDir.getPath() );
      }
    }

    File catFile = new File( catWriteDir, "catalog.xml" );

    InvCatalogImpl cat = catBuilder.generateCatalog( catCrDs );

    factory.writeXML( cat, catFile.getAbsolutePath());

    // Find child datasets that are collections and generate catalogs for those as well.
    List collectionChildren = catCrDs.listDatasets( collectionOnlyFilter);
    for ( Iterator it = collectionChildren.iterator(); it.hasNext(); )
    {
      CrawlableDataset curCrDs = (CrawlableDataset) it.next();

      genCatAndSubCats( curCrDs );
    }

  }

  static class CollectionOnlyCrDsFilter implements CrawlableDatasetFilter
  {
    private CrawlableDatasetFilter filter;

    CollectionOnlyCrDsFilter( CrawlableDatasetFilter filter )
    {
      this.filter = filter;
    }

    public boolean accept( CrawlableDataset dataset )
    {
      if ( dataset.isCollection() )
      {
        if ( filter == null )
          return true;
        else
          return filter.accept( dataset );
      }
      return false;
    }

    public Object getConfigObject()
    {
      return null;
    }
  }
}