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

package thredds.ldm;

import java.io.IOException;
import java.io.EOFException;

/**
 * Class Description.
 *
 * @author org.apache.lucene.store.IndexOutput
 * @author caron
 * @since Aug 14, 2008
 */
public abstract class VlenIO {

  abstract protected void write(byte b) throws IOException;
  abstract protected void write(byte[] b, int offset, int length) throws IOException;

  public int writeByte(byte b) throws IOException {
    write(b);
    return 1;
  }

  public int writeBytes(byte[] b, int offset, int length) throws IOException {
    write(b, offset, length);
    return length;
  }

  public int writeBytes(byte[] b) throws IOException {
    return writeBytes(b, 0, b.length);
  }

  // bigendian
  public final void writeInt(int v) throws IOException {
    write( int3(v));
    write( int2(v));
    write( int1(v));
    write( int0(v));
  }

  // bigendian
  public void writeShort(short v) throws IOException {
    write( short1(v));
    write( short0(v));
  }

  /**
   * Writes an int in a variable-length format.  Writes between one and five
   * bytes.  Smaller values take fewer bytes.  Negative numbers are not
   * supported.
   */
  public int writeVInt(int i) throws IOException {
    int count = 0;
    while ((i & ~0x7F) != 0) {
      writeByte((byte) ((i & 0x7f) | 0x80));
      i >>>= 7;
      count++;
    }
    writeByte((byte) i);
    return count + 1;
  }

  /**
   * Writes an long in a variable-length format.  Writes between one and nine(?)
   * bytes.  Smaller values take fewer bytes.  Negative numbers are not
   * supported.
   */
  public int writeVLong(long i) throws IOException {
    int count = 0;
    while ((i & ~0x7F) != 0) {
      writeByte((byte) ((i & 0x7f) | 0x80));
      i >>>= 7;
      count++;
    }
    writeByte((byte) i);
    return count + 1;
  }

  /**
   * Writes a string.
   *   (vlen) [char]
   */
  public int writeString(String s) throws IOException {
    int length = s.length();
    int count = writeVInt(length);
    count += writeChars(s, 0, length);
    return count;
  }

  /**
   * Writes a sequence of UTF-8 encoded characters from a string.
   *
   * @param s      the source of the characters
   * @param start  the first character in the sequence
   * @param length the number of characters in the sequence
   */
  public int writeChars(String s, int start, int length) throws IOException {
    final int end = start + length;
    int count = 0;
    for (int i = start; i < end; i++) {
      final int code = (int) s.charAt(i);
      if (code >= 0x01 && code <= 0x7F) {
        writeByte((byte) code);
        count++;
      } else if (((code >= 0x80) && (code <= 0x7FF)) || code == 0) {
        writeByte((byte) (0xC0 | (code >> 6)));
        writeByte((byte) (0x80 | (code & 0x3F)));
        count += 2;
      } else {
        writeByte((byte) (0xE0 | (code >>> 12)));
        writeByte((byte) (0x80 | ((code >> 6) & 0x3F)));
        writeByte((byte) (0x80 | (code & 0x3F)));
        count += 3;
      }
    }
    return count;
  }

  // from java.nio.Bits
  private static byte int3(int x) { return (byte)(x >> 24); }
  private static byte int2(int x) { return (byte)(x >> 16); }
  private static byte int1(int x) { return (byte)(x >>  8); }
  private static byte int0(int x) { return (byte)(x >>  0); }

  private static byte short1(short x) { return (byte)(x >> 8); }
  private static byte short0(short x) { return (byte)(x >> 0); }

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

  abstract protected  byte readByte() throws IOException;
  //  return in.readByte();

  abstract protected  void readBytes(byte[] b) throws IOException;
  //  in.readFully(b, 0, b.length);

  public short readShort() throws IOException {
    byte b1 = readByte();
    byte b0 = readByte();
    return (short)((b1 << 8) | (b0 & 0xff));
  }

  public int readVInt() throws IOException {
    byte b = readByte();
    int i = b & 0x7F;
    for (int shift = 7; (b & 0x80) != 0; shift += 7) {
      b = readByte();
      i |= (b & 0x7F) << shift;
    }
    return i;
  }

  public long readVLong() throws IOException {
    byte b = readByte();
    long lval = b & 0x7F;
    for (int shift = 7; (b & 0x80) != 0; shift += 7) {
      b = readByte();
      lval |= (b & 0x7F) << shift;
    }
    return lval;
  }

  //private char[] chars;
  public String readString() throws IOException {
    int length = readVInt();
    // if (chars == null || length > chars.length)
    char[] chars = new char[length];
    readChars(chars, 0, length);
    return new String(chars, 0, length);
  }

  /**
   * Reads UTF-8 encoded characters into an array.
   *
   * @param buffer the array to read characters into
   * @param start  the offset in the array to start storing characters
   * @param length the number of characters to read
   */
  public void readChars(char[] buffer, int start, int length) throws IOException {
    final int end = start + length;
    for (int i = start; i < end; i++) {
      byte b = readByte();
      if ((b & 0x80) == 0)
        buffer[i] = (char) (b & 0x7F);
      else if ((b & 0xE0) != 0xE0) {
        buffer[i] = (char) (((b & 0x1F) << 6)
            | (readByte() & 0x3F));
      } else
        buffer[i] = (char) (((b & 0x0F) << 12)
            | ((readByte() & 0x3F) << 6)
            | (readByte() & 0x3F));
    }
  }

}


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

package ucar.nc2.iosp.sigmet;

import ucar.ma2.IndexIterator;
import ucar.ma2.Range;
import ucar.unidata.io.RandomAccessFile;

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

/**
 * @author yuanho
 * @since Apr 7, 2010
 */
public class Ray {
  private short bins;
  int dataRead;
  int offset;
  int offset1;
  private float range, step, az, elev;
  private short time;
  //  private float[] val;
  String varName;
  int nsweep;
  short datatype;

  public Ray(float range, float step, float az, float elev, short bins, short time, int offset, int dataRead,
             int offset1, int nsweep, String name,
             short datatype) {

    //  this.val = new float[bins];
    setRange(range);
    setStep(step);
    setAz(az);
    setElev(elev);
    setBins(bins);
    setTime(time);
    setOffset(offset);
    setDataRead(dataRead);
    setOffset1(offset1);
    // setVal(val);
    setName(name);
    setNsweep(nsweep);
    setDataType(datatype);

  }

  public short getDataType() {
    return datatype;
  }

  public void setDataType(short datatype) {
    this.datatype = datatype;
  }

  public float getRange() {
    return range;
  }

  public void setRange(float range) {
    this.range = range;
  }

  public float getStep() {
    return step;
  }

  public void setStep(float step) {
    this.step = step;
  }

  public int getNsweep() {
    return nsweep;
  }

  public void setNsweep(int nsweep) {
    this.nsweep = nsweep;
  }

  public float getAz() {
    if (az < 0.0f & az > -361.0f) {
      az = 360.0f + az;
    }

    return az;
  }

  public void setAz(float az) {
    this.az = az;
  }

  public float getElev() {
    return elev;
  }

  public void setElev(float elev) {
    this.elev = elev;
  }

  public short getBins() {
    return bins;
  }

  public void setBins(short bins) {
    this.bins = bins;
  }

  public short getTime() {
    return time;
  }

  public void setTime(short time) {
    this.time = time;
  }

  public int getOffset() {
    return offset;
  }

  public void setOffset(int offset) {
    this.offset = offset;
  }

  public int getDataRead() {
    return dataRead;
  }

  public void setDataRead(int dataRead) {
    this.dataRead = dataRead;
  }

  public int getOffset1() {
    return offset1;
  }

  public void setOffset1(int offset1) {
    this.offset1 = offset1;
  }

  /*   public float[] getVal() {
        return val;
    }

    public void setVal(float[] val) {
        System.arraycopy(val, 0, this.val, 0, bins);
    }
  */
  public void setName(String name) {
    this.varName = name;
  }

  public String getName() {
    return varName;
  }

  public boolean equals(Object o) {
    if (this == o) {
      return true;
    } else if (o instanceof Ray) {
      Ray oo = (Ray) o;

      return (range == oo.range & step == oo.step & az == oo.az & elev == oo.elev & bins == oo.bins
              & time == oo.time);
    } else {
      return false;
    }
  }

  public int hashCode() {
    return new Float(range).hashCode() + new Float(step).hashCode() + new Float(az).hashCode()
            + new Float(elev).hashCode() + new Short(bins).hashCode() + new Short(time).hashCode();

    // val.hashCode();
  }

  public String toString() {
    Formatter sb = new Formatter();

    sb.format("Range=%f Step=%f", range, step);
    if (az > -361.0f & az < 0.0f) {
      az = 360.0f + az;
    }

    sb.format(" Az=%f Elev=%f Bins=%d Time=%d", az, elev, bins, time);
    // for (int i=0; i<bins; i++) { sb.append(" "+val[i]); }
    return sb.toString();
  }

  /**
   * Read data from this record.
   *
   * @param raf       read from this file
   * @param gateRange handles the possible subset of data to return
   * @param ii        put the data here
   * @throws java.io.IOException on read error
   */
  public void readData(RandomAccessFile raf, Range gateRange, IndexIterator ii) throws IOException {
    final int REC_SIZE = 6144;
    raf.seek(offset);
    byte[] data = new byte[bins];
    float[] dd = new float[bins];
    byte d;
    int nb = 0;
    short a00;
    // raf.readFully(data);
    if (dataRead > 0) {
      raf.seek(offset);

      for (int i = 0; i < dataRead; i++) {
        d = raf.readByte();
        dd[i] = SigmetIOServiceProvider.calcData(SigmetIOServiceProvider.recHdr, getDataType(), d);
        nb++;
      }
    }
    // System.out.println("this is az " + getAz());
    raf.seek(offset1);
    int cur_len = offset1;

    while (nb < (int) bins) {
      // --- Check if the code=1 ("1" means an end of a ray)
      a00 = raf.readShort();
      cur_len = cur_len + 2;

      if (a00 == (short) 1) {
        for (int uk = 0; uk < (int) bins; uk++) {
          dd[uk] = -999.99f;
        }
        break;
      }

      if (a00 < 0) {    // -- This is data
        int nwords = a00 & 0x7fff;
        int dataRead1 = nwords * 2;
        int pos = 0;
        if (cur_len % REC_SIZE == 0) {
          pos = 0;
          break;
        }
        raf.seek(cur_len);
        for (int i = 0; i < dataRead1; i++) {
          d = raf.readByte();
          dd[nb] = SigmetIOServiceProvider.calcData(SigmetIOServiceProvider.recHdr, getDataType(), d);
          nb = nb + 1;
          cur_len = cur_len + 1;
          if (nb % REC_SIZE == 0) {
            pos = i + 1;
            break;
          }
        }

        if (pos > 0) {
          break;
        }
      } else if (a00 > 0 & a00 != 1) {
        int num_zero = a00 * 2;
        int dataRead1 = num_zero;
        for (int k = 0; k < dataRead1; k++) {
          dd[nb + k] = SigmetIOServiceProvider.calcData(SigmetIOServiceProvider.recHdr, getDataType(), (byte) 0);
        }
        nb = nb + dataRead1;
        if (cur_len % REC_SIZE == 0) {
          break;
        }

      }
    }                     // ------ end of while for num_bins---------------------------------

    for (int gateIdx : gateRange) {
      if (gateIdx >= bins)
        ii.setFloatNext(Float.NaN);
      else
        ii.setFloatNext(dd[gateIdx]);
    }

  } // end of readData
}    // class Ray end------------------------------------------


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

