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

import java.io.*;
import java.net.URL;

/**
 * Class to read a dbase file in its entirety.
 *
 * @author Kirk Waters, NOAA Coastal Services Center, 1997.
 * @author Russ Rew, modified to restrict access to read-only
 */
public class DbaseFile {

  static public int DBASEIII = 0;
  static public int DBASEIV = 1;
  static public int DBASE5DOS = 2;
  static public int DBASE5WIN = 3;

  URL url;
  byte filetype;
  int nrecords;
  int nfields;
  short nbytesheader;
  DbaseFieldDesc[] FieldDesc;
  DbaseData[] data;
  byte[] Header;
  private boolean headerLoaded = false;
  private boolean dataLoaded = false;
  InputStream stream = null;
  DataInputStream ds = null;

  /**
   * @param url URL to the *.dbf file
   */
  public DbaseFile(URL url) throws java.io.IOException {
    this.url = url;
    stream = url.openStream();
  }

  /**
   * @param spec Location of the *.dbf file, as either a URL or filename
   */
  public DbaseFile(String spec) throws java.io.IOException {
    try {
      url = new URL(spec);
      stream = url.openStream();
    } catch (java.net.MalformedURLException e) {
      stream = new FileInputStream(spec);
    }
  }

  /**
   * @param file A file object of the *.dbf file.
   */
  public DbaseFile(File file) {
    try {
      stream = new FileInputStream(file);
    } catch (java.io.FileNotFoundException e) {
      System.out.println("Failed to open file " + file);
      stream = null;
    }
  }

  public DbaseFile(InputStream s) {
    stream = s;
  }

  /**
   * Load the dbase file header.
   *
   * @return 0 for success, -1 for failure
   */
  private int loadHeader() {
    if (headerLoaded) return 0;
    InputStream s = stream;
    if (s == null) return -1;
    try {
      BufferedInputStream bs = new BufferedInputStream(s);
      ds = new DataInputStream(bs);
      /* read the header as one block of bytes*/
      Header = new byte[32];
      ds.readFully(Header);
      //System.out.println("dbase header is " + Header);
      if (Header[0] == '<') { //looks like html coming back to us!
        close(ds);
        return -1;
      }
      filetype = Header[0];
      /* 4 bytes for number of records is in little endian */
      nrecords = Swap.swapInt(Header, 4);
      nbytesheader = Swap.swapShort(Header, 8);

      /* read in the Field Descriptors */
      /* have to figure how many there are from
      * the header size.  Should be nbytesheader/32 -1
      */
      nfields = (nbytesheader / 32) - 1;
      if (nfields < 1) {
        System.out.println("nfields = " + nfields);
        System.out.println("nbytesheader = " +
                nbytesheader);
        return -1;
      }
      FieldDesc = new DbaseFieldDesc[nfields];
      data = new DbaseData[nfields];
      for (int i = 0; i < nfields; i++) {
        FieldDesc[i] = new DbaseFieldDesc(ds, filetype);
        data[i] = new DbaseData(FieldDesc[i],
                nrecords);
      }

      /* read the last byte of the header (0x0d) */
      ds.readByte();

      headerLoaded = true;
    } catch (java.io.IOException e) {
      close(s);
      return -1;
    }
    return 0;
  }

  /**
   * Load the dbase file data.
   *
   * @return 0 for success, -1 for failure
   */
  private int loadData() {
    if (!headerLoaded) return -1;
    if (dataLoaded) return 0;
    InputStream s = stream;
    if (s == null) return -1;
    try {
      /* read in the data */
      for (int i = 0; i < nrecords; i++) {
        /* read the data record indicator */
        byte recbyte = ds.readByte();
        if (recbyte == 0x20) {
          for (int j = 0; j < nfields; j++) {
            data[j].readRowN(ds, i);
          }
        } else {
          /* a deleted record */
          nrecords--;
          i--;
        }
      }
      dataLoaded = true;
    } catch (java.io.IOException e) {
      close(s);
      return -1;
    } finally {
      close(s);
    }
    return 0;
  }

  private void close(InputStream d) {
    if (d == null) return;
    try {
      d.close();
    } catch (java.io.IOException e) {
      System.out.printf("filed to close %s%n", e);
    }
  }

  /**
   * Extract the data for a field by field index number.
   *
   * @param index Column number of the field to extract.
   * @return A DbaseData object if the column is within bounds. Otherwise, null.
   */
  public DbaseData getField(int index) {
    if (index < 0 || index >= nfields) return null;
    return data[index];
  }

  /**
   * Extract the data for a given field by name.
   *
   * @param Name String with the name of the field to retrieve.
   * @return A DbaseData object if the name was found or null if not found
   */
  public DbaseData getField(String Name) {
    for (int i = 0; i < nfields; i++) {
      if (FieldDesc[i].Name.equals(Name)) return data[i];
    }
    return null;
  }

  /**
   * Extract the double array of data for a field by Name.
   *
   * @param Name String with the name of the field to retrieve
   * @return A double[] if valid numeric field, otherwise null
   */
  public double[] getDoublesByName(String Name) {
    DbaseData d;
    if ((d = getField(Name)) == null) return null;
    if (d.getType() == DbaseData.TYPE_CHAR) {
      String[] s = d.getStrings();
      double[] dd = new double[s.length];
      for (int i = 0; i < s.length; i++) {
        dd[i] = Double.valueOf(s[i]);
      }
      return dd;
    }
    if (d.getType() == DbaseData.TYPE_BOOLEAN) {
      boolean[] b = d.getBooleans();
      double[] dd = new double[b.length];
      for (int i = 0; i < b.length; i++) {
        if (b[i]) {
          dd[i] = 1;
        } else {
          dd[i] = 0;
        }
      }
      return dd;

    }
    return d.getDoubles();
  }

  /**
   * Extract the string array of data for a field by Name.
   *
   * @param Name String with the name of the field to retrieve
   * @return A String[] if valid character field, otherwise null
   */
  public String[] getStringsByName(String Name) {
    DbaseData d;
    if ((d = getField(Name)) == null) return null;
    if (d.getType() != DbaseData.TYPE_CHAR) return null;
    return d.getStrings();
  }

  /**
   * Extract the boolean array of data for a field by Name.
   *
   * @param Name String with the name of the field to retrieve
   * @return A boolean[] if valid character field, otherwise null
   */
  public boolean[] getBooleansByName(String Name) {
    DbaseData d;
    if ((d = getField(Name)) == null) return null;
    if (d.getType() != DbaseData.TYPE_BOOLEAN) return null;
    return d.getBooleans();
  }

  /**
   * Get the name of a field by column number.
   *
   * @param i The column number of the field name.
   * @return A String with the field name or null if out of bounds
   */
  public String getFieldName(int i) {
    if (i >= nfields || i < 0) {
      return null;
    }
    return (FieldDesc[i].Name);
  }

  /**
   * Get a list of all the field names in the dbase file
   *
   * @return A String array of all the field names
   */
  public String[] getFieldNames() {
    String[] s = new String[nfields];
    for (int i = 0; i < nfields; i++) {
      s[i] = getFieldName(i);
    }
    return s;
  }

  /**
   * Get the number of fields in the file.
   *
   * @return number of fields
   */
  public int getNumFields() {
    return (nfields);
  }

  /**
   * @return number of records in the dbase file
   */
  public int getNumRecords() {
    return (nrecords);
  }

  /**
   * @return Boolean true if the data has been loaded, otherwise false.
   */
  public boolean isLoaded() {
    return (dataLoaded);
  }