/**
 * GRIB constants.
 *
 * @author caron
 * @since 2/23/2016.
 */
public class Grib {

  public static final String VARIABLE_ID_ATTNAME = "Grib_Variable_Id";
  public static final String GRIB_VALID_TIME = "GRIB forecast or observation time";
  public static final String GRIB_RUNTIME = "GRIB reference time";
  public static final String GRIB_STAT_TYPE = "Grib_Statistical_Interval_Type";

  // do not use
  public static boolean debugRead = false;
  public static boolean debugGbxIndexOnly = false;  // we are running with only ncx and gbx index files, no data
  static boolean debugIndexOnlyShow = false;  // debugIndexOnly must be true; show record fetch
  static boolean debugIndexOnly = false;      // we are running with only ncx index files, no data

  public static void setDebugFlags(ucar.nc2.util.DebugFlags debugFlag) {
    debugRead = debugFlag.isSet("Grib/showRead");
    debugIndexOnly = debugFlag.isSet("Grib/indexOnly");
    debugIndexOnlyShow = debugFlag.isSet("Grib/indexOnlyShow");
    debugGbxIndexOnly = debugFlag.isSet("Grib/debugGbxIndexOnly");
  }

  // Class, not interface, per Bloch edition 2 item 19
  private Grib() {} // disable instantiation
}


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

package ucar.nc2.ft.point.standard.plug;

import ucar.nc2.constants.CDM;
import ucar.nc2.ft.point.standard.*;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.constants.AxisType;
import ucar.nc2.Dimension;

import java.util.Formatter;

/**
 * Ndbc (National Data Buoy Center) Coards Convention (National Data Buoy Center)
 * @author caron
 * @since Apr 23, 2008
 */
public class NdbcCoards extends TableConfigurerImpl  {

  public boolean isMine(FeatureType wantFeatureType, NetcdfDataset ds) {
    if (!ds.findAttValueIgnoreCase(null, CDM.CONVENTIONS, "").equalsIgnoreCase("COARDS"))
      return false;

    String dataProvider = ds.findAttValueIgnoreCase(null, "data_provider", null);
    if (dataProvider == null)
      dataProvider = ds.findAttValueIgnoreCase(null, "institution", "");
    if (!dataProvider.contains("National Data Buoy Center"))
      return false;

    if (null == ds.findAttValueIgnoreCase(null, "station", null)) return false;
    if (null == ds.findAttValueIgnoreCase(null, "location", null)) return false;

    //if (ds.findVariable("lat") == null) return false;
    //if (ds.findVariable("lon") == null) return false;

    // DODS wont have record !!
    if (!ds.hasUnlimitedDimension()) return false;

    return true;
  }

  /*
  <!-- C:/data/dt2/station/ndbc.nc -->
  <stationFeature>
    <stationId>":station"</stationId>
    <stationDesc>":description"</stationDesc>
    <coordAxis type="lat">lat</coordAxis>
    <coordAxis type="lon">lon</coordAxis>
    <coordAxis type="height">0</coordAxis>
    <table dim="time">
      <coordAxis type="time">time</coordAxis>
    </table>
  </stationFeature>
   */

   public TableConfig getConfig(FeatureType wantFeatureType, NetcdfDataset ds, Formatter errlog) {
    Dimension obsDim = ds.getUnlimitedDimension();
    if (obsDim == null) {
      CoordinateAxis axis = CoordSysEvaluator.findCoordByType(ds, AxisType.Time);
      if ((axis != null) && axis.isScalar())
        obsDim = axis.getDimension(0);
    }

    if (obsDim == null) {
      errlog.format("Must have an Observation dimension: unlimited dimension, or from Time Coordinate");
      return null;
    }
     boolean hasStruct = Evaluator.hasNetcdf3RecordStructure(ds);

    // wants a Point
    if ((wantFeatureType == FeatureType.POINT)) {
      TableConfig nt = new TableConfig(Table.Type.Structure, hasStruct ? "record" : obsDim.getShortName() );
      nt.structName = "record";
      nt.structureType = hasStruct ? TableConfig.StructureType.Structure : TableConfig.StructureType.PsuedoStructure;
      nt.featureType = FeatureType.POINT;
      CoordSysEvaluator.findCoords(nt, ds, null);
      return nt;
    }

    // otherwise, make it a Station
    TableConfig nt = new TableConfig(Table.Type.Top, "station");
    nt.featureType = FeatureType.STATION;

    nt.lat = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Lat);
    nt.lon = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Lon);

    nt.stnId = ds.findAttValueIgnoreCase(null, "station", null);
    nt.stnDesc = ds.findAttValueIgnoreCase(null, "description", null);
    if (nt.stnDesc == null)
      nt.stnDesc = ds.findAttValueIgnoreCase(null, "comment", null);

    TableConfig obs = new TableConfig(Table.Type.Structure, hasStruct ? "record" : obsDim.getShortName());
    obs.structName = "record";
    obs.structureType = hasStruct ? TableConfig.StructureType.Structure : TableConfig.StructureType.PsuedoStructure;
    obs.dimName = obsDim.getShortName();
    obs.time = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Time);
    nt.addChild(obs);

    return nt;
  }
}


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

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

import ucar.ma2.Array;
import ucar.ma2.ArrayDouble;
import ucar.ma2.ArrayDouble.D1;
import ucar.ma2.Index;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Variable;
import ucar.nc2.constants.CDM;
import ucar.nc2.units.SimpleUnit;
import ucar.unidata.util.Parameter;

/**
 * Create a 3D height(z,y,x) array using the CF formula for
 * "atmospheric sigma vertical coordinate".
 *
pressure(x,y,z) = ptop + sigma(z)*surfacePressure(x,y)
 *
 * @author caron
 * @see http://cf-pcmdi.llnl.gov/
 */

public class AtmosSigma extends VerticalTransformImpl {

  /**
   * P-naught identifier
   */
  public static final String PTOP = "PressureTop_variableName";

  /**
   * Surface pressure name identifier
   */
  public static final String PS = "SurfacePressure_variableName";

  /**
   * The "depth" variable name identifier
   */
  public static final String SIGMA = "Sigma_variableName";

  /**
   * The ps, sigma variables
   */
  private Variable psVar;

  /**
   * The sigma array, function of z
   */
  private double[] sigma;

  /**
   * Top of the model
   */
  private double ptop;

  /**
   * Create a new vertical transform for Ocean S coordinates
   *
   * @param ds      dataset
   * @param timeDim time dimension
   * @param params  list of transformation Parameters
   */
  public AtmosSigma(NetcdfFile ds, Dimension timeDim, List<Parameter> params) {
    super(timeDim);

    String psName = getParameterStringValue(params, PS);
    psVar = ds.findVariable(psName);

    String ptopName = getParameterStringValue(params, PTOP);
    Variable ptopVar = ds.findVariable(ptopName);
    try {
      this.ptop = ptopVar.readScalarDouble();
    } catch (IOException e) {
      throw new IllegalArgumentException("AtmosSigma failed to read "
          + ptopVar + " err= " + e.getMessage());
    }

    String sigmaName = getParameterStringValue(params, SIGMA);
    Variable sigmaVar = ds.findVariable(sigmaName);

    try {
      Array data = sigmaVar.read();
      sigma = (double[]) data.get1DJavaArray(double.class);
    } catch (IOException e) {
      throw new IllegalArgumentException("AtmosSigma failed to read "
          + sigmaName + " err= " + e.getMessage());
    }

    units = ds.findAttValueIgnoreCase(psVar, CDM.UNITS, "none");

    String ptopUnitStr = ds.findAttValueIgnoreCase(ptopVar, CDM.UNITS, "none");
    if (!units.equalsIgnoreCase(ptopUnitStr)) {
      // Convert ptopVar to units of psVar
      SimpleUnit psUnit = SimpleUnit.factory(units);
      SimpleUnit ptopUnit = SimpleUnit.factory(ptopUnitStr);
      double factor = ptopUnit.convertTo(1.0, psUnit);
      this.ptop = this.ptop * factor;
    }

  }

  /**
   * Get the 3D vertical coordinate array for this time step.
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @return vertical coordinate array
   * @throws IOException           problem reading data
   * @throws InvalidRangeException _more_
   */
  public ArrayDouble.D3 getCoordinateArray(int timeIndex)
      throws IOException, InvalidRangeException {
    Array ps = readArray(psVar, timeIndex);
    Index psIndex = ps.getIndex();

    int nz = sigma.length;
    int[] shape2D = ps.getShape();
    int ny = shape2D[0];
    int nx = shape2D[1];

    ArrayDouble.D3 result = new ArrayDouble.D3(nz, ny, nx);

    for (int y = 0; y < ny; y++) {
      for (int x = 0; x < nx; x++) {
        double psVal = ps.getDouble(psIndex.set(y, x));
        for (int z = 0; z < nz; z++) {
          result.set(z, y, x, ptop + sigma[z] * (psVal - ptop));
        }
      }
    }

    return result;
  }

  /**
   * Get the 1D vertical coordinate array for this time step and point
   *
   * (needds test!!!)
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @param xIndex    the x index
   * @param yIndex    the y index
   * @return vertical coordinate array
   * @throws java.io.IOException problem reading data
   * @throws ucar.ma2.InvalidRangeException _more_
   */
  public D1 getCoordinateArray1D(int timeIndex, int xIndex, int yIndex)
  		throws IOException, InvalidRangeException {

	    Array ps = readArray(psVar, timeIndex);
	    Index psIndex = ps.getIndex();
	    int nz = sigma.length;
	    ArrayDouble.D1 result = new ArrayDouble.D1(nz);

        double psVal = ps.getDouble(psIndex.set(yIndex, xIndex));
        for (int z = 0; z < nz; z++) {
          result.set(z,  ptop + sigma[z] * (psVal - ptop));
        }

	    return result;

  }

}


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

import com.google.common.base.MoreObjects;
import ucar.nc2.NetcdfFile;
import ucar.nc2.util.Indent;

import java.util.*;

/**
 * A Collection of members contained in a StructureData.
 *
 * @author caron
 */
// Effective Java 2nd Edition, Item 17: Design and document for inheritance or else prohibit it.
public final class StructureMembers {
  private String name;
  private Map<String, Member> memberHash;
  private List<Member> members;
  private int structureSize = -1;

  public StructureMembers(String name) {
    this.name = name;
    members = new ArrayList<>();
  }

  public StructureMembers(StructureMembers from) {
    this.name = from.name;
    members = new ArrayList<>(from.getMembers().size());
    for (Member m : from.members) {
      Member nm = new Member(m); // make copy - without the data info
      addMember( nm);
      if (m.members != null) // recurse
        nm.members = new StructureMembers(m.members);
    }
  }

  /**
   * Get the name.
   *
   * @return the name.
   */
  public String getName() {
    return name;
  }

  /**
   * Add a member.
   *
   * @param m member to add
   */
  public void addMember(Member m) {
    members.add(m);
    if (memberHash != null)
      memberHash.put(m.getName(), m);
  }

  /**
   * Add a member at the given position.
   *
   * @param m member to add
   */
  public void addMember(int pos, Member m) {
    members.add(pos, m);
    if (memberHash != null)
      memberHash.put(m.getName(), m);
  }

  public Member addMember(String name, String desc, String units, DataType dtype, int[] shape) {
    Member m = new Member(name, desc, units, dtype, shape);
    addMember(m);
    return m;
  }

  /**
   * Remove the given member
   * @param m member
   * @return position that it used to occupy, or -1 if not found
   */
  public int hideMember(Member m) {
    if (m == null) return -1;
    int index = members.indexOf(m);
    members.remove(m);
    if (memberHash != null) memberHash.remove(m.getName());
    return index;
  }

  /**
   * Get the total size of the Structure in bytes.
   *
   * @return the total size of the Structure in bytes.
   */
  public int getStructureSize() {
    if (structureSize < 0)
      calcStructureSize();
    return structureSize;
  }

  private void calcStructureSize() {
    structureSize = 0;
    for (Member member : members) {
      structureSize += member.getSizeBytes();
      // System.out.println(member.getName()+" size="+member.getTotalSize());
    }
  }

  /**
   * Set the total size of the Structure in bytes.
   *
   * @param structureSize set to this value
   */
  public void setStructureSize(int structureSize) {
    this.structureSize = structureSize;
  }

  /**
   * Get the list of Member objects.
   *
   * @return the list of Member objects.
   */
  public java.util.List<Member> getMembers() {
    return members;
  }

  /**
   * Get the names of the members.
   *
   * @return List of type String.
   */
  public java.util.List<String> getMemberNames() {
    List<String> memberNames = new ArrayList<>();
    for (Member m : members) {
      memberNames.add(m.getName());
    }
    return memberNames;
  }

  /**
   * Get the index-th member
   *
   * @param index of member
   * @return Member
   */
  public Member getMember(int index) {
    return members.get(index);
  }

  /**
   * Find the member by its name.
   *
   * @param memberName find by this name
   * @return Member matching the name, or null if not found
   */
  public Member findMember(String memberName) {
    if (memberName == null) return null;

    if (memberHash == null) { // delay making the hash table until needed
      int initial_capacity = (int) (members.size() / .75) + 1;
      memberHash = new HashMap<>(initial_capacity);
      for (Member m : members)
        memberHash.put(m.getName(), m);
    }
    return memberHash.get(memberName);
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
            .add("name", name)
            .add("members", members)
            .add("structureSize", structureSize)
            .toString();
  }

  /**
   * A member of a StructureData.
   */
  // Effective Java 2nd Edition, Item 17: Design and document for inheritance or else prohibit it.
  public final class Member {
    private String name, desc, units;
    private DataType dtype;
    private int size = 1;
    private int[] shape;
    private StructureMembers members; // only if member is type Structure
    private boolean isVariableLength = false;

    // optional, use depends on ArrayStructure subclass
    private Array dataArray;
    private Object dataObject;
    private int dataParam;

    public Member(String name, String desc, String units, DataType dtype, int[] shape) {
      this.name = Objects.requireNonNull(name);
      this.desc = desc;
      this.units = units;
      this.dtype = Objects.requireNonNull(dtype);
      setShape(shape);
    }

    public Member(Member from) {
      this.name = from.name;
      this.desc = from.desc;
      this.units = from.units;
      this.dtype = from.dtype;
      setStructureMembers(from.members);
      setShape(from.shape);
    }

    /**
     * If member is type Structure, you must set its constituent members.
     *
     * @param  members set to this value
     * @throws  IllegalArgumentException if {@code members} is this Member's enclosing class instance.
     */
    public void setStructureMembers(StructureMembers members) {
      if (members == StructureMembers.this) {
        throw new IllegalArgumentException(String.format(
                "%s is already the parent of this Member '%s'; it cannot also be the child.", members, this));
      }
      this.members = members;
    }

    public StructureMembers getStructureMembers() {
      return members;
    }

    public void setShape(int[] shape) {
      this.shape = Objects.requireNonNull(shape);
      this.size = (int) Index.computeSize(shape);
      this.isVariableLength = (shape.length > 0 && shape[shape.length - 1] < 0);
    }

    /**
     * Get the short name.
     *
     * @return the short name.
     */
    public String getName() {
      return name;
    }

    public String getFullNameEscaped() {
      return NetcdfFile.makeValidPathName(StructureMembers.this.getName()) + "." +  NetcdfFile.makeValidPathName(name);
    }

    public String getFullName() {
      return StructureMembers.this.getName() + "." +  name;
    }

    /**
     * Get the units string, if any.
     *
     * @return the units string, or null if none.
     */
    public String getUnitsString() {
      return units;
    }

    /**
     * Get the description, if any.
     *
     * @return the description, or null if none.
     */
    public String getDescription() {
      return desc;
    }

    /**
     * Get the DataType.
     *
     * @return the DataType.
     */
    public DataType getDataType() {
      return dtype;
    }

    /**
     * Get the array shape. This does not have to match the VariableSimpleIF.
     *
     * @return the array shape.
     */
    public int[] getShape() {
      return shape;
    }

    /**
     * Get the total number of elements.
     * This does not have to match the VariableSimpleIF.
     *
     * @return the total number of elements.
     */
    public int getSize() {
      return size;
    }

    public boolean isVariableLength() {
      return isVariableLength;
    }

    /**
     * Get the total size in bytes. This does not have to match the VariableSimpleIF.
     *
     * Note that this will not be correct when containing a member of type Sequence, or String, since those
     * are variable length. In that case
     *
     * @return total size in bytes
     */
    public int getSizeBytes() {
      if (getDataType() == DataType.SEQUENCE)
        return getDataType().getSize();
      else if (getDataType() == DataType.STRING)
        return getDataType().getSize();
      else if (getDataType() == DataType.STRUCTURE)
        return size * members.getStructureSize();
      //else if (this.isVariableLength())
      //    return 0; // do not know
      else
        return size * getDataType().getSize();
    }

    /**
     * Is this a scalar (size == 1).
     * This does not have to match the VariableSimpleIF.
     *
     * @return if this is a scalar
     */
    public boolean isScalar() {
      return size == 1;
    }

    ////////////////////////////////////////////////
    // these should not really be public

    /**
     * Get the data parameter value, for use behind the scenes.
     *
     * @return data parameter value
     */
    public int getDataParam() {
      return dataParam;
    }

    /**
     * Set the data parameter value, for use behind the scenes.
     *
     * @param dataParam set to this value
     */
    public void setDataParam(int dataParam) {
      this.dataParam = dataParam;
    }

    /**
     * Get the data array, if any. Used for implementation, DO NOT USE DIRECTLY!
     * @return  data object, may be null
     */
    public Array getDataArray() {
      return dataArray;
    }

    /**
     * Set the data array. Used for implementation, DO NOT USE DIRECTLY!
     * @param data set to this Array. must not be a logical view
     */
    public void setDataArray(Array data) {
      this.dataArray = data;
    }

    /**
     * Get an opaque data object, for use behind the scenes. May be null
     * @return data object, may be null
     */
    public Object getDataObject() {
      return dataObject;
    }

    /**
     * Set an opaque data object, for use behind the scenes.
     * @param o set to this value
     */
    public void setDataObject(Object o) {
      this.dataObject = o;
    }

    public void setVariableInfo(String vname, String desc, String unitString, DataType dtype) {
      if (!this.name.equals(vname) && (memberHash != null)) {
        memberHash.remove(name);
        memberHash.put(vname, this);
      }
      name = vname;

      if (dtype != null)
        this.dtype = dtype;
      if (unitString != null)
        this.units = unitString;
      if (desc != null)
        this.desc = desc;
    }

    public void showInternal(Formatter f, Indent indent) {
      f.format("%sname='%s' desc='%s' units='%s' dtype=%s size=%d dataObject=%s dataParam=%d",
              indent, name, desc, units, dtype, size, dataObject, dataParam);
      if (members != null) {
        indent.incr();
        f.format("%n%sNested members %s%n", indent, members.getName());
        for (StructureMembers.Member m : members.getMembers())
          m.showInternal(f, indent);
        indent.decr();
      }
      f.format("%n");
    }

    public String toString() {
      return name;
    }
  }
}


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

package ucar.nc2.grib.grib1;

import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.nc2.grib.GribNumbers;
import ucar.unidata.io.RandomAccessFile;

import javax.annotation.concurrent.Immutable;
import java.io.IOException;

/**
 * Grib1 Section 3 (BitMap)
 *
 * @author John
 * @since 9/3/11
 */
@Immutable
public class Grib1SectionBitMap {
  private static final Logger logger = LoggerFactory.getLogger(Grib1SectionBitMap.class);

  private final long startingPosition;

  Grib1SectionBitMap(RandomAccessFile raf) throws IOException {
    startingPosition = raf.getFilePointer();

    // octets 1-3 (Length of section)
    int length = GribNumbers.int3(raf);

    raf.seek(startingPosition + length);
  }

  Grib1SectionBitMap(long startingPosition) {
    this.startingPosition = startingPosition;
  }

  public long getStartingPosition() {
    return startingPosition;
  }