  /**
   * Test program, dumps a Dbase file to stdout.
   */
  public static void main(String[] args) {
    if (args.length < 1) {
      System.out.println("filename or URL required");
      System.exit(-1);
    }
    for (String s : args) {
      System.out.println("*** Dump of Dbase " + s + ":");
      try {
        DbaseFile dbf = new DbaseFile(s);
        // load() method reads all data at once
        if (dbf.loadHeader() != 0) {
          System.out.println("Error loading header" + s);
          System.exit(-1);
        }

        // output schema as [type0 field0, type1 field1, ...]
        String[] fieldNames = dbf.getFieldNames();
        System.out.print("[");

        int nf = dbf.getNumFields();
        DbaseData[] dbd = new DbaseData[nf];
        for (int field = 0; field < nf; field++) {
          dbd[field] = dbf.getField(field);
          switch (dbd[field].getType()) {
            case DbaseData.TYPE_BOOLEAN:
              System.out.print("boolean ");
              break;
            case DbaseData.TYPE_CHAR:
              System.out.print("String ");
              break;
            case DbaseData.TYPE_NUMERIC:
              System.out.print("double ");
              break;
          }
          System.out.print(fieldNames[field]);
          if (field < nf - 1)
            System.out.print(", ");
        }
        System.out.println("]");

        if (dbf.loadData() != 0) {
          System.out.println("Error loading data" + s);
          System.exit(-1);
        }

        // output data
        for (int rec = 0; rec < dbf.getNumRecords(); rec++) {
          for (int field = 0; field < nf; field++) {
            System.out.print(dbd[field].getData(rec));
            if (field < nf - 1)
              System.out.print(", ");
            else
              System.out.println();
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
        break;
      }
    }
  }
}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information.
*/

package dap4.core.dmr;

import dap4.core.util.DapException;
import dap4.core.util.DapSort;
import dap4.core.util.Escape;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

abstract public class DapNode
{

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

    /**
     * Assign a "sort" to this node to avoid
     * use of instanceof().
     */
    protected DapSort sort = null;

    /**
     * Assign unique id to all nodes.
     * Id is unique relative to the whole tree only.
     * and reflects the order within nodelist
     */
    protected int index;

    /**
     * Unqualified (short) name of this node wrt the tree.
     * This is raw (unescaped)
     */
    protected String shortname = null;

    /**
     * Escaped version of shortname
     */
    protected String escapedname = null;

    /**
     * Fields to support threading of the tree
     */

    /**
     * Parent DapNode; may be:
     * - Group (for e.g. variables, dimensions, and Types),
     * - Structure, Sequence for Fields
     * - Variable (for e.g. attributes or maps).
     */
    protected DapNode parent = null;

    protected DapDataset dataset = null; // top-level group

    /**
     * Fully qualified name; note that this is always backslash escaped
     */
    protected String fqn = null;

    /**
     * DAP Attributes attached to this node (as opposed to the xml attributes)
     */
    protected Map<String, DapAttribute> attributes = new HashMap<>();

    /**
     * XML Attributes attached to this node. Used to pass reserved extra
     * info to the client.
     */
    protected Map<String, String> xmlattributes = new HashMap<>();

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

    public DapNode()
    {
        // Use Instanceof to figure out the sort
        // Because of subclass relationships, order is important
        if(this instanceof DapVariable)
            this.sort = DapSort.VARIABLE;
        else if(this instanceof DapSequence)
            this.sort = DapSort.SEQUENCE;
        else if(this instanceof DapStructure)
            this.sort = DapSort.STRUCTURE;
        else if(this instanceof DapOtherXML)
            this.sort = DapSort.OTHERXML;
        else if(this instanceof DapAttributeSet)
            this.sort = DapSort.ATTRIBUTESET;
        else if(this instanceof DapAttribute)
            this.sort = DapSort.ATTRIBUTE;
        else if(this instanceof DapDataset) // test dataset before group
            this.sort = DapSort.DATASET;
        else if(this instanceof DapGroup)
            this.sort = DapSort.GROUP;
        else if(this instanceof DapDimension)
            this.sort = DapSort.DIMENSION;
        else if(this instanceof DapEnumeration)
            this.sort = DapSort.ENUMERATION;
        else if(this instanceof DapType) // must follow enumeration
            this.sort = DapSort.ATOMICTYPE;
        else if(this instanceof DapEnumConst)
            this.sort = DapSort.ENUMCONST;
        else if(this instanceof DapEnumeration)
            this.sort = DapSort.ENUMERATION;
        else if(this instanceof DapMap)
            this.sort = DapSort.MAP;
        else
            assert (false) : "Internal error";
    }

    public DapNode(String shortname)
    {
        this();
        setShortName(shortname);
    }

    //////////////////////////////////////////////////
    // Annotatations

    /* Purpose of annotation is to (sort of) get around
       single inheritance by allowing a node to store
       a single arbitrary piece of state.
    */

    protected Map<Object, Object> annotations = null;

    public DapNode annotate(Object id, Object value)
    {
        if(this.annotations == null)
            this.annotations = new HashMap<>();
        assert this.annotations.get(id) == null;
        this.annotations.put(id, value);
        return this;
    }

    public Object annotation(Object id)
    {
        return this.annotations.get(id);
    }

    //////////////////////////////////////////////////
    // Attribute support
    // Note that depending on the semantics,
    // attributes are not allowed on some node types

    public Map<String, DapAttribute> getAttributes()
    {
        if(attributes == null) attributes = new HashMap<String, DapAttribute>();
        return attributes;
    }

    public void setAttributes(Map<String, DapAttribute> alist)
    {
        this.attributes = alist;
    }

    // This may  occur after initial construction
    synchronized public DapAttribute setAttribute(DapAttribute attr)
            throws DapException
    {
        if(attributes == null)
            attributes = new HashMap<String, DapAttribute>();
        DapAttribute old = attributes.get(attr.getShortName());
        attributes.put(attr.getShortName(), attr);
        attr.setParent(this);
        return old;
    }

    public synchronized void addAttribute(DapAttribute attr)
            throws DapException
    {
        String name = attr.getShortName();
        if(this.attributes == null)
            this.attributes = new HashMap<String, DapAttribute>();
        if(this.attributes.containsKey(name))
            throw new DapException("Attempt to add duplicate attribute: " + attr.getShortName());
        setAttribute(attr);
    }

    /**
     * Used by AbstractDSP to suppress certain attributes.
     *
     * @param attr
     * @throws DapException
     */
    public synchronized void removeAttribute(DapAttribute attr)
            throws DapException
    {
        if(this.attributes == null)
            return;
        String name = attr.getShortName();
        if(this.attributes.containsKey(name))
            this.attributes.remove(name);
    }

    public synchronized DapAttribute findAttribute(String name)
    {
        return this.attributes.get(name);
    }

    //////////////////////////////////////////////////
    // XML attributes

    public synchronized Map<String, String>
    getXMLAttributes()
    {
        return this.xmlattributes;
    }

    public synchronized void
    addXMLAttribute(String name, String value)
            throws DapException
    {
        if(this.xmlattributes == null)
            this.xmlattributes = new HashMap<String, String>();
        if(this.xmlattributes.containsKey(name))
            throw new DapException("Attempt to add duplicate XML attribute: " + name);
        this.xmlattributes.put(name, value);
    }

    public synchronized void
    removeXMLAttribute(String name)
            throws DapException
    {
        if(this.xmlattributes == null)
            return;
        if(this.xmlattributes.containsKey(name))
            this.xmlattributes.remove(name);
    }

    //////////////////////////////////////////////////
    // Get/set

    public DapSort getSort()
    {
        return sort;
    }

    public void setSort(DapSort sort)
    {
        this.sort = sort;
    }

    public int getIndex()
    {
        return index;
    }

    public void setIndex(int index)
    {
        this.index = index;
    }

    public DapDataset getDataset()
    {
        if(this.dataset == null) {
            // walk up to find
            DapNode curr, next = this;
            do {
                curr = next;
                assert curr != null;
                next = curr.getGroup();
            } while(curr.getSort() != DapSort.DATASET);
            setDataset((DapDataset) curr);
        }
        return this.dataset;
    }

    public void setDataset(DapDataset dataset)
    {
        this.dataset = dataset;
        if(dataset != null && this != dataset)
            dataset.addNode(this);
    }

    /**
     * Closest containing group
     *
     * @returns closest group not equal to this
     * or null if this is a DapDataset
     */

    public DapGroup getGroup()
    {
        if(this.sort == DapSort.DATASET)
            return null;
        // Walk the parent node until we find a group
        DapNode group = parent;
        while(group != null) {
            switch (group.getSort()) {
            case DATASET:
            case GROUP:
                return (DapGroup) group;
            default:
                group = group.getParent();
                break;
            }
        }
        return (DapGroup) group;
    }

    /**
     * Closest containing group, structure, sequence
     *
     * @returns closest container
     */

    public DapNode getContainer()
    {
        DapNode parent = this.parent;
        switch (getSort()) {
        default:
            break;
        case ENUMCONST:
            parent = ((DapEnumConst) this).getParent().getContainer();
            break;
        case ATTRIBUTE:
        case ATTRIBUTESET:
        case OTHERXML:
            parent = ((DapAttribute) this).getParent();
            if(parent instanceof DapVariable)
                parent = parent.getContainer();
            break;
        case MAP:
            parent = ((DapMap) this).getVariable().getContainer();
            break;
        }
        return parent;
    }

    public DapNode getParent()
    {
        return parent;
    }

    /**
     * Set the parent DapNode; may sometimes be same as container,
     * but not always (think attributes or maps).
     * Invariant: parent must be either a group or a variable.
     * We can infer the container, so set that also.
     *
     * @param parent the proposed parent node
     */
    public void setParent(DapNode parent)
    {
        assert this.parent == null;
        assert (
                (this.getSort() == DapSort.ENUMCONST && parent.getSort() == DapSort.ENUMERATION)
                        || parent.getSort().isa(DapSort.GROUP)
                        || parent.getSort() == DapSort.VARIABLE
                        || parent.getSort() == DapSort.STRUCTURE
                        || parent.getSort() == DapSort.SEQUENCE
                        || this.getSort() == DapSort.ATTRIBUTE
                        || this.getSort() == DapSort.ATTRIBUTESET
        );
        this.parent = parent;
    }

    /**
      * Same as setparent except existing parent can be non-null
      *
      * @param parent the proposed parent node
      */
     public void overrideParent(DapNode parent)
     {
         this.parent = null;
         setParent(parent);
     }

    public String getShortName()
    {
        return shortname;
    }

    public void setShortName(String shortname)
    {
        this.shortname = shortname;
        // force recomputation
        this.escapedname = null;
        this.fqn = null;
    }

    /**
     * Here, escaped means backslash escaped short name
     *
     * @return escaped name
     */
    public String getEscapedShortName()
    {
        if(this.escapedname == null)
            this.escapedname = Escape.backslashEscape(getShortName(), null);
        return this.escapedname;
    }

    public String getFQN()
    {
        if(this.fqn == null)
            this.fqn = computefqn();
        assert (fqn.length() > 0 || this.getSort() == DapSort.DATASET);
        return this.fqn;
    }

    /**
     * Compute the path upto, and including
     * some specified containing node (null=>root)
     * The containing node is included as is this node.
     *
     * @return ordered list of parent nodes
     */

    public List<DapNode>
    getPath()
    {
        List<DapNode> path = new ArrayList<DapNode>();
        DapNode current = this;
        for(; ; ) {
            path.add(0, current);
            current = current.getParent();
            if(current == null)
                break;
        }
        return path;
    }

    /**
     * Get the transitive list of containers
     * Not including this node
     *
     * @return list of container nodes
     */
    public List<DapNode> getContainerPath()
    {
        List<DapNode> path = new ArrayList<DapNode>();
        DapNode current = this.getContainer();
        for(; ; ) {
            path.add(0, current);
            if(current.getContainer() == null)
                break;
            current = current.getContainer();
        }
        return path;
    }

    /**
     * Get the transitive list of containing groups
     * Possibly including this node
     *
     * @return list of group nodes
     */
    public List<DapGroup> getGroupPath()
    {
        List<DapGroup> path = new ArrayList<DapGroup>();
        DapNode current = this;
        for(; ; ) {
            if(current.getSort() == DapSort.GROUP
                    || current.getSort() == DapSort.DATASET)
                path.add(0, (DapGroup) current);
            if(current.getContainer() == null)
                break;
            current = current.getContainer();
        }
        return path;
    }

    //////////////////////////////////////////////////
    // Compute fqn of this node

    /**
     * Compute the FQN of this node
     */
    public String
    computefqn()
    {
        List<DapNode> path = getPath(); // excludes root/wrt
        StringBuilder fqn = new StringBuilder();
        DapNode parent = path.get(0);
        for(int i = 1; i < path.size(); i++) {   // start at 1 to skip root
            DapNode current = path.get(i);
            // Depending on what parent is, use different delimiters
            switch (parent.getSort()) {
            case DATASET:
            case GROUP:
            case ENUMERATION:
                fqn.append('/');
                fqn.append(Escape.backslashEscape(current.getShortName(),"/."));
                break;
            // These use '.'
            case STRUCTURE:
            case SEQUENCE:
            case ENUMCONST:
            case VARIABLE:
                fqn.append('.');
                fqn.append(current.getEscapedShortName());
                break;
            default: // Others should never happen
                throw new IllegalArgumentException("Illegal FQN parent");
            }
            parent = current;
        }
        return fqn.toString();
    }

    //////////////////////////////////////////////////
    // Misc. Methods

    public boolean isTopLevel()
    {
        return parent == null
                || parent.getSort() == DapSort.DATASET
                || parent.getSort() == DapSort.GROUP;
    }

    //////////////////////////////////////////////////
    // Common Methods

    public String toString()
    {
        String sortname = (sort == null ? "undefined" : sort.name());
        //String name = getFQN();
        String name = null;
        if(name == null) name = getShortName();
        if(name == null) name = "?";
        return sortname + "::" + name;
    }

} // class DapNode


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

import ucar.ma2.*;

import ucar.nc2.Attribute;
import ucar.nc2.Variable;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.RadialDatasetSweep;
import ucar.nc2.ft.FeatureDataset;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.units.DateUnit;

import ucar.unidata.geoloc.Earth;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;

import java.io.IOException;

import java.util.*;

import static ucar.ma2.MAMath.nearlyEquals;

/**
 * CF-Radial
 */
public class CFRadialAdapter extends AbstractRadialAdapter {

  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CFRadialAdapter.class);

  private NetcdfDataset ds = null;
  private double latv, lonv, elev;
  private double[] time;
  private float[] elevation;
  private float[] azimuth;
  private float[] range;
  private int[] rayStartIdx;
  private int[] rayEndIdx;
  private int[] ray_n_gates;
  private int[] ray_start_index;
  private int nsweeps;
  private boolean isStationary;
  private boolean isStationaryChecked = false;

  /////////////////////////////////////////////////
  // TypedDatasetFactoryIF

  public Object isMine( FeatureType wantFeatureType, NetcdfDataset ncd, Formatter errlog) throws IOException {
    String convStr = ncd.findAttValueIgnoreCase(null, "Conventions", null);
    if ((null != convStr) && convStr.startsWith("CF/Radial"))
      return this;
    return null;
  }

  public FeatureDataset open( FeatureType ftype, NetcdfDataset ncd, Object analysis, ucar.nc2.util.CancelTask task, Formatter errlog) throws IOException {
    return new CFRadialAdapter(ncd);
  }

  public FeatureType getScientificDataType() {
    return FeatureType.RADIAL;
  }

  // needed for FeatureDatasetFactory
  public CFRadialAdapter() {
  }

  /**
   * Constructor.
   *
   * @param ds Source NetCDF dataset
   */
  public CFRadialAdapter(NetcdfDataset ds) {
    this.ds = ds;
    desc = "CF/Radial radar dataset";
    init();

    for (Variable var : ds.getVariables()) {
      addRadialVariable(ds, var);
    }
  }

  public void init() {

    setEarthLocation();
    try {
      Variable t = ds.findVariable("time");
      Array tArray = t.read();
      time = (double[]) tArray.copyTo1DJavaArray();

      Variable ele = ds.findVariable("elevation");
      Array eArray = ele.read();
      elevation = (float[]) eArray.copyTo1DJavaArray();

      Variable azi = ds.findVariable("azimuth");
      Array aArray = azi.read();
      azimuth = (float[]) aArray.copyTo1DJavaArray();

      Variable rng = ds.findVariable("range");
      Array rArray = rng.read();
      range = (float[]) rArray.copyTo1DJavaArray();

      Variable sidx0 = ds.findVariable("sweep_start_ray_index");
      rayStartIdx = (int[]) sidx0.read().copyTo1DJavaArray();

      Variable sidx1 = ds.findVariable("sweep_end_ray_index");
      rayEndIdx = (int[]) sidx1.read().copyTo1DJavaArray();

      nsweeps = ds.findDimension("sweep").getLength();

      Variable var = ds.findVariable("ray_n_gates");
      if (var != null)
          ray_n_gates = (int[]) var.read().copyTo1DJavaArray();

      var = ds.findVariable("ray_start_index");
      if (var != null)
          ray_start_index = (int[]) var.read().copyTo1DJavaArray();

      setTimeUnits();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    setStartDate();
    setEndDate();
    setBoundingBox();
  }

  protected void setBoundingBox() {
    LatLonRect bb;

    if (origin == null) {
      return;
    }

    double dLat = Math.toDegrees(getMaximumRadialDist()
            / Earth.getRadius());
    double latRadians = Math.toRadians(origin.getLatitude());
    double dLon = dLat * Math.cos(latRadians);

    double lat1 = origin.getLatitude() - dLat / 2;
    double lon1 = origin.getLongitude() - dLon / 2;
    bb = new LatLonRect(new LatLonPointImpl(lat1, lon1), dLat, dLon);

    boundingBox = bb;
  }

  double getMaximumRadialDist() {
    double maxdist = 0.0;

    for (Object dataVariable : dataVariables) {
      RadialVariable rv = (RadialVariable) dataVariable;
      Sweep sp = rv.getSweep(0);
      double dist = sp.getGateNumber() * sp.getGateSize();

      if (dist > maxdist) {
        maxdist = dist;
      }
    }

    return maxdist;
  }

  protected void setEarthLocation() {

    try {
      Variable ga = ds.findVariable("latitude");
      if (ga != null) {
        if(ga.isScalar()) {
          latv = ga.readScalarDouble();
        } else {
          Array gar = ga.read();
          latv = gar.getDouble(0);
        }
      } else {
        latv = 0.0;
      }

      ga = ds.findVariable("longitude");

      if (ga != null) {
        if(ga.isScalar()) {
          lonv = ga.readScalarDouble();
        } else {
          Array gar = ga.read();
          lonv = gar.getDouble(0);
        }      } else {
        lonv = 0.0;
      }

      ga = ds.findVariable("altitude");
      if (ga != null) {
        if(ga.isScalar()) {
          elev = ga.readScalarDouble();
        } else {
          Array gar = ga.read();
          elev = gar.getDouble(0);
        }
      } else {
        elev = 0.0;
      }
    } catch (IOException e) {
    }

    origin = new ucar.unidata.geoloc.EarthLocationImpl(latv, lonv, elev);
  }

  @Override
  public ucar.unidata.geoloc.EarthLocation getCommonOrigin() {
    return origin;
  }

  @Override
  public String getRadarID() {
    Attribute ga = ds.findGlobalAttribute("Station");
    if (ga != null) {
      return ga.getStringValue();
    } else {
      return "XXXX";
    }
  }

  @Override
  public String getRadarName() {
    Attribute ga = ds.findGlobalAttribute("instrument_name");
    if (ga != null) {
      return ga.getStringValue();
    } else {
      return "Unknown Station";
    }
  }

  @Override
  public String getDataFormat() {
    return "CF/RadialNetCDF";
  }

  @Override
  public boolean isVolume() {
    return true;
  }

  public boolean isHighResolution(NetcdfDataset nds) {
    return true;
  }

  public boolean isStationary() {
    // only check once
    if (!isStationaryChecked) {
      Variable lat = ds.findVariable("latitude");
      if (lat != null) {
        if (lat.isScalar())
          isStationary = lat.getSize() == 1;
        else {
          // if array, check to see if all of the values are
          // approximately the same
          Array gar = null;
          try {
            gar = lat.read();
            Object firstVal = gar.getObject(0);
            Array gar2 = gar.copy();
            for (int i = 1; i < gar.getSize(); i++) {
              gar2.setObject(i, firstVal);
            }
            isStationary = nearlyEquals(gar, gar2);
          } catch (IOException e) {
            log.error("Error reading latitude variable {}. Cannot determine if " +
                    "platform is stationary. Setting to default (false).", lat.getFullName());
          }
        }
      }
      isStationaryChecked = true;
    }

    return isStationary;
  }

  protected void setTimeUnits() throws Exception {
    Variable t = ds.findVariable("time");
    String ut = t.getUnitsString();
    dateUnits = new DateUnit(ut);
    calDateUnits = CalendarDateUnit.of(null, ut);
  }

  protected void setStartDate() {
    String datetime = ds.findAttValueIgnoreCase(null, "time_coverage_start", null);
    if (datetime != null) {
      startDate = CalendarDate.parseISOformat(null, datetime).toDate();
    } else {
      startDate = calDateUnits.makeCalendarDate(time[0]).toDate();
    }
  }

  protected void setEndDate() {
    String datetime = ds.findAttValueIgnoreCase(null, "time_coverage_end", null);
    if (datetime != null) {
      endDate = CalendarDate.parseISOformat(null, datetime).toDate();
    } else {
      endDate = calDateUnits.makeCalendarDate(time[time.length - 1]).toDate();
    }
  }

  public void clearDatasetMemory() {
    for (VariableSimpleIF rvar : getDataVariables()) {
        RadialVariable radVar = (RadialVariable) rvar;
        radVar.clearVariableMemory();
    }
  }

  /**
   * _more_
   *
   * @param nds _more_
   * @param var _more_
   */
  protected void addRadialVariable(NetcdfDataset nds, Variable var) {

    RadialVariable rsvar = null;
    String vName = var.getShortName();
    int tIdx = var.findDimensionIndex("time");
    int rIdx = var.findDimensionIndex("range");
    int ptsIdx = var.findDimensionIndex("n_points");

    if (((tIdx == 0) && (rIdx == 1)) || (ptsIdx == 0)) {
      VariableSimpleIF v = new MyRadialVariableAdapter(vName, var.getAttributes());
      rsvar = makeRadialVariable(nds, v, var);
    }

    if (rsvar != null) {
      dataVariables.add(rsvar);
    }
  }

  /**
   * _more_
   *
   * @param nds _more_
   * @param v   _more_
   * @param v0  _more_
   * @return _more_
   */
  protected RadialVariable makeRadialVariable(NetcdfDataset nds,
                                              VariableSimpleIF v, Variable v0) {
    // this function is null in level 2
    return new CFRadial2Variable(nds, v0);
  }

  /**
   * _more_
   *
   * @return _more_
   */
  public String getInfo() {
    StringBuffer sbuff = new StringBuffer();
    sbuff.append("CFRadial2Dataset\n");
    sbuff.append(super.getDetailInfo());
    sbuff.append("\n\n");
    sbuff.append(parseInfo.toString());
    return sbuff.toString();
  }

  /**
   * Class description
   *
   * @author Enter your name here...
   * @version Enter version here..., Mon, Jun 13, '11
   */
  private class CFRadial2Variable extends MyRadialVariableAdapter implements RadialDatasetSweep.RadialVariable {

    /**
     * _more_
     */
    ArrayList<CFRadial2Sweep> sweeps;

    /**
     * _more_
     */
    String name;
    private boolean flattened;

    /**
     * _more_
     *
     * @param nds _more_
     * @param v0  _more_
     */
    private CFRadial2Variable(NetcdfDataset nds, Variable v0) {
      super(v0.getShortName(), v0.getAttributes());

      sweeps = new ArrayList<>();
      name = v0.getShortName();

      int[] shape = v0.getShape();
      int ngates = shape[v0.getRank() - 1];
      flattened = v0.findDimensionIndex("n_points") == 0;

      for (int i = 0; i < nsweeps; i++) {
        // For flattened (1D stored data) find max number of gates
        if (flattened) {
            ngates = ray_n_gates[rayStartIdx[i]];
            for (int ray = rayStartIdx[i]; ray <= rayEndIdx[i]; ++ray)
                ngates = ray_n_gates[ray] > ngates ? ray_n_gates[ray] : ngates;
        }
        sweeps.add(new CFRadial2Sweep(v0, i, ngates, rayStartIdx[i],
                rayEndIdx[i]));
      }
    }

    /**
     * _more_
     *
     * @return _more_
     */
    public String toString() {
      return name;
    }

    /**
     * _more_
     *
     * @return _more_
     */
    public int getNumSweeps() {
      return nsweeps;
    }

    /**
     * _more_
     *
     * @param sweepNo _more_
     * @return _more_
     */
    public Sweep getSweep(int sweepNo) {
      return sweeps.get(sweepNo);
    }

    /**
     * _more_
     *
     * @return _more_
     */
    public int getNumRadials() {
      return azimuth.length;
    }

    // a 3D array nsweep * nradials * ngates
    // if high resolution data, it will be transferred to the same dimension

    /**
     * _more_
     *
     * @return _more_
     * @throws IOException _more_
     */
    public float[] readAllData() throws IOException {
      Array allData;
      Sweep spn = sweeps.get(0);
      Variable v = spn.getsweepVar();
      Attribute missing = v.findAttribute("_FillValue");
      float missingVal = missing == null ?
              Float.NaN : missing.getNumericValue().floatValue();

      int minRadial = getMinRadialNumber();
      int radials = getNumRadials();
      int gates = range.length;
      try {
        allData = v.read();
      } catch (IOException e) {
        throw new IOException(e.getMessage());
      }
      if (flattened) {
          float[] fa0 = (float[]) allData.get1DJavaArray(float.class);
          float[] fa = new float[minRadial * gates * nsweeps];
          Arrays.fill(fa, missingVal);
          for (int s = 0; s < nsweeps; ++s) {
              for (int r = 0; r < minRadial; ++r) {
                  System.arraycopy(fa0, ray_start_index[rayStartIdx[s] + r],
                          fa, s * minRadial * gates + r * gates,
                          ray_n_gates[rayStartIdx[s] + r]);
              }
          }

          return fa;
      } else if (minRadial == radials) {
        return (float[]) allData.get1DJavaArray(float.class);
      } else {
        float[] fa = new float[minRadial * gates * nsweeps];
        float[] fa0 = (float[]) allData.get1DJavaArray(float.class);
        int pos = 0;
        for (int i = 0; i < nsweeps; i++) {

          int startIdx = rayStartIdx[i];
          // int endIdx = rayEndIdx[i];
          int len = minRadial * gates;
          System.arraycopy(fa0, startIdx * gates, fa, pos, len);
          pos = pos + len;
        }
        return fa;
      }
    }

    public int getMinRadialNumber() {
      int minRadialNumber = Integer.MAX_VALUE;
      for (int i = 0; i < nsweeps; i++) {
        Sweep swp = this.sweeps.get(i);
        int radialNumber = swp.getRadialNumber();
        if (radialNumber < minRadialNumber) {
          minRadialNumber = radialNumber;
        }
      }

      return minRadialNumber;
    }

    /**
     * _more_
     */
    public void clearVariableMemory() {
      for (int i = 0; i < nsweeps; i++) {
      }
    }

    //////////////////////////////////////////////////////////////////////
    // Checking all azi to make sure there is no missing data at sweep
    // level, since the coordinate is 1D at this level, this checking also
    // remove those missing radials within a sweep.

    /**
     * Class description
     *
     * @author Enter your name here...
     * @version Enter version here..., Mon, Jun 13, '11
     */
    private class CFRadial2Sweep implements RadialDatasetSweep.Sweep {

      /**
       * _more_
       */
      double meanElevation = Double.NaN;

      /**
       * _more_
       */
      double meanAzimuth = Double.NaN;

      /**
       * _more_
       */
      int ngates;

      /**
       * _more_
       */
      public int startIdx, endIdx, numRays;

      /**
       * _more_
       */
      int sweepno;

      /**
       * _more_
       */
      Variable sweepVar;

      /**
       * _more_
       *
       * @param v        _more_
       * @param sweepno  _more_
       * @param gates    _more_
       * @param startIdx _more_
       * @param endIdx   _more_
       */
      CFRadial2Sweep(Variable v, int sweepno, int gates,
                     int startIdx, int endIdx) {
        this.sweepVar = v;
        this.sweepno = sweepno;
        this.ngates = gates;
        this.startIdx = startIdx;
        this.endIdx = endIdx;
        this.numRays = endIdx - startIdx + 1;
      }

      public int getStartIdx() {
        return startIdx;
      }

      public int getEndIdx() {
        return endIdx;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public Variable getsweepVar() {
        return sweepVar;
      }

            /* read 2d sweep data nradials * ngates */

      /**
       * _more_
       *
       * @return _more_
       * @throws java.io.IOException _more_
       */
      public float[] readData() throws java.io.IOException {
        return sweepData();
      }

      /**
       * _more_
       *
       * @return _more_
       */
      private float[] sweepData() throws IOException {
        int[] origin;
        int[] shape;

        // init section
        try {
            if (flattened) {
                // Get the 1D data for the sweep
                origin = new int[1];
                origin[0] = ray_start_index[startIdx];
                shape = new int[1];
                shape[0] = ray_start_index[endIdx] + ray_n_gates[endIdx] -
                        origin[0];
                Array tempArray = sweepVar.read(origin, shape).reduce();
                float[] tempD = (float[]) tempArray.get1DJavaArray(Float.TYPE);

                // Figure out what to use as the initializer
                float missingVal = Float.NaN;
                Attribute missing = sweepVar.findAttribute("_FillValue");
                if (missing != null)
                    missingVal = missing.getNumericValue().floatValue();

                // Create evenly strided output array and fill
                float[] ret = new float[ngates * numRays];
                Arrays.fill(ret, missingVal);
                int srcInd = 0;
                for (int ray = 0; ray < numRays; ++ray) {
                    int gates = ray_n_gates[startIdx + ray];
                    System.arraycopy(tempD, srcInd, ret, ray * ngates, gates);
                    srcInd += gates;
                }

                return ret;
            } else {
                origin = new int[2];
                origin[0] = startIdx;
                shape = sweepVar.getShape();
                shape[0] = numRays;
                Array sweepTmp = sweepVar.read(origin, shape).reduce();
                return (float[]) sweepTmp.get1DJavaArray(Float.TYPE);
            }
        } catch (ucar.ma2.InvalidRangeException e) {
          throw new IOException(e);
        }
      }

      /**
       * Return data for 1 ray
       *
       * @param ray _more_
       * @return _more_
       * @throws java.io.IOException _more_
       */
      public float[] readData(int ray) throws java.io.IOException {
        return rayData(ray);
      }

            /* read the radial data from the radial variable */

      /**
       * _more_
       *
       * @param ray _more_
       * @return _more_
       * @throws java.io.IOException _more_
       */
      public float[] rayData(int ray) throws java.io.IOException {
        int[] origin;
        int[] shape;

        // init section
        if (flattened) {
            origin = new int[1];
            origin[0] = ray_start_index[startIdx + ray];
            shape = new int[1];
            shape[0] = ray_n_gates[startIdx + ray];
        } else {
            origin = new int[2];
            origin[0] = startIdx + ray;
            shape = sweepVar.getShape();
            shape[0] = 1;
        }

        try {
          Array sweepTmp = sweepVar.read(origin, shape).reduce();
          return (float[]) sweepTmp.get1DJavaArray(Float.TYPE);
        } catch (ucar.ma2.InvalidRangeException e) {
          throw new IOException(e);
        }
      }

      /**
       * _more_
       */
      public void setMeanElevation() {
        double sum = 0.0;
        int sumSize = 0;
        for (int i = 0; i < numRays; i++) {
          if (!Double.isNaN(elevation[i])) {
            sum = sum + elevation[startIdx + i];
            sumSize++;
          }
        }
        if (sumSize > 0)
            meanElevation = sum / sumSize;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getMeanElevation() {
        if (Double.isNaN(meanElevation)) {
          setMeanElevation();
        }
        return (float) meanElevation;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public int getGateNumber() {
        return ngates;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public int getRadialNumber() {
        return numRays;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public RadialDatasetSweep.Type getType() {
        return null;
      }

      /**
       * _more_
       *
       * @param ray _more_
       * @return _more_
       */
      public ucar.unidata.geoloc.EarthLocation getOrigin(int ray) {
        return origin;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public Date getStartingTime() {
        return startDate;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public Date getEndingTime() {
        return endDate;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public int getSweepIndex() {
        return sweepno;
      }

      /**
       * _more_
       */
      public void setMeanAzimuth() {
        double sum = 0.0;
        int sumSize = 0;
        for (int i = 0; i < numRays; i++) {
          if (!Double.isNaN(azimuth[i])) {
            sum = sum + azimuth[startIdx + i];
            sumSize++;
          }
        }
        if (sumSize > 0)
            meanAzimuth = sum / sumSize;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getMeanAzimuth() {
        if (Double.isNaN(meanAzimuth)) {
          setMeanAzimuth();
        }
        return (float) meanAzimuth;
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public boolean isConic() {
        return true;
      }

      /**
       * _more_
       *
       * @param ray _more_
       * @return _more_
       * @throws IOException _more_
       */
      public float getElevation(int ray) throws IOException {
        return elevation[ray + startIdx];
      }

      /**
       * _more_
       *
       * @return _more_
       * @throws IOException _more_
       */
      public float[] getElevation() throws IOException {
        float[] elev = new float[numRays];
        System.arraycopy(elevation, startIdx, elev, 0, numRays);
        return elev;
      }

      /**
       * _more_
       *
       * @return _more_
       * @throws IOException _more_
       */
      public float[] getAzimuth() throws IOException {
        float[] azimu = new float[numRays];
        System.arraycopy(azimuth, startIdx, azimu, 0, numRays);
        return azimu;
      }

      /**
       * _more_
       *
       * @param ray _more_
       * @return _more_
       * @throws IOException _more_
       */
      public float getAzimuth(int ray) throws IOException {
        return azimuth[ray + startIdx];
      }

      /**
       * _more_
       *
       * @param gate _more_
       * @return _more_
       * @throws IOException _more_
       */
      public float getRadialDistance(int gate) throws IOException {
        return range[gate];
      }

      /**
       * _more_
       *
       * @param ray _more_
       * @return _more_
       * @throws IOException _more_
       */
      public float getTime(int ray) throws IOException {

        return (float) time[ray + startIdx];

      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getBeamWidth() {
        return 0.95f;  // degrees, info from Chris Burkhart
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getNyquistFrequency() {
        return 0;  // LOOK this may be radial specific
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getRangeToFirstGate() {
        try {
          return getRadialDistance(0);
        } catch (IOException e) {
          e.printStackTrace();
          return 0.0f;
        }
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public float getGateSize() {
        try {
          return getRadialDistance(1) - getRadialDistance(0);
        } catch (IOException e) {
          e.printStackTrace();
          return 0.0f;
        }
      }

      /**
       * _more_
       *
       * @return _more_
       */
      public boolean isGateSizeConstant() {
        return true;
      }

      /**
       * _more_
       */
      public void clearSweepMemory() {
      }
    }  // LevelII2Sweep class

  }      // LevelII2Variable
}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information.
*/

package dap4.dap4lib.serial;

import dap4.core.dmr.DapAttribute;
import dap4.core.dmr.DapDataset;
import dap4.core.util.DapDump;
import dap4.core.util.DapException;
import dap4.core.util.DapUtil;
import dap4.dap4lib.AbstractDSP;
import dap4.dap4lib.DMRPrinter;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * DAP4 Serial to DSP interface
 * This code should be completely independent of thredds.
 * Its goal is to provide a DSP interface to
 * a sequence of bytes representing serialized data, possibly
 * including a leading DMR.
 */

abstract public class D4DSP extends AbstractDSP
{
    //////////////////////////////////////////////////
    // Constants

    static public boolean DEBUG = false;
    static public boolean DUMPDMR = false;
    static public boolean DUMPDAP = false;

    static protected final String DAPVERSION = "4.0";
    static protected final String DMRVERSION = "1.0";

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

    protected ByteBuffer databuffer = null; // local copy of AbstractDSP.getSource

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

    public D4DSP()
    {
        super();
    }

    //////////////////////////////////////////////////
    // DSP API
    // Most is left to be subclass defined;

    //////////////////////////////////////////////////
    // (Other) Accessors

    /*packge*/ ByteBuffer
    getBuffer()
    {
        return databuffer;
    }

    //////////////////////////////////////////////////
    // Compilation

    protected void
    build(String document, byte[] serialdata, ByteOrder order)
            throws DapException
    {
        DapDataset dmr = parseDMR(document);

        if(DEBUG || DUMPDMR) {
            System.err.println("\n+++++++++++++++++++++");
            System.err.println(dmr);
            System.err.println("+++++++++++++++++++++\n");
        }
        if(DEBUG || DUMPDAP) {
            ByteBuffer data = ByteBuffer.wrap(serialdata);
            System.err.println("+++++++++++++++++++++");
            System.err.println("\n---------------------");
            DapDump.dumpbytes(data, false);
            System.err.println("\n---------------------\n");
        }
        build(dmr, serialdata, order);
    }

    /**
     * Build the data from the incoming serial data
     * Note that some DSP's will not use
     *
     * @param dmr
     * @param serialdata
     * @param order
     * @throws DapException
     */
    protected void
    build(DapDataset dmr, byte[] serialdata, ByteOrder order)
            throws DapException
    {
        setDMR(dmr);
        // "Compile" the databuffer section of the server response
        this.databuffer = ByteBuffer.wrap(serialdata).order(order);
        D4DataCompiler compiler = new D4DataCompiler(this, getChecksumMode(), getOrder(), this.databuffer);
        compiler.compile();
    }

}


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

package ucar.nc2.dt.point;

import ucar.nc2.dt.*;
import ucar.nc2.*;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.units.DateFormatter;
import ucar.ma2.*;
import ucar.ma2.DataType;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.LatLonPointImpl;

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

/**
 * Write StationObsDataset in Unidata Station Obs Convention.
 *
 * @deprecated use ucar.nc2.ft.point
 * @author caron
 */
public class WriterStationObsDataset {
  private static final String recordDimName = "record";
  private static final String stationDimName = "station";
  private static final String latName = "latitude";
  private static final String lonName = "longitude";
  private static final String altName = "altitude";
  private static final String timeName = "time";
  private static final String idName = "station_id";
  private static final String descName = "station_description";
  private static final String wmoName = "wmo_id";

  private static final String firstChildName = "firstChild";
  private static final String lastChildName = "lastChild";
  private static final String numChildName = "numChildren";
  private static final String nextChildName = "nextChild";
  private static final String prevChildName = "prevChild";
  private static final String parentName = "parent_index";
  // private static final String timeStrLenDim = "time_strlen";

  private DateFormatter dateFormatter = new DateFormatter();
  private int name_strlen, desc_strlen, wmo_strlen;

  private NetcdfFileWriteable ncfile;
  private String title;

  private Set<Dimension> dimSet = new HashSet<Dimension>();
  private List<Dimension> recordDims = new ArrayList<Dimension>();
  private List<Dimension> stationDims = new ArrayList<Dimension>();
  private List<ucar.unidata.geoloc.Station> stnList;
  private Date minDate = null;
  private Date maxDate = null;

  private boolean useAlt = false;
  private boolean useWmoId = false;

  private boolean debug = false;

  public WriterStationObsDataset(String fileOut, String title) throws IOException {
    ncfile = NetcdfFileWriteable.createNew(fileOut, false);
    ncfile.setFill( false);
    this.title = title;
  }

  public void writeHeader(List<ucar.unidata.geoloc.Station> stns, List<VariableSimpleIF> vars) throws IOException {
    createGlobalAttributes();
    createStations(stns);
    createDataVariables(vars);

    ncfile.create(); // done with define mode

    writeStationData(stns); // write out the station info

    // now write the observations
    if (! (Boolean) ncfile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE))
      throw new IllegalStateException("can't add record variable");
  }

  private void createGlobalAttributes() {
    ncfile.addGlobalAttribute("Conventions", "Unidata Observation Dataset v1.0");
    ncfile.addGlobalAttribute("cdm_datatype", "Station");
    ncfile.addGlobalAttribute("title", title);
    ncfile.addGlobalAttribute("desc", "Extracted by THREDDS/Netcdf Subset Service");
    /* ncfile.addGlobalAttribute("observationDimension", recordDimName);
    ncfile.addGlobalAttribute("stationDimension", stationDimName);
    ncfile.addGlobalAttribute("latitude_coordinate", latName);
    ncfile.addGlobalAttribute("longitude_coordinate", lonName);
    ncfile.addGlobalAttribute("time_coordinate", timeName); */
  }

  private void createStations(List<ucar.unidata.geoloc.Station> stnList) throws IOException {
    int nstns = stnList.size();

    // see if there's altitude, wmoId for any stations
    for (int i = 0; i < nstns; i++) {
      ucar.unidata.geoloc.Station stn = stnList.get(i);

      if (!Double.isNaN(stn.getAltitude()))
        useAlt = true;
      if ((stn.getWmoId() != null) && (stn.getWmoId().trim().length() > 0))
        useWmoId = true;
    }

    /* if (useAlt)
      ncfile.addGlobalAttribute("altitude_coordinate", altName); */

    // find string lengths
    for (int i = 0; i < nstns; i++) {
      ucar.unidata.geoloc.Station station = stnList.get(i);
      name_strlen = Math.max(name_strlen, station.getName().length());
      desc_strlen = Math.max(desc_strlen, station.getDescription().length());
      if (useWmoId) wmo_strlen = Math.max(wmo_strlen, station.getName().length());
    }

    LatLonRect llbb = getBoundingBox(stnList);
    ncfile.addGlobalAttribute("geospatial_lat_min", Double.toString(llbb.getLowerLeftPoint().getLatitude()));
    ncfile.addGlobalAttribute("geospatial_lat_max", Double.toString(llbb.getUpperRightPoint().getLatitude()));
    ncfile.addGlobalAttribute("geospatial_lon_min", Double.toString(llbb.getLowerLeftPoint().getLongitude()));
    ncfile.addGlobalAttribute("geospatial_lon_max", Double.toString(llbb.getUpperRightPoint().getLongitude()));

    // add the dimensions
    Dimension recordDim = ncfile.addUnlimitedDimension(recordDimName);
    recordDims.add(recordDim);

    Dimension stationDim = ncfile.addDimension(stationDimName, nstns);
    stationDims.add(stationDim);

    // add the station Variables using the station dimension
    Variable v = ncfile.addVariable(latName, DataType.DOUBLE, stationDimName);
    ncfile.addVariableAttribute(v, new Attribute("units", "degrees_north"));
    ncfile.addVariableAttribute(v, new Attribute("long_name", "station latitude"));

    v = ncfile.addVariable(lonName, DataType.DOUBLE, stationDimName);
    ncfile.addVariableAttribute(v, new Attribute("units", "degrees_east"));
    ncfile.addVariableAttribute(v, new Attribute("long_name", "station longitude"));

    if (useAlt) {
      v = ncfile.addVariable(altName, DataType.DOUBLE, stationDimName);
      ncfile.addVariableAttribute(v, new Attribute("units", "meters"));
      ncfile.addVariableAttribute(v, new Attribute("long_name", "station altitude"));
    }

    v = ncfile.addStringVariable(idName, stationDims, name_strlen);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "station identifier"));

    v = ncfile.addStringVariable(descName, stationDims, desc_strlen);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "station description"));

    if (useWmoId) {
      v = ncfile.addStringVariable(wmoName, stationDims, wmo_strlen);
      ncfile.addVariableAttribute(v, new Attribute("long_name", "station WMO id"));
    }

    v = ncfile.addVariable(numChildName, DataType.INT, stationDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "number of children in linked list for this station"));

    v = ncfile.addVariable(lastChildName, DataType.INT, stationDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "record number of last child in linked list for this station"));

    v = ncfile.addVariable(firstChildName, DataType.INT, stationDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "record number of first child in linked list for this station"));

    // time variable
    Variable timeVar = ncfile.addStringVariable(timeName, recordDims, 20);
    ncfile.addVariableAttribute(timeVar, new Attribute("long_name", "ISO-8601 Date"));

    v = ncfile.addVariable(prevChildName, DataType.INT, recordDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "record number of previous child in linked list"));

    v = ncfile.addVariable(parentName, DataType.INT, recordDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "index of parent station"));

    v = ncfile.addVariable(nextChildName, DataType.INT, recordDimName);
    ncfile.addVariableAttribute(v, new Attribute("long_name", "record number of next child in linked list"));

  }

  private void createDataVariables(List<VariableSimpleIF> dataVars) throws IOException {

    // find all dimensions needed by the data variables
    for (VariableSimpleIF var : dataVars) {
      List<Dimension> dims = var.getDimensions();
      dimSet.addAll(dims);
    }

    // add them
    for (Dimension d : dimSet) {
      if (!d.isUnlimited())
        ncfile.addDimension(d.getShortName(), d.getLength(), d.isShared(), false, d.isVariableLength());
    }

    // add the data variables all using the record dimension
    for (VariableSimpleIF oldVar : dataVars) {
      List<Dimension> dims = oldVar.getDimensions();
      StringBuilder dimNames = new StringBuilder(recordDimName);
      for (Dimension d : dims) {
        if (!d.isUnlimited())
          dimNames.append(" ").append(d.getShortName());
      }
      Variable newVar = ncfile.addVariable(oldVar.getShortName(), oldVar.getDataType(), dimNames.toString());

      List<Attribute> atts = oldVar.getAttributes();
      for (Attribute att : atts) {
        ncfile.addVariableAttribute(newVar, att);
      }
    }
  }

  private HashMap<String, StationTracker> stationMap;

  private class StationTracker {
    int numChildren = 0;
    int lastChild = -1;
    int parent_index;
    List<Integer> link = new ArrayList<Integer>(); // recnums

    StationTracker(int parent_index) {
      this.parent_index = parent_index;
    }
  }

  private void writeStationData(List<ucar.unidata.geoloc.Station> stnList) throws IOException {
    this.stnList = stnList;
    int nstns = stnList.size();
    stationMap = new HashMap<String, StationTracker>(2 * nstns);
    if (debug) System.out.println("stationMap created");

    // now write the station data
    ArrayDouble.D1 latArray = new ArrayDouble.D1(nstns);
    ArrayDouble.D1 lonArray = new ArrayDouble.D1(nstns);
    ArrayDouble.D1 altArray = new ArrayDouble.D1(nstns);
    ArrayObject.D1 idArray = new ArrayObject.D1(DataType.STRING, String.class, false, nstns);
    ArrayObject.D1 descArray = new ArrayObject.D1(DataType.STRING, String.class, false, nstns);
    ArrayObject.D1 wmoArray = new ArrayObject.D1(DataType.STRING, String.class, false, nstns);

    for (int i = 0; i < stnList.size(); i++) {
      ucar.unidata.geoloc.Station stn = stnList.get(i);
      stationMap.put(stn.getName(), new StationTracker(i));

      latArray.set(i, stn.getLatitude());
      lonArray.set(i, stn.getLongitude());
      if (useAlt) altArray.set(i, stn.getAltitude());

      idArray.set(i, stn.getName());
      descArray.set(i, stn.getDescription());
      if (useWmoId) wmoArray.set(i, stn.getWmoId());
    }

    try {
      ncfile.write(latName, latArray);
      ncfile.write(lonName, lonArray);
      if (useAlt) ncfile.write(altName, altArray);
      ncfile.writeStringData(idName, idArray);
      ncfile.writeStringData(descName, descArray);
      if (useWmoId) ncfile.writeStringData(wmoName, wmoArray);

    } catch (InvalidRangeException e) {
      e.printStackTrace();
      throw new IllegalStateException(e);
    }
  }

  private void writeDataFinish() throws IOException {
    ArrayInt.D1 nextChildArray = new ArrayInt.D1(recno, false);

    int nstns = stnList.size();
    ArrayInt.D1 firstArray = new ArrayInt.D1(nstns, false);
    ArrayInt.D1 lastArray = new ArrayInt.D1(nstns, false);
    ArrayInt.D1 numArray = new ArrayInt.D1(nstns, false);

    for (int i = 0; i < stnList.size(); i++) {
      ucar.unidata.geoloc.Station stn = stnList.get(i);
      StationTracker tracker = stationMap.get(stn.getName());

      lastArray.set(i, tracker.lastChild);
      numArray.set(i, tracker.numChildren);

      int first = (tracker.link.size() > 0) ? tracker.link.get(0) : -1;
      firstArray.set(i, first);

      if (tracker.link.size() > 0) {
        // construct forward link
        List<Integer> nextList = tracker.link;
        for (int j = 0; j < nextList.size() - 1; j++) {
          Integer curr = nextList.get(j);
          Integer next = nextList.get(j + 1);
          nextChildArray.set(curr, next);
        }
        Integer curr = nextList.get(nextList.size() - 1);
        nextChildArray.set(curr, -1);
      }
    }

    try {
      ncfile.write(firstChildName, firstArray);
      ncfile.write(lastChildName, lastArray);
      ncfile.write(numChildName, numArray);
      ncfile.write(nextChildName, nextChildArray);

    } catch (InvalidRangeException e) {
      e.printStackTrace();
      throw new IllegalStateException(e);
    }

    // if there is no data
    if (minDate == null) minDate = new Date();
    if (maxDate == null) maxDate = new Date();

    ncfile.updateAttribute(null, new Attribute("time_coverage_start", dateFormatter.toDateTimeStringISO(minDate)));
    ncfile.updateAttribute(null, new Attribute("time_coverage_end", dateFormatter.toDateTimeStringISO(maxDate)));
  }

  private int recno = 0;
  private ArrayObject.D1 timeArray = new ArrayObject.D1(DataType.STRING, String.class, false, 1);
  private ArrayInt.D1 prevArray = new ArrayInt.D1(1, false);
  private ArrayInt.D1 parentArray = new ArrayInt.D1(1, false);
  private int[] origin = new int[1];
  private int[] originTime = new int[2];

  public void writeRecord(StationObsDatatype sobs, StructureData sdata) throws IOException {
    if (debug) System.out.println("sobs= " + sobs + "; station = " + sobs.getStation());
    writeRecord(sobs.getStation().getName(), sobs.getObservationTimeAsDate(), sdata);
  }

  public void writeRecord(String stnName, Date obsDate, StructureData sdata) throws IOException {
    StationTracker tracker = stationMap.get(stnName);

    // needs to be wrapped as an ArrayStructure, even though we are only writing one at a time.
    ArrayStructureW sArray = new ArrayStructureW(sdata.getStructureMembers(), new int[]{1});
    sArray.setStructureData(sdata, 0);

    // date is handled specially
    if ((minDate == null) || minDate.after(obsDate)) minDate = obsDate;
    if ((maxDate == null) || maxDate.before(obsDate)) maxDate = obsDate;

    timeArray.set(0, dateFormatter.toDateTimeStringISO(obsDate));
    prevArray.set(0, tracker.lastChild);
    parentArray.set(0, tracker.parent_index);
    tracker.link.add(recno);
    tracker.lastChild = recno;
    tracker.numChildren++;

    // write the recno record
    origin[0] = recno;
    originTime[0] = recno;
    try {
      ncfile.write("record", origin, sArray);
      ncfile.writeStringData(timeName, originTime, timeArray);
      ncfile.write(prevChildName, originTime, prevArray);
      ncfile.write(parentName, originTime, parentArray);

    } catch (InvalidRangeException e) {
      e.printStackTrace();
      throw new IllegalStateException(e);
    }

    recno++;
  }

  public void finish() throws IOException {
    writeDataFinish();
    ncfile.close();
  }

  private LatLonRect getBoundingBox(List stnList) {
    ucar.unidata.geoloc.Station s = (ucar.unidata.geoloc.Station) stnList.get(0);
    LatLonPointImpl llpt = new LatLonPointImpl();
    llpt.set(s.getLatitude(), s.getLongitude());
    LatLonRect rect = new LatLonRect(llpt, .001, .001);

    for (int i = 1; i < stnList.size(); i++) {
      s = (ucar.unidata.geoloc.Station) stnList.get(i);
      llpt.set(s.getLatitude(), s.getLongitude());
      rect.extend(llpt);
    }

    return rect;
  }

  // not tested
  private void write(StationObsDataset sobsDataset) throws IOException {
    createGlobalAttributes();
    createStations(sobsDataset.getStations());

    ncfile.addGlobalAttribute("time_coverage_start", dateFormatter.toDateTimeStringISO(sobsDataset.getStartDate()));
    ncfile.addGlobalAttribute("time_coverage_end", dateFormatter.toDateTimeStringISO(sobsDataset.getEndDate()));

    createDataVariables(sobsDataset.getDataVariables());

    // global attributes
    List gatts = sobsDataset.getGlobalAttributes();
    for (int i = 0; i < gatts.size(); i++) {
      Attribute att = (Attribute) gatts.get(i);
      ncfile.addGlobalAttribute(att);
    }

    // done with define mode
    ncfile.create();

    // write out the station info
    writeStationData(sobsDataset.getStations());

    // now write the observations
    if (! (Boolean) ncfile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE))
      throw new IllegalStateException("can't add record variable");

    int[] origin = new int[1];
    int[] originTime = new int[2];
    int recno = 0;
    ArrayStructureW sArray = null;
    ArrayObject.D1 timeArray = new ArrayObject.D1(DataType.STRING, String.class, false, 1);

    DataIterator diter = sobsDataset.getDataIterator(1000 * 1000);
    while (diter.hasNext()) {
      StationObsDatatype sobs = (StationObsDatatype) diter.nextData();
      StructureData recordData = sobs.getData();

      // needs to be wrapped as an ArrayStructure, even though we are only writing one at a time.
      if (sArray == null)
        sArray = new ArrayStructureW(recordData.getStructureMembers(), new int[]{1});
      sArray.setStructureData(recordData, 0);

      // date is handled specially
      timeArray.set(0, dateFormatter.toDateTimeStringISO(sobs.getObservationTimeAsDate()));

      // write the recno record
      origin[0] = recno;
      originTime[0] = recno;
      try {
        ncfile.write("record", origin, sArray);
        ncfile.writeStringData(timeName, originTime, timeArray);

      } catch (InvalidRangeException e) {
        e.printStackTrace();
        throw new IllegalStateException(e);
      }
      recno++;
    }

    ncfile.close();
  }

  public static void main(String args[]) throws IOException {
    long start = System.currentTimeMillis();

    String location = "C:/data/metars/Surface_METAR_20070513_0000.nc";
    StringBuilder errlog = new StringBuilder();
    StationObsDataset sobs = (StationObsDataset) TypedDatasetFactory.open(FeatureType.STATION, location, null, errlog);

    String fileOut = "C:/temp/Surface_METAR_20070513_0000.rewrite.nc";
    WriterStationObsDataset writer = new WriterStationObsDataset(fileOut, "test");

    List stns = sobs.getStations();
    List<ucar.unidata.geoloc.Station> stnList = new ArrayList<ucar.unidata.geoloc.Station>();
    ucar.unidata.geoloc.Station s = (ucar.unidata.geoloc.Station) stns.get(0);
    stnList.add(s);

    List<VariableSimpleIF> varList = new ArrayList<VariableSimpleIF>();
    varList.add(sobs.getDataVariable("wind_speed"));

    writer.writeHeader(stnList, varList);

    DataIterator iter = sobs.getDataIterator(s);
    while (iter.hasNext()) {
      StationObsDatatype sobsData = (StationObsDatatype) iter.nextData();
      StructureData data = sobsData.getData();
      writer.writeRecord(sobsData, data);
    }

    writer.finish();

    long took = System.currentTimeMillis() - start;
    System.out.println("That took = " + took);
  }

}


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

package ucar.util.prefs;

import java.util.*;
import java.util.prefs.*;

/**
 * An extension of java.util.prefs.Preferences (jdk 1.4) that provides a
 * platform-independent implementation using XML files as backing store.
 * <p> To save Java beans, use putBean() and putBeanCollection(). This uses
 *  reflection to get/set properties that have simple single-valued accessor methods
 *  of primitive and String type.
 * <p> For arbitrary objects, use putBeanObject(), which uses the
 * XMLEncode/XMLDecode API (jdk 1.4).
 *
 * To obtain a PreferencesExt object, instantiate an XMLStore object and
 * call XMLStore.getPreferences().
 *
 * @see ucar.util.prefs.XMLStore
 * @see java.util.prefs.Preferences
 * @author John Caron
 */

public class PreferencesExt extends java.util.prefs.AbstractPreferences implements ucar.util.prefs.PersistenceManager {
    static Preferences userRoot = new PreferencesExt(null, ""); // ??
    static Preferences systemRoot = new PreferencesExt(null, "");

    /** Set the user root you get when you call Preferences.userRoot(). */
    static public void setUserRoot( PreferencesExt prefs) { userRoot = prefs; }
    /** Set the system root you get when you call Preferences.systemRoot(). */
    static public void setSystemRoot( PreferencesExt prefs) {
      systemRoot = prefs;
    }

    private PreferencesExt parent;
    private HashMap keyValues, children;
    private PreferencesExt storedDefaults = null;

    /**
     * Constructor. Usually you get a PreferencesExt object from XMLStore.getPrefs(),
     *  rather than constructing one directly.
     *  For the root node, parent = null and name = "".
     */
    public PreferencesExt(PreferencesExt parent, String name) {
      super(parent, name);
      this.parent = parent;

      keyValues = new HashMap(20);
      children = new HashMap(10);
    }
    void setStoredDefaults( PreferencesExt storedDefaults) {
      this.storedDefaults = storedDefaults;
    }
    private PreferencesExt getStoredDefaults() {
      return (parent == null) ? storedDefaults : parent.getStoredDefaults();
    }

    /** return true unless this is the systemRoot node */
    public boolean isUserNode() { return this != systemRoot; } // ??

    ////////////////////////////////////////////////////////
    // get/put beans

    /**
     * Get the object that has the specified key.
     * This returns the object itself, not a copy, so
     * if you change the bean and call store.save(), any changes to the object will be saved,
     * even without calling putBean(). If you want to change the object without saving the
     * changes, you must make a copy of the object yourself.
     *
     * @param key get the object with this key.
     * @param def the default value to be returned in the event that this
     *        preference node has no value associated with <tt>key</tt>.
     * @return the value associated with <tt>key</tt>, or <tt>def</tt>
     *         if no value is associated with <tt>key</tt>.
     * @throws IllegalStateException if this node (or an ancestor) has been
     *         removed with the {@link #removeNode()} method.
     * @throws NullPointerException if key is <tt>null</tt>.  (A
     *         <tt>null</tt> default <i>is</i> permitted.)
     */
    public Object getBean(String key, Object def) {
      if (key==null)
        throw new NullPointerException("Null key");
      if (isRemoved())
        throw new IllegalStateException("Node has been removed.");

      synchronized(lock) {
        Object result = null;
        try {
          result = _getObject( key);
          if (result != null) {
            if (result instanceof Bean.Collection)
              result = ((Bean.Collection)result).getCollection();
            else if (result instanceof Bean)
              result = ((Bean)result).getObject();
          }
        } catch (Exception e) {
          // Ignoring exception causes default to be returned
        }
        return (result==null ? def : result);
      }
    }

    /**
     *  Stores an object using simple bean properties.
     *  If the exact key and value are already in
     *  the storedDefaults (using equals() to test for equality), then it is not
     *  stored.
     *
     * @param key key with which the specified value is to be associated.
     * @param newValue store this bean.
     * @throws NullPointerException if key or value is <tt>null</tt>.
     * @throws IllegalStateException if this node (or an ancestor) has been
     *         removed with the {@link #removeNode()} method.
     */
    public void putBean(String key, Object newValue) {
      // if matches a stored Default, dont store
      Object oldValue = getBean(key, null);
      if ((oldValue == null) || !oldValue.equals( newValue))
        keyValues.put( key, new Bean(newValue));
    }

    /**
     *  Stores a Collection of beans. The beans are stored using simple bean properties.
     *   The collection of beans must all be of the same class.
     *
     * @param key key with which the specified collection is to be associated.
     * @param newValue store this collection of beans.
     * @throws NullPointerException if key or value is <tt>null</tt>.
     * @throws IllegalStateException if this node (or an ancestor) has been
     *         removed with the {@link #removeNode()} method.
     */
    public void putBeanCollection(String key, Collection newValue) {
      // if matches a stored Default, dont store
      Object oldValue = getBean(key, null);
      if ((oldValue == null) || !oldValue.equals( newValue))
        keyValues.put( key, new Bean.Collection(newValue));
    }

    /**
     *  Stores an object using XMLEncoder/XMLDecoder. Use this for arbitrary objects.
     *  If the exact key and value are already in
     *  the storedDefaults (using equals() to test for equality), then it is not
     *  stored.
     *
     * @param key key with which the specified value is to be associated.
     * @param newValue store this bean object.
     * @throws NullPointerException if key or value is <tt>null</tt>.
     * @throws IllegalStateException if this node (or an ancestor) has been
     *         removed with the {@link #removeNode()} method.
     */
    public void putBeanObject(String key, Object newValue) {
      // if matches a stored Default, dont store
      Object oldValue = getBean(key, null);
      if ((oldValue == null) || !oldValue.equals( newValue))
        keyValues.put( key, newValue);
    }

    ////////////////////////////////////////////////////////
    // get/put list

    /**
     * Get an arrayList. This returns a copy of the stored list.
     *
     * @param key key whose associated value is to be returned.
     * @param def the value to be returned in the event that this
     *        preference node has no value associated with <tt>key</tt>.
     * @return the value associated with <tt>key</tt>, or <tt>def</tt>
     *         if no value is associated with <tt>key</tt>.
     */
    public List getList(String key, List def) {
      try {
        Object bean = getBean(key, def);
        return (List) bean;
      } catch (Exception e) {
        e.printStackTrace();
        return null;
      }
    }

    /**
     *  Stores the value with this key, if the exact key and value are not already in
     *  the storedDefaults (using equals() to test for equality).
     *  "Two lists are defined to be equal if they contain the same elements in the same order."
     *
     * @param key key with which the specified value is to be associated.
     * @param newValue value to be associated with the specified key.
     */
    public void putList(String key, List newValue) {
      putBeanObject(key, newValue);
    }

    /////// the SPI interface
    /**
     * Implements <tt>AbstractPreferences</tt> <tt>childrenNamesSpi()</tt> method.
     * Find all children nodes of this node (or of identically named nodes in
     * storedDefaults)
     */
    protected String[] childrenNamesSpi() {
      HashSet allKids = new HashSet(children.keySet());
      PreferencesExt sd = getStoredDefaults();
      if (sd != null)
        allKids.addAll(  sd.childrenNamesSpi(absolutePath()));

      ArrayList list = new ArrayList( allKids);
      Collections.sort(list);
      String result[] = new String[list.size()];
      for (int i=0; i<list.size(); i++)
        result[i] = list.get(i).toString();
      return result;
    }

    /* Find all children nodes of named node (or of identically named nodes in
       storedDefaults) */
    protected Collection childrenNamesSpi(String nodePath) {
      HashSet allKids = new HashSet();
      try {
        if (nodeExists( nodePath)) {
          PreferencesExt node = (PreferencesExt) node( nodePath);
          allKids.addAll( node.children.keySet());
        }
      } catch (java.util.prefs.BackingStoreException e) { }

      PreferencesExt sd = getStoredDefaults();
      if (sd != null)
        allKids.addAll(  sd.childrenNamesSpi(nodePath));
      return allKids;
    }

   String[] keysNoDefaults() throws BackingStoreException{
      HashSet allKeys = new HashSet(keyValues.keySet());

      ArrayList list = new ArrayList( allKeys);
      Collections.sort(list);
      String result[] = new String[list.size()];
      for (int i=0; i<list.size(); i++)
        result[i] = list.get(i).toString();
      return result;
    }

    /* Find all key names of this node (or of identically named nodes in
      storedDefaults)

      (The returned array will be of size zero if this node has no preferences.)
      It is guaranteed that this node has not been removed.

      This method is invoked with the lock on this node held.
     */
    protected String[] keysSpi() throws BackingStoreException{
      HashSet allKeys = new HashSet(keyValues.keySet());
      //show( "allKeys1 ", allKeys);

      PreferencesExt sd = getStoredDefaults();
      if (sd != null)
        allKeys.addAll(  sd.keysSpi(absolutePath()));
      //show( "allKeys2 ", allKeys);

      ArrayList list = new ArrayList( allKeys);
      Collections.sort(list);
      //show( "allKeys3 ", list);

      String result[] = new String[list.size()];
      for (int i=0; i<list.size(); i++)
        result[i] = list.get(i).toString();
      return result;
    }

    /* Find all keys of named node (or of identically named nodes in
       storedDefaults) */
    protected Collection keysSpi(String nodePath) {
      HashSet allKeys = new HashSet();
      try {
        if (nodeExists( nodePath)) {
          PreferencesExt node = (PreferencesExt) node( nodePath);
          //show( "subKeys1 "+nodePath, node.keyValues.keySet());
          allKeys.addAll( node.keyValues.keySet());
        }
      } catch (java.util.prefs.BackingStoreException e) { }

      PreferencesExt sd = getStoredDefaults();
      if (sd != null) {
        allKeys.addAll( sd.keysSpi(nodePath));
        //show( "subKeys2 ", allKeys);
      }

      return allKeys;
    }

    void dump() throws BackingStoreException {
         // Put map in xml element
      String[] keys = keys();
      for (int i=0; i<keys.length; i++) {
          System.out.println("key = " + keys[i]+" value= "+ get(keys[i], null));
      }
      String[] kidNames = childrenNames();
      for (int i = 0; i <  kidNames.length; i++)
        ((PreferencesExt) node(kidNames[i])).dump();
  }

  void show( String what, Collection c) {
    try {
      System.out.println("---"+what+":");
      Iterator iter = c.iterator();
      while(iter.hasNext()) {
        Object o = iter.next();
        System.out.println("  "+o.toString()+" "+o.getClass().getName());
      }
    } catch (Exception e) {
      System.out.println(e);
      e.printStackTrace();
    }
    System.out.println("***");
  }
    /*
      Returns the named child of this preference node, creating it if it does not
      already exist. It is guaranteed that name is non-null, non-empty, does not
      contain the slash character ('/'), and is no longer than
      Preferences.MAX_NAME_LENGTH characters. Also, it is guaranteed that this node
      has not been removed. (The implementor needn't check for any of these things.)

      Finally, it is guaranteed that the named node has not been returned by a
      previous invocation of this method or getChild(String) after the last time
      that it was removed. In other words, a cached value will always be used in
      preference to invoking this method. Subclasses need not maintain their own
      cache of previously returned children.

      The implementer must ensure that the returned node has not been removed.
      If a like-named child of this node was previously removed, the implementer
      must return a newly constructed AbstractPreferences node; once removed, an
      AbstractPreferences node cannot be "resuscitated."

      If this method causes a node to be created, this node is not guaranteed to
      be persistent until the flush method is invoked on this node or one of its
      ancestors (or descendants).

      This method is invoked with the lock on this node held.
     */
    protected AbstractPreferences childSpi(String name) {
      PreferencesExt child;

      if (null != (child = (PreferencesExt) children.get(name)))
        return child;

      child = new PreferencesExt(this, name);
      children.put( name, child);
      child.newNode = true;
      return child;
    }

   /**
    * Empty, never used implementation  of AbstractPreferences.flushSpi().
    */
    protected void flushSpi() throws BackingStoreException {
        // assert false;
    }

    /* Gets the value with this keyName, if not found, look in storedDefaults.

      Return the value associated with the specified key at this preference node,
      or null if there is no association for this key, or the association cannot be
      determined at this time. It is guaranteed that key is non-null. Also, it is
      guaranteed that this node has not been removed. (The implementor needn't
      check for either of these things.)

      Generally speaking, this method should not throw an exception under any
      circumstances. If, however, if it does throw an exception, the exception
      will be intercepted and treated as a null return value.

      This method is invoked with the lock on this node held.
     */
    protected String getSpi(String keyName) {
      Object o = _getObject( keyName);
      return (o == null) ? null : o.toString();
    }

    /* Stores the value with this key, if not already in storedDefaults.

      Put the given key-value association into this preference node. It is guaranteed
      that key and value are non-null and of legal length. Also, it is guaranteed
      that this node has not been removed. (The implementor needn't check for any of these things.)
      This method is invoked with the lock on this node held.
    */
    protected void putSpi(String key,  String newValue) {
      // if matches a stored Default, dont store
      String oldValue = getSpi(key);
      if ((oldValue == null) || !oldValue.equals( newValue))
        keyValues.put( key, newValue);
    }

    /* removes node, no effect on storedDefaults

      Removes this preference node, invalidating it and any preferences that
      it contains. The named child will have no descendants at the time this
      invocation is made (i.e., the Preferences.removeNode() method invokes
      this method repeatedly in a bottom-up fashion, removing each of a node's
      descendants before removing the node itself).

      This method is invoked with the lock held on this node and its parent (
      and all ancestors that are being removed as a result of a single invocation
      to Preferences.removeNode()).

      The removal of a node needn't become persistent until the flush method is
      invoked on this node (or an ancestor).
     */
    protected void removeNodeSpi() throws BackingStoreException {
      //System.out.println(" removeNodeSpi :"+name());
      if (parent != null) {
        if (null == parent.children.remove( name()))
          System.out.println("ERROR PreferencesExt.removeNodeSpi :"+name());
      }
    }

    /** removes key/value if exists, no effect on storedDefaults
     *
      Remove the association (if any) for the specified key at this preference node.
      It is guaranteed that key is non-null. Also, it is guaranteed that this node
      has not been removed. (The implementor needn't check for either of these things.)
      This method is invoked with the lock on this node held.
     */
    protected void removeSpi(String key) {
      keyValues.remove( key);
    }

    /*
      This method is invoked with this node locked. The contract of this method is
      to synchronize any cached preferences stored at this node with any stored
      in the backing store. (It is perfectly possible that this node does not
      exist on the backing store, either because it has been deleted by another
      VM, or because it has not yet been created.) Note that this method should
      not synchronize the preferences in any subnodes of this node. If the backing
      store naturally syncs an entire subtree at once, the implementer is encouraged
      to override sync(), rather than merely overriding this method.

      If this node throws a BackingStoreException, the exception will propagate
      out beyond the enclosing sync() invocation.
    */
    protected void syncSpi() throws BackingStoreException {
        // assert false;
    }

    ////////////////////////////////////////////////////////////////////////////
    // low level

    Object getObjectNoDefaults(String keyName) {
      return keyValues.get( keyName);
    }

    public void putObject(String keyName, Object value) {
      if (keyName == null)
        throw new IllegalArgumentException("PreferencesExt try to store null keyname");
      keyValues.put( keyName, value);
    }

    public Object getObject(String key) {
      synchronized(lock) {
        Object result = null;
        try {
          result = _getObject(key);
        }
        catch (Exception e) {
          // Ignoring exception
        }
        return result;
      }
    }

    // assume key non-null, locked node
    private Object _getObject(String keyName) {
      Object result = null;
      try {
        result = keyValues.get( keyName);
        if (result == null) {
          // if failed, check the stored Defaults
          PreferencesExt sd = getStoredDefaults();
          if (sd != null)
            result = sd.getObjectFromNode( absolutePath(), keyName);
        }
      } catch (Exception e) {
              // Ignoring exception causes default to be returned
      }
      return result;
    }

    private Object getObjectFromNode(String nodePath, String keyName) {
      Object result = null;
      try {
        if (nodeExists( nodePath)) {
          PreferencesExt node = (PreferencesExt) node( nodePath);
          synchronized (node) {
            result = node._getObject( keyName);
          }
        }
      } catch (java.util.prefs.BackingStoreException e) { }

      // if failed, check the stored Defaults
      PreferencesExt sd = getStoredDefaults();
      if ((result == null) && (sd != null)) {
        synchronized (sd) {
          result = sd.getObjectFromNode( nodePath, keyName);
        }
      }
      return result;
    }

}