  /**
   * Read the bitmap array when needed, return null if none.
   */
  @Nullable
  public byte[] getBitmap(RandomAccessFile raf) throws IOException {
    if (startingPosition <= 0) {
      throw new IllegalStateException("Grib1 Bit map has bad starting position");
    }

    raf.seek(startingPosition);

    // octet 1-3 (length of section)
    int length = GribNumbers.uint3(raf);

    // octet 4 unused bits
    raf.read();   // unused

    // octets 5-6
    int bm = raf.readShort();
    if (bm != 0) {
      logger.warn("Grib1 Bit map section pre-defined (provided by center) bitmap number = {}", bm);
      return null;
    }

    // not sure if length is set correctly when pre-define bitmap is used, so  wait until that to test
    // seeing a -1, bail out
    if (length <= 6 || length > 10e6) {   // look max  ??
      return null;
    }

    // read the bits as integers
    int n = length - 6;
    byte[] data = new byte[n];
    raf.readFully(data);
    return data;
  }

  int getLength(RandomAccessFile raf) throws IOException {
    raf.seek(startingPosition);
    return GribNumbers.uint3(raf);
  }

}


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

// $Id: JTreeTableSorted.java 50 2006-07-12 16:30:06Z caron $

package ucar.nc2.ui.table;

import ucar.nc2.ui.widget.BAMutil;
import ucar.nc2.ui.widget.MyMouseAdapter;
import ucar.nc2.ui.widget.PopupMenu;
import ucar.nc2.util.ListenerManager;
import ucar.util.prefs.PreferencesExt;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.table.*;

import javax.swing.tree.TreePath;

/**
 * JTreeTableSorted adds sorting functionality to a JTreeTable.
 * JTreeTable is a class that combines a JTable with a JTree.
 * Note that JTreeTableSorted is a JPanel, and has-a JTreeTable.
 * It throws ListSelectionEvent events when the selection changes.
 * It throws a UIChangeEvent, property = "sort" just before a sort is going to happen.
 */
public class JTreeTableSorted extends JPanel {
  // for HeaderRenderer
  static private Icon sortDownIcon = BAMutil.getIcon("SortDown", true);
  static private Icon sortUpIcon = BAMutil.getIcon("SortUp", true);
  static private Icon threadSortIcon = BAMutil.getIcon("ThreadSorted", true);
  static private Icon threadUnSortIcon = BAMutil.getIcon("ThreadUnsorted", true);

  // main stuff
  private JTreeTable table;
  private TreeTableModelSorted model;

  private ThreadHeaderRenderer threadHeaderRenderer = null;
  private int threadCol = -1;
  private TableRow selectedRow;

  private JScrollPane scrollPane;
  private PopupMenu popupMenu = null;
  private PopupAction[] acts;

  private boolean treeSort;
  private boolean useThreads;

  private ListenerManager lm;
  private ListSelectionEvent listSelectionEvent = null;

  private MouseAdapter allowSortColChangeMouseListener;
  private boolean allowSortColChange = false;

  private boolean debug = false, debugSetPath = false, debugEvent = false;

  /**
   * Constructor.
   *
   * @param m TreeTableModelSorted m
   */
  public JTreeTableSorted(TreeTableModelSorted m, boolean allowSortColChange) {

    this.model = m;
    this.useThreads = model.useThreads();
    this.treeSort = model.isTreeSort();

    // create the ui
    table = new JTreeTable(model);
    setLayout(new BorderLayout());
    scrollPane = new JScrollPane(table);
    add(scrollPane, BorderLayout.CENTER);

    //table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION);
    table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
    //table.setFont( table.getFont().deriveFont( Font.BOLD));

    // now set the header renderers
    TableColumnModel tcm = table.getColumnModel();
    int ncolwt = useThreads ? table.getColumnCount() - 1 : table.getColumnCount();
    for (int i = 0; i < ncolwt; i++) {
      TableColumn tc = tcm.getColumn(i);
      tc.setHeaderRenderer(new SortedHeaderRenderer(model.getColumnName(i), i));
    }
    if (useThreads) {
      threadCol = ncolwt;
      threadHeaderRenderer = new ThreadHeaderRenderer(threadCol);
      tcm.getColumn(threadCol).setHeaderRenderer(threadHeaderRenderer);
    }

    // popupMenu
    popupMenu = new ucar.nc2.ui.widget.PopupMenu(table.getTableHeader(), "Visible");
    int ncols = model.getColumnCount();
    acts = new PopupAction[ncols];
    for (int i = 0; i < ncols; i++) {
      acts[i] = new PopupAction(model.getColumnName(i));
      popupMenu.addActionCheckBox(model.getColumnName(i), acts[i], true);
    }

    // listen for list selection
    table.getSelectionModel().addListSelectionListener(e -> {
        if (!e.getValueIsAdjusting() && lm.hasListeners() && (listSelectionEvent == null)) {
          listSelectionEvent = e;
          if (debugEvent) System.out.println(" JTreeTableSorted message selected = " + e);
          SwingUtilities.invokeLater(new Runnable() {  // gotta do this after the dust settles

            public void run() {
              lm.sendEvent(listSelectionEvent);
              listSelectionEvent = null;  // dont like this
            }
          }); // new Runnable
        }
    }); // new ListSelectionListener

    // listen for mouse clicks on the column header
    allowSortColChangeMouseListener = new MyMouseAdapter() {
      public void click(MouseEvent e) {
        TableColumnModel tcm2 = table.getColumnModel();
        int colIdx = tcm2.getColumnIndexAtX(e.getX());
        int colNo = table.convertColumnIndexToModel(colIdx);

        // keep track of selection
        selectedRow = getSelectedRow();
        if (debug) System.out.println("----selectedRow = " + selectedRow);

        if (colNo == threadCol) { // toggle threads
          threadHeaderRenderer.setOn(!threadHeaderRenderer.isOn);
          model.setThreadsOn(threadHeaderRenderer.isOn);
          model.sort();
        } else {
          boolean reverse = model.sort(colNo);
          setSortCol(colNo, reverse);
        }

        table.fireDataChanged();
        invokeSetPath();
      }
    };
    allowSortColChange(allowSortColChange);

    // event manager for ListSelection
    lm = new ListenerManager(
            "javax.swing.event.ListSelectionListener",
            "javax.swing.event.ListSelectionEvent",
            "valueChanged");

    // default sort
    setSortCol(model.getSortCol(), model.getReverse());
  }

  /**
   * add ListSelectionEvent listener
   */
  public void addListSelectionListener(ListSelectionListener l) {
    lm.addListener(l);
  }

  /**
   * remove ListSelectionEvent listener
   */
  public void removeListSelectionListener(ListSelectionListener l) {
    lm.removeListener(l);
  }

  public void allowSortColChange(boolean allow) {
    JTableHeader hdr = table.getTableHeader();
    if (allow && !allowSortColChange)
      hdr.addMouseListener(allowSortColChangeMouseListener);
    else if (!allow && allowSortColChange)
      hdr.removeMouseListener(allowSortColChangeMouseListener);
    allowSortColChange = allow;
  }

  public TreeTableModelSorted getModel() {
    return model;
  }

  public JTable getTable() {
    return table;
  }

  public TableRow getRow(int row) {
    return model.getRow(table.getPathForRow(row));
  }

  /**
   * Set the state from the last saved in the PreferencesExt.
   *
   * @param store ok if null or empty
   */
  public void restoreState(PreferencesExt store) {
    if (store == null)
      return;

    int ncols = table.getColumnCount();

    // stored column order
    int[] modelIndex = (int[]) store.getBean("ColumnOrder", null);

    if ((modelIndex != null) && (modelIndex.length == ncols)) { // what about invisible ??

      // make invisible any not stored
      boolean[] visible = new boolean[ncols];
      for (int aModelIndex : modelIndex)
        if (aModelIndex < ncols)
          visible[aModelIndex] = true;

      // modify popup menu
      for (int i = 0; i < ncols; i++)
        if (!visible[i]) {
          //System.out.println( colName[i]+" hide "+i);
          acts[i].hideColumn();
          acts[i].putValue(BAMutil.STATE, new Boolean(false));
        }

      // now set the header order
      TableColumnModel tcm = table.getColumnModel();
      int n = Math.min(modelIndex.length, table.getColumnCount());
      for (int i = 0; i < n; i++) {
        TableColumn tc = tcm.getColumn(i);
        tc.setModelIndex(modelIndex[i]);
        String name = model.getColumnName(modelIndex[i]);
        tc.setHeaderValue(name);
        tc.setIdentifier(name);
        if (useThreads && (modelIndex[i] == threadCol)) {
          threadHeaderRenderer = new ThreadHeaderRenderer(threadCol);
          tc.setHeaderRenderer(threadHeaderRenderer);
        } else
          tc.setHeaderRenderer(new SortedHeaderRenderer(name, modelIndex[i]));
      }
    }

    // set the column widths
    Object colWidths = store.getBean("ColumnWidths", null);
    if (colWidths == null)
      return;
    int[] size = (int[]) colWidths;
    setColumnWidths(size);

    if (debug) {
      System.out.println(" read widths = ");
      for (int aSize : size)
        System.out.print(" " + aSize);
      System.out.println();
    }

    boolean isThreadsOn = store.getBoolean("isThreadsOn", false);
    if (useThreads) {
      model.setThreadsOn(isThreadsOn);
      threadHeaderRenderer.setOn(isThreadsOn);
    }

    int colNo = store.getInt("SortOnCol", 0);
    boolean reverse = store.getBoolean("SortReverse", false);
    model.setSortCol(colNo);
    model.setReverse(reverse);
    setSortCol(colNo, reverse);

    model.sort();
    table.fireDataChanged();
  }

  private void setColumnWidths(int[] sizes) {
    TableColumnModel tcm = table.getColumnModel();
    for (int i = 0; i < table.getColumnCount(); i++) {
      TableColumn tc = tcm.getColumn(i);
      int maxw = ((sizes == null) || (i >= sizes.length)) ? 10 : sizes[i];
      //     model.getPreferredWidthForColumn(tc) : sizes[i];
      tc.setPreferredWidth(maxw);
    }
    //table.sizeColumnsToFit(0);     //  must be called due to a JTable bug
  }

  public void setColOn(int colno, boolean state, int pos) {
    // System.out.println("setColOn "+colno+" "+state+" "+pos);
    acts[colno].putValue(BAMutil.STATE, new Boolean(state));
    if (state)
      acts[colno].addAtPos(pos);
    else
      acts[colno].hideColumn();
  }

  public void registerKeyboardAction(ActionListener act, KeyStroke key, int when) {
    table.registerKeyboardAction(act, key, when);
  }

  public void setFontSize(int size) {
    table.setFont(table.getFont().deriveFont((float) size));
  }

  /**
   * Save state to the PreferencesExt.
   */
  public void saveState(PreferencesExt store) {
    if (store == null)
      return;

    int ncols = table.getColumnCount();
    int[] size = new int[ncols];
    int[] modelIndex = new int[ncols];

    TableColumnModel tcm = table.getColumnModel();
    for (int i = 0; i < ncols; i++) {
      TableColumn tc = tcm.getColumn(i);
      size[i] = tc.getWidth();
      modelIndex[i] = tc.getModelIndex();
    }
    store.putBeanObject("ColumnWidths", size);
    store.putBeanObject("ColumnOrder", modelIndex);

    store.putInt("SortOnCol", model.getSortCol());
    store.putBoolean("SortReverse", model.getReverse());
    store.putBoolean("isThreadsOn", model.isThreadsOn());

    if (debug) {
      System.out.println(" store widths = ");
      for (int aSize : size)
        System.out.print(" " + aSize);
      System.out.println();
    }

  }

  public ArrayList getRows() {
    return model.getRows();
  }

  /**
   * Replace the rowList with this one.
   *
   * @param rows list of rows
   */
  public void setRows(ArrayList rows) {
    model.setRows(rows);

/*    if (rowList.size() > 0)
      table.setRowSelectionInterval(0, 0);
    else
      table.clearSelection(); */

    //table.clearSelection();
    table.fireDataChanged();
  }

  /**
   Remove elem from rowList, update the table.
   Searches for match using object identity (==)
   @param Object elem
    *
   public void removeRow ( Object elem) {
   Iterator iter = rowList.iterator();
   while (iter.hasNext()) {
   Object row = iter.next();
   if (row == elem) {
   iter.remove();
   break;
   }
   }
   table.revalidate();
   } */

  // public int getRowCount() { return table.getRowCount(); }
  //int getSelectedRowIndex() { return table.getSelectedRow(); }   // for SuperComboBox
  //void setSortOK(boolean sortOK) { this.sortOK = sortOK; }       // for SuperComboBox

  /**
   * Get the currently selected row.
   *
   * @return selected TableRow
   */
  public TableRow getSelectedRow() {
    return model.getRow(table.getSelectionPath());
  }

  /**
   * Get the currently selected rows.
   *
   * @return an Iterator whose objects are TableRow
   */
  public Iterator getSelectedRows() {
    TreePath[] paths = table.getSelectionPaths();
    if ((paths == null) || (paths.length < 1))
      return null;

    HashSet set = new HashSet(2 * paths.length);
    for (TreePath path : paths) {
      model.addRowsToSetFromPath(table.getTree(), path, set);
    }

    return set.iterator();
  }

  /**
   * Set the current selection to this row.
   *
   * @param rowno index into rowList
   */
  public void setSelectedRow(int rowno) {
    if ((rowno < 0) || (rowno >= model.getRowCount()))
      return;
    if (debugSetPath) System.out.println("TreeTableSorted setSelected " + rowno);

    selectedRow = model.getRow(rowno);
    TreePath path = model.getPath(selectedRow);
    if (path != null) table.setSelectionPath(path);

    // for mysterious reasons, gotta do it again later
    invokeSetPath();
    ensureRowIsVisible(rowno);
  }

  private void invokeSetPath() {
    // gotta do this after the dust settles
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        TreePath path = model.getPath(selectedRow);
        if (path != null) {
          int rowno = table.setSelectionPath(path);
          if (rowno >= 0) ensureRowIsVisible(rowno);
          if (debugSetPath) System.out.println("----reset selectedRow = " + rowno + " " + path);
        }
      }
    });
  }

  /**
   * Increment or decrement the current selection by one row.
   *
   * @param increment true=increment, false=decrement
   */
  public void incrSelected(boolean increment) {
    int rowno = table.incrSelected(increment);
    if (rowno > 0) ensureRowIsVisible(rowno);
  }

  // Get the JTable delegate so you can do nasty things to it

  public void setDefaultRenderer(Class columnClass, TableCellRenderer renderer) {
    table.setDefaultRenderer(columnClass, renderer);
  }

  /**
   * this array translates the column index to the model index
   */
  public int[] getModelIndex() {
    int[] modelIndex = new int[model.getColumnCount()];

    try {
      TableColumnModel tcm = table.getColumnModel();
      for (int i = 0; i < model.getColumnCount(); i++) {
        TableColumn tc = tcm.getColumn(i);
        modelIndex[i] = tc.getModelIndex();
      }
    } catch (java.lang.ArrayIndexOutOfBoundsException e) {
      //can happen when model size increases
    }

    return modelIndex;
  }

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

  private void ensureRowIsVisible(int nRow) {
    Rectangle visibleRect = table.getCellRect(nRow, 0, true);
    if (debugSetPath) System.out.println("----ensureRowIsVisible = " + visibleRect);
    if (visibleRect != null) {
      visibleRect.x = scrollPane.getViewport().getViewPosition().x;
      table.scrollRectToVisible(visibleRect);
      table.repaint();
    }
  }

  public void setSortCol(int sortCol, boolean reverse) {
    TableColumnModel tcm = table.getColumnModel();
    for (int i = 0; i < table.getColumnCount(); i++) {
      TableColumn tc = tcm.getColumn(i);
      SortedHeaderRenderer shr = (SortedHeaderRenderer) tc.getHeaderRenderer();
      shr.setSortCol(sortCol, reverse);
    }
  }

  private class PopupAction extends AbstractAction {
    private String id;
    private TableColumn tc = null;

    PopupAction(String id) {
      this.id = id;
    }

    public void actionPerformed(ActionEvent e) {
      boolean state = ((Boolean) getValue(BAMutil.STATE)).booleanValue();
      TableColumnModel tcm = table.getColumnModel();

      if (state) {
        if (tc != null) tcm.addColumn(tc);
      } else
        hideColumn();

      JTreeTableSorted.this.revalidate();
      //System.out.println(id+" "+state);
    }

    public void addAtPos(int pos) {
      if (tc == null) return;

      TableColumnModel tcm = table.getColumnModel();

      // make sure it doesnt already exist
      try {
        tcm.addColumn(tc);
        int idx = tcm.getColumnIndex(id);
        tcm.moveColumn(idx, 0);
      } catch (Exception e) {
        System.out.println("addAtPos failed" + e);
      }

    }

    public void hideColumn() {
      //System.out.println("hideColumn "+id);
      TableColumnModel tcm = table.getColumnModel();
      int idx = tcm.getColumnIndex(id);
      tc = tcm.getColumn(idx);
      tcm.removeColumn(tc);
    }
  }

  private class SortedHeaderRenderer implements TableCellRenderer {
    int modelCol;
    Component comp;

    JPanel compPanel;
    JLabel upLabel, downLabel;
    boolean hasSortIndicator = false;
    boolean reverse = false;

    protected SortedHeaderRenderer(int modelCol) {
      this.modelCol = modelCol;
    }

    SortedHeaderRenderer(String name, int modelCol) {
      this.modelCol = modelCol;
      upLabel = new JLabel(sortUpIcon);
      downLabel = new JLabel(sortDownIcon);

      compPanel = new JPanel(new BorderLayout());
      compPanel.setBorder(new BevelBorder(BevelBorder.RAISED));
      compPanel.add(new JLabel(name), BorderLayout.CENTER);
      comp = compPanel;
    }

    void setSortCol(int sortCol, boolean reverse) {
      if (sortCol == modelCol) {

        if (!hasSortIndicator)
          compPanel.add(reverse ? upLabel : downLabel, BorderLayout.EAST);
        else if (reverse != this.reverse) {
          compPanel.remove(1);
          compPanel.add(reverse ? upLabel : downLabel, BorderLayout.EAST);
        }

        this.reverse = reverse;
        hasSortIndicator = true;

        //System.out.println("setSortCol on "+modelCol+" "+sortCol+" "+reverse);
      } else if (hasSortIndicator) {
        compPanel.remove(1);
        hasSortIndicator = false;
        //System.out.println("setSortCol off "+modelCol+" "+sortCol+" "+reverse);
      }
    }

    public Component getTableCellRendererComponent(JTable table, Object value,
                                                   boolean isSelected, boolean hasFocus, int row, int column) {

      return comp;
    }

  }

  private class ThreadHeaderRenderer extends SortedHeaderRenderer {
    JPanel sort, unsort;
    boolean isOn = false;

    ThreadHeaderRenderer(int modelCol) {
      super(modelCol);

      sort = new JPanel(new BorderLayout());
      sort.setBorder(new BevelBorder(BevelBorder.RAISED));
      sort.add(new JLabel(threadSortIcon), BorderLayout.CENTER);

      unsort = new JPanel(new BorderLayout());
      unsort.setBorder(new BevelBorder(BevelBorder.RAISED));
      unsort.add(new JLabel(threadUnSortIcon), BorderLayout.CENTER);

      comp = unsort;
    }

    void setOn(boolean setOn) {
      isOn = setOn;
      comp = (isOn) ? sort : unsort;
    }

    void setSortCol(int sortCol, boolean reverse) { }
  }
}


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

import ucar.nc2.util.Indent;

import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;

/**
 * A StructureData implementation that has its data self-contained.
 * This is often the easiest to construct, but not very efficient for large arrays of Structures.
 * You should call setMemberData() for each member.
 *
 * @author caron
 */

public class StructureDataW extends StructureData {
  protected final Map<StructureMembers.Member,Array> memberData;

  /**
   * Constructor.
   *
   * @param members    StructureData is always contained in a StructureArray.
   */
  public StructureDataW(StructureMembers members) {
    super(members);
    memberData = new HashMap<>(2*members.getMembers().size());
  }

  public StructureDataW(StructureMembers members, int size) {
    super(members);
    memberData = new HashMap<>(2*size);
  }

  /* Copy constructor.
   *  This makes a local copy of all the data in the from StrucureData.
   * @param from copy from here
   *
  public StructureDataW (StructureData from) {
    this(from.getStructureMembers());
    List<StructureMembers.Member> members = getMembers();
    for (StructureMembers.Member m : members) {
      Array data = from.getArray(m);
      setMemberData(m, data.copy());  // LOOK wont work for StructureData
    }
  } */

  public void setMemberData(StructureMembers.Member m, Array data) {
    if (data == null)
      throw new IllegalArgumentException("data cant be null");

    memberData.put(m, data);
  }

  public void setMemberData(String memberName, Array data) {
    StructureMembers.Member m = members.findMember(memberName);
    if (m == null)
      throw new IllegalArgumentException("illegal member name =" + memberName);
    setMemberData(m, data);
  }

  /**
   * Get member data array of any type as an Array.
   * @param m get data from this StructureMembers.Member.
   * @return Array values.
   */
  public Array getArray(StructureMembers.Member m) {
    if (m == null) throw new IllegalArgumentException("member is null");
    return memberData.get(m);
  }

  public float convertScalarFloat(StructureMembers.Member m) {
    return getScalarFloat(m);
  }

  public double convertScalarDouble(StructureMembers.Member m) {
    return getScalarDouble(m);
  }

  public int convertScalarInt(StructureMembers.Member m) {
    return getScalarInt(m);
  }

  public long convertScalarLong(StructureMembers.Member m) {
    return getScalarLong(m);
  }

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

  /**
   * Get member data of type double.
   * @param m get data from this StructureMembers.Member. Must be of type double.
   * @return scalar double value
   */
  public double getScalarDouble(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getDouble(Index.scalarIndexImmutable);
  }

  /**
   * Get java double array for a member of type double.
   * @param m get data from this StructureMembers.Member. Must be of type double.
   * @return 1D java array of doubles
   */
  public double[] getJavaArrayDouble(StructureMembers.Member m) {
    Array data = getArray(m);
    return (double []) data.getStorage();
  }

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

  /**
   * Get member data of type float.
   * @param m get data from this StructureMembers.Member. Must be of type float.
   * @return scalar double value
   */
  public float getScalarFloat(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getFloat(Index.scalarIndexImmutable);
  }

  /**
   * Get java float array for a member of type float.
   * @param m get data from this StructureMembers.Member. Must be of type float.
   * @return 1D java array of floats
   */
  public float[] getJavaArrayFloat(StructureMembers.Member m) {
    Array data = getArray(m);
    return (float []) data.getStorage();
  }

  /////

  /**
   * Get member data of type byte.
   * @param m get data from this StructureMembers.Member. Must be of type byte.
   * @return scalar byte value
   */
  public byte getScalarByte(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getByte(Index.scalarIndexImmutable);
  }

  /**
   * Get java byte array for a member of type byte.
   * @param m get data from this StructureMembers.Member. Must be of type byte.
   * @return 1D java array of bytes
   */
  public byte[] getJavaArrayByte(StructureMembers.Member m) {
    Array data = getArray(m);
    return (byte []) data.getStorage();
  }

  /////

  /**
   * Get member data of type int.
   * @param m get data from this StructureMembers.Member. Must be of type int.
   * @return scalar int value
   */
  public int getScalarInt(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getInt(Index.scalarIndexImmutable);
  }

  /**
   * Get java int array for a member of type int.
   * @param m get data from this StructureMembers.Member. Must be of type int.
   * @return 1D java array of ints
   */
  public int[] getJavaArrayInt(StructureMembers.Member m) {
    Array data = getArray(m);
    return (int []) data.getStorage();
  }

  /////

  /**
   * Get member data of type short.
   * @param m get data from this StructureMembers.Member. Must be of type short.
   * @return scalar short value
   */
  public short getScalarShort(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getShort(Index.scalarIndexImmutable);
  }

  /**
   * Get java short array for a member of type short.
   * @param m get data from this StructureMembers.Member. Must be of type short.
   * @return 1D java array of shorts
   */
  public short[] getJavaArrayShort(StructureMembers.Member m) {
    Array data = getArray(m);
    return (short []) data.getStorage();
  }

  /////

  /**
   * Get member data of type long.
   * @param m get data from this StructureMembers.Member. Must be of type long.
   * @return scalar long value
   */
  public long getScalarLong(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getLong(Index.scalarIndexImmutable);
  }

  /**
   * Get java long array for a member of type long.
   * @param m get data from this StructureMembers.Member. Must be of type long.
   * @return 1D java array of longs
   */
  public long[] getJavaArrayLong(StructureMembers.Member m) {
    Array data = getArray(m);
    return (long []) data.getStorage();
  }

/////

  /**
   * Get member data of type char.
   * @param m get data from this StructureMembers.Member. Must be of type char.
   * @return scalar char value
   */
  public char getScalarChar(StructureMembers.Member m) {
    Array data = getArray(m);
    return data.getChar(Index.scalarIndexImmutable);
  }

  /**
   * Get java char array for a member of type char.
   * @param m get data from this StructureMembers.Member. Must be of type char.
   * @return 1D java array of chars
   */
  public char[] getJavaArrayChar(StructureMembers.Member m) {
    Array data = getArray(m);
    return (char []) data.getStorage();
  }

  /**
   * Get String value, from rank 0 String or rank 1 char member array.
   * @param m get data from this StructureMembers.Member. Must be of type char or String.
   */
  public String getScalarString(StructureMembers.Member m) {
    if (m.getDataType() == DataType.STRING) {
      Array data = getArray(m);
      if (data == null)
        data = getArray(m);
      return (String) data.getObject(0);
    } else {
      char[] ba = getJavaArrayChar(m);
      int count = 0;
      while (count < ba.length) {
        if (0 == ba[count]) break;
        count++;
      }
      return new String(ba, 0, count);
    }
  }

  public String[] getJavaArrayString(StructureMembers.Member m) {
    if (m.getDataType() == DataType.STRING) {
      Array data = getArray(m);
      int n = m.getSize();
      String[] result = new String[n];
      for (int i = 0; i < result.length; i++)
        result[i] = (String) data.getObject(i);
     return result;

    } else if (m.getDataType() == DataType.CHAR) {
      ArrayChar data = (ArrayChar) getArray(m);
      ArrayChar.StringIterator iter = data.getStringIterator();
      String[] result = new String[ iter.getNumElems()];
      int count = 0;
      while (iter.hasNext())
        result[count++] =  iter.next();
      return result;
    }

    throw new IllegalArgumentException("getJavaArrayString: not String DataType :"+m.getDataType());
  }

  /**
   * Get member data of type Structure.
   * @param m get data from this StructureMembers.Member. Must be of type Structure.
   * @return StructureData
   */
  public StructureData getScalarStructure(StructureMembers.Member m) {
    ArrayStructure data = (ArrayStructure) getArray(m);
    return data.getStructureData(0);
  }

  /**
   * Get member data of type Structure.
   * @param m get data from this StructureMembers.Member. Must be of type Structure.
   * @return StructureData
   */
  @Override
  public ArrayStructure getArrayStructure(StructureMembers.Member m) {
    return (ArrayStructure) getArray(m);
  }

  public ArraySequence getArraySequence(StructureMembers.Member m) {
    return (ArraySequence) getArray(m);
  }

  @Override
  public void showInternal(Formatter f, Indent indent) {
    super.showInternal(f, indent);
    for (StructureMembers.Member m : memberData.keySet()) {
      Array data = memberData.get(m);
      f.format("%s %s = %s%n", indent, m, data);
    }

  }

}


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

import thredds.catalog.*;
import thredds.catalog.crawl.CatalogCrawler;
import ucar.nc2.constants.CDM;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;

import org.jdom2.*;
import org.jdom2.output.*;

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

import ucar.nc2.units.TimeDuration;
import ucar.unidata.geoloc.*;
import ucar.unidata.util.StringUtil2;

public class DIFWriter {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DIFWriter.class);

  static private final Namespace defNS = Namespace.getNamespace("http://gcmd.gsfc.nasa.gov/Aboutus/xml/dif/");
  static private String schemaLocation ="http://gcmd.nasa.gov/Aboutus/xml/dif/dif.xsd";

  private String fileDir;
  private StringBuilder messBuffer;
  private boolean debug = false;

  public DIFWriter() {
  }

  /**
   * Write all harvestable datasets to DIF records that have at least the minimum metadata.
   * Call isDatasetUseable() to find out.
   *
   * @param cat harvest the datasets starting from here
   * @param fileDir write records to this directory. The dataset id is used as the filename, appending "dif.xml"
   * @param mess status messages are appended here
   */
  public void writeDatasetEntries( InvCatalogImpl cat, String fileDir, StringBuilder mess) {
    this.fileDir = fileDir;
    this.messBuffer = mess;

    File dir = new File(fileDir);
    if (!dir.exists()) {
      boolean ret = dir.mkdirs();
      assert ret;
    }

    CatalogCrawler.Listener listener = new CatalogCrawler.Listener() {
      public void getDataset(InvDataset ds, Object context) {
        doOneDataset(ds);
      }
      public boolean getCatalogRef(InvCatalogRef dd, Object context) { return true; }

    };

    ByteArrayOutputStream bis = new ByteArrayOutputStream();
    PrintWriter pw = new PrintWriter( new OutputStreamWriter(bis, CDM.utf8Charset));
    CatalogCrawler crawler = new CatalogCrawler( CatalogCrawler.USE_ALL, true, listener);
    crawler.crawl(cat, null, pw, null);
    mess.append("\n*********************\n");
    mess.append(new String(bis.toByteArray(), CDM.utf8Charset));
  }

  /**
   * Write a DIF record for a specific dataset
   * @param ds use this dataset
   */
  public void doOneDataset( InvDataset ds) {
    if (debug) System.out.println("doDataset "+ds.getName());

    if (isDatasetUseable( ds, messBuffer)) {
      String id = StringUtil2.replace(ds.getID(), "/", "-");
      String fileOutName = fileDir+"/"+id+".dif.xml";
      try {
        OutputStream out = new BufferedOutputStream(new FileOutputStream( fileOutName));
        // writeOneEntry(ds, System.out, mess);
        writeOneEntry(ds, out, messBuffer);
        out.close();
        messBuffer.append(" OK on Write\n");
      } catch (IOException ioe) {
        messBuffer.append("DIFWriter failed on write " + ioe.getMessage() + "\n");
        log.error("DIFWriter failed on write "+ioe.getMessage(), ioe);
      }
    }
  }

   /**
   * Write a DIF record for a specific dataset
   * @param ds use this dataset
   * @param fileDir write records to this directory. The dataset id is used as the filename, appending "dif.xml"
   * @param mess status messages are appended here
   */
  public void doOneDataset( InvDataset ds, String fileDir, StringBuilder mess) {
    if (debug) System.out.println("doDataset "+ds.getName());

    if (isDatasetUseable( ds, mess)) {
      String id = StringUtil2.replace(ds.getID(), "/", "-");
      String fileOutName = fileDir+"/"+id+".dif.xml";
      try {
        OutputStream out = new BufferedOutputStream(new FileOutputStream( fileOutName));
        // writeOneEntry(ds, System.out, mess);
        writeOneEntry(ds, out, mess);
        out.close();
        mess.append(" OK on Write\n");
      } catch (IOException ioe) {
        mess.append("DIFWriter failed on write " + ioe.getMessage() + "\n");
        log.error("DIFWriter failed on write "+ioe.getMessage(), ioe);
      }
    }
  }

  /**
   * See if a dataset is harvestable to a DIF record.
   *
   * @param ds check this dataset.
   * @param sbuff  put status messages here.
   * @return true if a DIF record can be written
   */
  public boolean isDatasetUseable(InvDataset ds, StringBuilder sbuff) {
    boolean ok = true;
    sbuff.append("Dataset " + ds.getName() + " id = " + ds.getID() + ": ");

    if (!ds.isHarvest()) {
      ok = false;
      sbuff.append("Dataset " + ds.getName() + " id = " + ds.getID() + " has harvest = false\n");
    }

    if (ds.getName() == null) {
      ok = false;
      sbuff.append(" missing Name field\n");
    }

    if (ds.getUniqueID() == null) {
      ok = false;
      sbuff.append(" missing ID field\n");
    }

    ThreddsMetadata.Variables vs = ds.getVariables("DIF");
    if ((vs == null) || (vs.getVariableList().size() == 0))
      vs = ds.getVariables("GRIB-1");
    if ((vs == null) || (vs.getVariableList().size() == 0))
      vs = ds.getVariables("GRIB-2");
    if ((vs == null) || (vs.getVariableList().size() == 0)) {
      ok = false;
      sbuff.append(" missing Variables with DIF or GRIB compatible vocabulary\n");
    }

    List list = ds.getPublishers();
    if ((list == null) || (list.size() == 0)) {
      ok = false;
      sbuff.append(" must have publisher element that defines the data center\n");
    }

    String summary = ds.getDocumentation("summary");
    if (summary == null) {
      ok = false;
      sbuff.append(" must have documentation element of type summary\n");
    }

    sbuff.append(" useable= "+ok+"\n");
    return ok;
  }

  private void writeOneEntry( InvDataset ds, OutputStream out, StringBuilder mess) throws IOException {
    Element rootElem = new Element("DIF", defNS);
    Document doc = new Document(rootElem);
    writeDataset( ds, rootElem, mess);
    rootElem.addNamespaceDeclaration(defNS);
    rootElem.addNamespaceDeclaration(XMLEntityResolver.xsiNS);
    rootElem.setAttribute("schemaLocation", defNS.getURI()+" "+schemaLocation, XMLEntityResolver.xsiNS);

    // Output the document, use standard formatter
    XMLOutputter fmt = new XMLOutputter( Format.getPrettyFormat());
    fmt.output( doc, out);
  }

  private Iterator translateGribVocabulary(ThreddsMetadata.Variables vs, boolean isGrib1, StringBuilder mess) {
    if (vs == null)
      return null;

    VocabTranslator vt;
    try {
      vt = isGrib1 ? (VocabTranslator) Grib1toDIF.getInstance() : (VocabTranslator) Grib2toDIF.getInstance();
    } catch (IOException e) {
      log.error("DIFWriter failed opening GribtoDIF VocabTranslator ", e);
      return null;
    }

    // hash on DIF names to eliminate duplicates
    Map<String, ThreddsMetadata.Variable> hash = new HashMap<String, ThreddsMetadata.Variable>(100);
    for (ThreddsMetadata.Variable v : vs.getVariableList()) {
      String fromVocabId = v.getVocabularyId();
      if (fromVocabId == null) {
        mess.append("** no id for " + v.getName() + "\n");
        continue;
      }

      String toVocabName = vt.translate(fromVocabId);
      if (toVocabName == null) {
        mess.append("** no translation for " + fromVocabId + " == " + v.getVocabularyName() + "\n");
        continue;
      }

      // do we already have it ?
      if (hash.get(toVocabName) == null) {
        ThreddsMetadata.Variable transV = new ThreddsMetadata.Variable(v.getName(), v.getDescription(), toVocabName,
                v.getUnits(), fromVocabId);
        hash.put(toVocabName, transV);
      }
    }
    return hash.values().iterator();
  }

  private void writeDataset(InvDataset ds, Element rootElem, StringBuilder mess) {
    String entryId = StringUtil2.allow(ds.getUniqueID(), "_-.", '-');
    rootElem.addContent( new Element("Entry_ID", defNS).addContent(entryId));
    rootElem.addContent( new Element("Entry_Title", defNS).addContent(ds.getFullName()));

    // parameters : look for DIF
    ThreddsMetadata.Variables vs = ds.getVariables("DIF");
    boolean hasVocab = (vs != null) && (vs.getVariableList().size() != 0);
    if (hasVocab) {
      for (ThreddsMetadata.Variable v : vs.getVariableList()) {
        writeVariable( rootElem, v);
      }
    } else {
      // look for GRIB-1
      vs = ds.getVariables("GRIB-1");
      if ((vs != null) && (vs.getVariableList().size() != 0)) {
        Iterator iter = translateGribVocabulary(vs, true, mess);
        while (iter.hasNext()) {
          ThreddsMetadata.Variable v = (ThreddsMetadata.Variable) iter.next();
          writeVariable( rootElem, v);
        }
      } else {
        // look for GRIB-2
        vs = ds.getVariables("GRIB-2");
        if ((vs != null) && (vs.getVariableList().size() != 0)) {
          Iterator iter = translateGribVocabulary(vs, false, mess);
          while ((iter != null) && iter.hasNext()) {
            ThreddsMetadata.Variable v = (ThreddsMetadata.Variable) iter.next();
            writeVariable( rootElem, v);
          }
        }
      }
    }

    // keywords
    List<ThreddsMetadata.Vocab> list = ds.getKeywords();
    if (list.size() > 0) {
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Vocab k = list.get(i);
        rootElem.addContent( new Element("Keyword", defNS).addContent( k.getText()));
      }
    }

     //temporal
    DateRange tm = ds.getTimeCoverage();
    if (tm != null) {
      DateType end = tm.getEnd();
      if (end.isPresent()) {
        TimeDuration duration = tm.getDuration();
        double ndays = duration.getValue("days");
        if (ndays > 0) {
          String reletiveTime = "RELATIVE_START_DATE: "+((int)-ndays);
          rootElem.addContent( new Element("Keyword", defNS).addContent(reletiveTime));
        }
      }
    }

    // LOOK KLUDGE - these need to be added to the catalog !!  see http://gcmd.nasa.gov/Resources/valids/sources.html
    Element platform = new Element("Source_Name", defNS);
    rootElem.addContent(platform);
    platform.addContent( new Element("Short_Name", defNS).addContent("MODELS"));

    if (tm != null) {
      Element tmElem = new Element("Temporal_Coverage", defNS);
      rootElem.addContent(tmElem);

      tmElem.addContent(new Element("Start_Date",
                        defNS).addContent(tm.getStart().toString()));
      tmElem.addContent(new Element("Stop_Date",
                        defNS).addContent(tm.getEnd().toString()));
    }

    //geospatial
    ThreddsMetadata.GeospatialCoverage geo = ds.getGeospatialCoverage();
    if (geo != null) {
      Element geoElem = new Element("Spatial_Coverage", defNS);
      rootElem.addContent(geoElem);

      double eastNormal = LatLonPointImpl.lonNormal(geo.getLonEast());
      double westNormal = LatLonPointImpl.lonNormal(geo.getLonWest());

      geoElem.addContent(new Element("Southernmost_Latitude",
                         defNS).addContent(Double.toString(geo.getLatSouth())));
      geoElem.addContent(new Element("Northernmost_Latitude",
                         defNS).addContent(Double.toString(geo.getLatNorth())));
      geoElem.addContent(new Element("Westernmost_Longitude",
                         defNS).addContent(Double.toString(westNormal)));
      geoElem.addContent(new Element("Easternmost_Longitude",
                         defNS).addContent(Double.toString(eastNormal)));
    }

    /* LOOK
    "<Data_Resolution>\n" +
            "    <Latitude_Resolution>12 Km</Latitude_Resolution>\n" +
            "    <Longitude_Resolution>12 Km</Longitude_Resolution>\n" +
            "    <Horizontal_Resolution_Range>10 km - < 50 km or approximately .09 degree - < .5 degree</Horizontal_Resolution_Range>\n" +
            "    <Temporal_Resolution>6 Hours</Temporal_Resolution>\n" +
            "    <Temporal_Resolution_Range>Hourly - < Daily</Temporal_Resolution_Range>\n" +
            "  </Data_Resolution> "
    */

    String rights = ds.getDocumentation("rights");
    if (rights != null)
      rootElem.addContent( new Element("Use_Constraints", defNS).addContent(rights));

    // data center
    List<ThreddsMetadata.Source> plist = ds.getPublishers();
    if (plist.size() > 0) {
      for (ThreddsMetadata.Source p : plist) {
        if (p.getNameVocab().getVocabulary().equalsIgnoreCase("DIF")) {
          Element dataCenter = new Element("Data_Center", defNS);
          rootElem.addContent(dataCenter);
          writeDataCenter(p, dataCenter);
          break;
        }
      }
    }

    String summary = ds.getDocumentation("summary");
    if (summary != null) {
      String summaryLines = StringUtil2.breakTextAtWords(summary, "\n", 80);
      rootElem.addContent( new Element("Summary", defNS).addContent(summaryLines));
    }

    URI uri;
    String href;
    if (ds instanceof InvCatalogRef) {      // LOOK !!
      InvCatalogRef catref = (InvCatalogRef) ds;
      uri = catref.getURI();
      href = uri.toString();
      int pos = href.lastIndexOf('.');
      href = href.substring(0,pos)+".html";

    } else {
      InvCatalogImpl cat = (InvCatalogImpl) ds.getParentCatalog();
      uri = cat.getBaseURI();
      String catURL = uri.toString();
      int pos = catURL.lastIndexOf('.');
      href = catURL.substring(0,pos)+".html";
      if (ds.hasAccess())
        href = href+"?dataset="+ds.getID();
    }

    rootElem.addContent( makeRelatedURL("GET DATA", "THREDDS CATALOG", uri.toString()));
    rootElem.addContent( makeRelatedURL("GET DATA", "THREDDS DIRECTORY", href));

    InvAccess access;
    if (null != (access = ds.getAccess(ServiceType.OPENDAP))) {
      rootElem.addContent( makeRelatedURL("GET DATA", "OPENDAP DATA", access.getStandardUrlName()));
    }
    if (null != (access = ds.getAccess(ServiceType.DAP4))) {
      rootElem.addContent( makeRelatedURL("GET DATA", "DAP4 DATA", access.getStandardUrlName()));
    }

    rootElem.addContent(new Element("Metadata_Name", defNS).addContent("CEOS IDN DIF"));
    rootElem.addContent(new Element("Metadata_Version", defNS).addContent("9.4"));
    DateType today = new DateType(false, new Date());
    rootElem.addContent(new Element("DIF_Creation_Date", defNS).addContent(today.toDateTimeStringISO()));
  }

  private Element makeRelatedURL(String type, String subtype, String url) {
    Element elem = new Element("Related_URL", defNS);
    Element uctElem = new Element("URL_Content_Type", defNS);
    elem.addContent(uctElem);
    uctElem.addContent( new Element("Type", defNS).addContent(type));
    uctElem.addContent( new Element("Subtype", defNS).addContent(subtype));
    elem.addContent( new Element("URL", defNS).addContent(url));
    return elem;
  }

  private void writeDataCenter(ThreddsMetadata.Source p, Element dataCenter) {
    Element name = new Element("Data_Center_Name", defNS);
    dataCenter.addContent( name);
    // dorky
    StringTokenizer stoker = new StringTokenizer(p.getName(), ">");
    int n = stoker.countTokens();
    if (n == 2) {
      name.addContent( new Element("Short_Name", defNS).addContent(stoker.nextToken().trim()));
      name.addContent( new Element("Long_Name", defNS).addContent(stoker.nextToken().trim()));
    } else {
      name.addContent( new Element("Short_Name", defNS).addContent(p.getName()));
    }

    if ((p.getUrl() != null) && p.getUrl().length() > 0)
      dataCenter.addContent( new Element("Data_Center_URL", defNS).addContent(p.getUrl()));

    Element person = new Element("Personnel", defNS);
    dataCenter.addContent( person);
    person.addContent( new Element("Role", defNS).addContent("DATA CENTER CONTACT"));
    person.addContent( new Element("Last_Name", defNS).addContent("Any"));
    person.addContent( new Element("Email", defNS).addContent(p.getEmail()));
  }

  private void writeVariable( Element rootElem, ThreddsMetadata.Variable v) {
    String vname = v.getVocabularyName();
    StringTokenizer stoker = new StringTokenizer(vname,">");
    int n = stoker.countTokens();
    if (n < 3) return; // gottta have the first 3

    Element param = new Element("Parameters", defNS);
    rootElem.addContent(param);

    if (stoker.hasMoreTokens())
      param.addContent( new Element("Category", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Topic", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Term", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Variable", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Detailed_Variable", defNS).addContent(stoker.nextToken().trim()));
  }

  // test
  private static void doCatalog( InvCatalogFactory fac, String url) {
    System.out.println("***read "+url);
    try {
      InvCatalogImpl cat = fac.readXML(url);
      StringBuilder buff = new StringBuilder();
      boolean isValid = cat.check( buff, false);
      System.out.println("catalog <" + cat.getName()+ "> "+ (isValid ? "is" : "is not") + " valid");
      System.out.println(" validation output=\n" + buff);
      System.out.println(" catalog=\n" + fac.writeXML(cat));

      DIFWriter w = new DIFWriter();
      StringBuilder sbuff = new StringBuilder();
      w.writeDatasetEntries( cat, "C:/temp/dif2", sbuff);
      System.out.println(" messages=\n"+sbuff);
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

   /** testing */
  public static void main (String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(true);

    doCatalog(catFactory, "http://thredds.ucar.edu:9080/thredds/idd/models.xml");
  }

}


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

import thredds.client.catalog.Dataset;
import thredds.client.catalog.DatasetNode;
import thredds.client.catalog.Service;
import thredds.client.catalog.ThreddsMetadata;

import java.util.*;

/**
 * Builder of client catalog Dataset
 *
 * @author caron
 * @since 1/8/2015
 */
public class DatasetBuilder {

  /**
   * Utility routine to keep list of objects small.
   * add fldValue to the fldName list in flds.
   * fldValue may be a list or an object.
   * if no list, just keep the object without creating a list (common case).
   * otherwise add it to the existing list.
   */
  public static void addToList(Map<String, Object> flds, String fldName, Object fldValue) {
    if (fldValue == null) return;
    Object prevVal = flds.get(fldName);
    if (prevVal == null) {
      flds.put(fldName, fldValue);
      return;
    }

    List prevList;
    if (prevVal instanceof List) {
      prevList = (List) prevVal;
    } else {
      prevList = new ArrayList(5);
      prevList.add(prevVal);
      flds.put(fldName, prevList);
    }

    if (fldValue instanceof List) {
      prevList.addAll((List) fldValue);
    } else {
      prevList.add(fldValue);
    }
  }

  public static void addToNewList(Map<String, Object> flds, String fldName, Object fldValue) {
    if (fldValue == null) return;
    List prevList;
    Object prevVal = flds.get(fldName);
    if (prevVal == null) {
      prevList = new ArrayList(5);
      flds.put(fldName, prevList);
    } else {
      prevList = (List) prevVal;
    }

    if (fldValue instanceof List) {
      prevList.addAll((List) fldValue);
    } else {
      prevList.add(fldValue);
    }
  }

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

  protected DatasetBuilder parent;  // null when its a top level dataset
  protected String name;
  protected Map<String, Object> flds = new HashMap<>(10);

  protected List<AccessBuilder> accessBuilders;
  protected List<DatasetBuilder> datasetBuilders;
  protected List<Service> services;

  public DatasetBuilder(DatasetBuilder parent) {
    this.parent = parent;
  }

  // turn a Dataset back into a DatasetBuilder so it mutable
  public DatasetBuilder(DatasetBuilder parent, Dataset from) {
    this.parent = parent;
    this.name = from.getName();
    for (Map.Entry<String, Object> entry : from.getFldIterator()) {
      if (!entry.getKey().equals(Dataset.Datasets) && !entry.getKey().equals(Dataset.Access)) // set seperately
        this.flds.put(entry.getKey(), entry.getValue());
    }
  }

  public DatasetBuilder getParent() {
    return parent;
  }

  private Object getInherited(String fldName) {
    ThreddsMetadata tmi = (ThreddsMetadata) get(Dataset.ThreddsMetadataInheritable);
    if (tmi != null) {
      Object value = tmi.getLocalField(fldName);
      if (value != null) return value;
    }
    if (parent != null) return parent.getInherited(fldName);
    return null;
  }

  public Object getFldOrInherited(String fldName) {
    Object value = flds.get(fldName);
    if (value != null) return value;
    return getInherited(fldName);
  }

  public Object get(String fldName) {
    return flds.get(fldName);
  }

  public void put(String fldName, Object fldValue) {
    if (fldValue != null)
      flds.put(fldName, fldValue);
    else
      flds.remove(fldName);
  }

  public void putInheritedField(String fldName, Object fldValue) {
    if (fldValue == null) return;

    ThreddsMetadata tmi = (ThreddsMetadata) get(Dataset.ThreddsMetadataInheritable);
    if (tmi == null) {
      tmi = new ThreddsMetadata();
      put(Dataset.ThreddsMetadataInheritable, tmi);
    }
    tmi.getFlds().put(fldName, fldValue);
  }

  public void addToList(String fldName, Object fldValue) {
    if (fldValue != null) addToList(flds, fldName, fldValue);
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void addDataset(DatasetBuilder d) {
    if (d == null) return;
    if (datasetBuilders == null) datasetBuilders = new ArrayList<>();
    datasetBuilders.add(d);
  }

  public void addAccess(AccessBuilder d) {
    if (accessBuilders == null) accessBuilders = new ArrayList<>();
    accessBuilders.add(d);
  }

  public void addServiceToCatalog(Service s) {
    if (s == null) return;
    if (services == null) services = new ArrayList<>();
    services.add(s);
  }

  public Dataset makeDataset(DatasetNode parent) {  // LOOK whats relationship of parent with this.parent ??
    ThreddsMetadata tmi = (ThreddsMetadata) get(Dataset.ThreddsMetadataInheritable);
    if (tmi != null) tmi.finish();
    return new Dataset(parent, name, flds, accessBuilders, datasetBuilders);
  }

  // make an immutable copy without changin DatasetBuilder
  public Dataset copyDataset(DatasetNode parent) {
    return new Dataset(parent, name, flds, accessBuilders, datasetBuilders);
  }

  public Iterable<DatasetBuilder> getDatasets() {
    if (datasetBuilders != null) return datasetBuilders;
    return new ArrayList<>(0);
  }

  public Iterable<Service> getServices() {
    if (services != null) return services;
    return new ArrayList<>(0);
  }

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

  // transfer all metadata, optionally also inheritable metadata from parents
  public void transferMetadata( DatasetNode from, boolean parentsAlso) {
    if (parentsAlso) {
      ThreddsMetadata inherit = getInheritableMetadata(); // make sure exists
      inheritMetadata(from, inherit.getFlds());
    }

    // local metadata
    for (Map.Entry<String, Object> entry : from.getFldIterator()) {
      if (parentsAlso && entry.getKey().equals(Dataset.ThreddsMetadataInheritable)) continue; // already did this
      if (Dataset.listFlds.contains(entry.getKey()))
        addToNewList(flds, entry.getKey(), entry.getValue());
      else
        flds.put(entry.getKey(), entry.getValue());
    }

    // tmi must be mutable, transfer if not
    ThreddsMetadata tmiOld = (ThreddsMetadata) get(Dataset.ThreddsMetadataInheritable);
    if (tmiOld != null && tmiOld.isImmutable()) {
      ThreddsMetadata tmiNew = new ThreddsMetadata(tmiOld);
      flds.put(Dataset.ThreddsMetadataInheritable, tmiNew);
    }
  }

  // transfer inherited metadata only, always include parents
  // place directly into flds (not in this.tmi) LOOK why not into tmi ?? LOOK put into tmi, see what breaks!
  public void transferInheritedMetadata( DatasetNode from) {
    ThreddsMetadata tmi = getInheritableMetadata();
    inheritMetadata(from, tmi.getFlds());
  }

  private void inheritMetadata(DatasetNode from, Map<String, Object> toFlds) {
   // depth first, so closer parents override;
    Dataset fromParent = from.getParentDataset();
    if (fromParent != null) {
      inheritMetadata(fromParent, toFlds);
    }

    ThreddsMetadata tmi = (ThreddsMetadata) from.get(Dataset.ThreddsMetadataInheritable);
    if (tmi == null) return;

    for (Map.Entry<String, Object> entry : tmi.getFldIterator()) {
      if (Dataset.listFlds.contains(entry.getKey()))
        addToNewList(toFlds, entry.getKey(), entry.getValue());
      else
        toFlds.put(entry.getKey(), entry.getValue());
    }
  }

  // get the inheritable ThreddsMetadata object. If doesnt exist, create new, empty one
  public ThreddsMetadata getInheritableMetadata() {
    ThreddsMetadata tmi = (ThreddsMetadata) get(Dataset.ThreddsMetadataInheritable);
    if (tmi == null) {
      tmi = new ThreddsMetadata();
      put(Dataset.ThreddsMetadataInheritable, tmi);
    }
    return tmi;
  }

}