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

import ucar.ma2.ArrayObject;
import ucar.nc2.*;
import ucar.nc2.constants.*;
import ucar.nc2.dataset.CoordinateSystem;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.ArrayChar;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Formatter;

import org.jdom2.Element;

/**
 * Parse structural metadata from HDF-EOS.
 * This allows us to use shared dimensions, identify Coordinate Axes, and the FeatureType.
 *

 *
from HDF-EOS.status.ppt:
 *

 * HDF-EOS is format for EOS  Standard Products
 *

 *
Landsat 7 (ETM+)
 *
Terra (CERES, MISR, MODIS, ASTER, MOPITT)
 *
Meteor-3M (SAGE III)
 *
Aqua (AIRS, AMSU-A, AMSR-E, CERES, MODIS)
 *
Aura(MLS, TES, HIRDLS, OMI
 *

 * HDF is used by other EOS missions
 *

 *
OrbView 2 (SeaWIFS)
 *
TRMM (CERES, VIRS, TMI, PR)
 *
Quickscat (SeaWinds)
 *
EO-1 (Hyperion, ALI)
 *
ICESat (GLAS)
 *
Calypso
 *

 *

 *

 *
 * @author caron
 * @since Jul 23, 2007
 */
public class HdfEos {
  static public final String HDF5_GROUP = "HDFEOS_INFORMATION";
  static public final String HDFEOS_CRS = "_HDFEOS_CRS";
  static public final String HDFEOS_CRS_Projection = "Projection";
  static public final String HDFEOS_CRS_UpperLeft = "UpperLeftPointMtrs";
  static public final String HDFEOS_CRS_LowerRight = "LowerRightMtrs";
  static public final String HDFEOS_CRS_ProjParams = "ProjParams";
  static public final String HDFEOS_CRS_SphereCode = "SphereCode";

  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HdfEos.class);
  static boolean showWork = false; // set in debug
  static private final String GEOLOC_FIELDS = "Geolocation Fields";
  static private final String GEOLOC_FIELDS2 = "Geolocation_Fields";
  static private final String DATA_FIELDS = "Data Fields";
  static private final String DATA_FIELDS2 = "Data_Fields";

  /**
   * Amend the given NetcdfFile with metadata from HDF-EOS structMetadata.
   * All Variables named StructMetadata.n, where n= 1, 2, 3 ... are read in and their contents concatenated
   * to make the structMetadata String.
   *
   * @param ncfile   Amend this file
   * @param eosGroup the group containing variables named StructMetadata.*
   * @return true if HDF-EOS info was found
   * @throws IOException on read error
   */
  static public boolean amendFromODL(NetcdfFile ncfile, Group eosGroup) throws IOException {
    String smeta = getStructMetadata(eosGroup);
    if (smeta == null) { return false; }

    HdfEos fixer = new HdfEos();
    fixer.fixAttributes(ncfile.getRootGroup());
    fixer.amendFromODL(ncfile, smeta);
    return true;
  }

    /**
     *
     */
  static public boolean getEosInfo(NetcdfFile ncfile, Group eosGroup, Formatter f) throws IOException {
    String smeta = getStructMetadata(eosGroup);
    if (smeta == null) {
      f.format("No StructMetadata variables in group %s %n", eosGroup.getFullName());
      return false;
    }
    f.format("raw = %n%s%n", smeta);
    ODLparser parser = new ODLparser();
    parser.parseFromString(smeta); // now we have the ODL in JDOM elements
    StringWriter sw = new StringWriter(5000);
    parser.showDoc(new PrintWriter(sw));
    f.format("parsed = %n%s%n", sw.toString());
    return true;
  }

    /**
     *
     */
  static private String getStructMetadata(Group eosGroup) throws IOException {
    StringBuilder sbuff = null;
    String structMetadata = null;

    int n = 0;
    while (true) {
      Variable structMetadataVar = eosGroup.findVariable("StructMetadata." + n);
      if (structMetadataVar == null) { break; }
      if ((structMetadata != null) && (sbuff == null)) { // more than 1 StructMetadata
        sbuff = new StringBuilder(64000);
        sbuff.append(structMetadata);
      }

      // read and parse the ODL
      Array A = structMetadataVar.read();
      if (A instanceof ArrayChar.D1) {
        ArrayChar ca = (ArrayChar) A;
        structMetadata = ca.getString(); // common case only StructMetadata.0, avoid extra copy
      } else if (A instanceof ArrayObject.D0) {
        ArrayObject ao = (ArrayObject) A;
        structMetadata = (String) ao.getObject(0);
      } else {
        log.error("Unsupported array type {} for StructMetadata", A.getElementType());
      }

      if (sbuff != null) {
        sbuff.append(structMetadata);
      }
      n++;
    }
    return (sbuff != null) ? sbuff.toString() : structMetadata;
  }

    /**
     * Amend the given NetcdfFile with metadata from HDF-EOS structMetadata
     *
     * @param ncfile         Amend this file
     * @param structMetadata structMetadata as String
     * @throws IOException on read error
     */
    private void amendFromODL(NetcdfFile ncfile, String structMetadata) throws IOException {
        Group rootg = ncfile.getRootGroup();

        ODLparser parser = new ODLparser();
        Element root = parser.parseFromString(structMetadata); // now we have the ODL in JDOM elements
        FeatureType featureType = null;

        // SWATH
        Element swathStructure = root.getChild("SwathStructure");
        if (swathStructure != null) {
            List<Element> swaths = swathStructure.getChildren();
            for (Element elemSwath : swaths) {
                Element swathNameElem = elemSwath.getChild("SwathName");
                if (swathNameElem == null) {
                    log.warn("No SwathName element in {} {} ", elemSwath.getName(), ncfile.getLocation());
                    continue;
                }
                String swathName = NetcdfFile.makeValidCdmObjectName(swathNameElem.getText().trim());
                Group swathGroup = findGroupNested(rootg, swathName);
                //if (swathGroup == null)
                //  swathGroup = findGroupNested(rootg, H4header.createValidObjectName(swathName));

                if (swathGroup != null) {
                    featureType = amendSwath(ncfile, elemSwath, swathGroup);
                } else {
                    log.warn("Cant find swath group {} {}", swathName, ncfile.getLocation());
                }
            }
        }

        // GRID
        Element gridStructure = root.getChild("GridStructure");
        if (gridStructure != null) {
            List<Element> grids = gridStructure.getChildren();
            for (Element elemGrid : grids) {
                Element gridNameElem = elemGrid.getChild("GridName");
                if (gridNameElem == null) {
                    log.warn("No GridName element in {} {} ", elemGrid.getName(), ncfile.getLocation());
                    continue;
                }
                String gridName = NetcdfFile.makeValidCdmObjectName(gridNameElem.getText().trim());
                Group gridGroup = findGroupNested(rootg, gridName);
                //if (gridGroup == null)
                //  gridGroup = findGroupNested(rootg, H4header.createValidObjectName(gridName));
                if (gridGroup != null) {
                    featureType = amendGrid(elemGrid, ncfile, gridGroup, ncfile.getLocation());
                } else {
                    log.warn("Cant find Grid group {} {}", gridName, ncfile.getLocation());
                }
            }
        }

        // POINT - NOT DONE YET
        Element pointStructure = root.getChild("PointStructure");
        if (pointStructure != null) {
            List<Element> pts = pointStructure.getChildren();
            for (Element elem : pts) {
                Element nameElem = elem.getChild("PointName");
                if (nameElem == null) {
                    log.warn("No PointName element in {} {}", elem.getName(), ncfile.getLocation());
                    continue;
                }
                String name = nameElem.getText().trim();
                Group ptGroup = findGroupNested(rootg, name);
                //if (ptGroup == null)
                //  ptGroup = findGroupNested(rootg, H4header.createValidObjectName(name));
                if (ptGroup != null) {
                    featureType = FeatureType.POINT;
                } else {
                    log.warn("Cant find Point group {} {}", name, ncfile.getLocation());
                }
            }
        }

    if (featureType != null) {
      if (showWork) { log.debug("***EOS featureType= {}",featureType.toString());}
      rootg.addAttribute(new Attribute(CF.FEATURE_TYPE, featureType.toString()));
      // rootg.addAttribute(new Attribute(CDM.CONVENTIONS, "HDFEOS"));
    }

  }

    /**
     *
     */
    private FeatureType amendSwath(NetcdfFile ncfile, Element swathElem, Group parent) {
        FeatureType featureType = FeatureType.SWATH;
        List<Dimension> unknownDims = new ArrayList<>();

        // Dimensions
        Element d = swathElem.getChild("Dimension");
        List<Element> dims = d.getChildren();
        for (Element elem : dims) {
            String name = elem.getChild("DimensionName").getText().trim();
            name = NetcdfFile.makeValidCdmObjectName(name);

      if (name.equalsIgnoreCase("scalar")){
        continue;
      }String sizeS = elem.getChild("Size").getText().trim();
      int length = Integer.parseInt(sizeS);
      if (length > 0) {
        Dimension dim = parent.findDimensionLocal(name);
        if (dim != null) {                 // already added - may be dimension scale ?
          if (dim.getLength() != length) { // ok as long as it matches
            log.error("Conflicting Dimensions = {} {}", dim, ncfile.getLocation());
            throw new IllegalStateException("Conflicting Dimensions = "+name);
          }
        } else {
          dim = new Dimension(name, length);
          if (parent.addDimensionIfNotExists(dim) && showWork) {
                        log.debug(" Add dimension {}",dim);
        }
      }} else {
        log.warn("Dimension {} has size {} {}", name,sizeS, ncfile.getLocation());
        Dimension udim = new Dimension(name, 1);
        udim.setGroup(parent);
        unknownDims.add( udim);
        if (showWork) {
                    log.debug(" Add dimension {}", udim);
      }
    }}

        // Dimension Maps
        Element dmap = swathElem.getChild("DimensionMap");
        List<Element> dimMaps = dmap.getChildren();
        for (Element elem : dimMaps) {
            String geoDimName = elem.getChild("GeoDimension").getText().trim();
            geoDimName = NetcdfFile.makeValidCdmObjectName(geoDimName);
            String dataDimName = elem.getChild("DataDimension").getText().trim();
            dataDimName = NetcdfFile.makeValidCdmObjectName(dataDimName);

            String offsetS = elem.getChild("Offset").getText().trim();
            String incrS = elem.getChild("Increment").getText().trim();
            int offset = Integer.parseInt(offsetS);
            int incr = Integer.parseInt(incrS);

            // make new variable for this dimension map
            Variable v = new Variable(ncfile, parent, null, dataDimName);
            v.setDimensions(geoDimName);
            v.setDataType(DataType.INT);
            int npts = (int) v.getSize();
            Array data = Array.makeArray(v.getDataType(), npts, offset, incr);
            v.setCachedData(data, true);
            v.addAttribute(new Attribute("_DimensionMap", ""));
            parent.addVariable(v);
            if (showWork) {
                log.debug(" Add dimensionMap {}", v);
            }
        }

        // Geolocation Variables
        Group geoFieldsG = parent.findGroup(GEOLOC_FIELDS);
        if (geoFieldsG == null) {
            geoFieldsG = parent.findGroup(GEOLOC_FIELDS2);
        }
        if (geoFieldsG != null) {
            Variable latAxis = null, lonAxis = null, timeAxis = null;
            Element floc = swathElem.getChild("GeoField");
            List<Element> varsLoc = floc.getChildren();
            for (Element elem : varsLoc) {
                String varname = elem.getChild("GeoFieldName").getText().trim();
                Variable v = geoFieldsG.findVariable(varname);
                //if (v == null)
                //  v = geoFieldsG.findVariable( H4header.createValidObjectName(varname));
                assert v != null : varname;
                AxisType axis = addAxisType(ncfile, v);
                if (axis == AxisType.Lat)  { latAxis = v; }
                if (axis == AxisType.Lon)  { lonAxis = v; }
                if (axis == AxisType.Time) { timeAxis = v; }

                Element dimList = elem.getChild("DimList");
                List<Element> values = dimList.getChildren("value");
                setSharedDimensions( v, values, unknownDims, ncfile.getLocation());
                if (showWork) {
                    log.debug(" set coordinate {}", v);
                }
            }

            // Treat possibility that this is a discrete geometry featureType.
            // We check if lat and lon axes are 2D and if not (1) see if it looks like a
            // trajectory, or (2) otherwise tag it as a profile.
            // This could/should be expanded to consider other FTs.
            if ((latAxis != null) && (lonAxis != null)) {
                log.debug ("found lonAxis and latAxis -- testing XY domain");
                int xyDomainSize = CoordinateSystem.countDomain(new Variable[]{latAxis, lonAxis});
                log.debug ("xyDomain size {}", xyDomainSize);
                if (xyDomainSize < 2) {
                    if (timeAxis != null) {
                        log.debug ("found timeAxis -- testing if trajectory");
                        Dimension dd1 = timeAxis.getDimension(0);
                        Dimension dd2 = latAxis.getDimension(0);
                        Dimension dd3 = lonAxis.getDimension(0);

                        if (dd1.equals (dd2) && dd1.equals(dd3)) {
                            featureType = FeatureType.TRAJECTORY;
                        } else {
                            featureType = FeatureType.PROFILE;  // ??
                        }
                    } else {
                        featureType = FeatureType.PROFILE;  // ??
                    }
                }
            }
        }

        // Data Variables
        Group dataG = parent.findGroup(DATA_FIELDS);
        if (dataG == null) {
            dataG = parent.findGroup(DATA_FIELDS2);
        }
        if (dataG != null) {
            Element f = swathElem.getChild("DataField");
            List<Element> vars = f.getChildren();
            for (Element elem : vars) {
                Element dataFieldNameElem = elem.getChild("DataFieldName");
                if (dataFieldNameElem == null) {
                    continue;
                }
                String varname = NetcdfFile.makeValidCdmObjectName(dataFieldNameElem.getText().trim());
                Variable v = dataG.findVariable(varname);
                //if (v == null)
                //  v = dataG.findVariable( H4header.createValidObjectName(varname));
                if (v == null) {
                    log.error("Cant find variable {} {}", varname, ncfile.getLocation());
                    continue;
                }

                Element dimList = elem.getChild("DimList");
                List<Element> values = dimList.getChildren("value");
                setSharedDimensions(v, values, unknownDims, ncfile.getLocation());
            }
        }

        return featureType;
    }

    /**
     *
     */
  private AxisType addAxisType(NetcdfFile ncfile, Variable v) {
    String name = v.getShortName();
    if (name.equalsIgnoreCase("Latitude") || name.equalsIgnoreCase("GeodeticLatitude")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));
      v.addAttribute(new Attribute(CDM.UNITS, CDM.LAT_UNITS));
      return AxisType.Lat;

    } else if (name.equalsIgnoreCase("Longitude")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
      v.addAttribute(new Attribute(CDM.UNITS, CDM.LON_UNITS));
      return AxisType.Lon;

    } else if (name.equalsIgnoreCase("Time")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
      if (v.findAttribute(CDM.UNITS) == null) {
        /*
        from http://newsroom.gsfc.nasa.gov/sdptoolkit/hdfeosfaq.html
        HDF-EOS uses the TAI93 (International Atomic Time) format. This means that time is stored as the number of
        elapsed seconds since January 1, 1993 (negative values represent times prior to this date).
        An 8 byte floating point number is used, producing microsecond accuracy from 1963 (when leap second records
        became available electronically) to 2100. The SDP Toolkit provides conversions from other date formats to and
        from TAI93. Other representations of time can be entered as ancillary data, if desired.
        For lists and descriptions of other supported time formats, consult the Toolkit documentation or write to
        landover_PGSTLKIT@raytheon.com.
         */
        v.addAttribute(new Attribute(CDM.UNITS, "seconds since 1993-01-01T00:00:00Z"));
        v.addAttribute(new Attribute(CF.CALENDAR, "TAI"));
        /* String tit = ncfile.findAttValueIgnoreCase(v, "Title", null);
        if (tit != null && tit.contains("TAI93")) {
          // Time is given in the TAI-93 format, i.e. the number of seconds passed since 01-01-1993, 00:00 UTC.
          v.addAttribute(new Attribute(CDM.UNITS, "seconds since 1993-01-01T00:00:00Z"));
          v.addAttribute(new Attribute(CF.CALENDAR, "TAI"));
        } else { // who the hell knows ??
          v.addAttribute(new Attribute(CDM.UNITS, "seconds since 1970-01-01T00:00:00Z"));
        }  */
      }
      return AxisType.Time;

    } else if (name.equalsIgnoreCase("Pressure")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Pressure.toString()));
      return AxisType.Pressure;

    } else if (name.equalsIgnoreCase("Altitude")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Height.toString()));
      v.addAttribute(new Attribute(CF.POSITIVE, CF.POSITIVE_UP)); // probably
      return AxisType.Height;
    }

    return null;
  }

  private FeatureType amendGrid(Element gridElem, NetcdfFile ncfile, Group parent, String location) {
    List<Dimension> unknownDims = new ArrayList<>();

    // always has x and y dimension
    String xdimSizeS = gridElem.getChild("XDim").getText().trim();
    String ydimSizeS = gridElem.getChild("YDim").getText().trim();
    int xdimSize = Integer.parseInt(xdimSizeS);
    int ydimSize = Integer.parseInt(ydimSizeS);
    parent.addDimensionIfNotExists(new Dimension("XDim", xdimSize));
    parent.addDimensionIfNotExists(new Dimension("YDim", ydimSize));

    /* see HdfEosModisConvention
    UpperLeftPointMtrs=(-20015109.354000,1111950.519667)
    		LowerRightMtrs=(-18903158.834333,-0.000000)
    		Projection=GCTP_SNSOID
    		ProjParams=(6371007.181000,0,0,0,0,0,0,0,0,0,0,0,0)
    		SphereCode=-1
     */
    Element proj = gridElem.getChild("Projection");
    if (proj != null) {
      Variable crs = new Variable(ncfile, parent, null, HDFEOS_CRS);
      crs.setDataType(DataType.SHORT);
      crs.setDimensions(""); // scalar
      crs.setCachedData(Array.makeArray(DataType.SHORT, 1, 0, 0)); // fake data
      parent.addVariable(crs);

      addAttributeIfExists(gridElem, HDFEOS_CRS_Projection, crs, false);
      addAttributeIfExists(gridElem, HDFEOS_CRS_UpperLeft, crs, true);
      addAttributeIfExists(gridElem, HDFEOS_CRS_LowerRight, crs, true);
      addAttributeIfExists(gridElem, HDFEOS_CRS_ProjParams, crs, true);
      addAttributeIfExists(gridElem, HDFEOS_CRS_SphereCode, crs, false);
    }

    // global Dimensions
    Element d = gridElem.getChild("Dimension");
    List<Element> dims = d.getChildren();
    for (Element elem : dims) {
      String name = elem.getChild("DimensionName").getText().trim();
      name = NetcdfFile.makeValidCdmObjectName(name);
      if (name.equalsIgnoreCase("scalar")) {
        continue;
      }

      String sizeS = elem.getChild("Size").getText().trim();
      int length = Integer.parseInt(sizeS);
      Dimension old = parent.findDimension(name);
      if ((old == null) || (old.getLength() != length)) {
        if (length > 0) {
          Dimension dim = new Dimension(name, length);
          if (parent.addDimensionIfNotExists(dim) && showWork) {
            log.debug(" Add dimension {}", dim);
          }
        } else {
          log.warn("Dimension {} has size {} {} ", sizeS, name, location);
          Dimension udim = new Dimension(name, 1);
          udim.setGroup(parent);
          unknownDims.add(udim);
          if (showWork) { log.debug(" Add dimension {}", udim); }
        }
      }
    }

    // Geolocation Variables
    Group geoFieldsG = parent.findGroup(GEOLOC_FIELDS);
    if (geoFieldsG == null)  {geoFieldsG = parent.findGroup(GEOLOC_FIELDS2);
}
    if (geoFieldsG != null) {
      Element floc = gridElem.getChild("GeoField");
      List<Element> varsLoc = floc.getChildren();
      for (Element elem : varsLoc) {
        String varname = elem.getChild("GeoFieldName").getText().trim();
        Variable v = geoFieldsG.findVariable(varname);
        //if (v == null)
        //  v = geoFieldsG.findVariable( H4header.createValidObjectName(varname));
        assert v != null : varname;

        Element dimList = elem.getChild("DimList");
        List<Element> values = dimList.getChildren("value");
        setSharedDimensions(v, values, unknownDims, location);
      }
    }

    // Data Variables
    Group dataG = parent.findGroup(DATA_FIELDS);
    if (dataG == null){ dataG = parent.findGroup(DATA_FIELDS2);  // eg C:\data\formats\hdf4\eos\mopitt\MOP03M-200501-L3V81.0.1.hdf
}
    if (dataG != null) {
      Element f = gridElem.getChild("DataField");
      List<Element> vars = f.getChildren();
      for (Element elem : vars) {
        String varname = elem.getChild("DataFieldName").getText().trim();
        varname = NetcdfFile.makeValidCdmObjectName(varname);
        Variable v = dataG.findVariable(varname);
        //if (v == null)
        //  v = dataG.findVariable( H4header.createValidObjectName(varname));
        assert v != null : varname;

        Element dimList = elem.getChild("DimList");
        List<Element> values = dimList.getChildren("value");
        setSharedDimensions(v, values, unknownDims, location);
      }

      // get projection
      String projS = null;
      Element projElem = gridElem.getChild("Projection");
      if (projElem != null) {
        projS = projElem.getText().trim();
      }
      boolean isLatLon = "GCTP_GEO".equals(projS);

      // look for XDim, YDim coordinate variables
      if (isLatLon) {
        for (Variable v : dataG.getVariables()) {
          if (v.isCoordinateVariable()) {
            if (v.getShortName().equals("YDim")) {
              v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));
              v.addAttribute(new Attribute(CDM.UNITS, CDM.LAT_UNITS));
            }
            if (v.getShortName().equals("XDim")) {
              v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
              v.addAttribute(new Attribute(CDM.UNITS, CDM.LON_UNITS));
            }
          }
        }
      }
    }
    return FeatureType.GRID;
  }

  private void addAttributeIfExists(Element elem, String name, Variable v, boolean isDoubleArray) {
    Element child = elem.getChild(name);
    if (child == null) { return; }
    if (isDoubleArray) {
      List<Element> vElems = child.getChildren();
      List<Double> values = new ArrayList<>();
      for (Element ve : vElems) {
        String valueS = ve.getText().trim();
        try {
          values.add(Double.parseDouble(valueS));
        } catch (NumberFormatException e) {
          log.warn("Cant parse double value "+valueS);
        }
      }
      Attribute att = new Attribute(name, values, false);
      v.addAttribute(att);
    } else {
      String value = child.getText().trim();
      Attribute att = new Attribute(name, value);
      v.addAttribute(att);
    }
  }

  // convert to shared dimensions
  private void setSharedDimensions(Variable v, List<Element> values, List<Dimension> unknownDims, String location) {
    if (values.size() == 0) {
      return;
    }

    // remove the "scalar" dumbension
    Iterator<Element> iter = values.iterator();
    while (iter.hasNext()) {
      Element value = iter.next();
      String dimName = value.getText().trim();
      if (dimName.equalsIgnoreCase("scalar")) {
        iter.remove();
      }
    }

    // gotta have same number of dimensions
    List<Dimension> oldDims = v.getDimensions();
    if (oldDims.size() != values.size()) {
      log.error("Different number of dimensions for {} {}", v, location);
      return;
    }

    List<Dimension> newDims = new ArrayList<>();
    Group group = v.getParentGroup();

    for (int i = 0; i < values.size(); i++) {
      Element value = values.get(i);
      String dimName = value.getText().trim();
      dimName = NetcdfFile.makeValidCdmObjectName(dimName);

      Dimension dim = group.findDimension(dimName);
      Dimension oldDim = oldDims.get(i);
      if (dim == null) {
        dim = checkUnknownDims(dimName, unknownDims, oldDim, location);
      }

      if (dim == null) {
        log.error("Unknown Dimension= {} for variable = {} {} ", dimName, v.getFullName(), location);
        return;
      }
      if (dim.getLength() != oldDim.getLength()) {
        log.error("Shared dimension ({}) has different length than data dimension ({}) shared={} org={} for {} {}",
                    dim.getShortName(),oldDim.getShortName(),
             dim.getLength() , oldDim.getLength() , v,location);
        return;
      }
      newDims.add(dim);
    }
    v.setDimensions(newDims);
    if (showWork) { log.debug(" set shared dimensions for {}", v.getNameAndDimensions()); }
  }

  // look if the wanted dimension is in the  unknownDims list.
  private Dimension checkUnknownDims(String wantDim, List<Dimension> unknownDims, Dimension oldDim, String location) {
    for (Dimension dim : unknownDims) {
      if (dim.getShortName().equals(wantDim)) {
        int len = oldDim.getLength();
        if (len == 0) {
          dim.setUnlimited(true); // allow zero length dimension !!
        }
        dim.setLength(len); // use existing (anon) dimension
        Group parent = dim.getGroup();
        parent.addDimensionIfNotExists(dim);  // add to the parent
        unknownDims.remove(dim); // remove from list LOOK is this ok?
        log.warn("unknownDim {} length set to {}{}", wantDim, oldDim.getLength(), location);
        return dim;
      }
    }
    return null;
  }

  // look for a group with the given name. recurse into subgroups if needed. breadth first
  private Group findGroupNested(Group parent, String name) {

    for (Group g : parent.getGroups()) {
      if (g.getShortName().equals(name)) { return g; }
    }
    for (Group g : parent.getGroups()) {
      Group result = findGroupNested(g, name);
      if (result != null) { return result; }
    }
    return null;
  }

  private void fixAttributes(Group g) {
    for (Variable v : g.getVariables()) {
      for (Attribute a : v.getAttributes()) {
        if (a.getShortName().equalsIgnoreCase("UNIT") || a.getShortName().equalsIgnoreCase("UNITS")) {
          a.setShortName(CDM.UNITS);
        }
        if (a.getShortName().equalsIgnoreCase("SCALE_FACTOR")) {
          a.setShortName(CDM.SCALE_FACTOR);
        }
        if (a.getShortName().equalsIgnoreCase("OFFSET")) {
          a.setShortName(CDM.ADD_OFFSET);
        }
      }
    }

    for (Group ng : g.getGroups()) {
      fixAttributes(ng);
    }

  }

}


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

import ucar.nc2.constants.CDM;

import javax.annotation.concurrent.Immutable;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;

/**
 * Client catalog documentation element
 *
 * @author caron
 * @since 1/9/2015
 */
@Immutable
public class Documentation {
  private final String href, title, type, inlineContent;
  private final URI uri; // resolved

  /**
   * Constructor.
   *
   * @param href          : href of documentation, may be null.
   * @param uri           : absolute URL, or null
   * @param title         : Xlink title, may be null.
   * @param type          : user-defined InvDocumentation type
   * @param inlineContent : optional inline contents.
   */
  public Documentation(String href, URI uri, String title, String type, String inlineContent) {
    this.href = href;
    this.uri = uri;
    this.type = type;
    this.inlineContent = inlineContent;

    if (title != null)
      this.title = title;
    else if (uri != null)
      this.title = uri.toString();
    else
      this.title = null;
  }

  public String getType() {
    return type;
  }

  public boolean hasXlink() {
    return uri != null;
  }

  /**
   * if its a XLink, get the absolute URI
   *
   * @return the XLink URI, else null
   */
  public URI getURI() {
    return uri;
  }

  /**
   * if its a XLink, get the title, to display the link to the user.
   *
   * @return the XLink title, else null
   */
  public String getXlinkTitle() {
    return title;
  }

  /**
   * if its a XLink, get the href, to display the link to the user.
   *
   * @return the XLink href, or null
   */
  public String getXlinkHref() {
    return href;
  }

  public String getInlineContent() {
    return inlineContent;
  }

  /**
   * Get inline content as a string, else null if there is none
   *
   * @return inline content as a string, else null
   */
  public String readXlinkContent() throws java.io.IOException {
    if (uri == null) return "";

    URL url = uri.toURL();
    InputStream is = url.openStream();
    ByteArrayOutputStream os = new ByteArrayOutputStream(is.available());

    // copy to string
    byte[] buffer = new byte[1024];
    while (true) {
      int bytesRead = is.read(buffer);
      if (bytesRead == -1) break;
      os.write(buffer, 0, bytesRead);
    }
    is.close();

    return new String(os.toByteArray(), CDM.utf8Charset);
  }

  @Override
  public String toString() {
    return "Documentation{" +
            "href='" + href + '\'' +
            ", title='" + title + '\'' +
            ", type='" + type + '\'' +
            ", inlineContent='" + inlineContent + '\'' +
            ", uri=" + uri +
            '}';
  }

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

    Documentation that = (Documentation) o;

    if (href != null ? !href.equals(that.href) : that.href != null) return false;
    if (inlineContent != null ? !inlineContent.equals(that.inlineContent) : that.inlineContent != null) return false;
    if (title != null ? !title.equals(that.title) : that.title != null) return false;
    if (type != null ? !type.equals(that.type) : that.type != null) return false;
    if (uri != null ? !uri.equals(that.uri) : that.uri != null) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = href != null ? href.hashCode() : 0;
    result = 31 * result + (title != null ? title.hashCode() : 0);
    result = 31 * result + (type != null ? type.hashCode() : 0);
    result = 31 * result + (inlineContent != null ? inlineContent.hashCode() : 0);
    result = 31 * result + (uri != null ? uri.hashCode() : 0);
    return result;
  }
}


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

package thredds.catalog;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.nc2.stream.CdmRemote;

import java.net.URI;

/**
 * Public interface to a catalog access element, defining how to access a specific web resource.
 *
 * @author john caron
 */

abstract public class InvAccess {
  static private final Logger logger = LoggerFactory.getLogger(InvAccess.class);

  protected InvDataset dataset;
  protected ServiceType type;
  protected DataFormatType dataFormat;
  protected InvService service;
  protected String urlPath;
  protected double dataSize = Double.NaN;

  /**
   * @return the parent dataset. Should not be null.
   */
  public thredds.catalog.InvDataset getDataset() {
    return dataset;
  }

  /**
   * @return the service. Should not be null.
   */
  public thredds.catalog.InvService getService() {
    return service;
  }

  /**
   * @return the urlPath. Should not be null.
   */
  public String getUrlPath() {
    return urlPath;
  }

  /**
   * @return the dataFormatType; may be null, or inherited from dataset.
   */
  public DataFormatType getDataFormatType() {
    return (dataFormat != null) ? dataFormat : dataset.getDataFormatType();
  }

  /**
   * @return the size in bytes. A value of 0.0 or Double.NaN means unknown.
   */
  public double getDataSize() {
    return dataSize;
  }

  /**
   * @return true if it has valid data size info
   */
  public boolean hasDataSize() {
    return dataSize != 0.0 && !Double.isNaN(dataSize);
  }

  /**
   * Get the standard URL, with resolution if the URL is reletive.
   * catalog.resolveURI( getUnresolvedUrlName())
   *
   * @return URL string, or null if error.
   */
  public String getStandardUrlName() {
    URI uri = getStandardUri();
    if (uri == null) return null;
    return uri.toString();
  }

  public String getWrappedUrlName() {
    URI uri = getStandardUri();
    if (uri == null) return null;
    return wrap(uri.toString());
  }

  /**
   * Construct the standard THREDDS access URI for this dataset access method,
   * resolve if the URI is relative.
   *
   * @return the standard fully resolved THREDDS access URI for this dataset access method, or null if error.
   */
  public URI getStandardUri() {
    try {
      InvCatalog cat = dataset.getParentCatalog();
      if (cat == null)
        return new URI(getUnresolvedUrlName());

      return cat.resolveUri(getUnresolvedUrlName());

    } catch (java.net.URISyntaxException e) {
      logger.warn("Error parsing URL= " + getUnresolvedUrlName());
      return null;
    }
  }

  /**
   * Construct "unresolved" URL: service.getBase() + getUrlPath() + service.getSuffix().
   * It is not resolved, so it may be a reletive URL.
   * @return Unresolved Url as a String
   */
  public String getUnresolvedUrlName() {
    return service.getBase() + getUrlPath() + service.getSuffix();
  }

  private String wrap(String url) {
    if (service.getServiceType() == ServiceType.THREDDS)
      return ucar.nc2.thredds.ThreddsDataFactory.SCHEME + url;
    if (service.getServiceType() == ServiceType.CdmRemote)
      return CdmRemote.SCHEME + url;
    return url;
  }
}


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

package opendap.util;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;

/**
 * This class encapsulates the old .ini file functionality
 * that we used to see (and still do) in the Microsoft Operating
 * Systems. This is a handy way of delivering configuration information
 * to software at runtime. The .ini file structure is as follows:
 *

 * [SectionName]

 * PropertyName = Property

 * NextPropertyName = Property2

 * AnotherPropertyName = Property3

 *

 * [AnotherSectionName]

 * PropertyName = Property

 * NextPropertyName = Property2

 * AnotherPropertyName = Property3

 *

 * This class opens and parses the iniFile it's constructor. If
 * the file isn't accesible or is unparsable then the class will
 * not contain any usable configuration information.
 *
 * @author ndp
 * @version $Revision: 15901 $
 */

public class iniFile
{

    private boolean Debug = false;

    private String iniFile;
    private Vector sectionNames;
    private Vector sectionProperties;
    private int currentSection;

    private String errMsg;

    //************************************************************************

    /**
     * We don't want this to get used so we made it protected...
     */
    protected iniFile()
    {
    }
    //************************************************************************

    /**
     * **********************************************************************
     * Create a iniFile object from the file named in the
     * parameter fname. The object will get the append to the
     * file name to the path returned by the call
     * System.getProperty("user.home").
     *
     * @param fname A String containing the name of the .ini file.
     */
    public iniFile(String fname)
    {

        this(null, fname, false);

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Create a iniFile object from the file named in the
     * parameter fname, and found on the parameter path
     *
     * @param path  A String containing the path to the .ini file.
     * @param fname A String containing the name of the .ini file.
     */
    public iniFile(String path, String fname)
    {

        this(path, fname, false);

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Create a iniFile object from the file named in the
     * parameter fname, and found on the parameter path
     *
     * @param path  A String containing the path to the .ini file.
     * @param fname A String containing the name of the .ini file.
     * @param dbg   A boolean that toggles debugging output.
     */
    public iniFile(String path, String fname, boolean dbg)
    {

        Debug = dbg;

        if(path == null)
            path = System.getProperty("user.home");

        String fileSeperator = System.getProperty("file.separator");

        iniFile = path + fileSeperator + fname;
        errMsg = "The file: \"" + iniFile + "\" did not contain recognizable init information.";

        currentSection = -1;
        sectionNames = null;
        sectionProperties = null;
        parseFile();

        if(sectionNames == null)
            System.err.println(errMsg);

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Get the name of the .ini file that was used to create this object.
     *

     * returns A String containing the name of the .ini file
     * that was opened a parsed when this object was instantiated.
     */
    public String getFileName()
    {
        return (iniFile);
    }
    //************************************************************************

    /**
     * **********************************************************************
     * Parse the .ini file indicated by the private String iniFile.
     */
    private void parseFile()
    {

        try {
            try (
                BufferedReader fp = new BufferedReader(new InputStreamReader(new FileInputStream(iniFile), Charset.forName("UTF-8")));
            ) {
                boolean done = false;
                while(!done) {
                    String thisLine = fp.readLine();
		            if(thisLine != null && thisLine.trim().length() == 0)
			            thisLine = null;
                    if(thisLine != null) {
                        if(Debug) System.out.println("Read: \"" + thisLine + "\"");

                        if(thisLine.startsWith(";") || thisLine.equalsIgnoreCase("")) {
                            // Do nothing, it's a comment
                            if(Debug) System.out.println("Ignoring comment or blank line...");
                        } else {
                            int cindx = thisLine.indexOf(";");

                            if(cindx > 0)
                                thisLine = thisLine.substring(0, cindx).trim();

                            if(Debug) System.out.println("Comments removed: \"" + thisLine + "\"");

                            if(thisLine.startsWith("[") && thisLine.endsWith("]")) {

                                String sname = thisLine.substring(1, thisLine.length() - 1).trim();

                                if(Debug) System.out.println("Found Section Name: " + sname);

                                if(sectionNames == null)
                                    sectionNames = new Vector();

                                sectionNames.add(sname);

                                if(sectionProperties == null)
                                    sectionProperties = new Vector();

                                sectionProperties.add(new Vector());

                            } else if(sectionNames != null && sectionProperties != null) {
                                int eqidx = thisLine.indexOf("=");

                                if(eqidx != -1) {
                                    String pair[] = new String[2];
                                    pair[0] = thisLine.substring(0, eqidx).trim();
                                    pair[1] = thisLine.substring(eqidx + 1, thisLine.length()).trim();

                                    if(Debug)
                                        System.out.println("pair[0]: \"" + pair[0] + "\"   pair[1]: \"" + pair[1] + "\"");

                                    // Add the pair to the current property list, which is the
                                    // last element in the sectionProperties vector.
                                    ((Vector) sectionProperties.lastElement()).add(pair);
                                }
                            }
                        }
                    } else {
                        done = true;
                    }
                }
            }
        } catch (FileNotFoundException e) {
            System.err.println("Could Not Find ini File: \"" + iniFile + "\"");
        } catch (IOException e) {
            System.err.println("Could Not Read ini File: \"" + iniFile + "\"");
        }
    }

    //************************************************************************

    /**
     * **********************************************************************
     * Get the list of properties for the section sectionName.
     *
     * @param sectionName A String containing the name of the
     *                    section whose property list is desired.
     * @return An enumeration of the properties in the sectionName
     * Returns null if the section name doesn't exist or there are
     * no properties for the section.
     */
    public Enumeration getPropList(String sectionName)
    {

        if(sectionNames == null) {
            System.err.println(errMsg);
            return (null);
        }

        int sectionIndex = 0;

        Enumeration e = sectionNames.elements();

        boolean done = false;
        while(!done && e.hasMoreElements()) {
            String thisName = (String) e.nextElement();
            if(sectionName.equalsIgnoreCase(thisName))
                done = true;
            else
                sectionIndex++;
        }
        if(!done)
            return (null);
        return (((Vector) sectionProperties.elementAt(sectionIndex)).elements());

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Get the named property from the current section.
     *
     * @param propertyName The name of the desired property.
     * @return A String containing the value of property of the
     * passed property name. Returns null if the property name doesn't exist
     * or is not set.
     */
    public String getProperty(String propertyName)
    {

        if(currentSection < 0) {
            String msg = "You must use the setSection() method before you can use getProperty().";
            System.err.println(msg);
            return (msg);
        }
        String pair[] = null;

        Enumeration e = ((Vector) sectionProperties.elementAt(currentSection)).elements();
        boolean done = false;
        while(!done && e.hasMoreElements()) {

            pair = (String[]) e.nextElement();

            if(pair[0].equalsIgnoreCase(propertyName))
                done = true;
        }

        if(done)
            return (pair[1]);

        return (null);

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Get the list of Sections of this .ini File
     *
     * @return An enumeration of the sections in iniFile
     */
    public Enumeration getSectionList()
    {

        if(sectionNames == null)
            return (null);

        return (sectionNames.elements());

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Prints the iniFile.
     *
     * @param ps The PrintStream to which to print.
     */
    public void printProps(PrintStream ps)
    {

        Enumeration se = getSectionList();

        if(se == null) {
            ps.println(errMsg);
        } else {

            while(se.hasMoreElements()) {
                String sname = (String) se.nextElement();
                setSection(sname);
                ps.println("[" + sname + "]");
                Enumeration pe = getPropList(sname);
                while(pe != null && pe.hasMoreElements()) {
                    String pair[] = (String[]) pe.nextElement();
                    String prop = pair[0];
                    String valu = getProperty(prop);
                    ps.println("    \"" + prop + "\" = \"" + valu + "\"");
                }
            }
        }

    }
    //************************************************************************

    /**
     * **********************************************************************
     * Set the section of the iniFile that you wish to work with. This is
     * persistent for the life of the object, or until it's set again.
     *
     * @param sectionName A String containing the name of the
     *                    section that is desired.
     * @return true if the section exists and the operation was successful,
     * false otherwise.
     */
    public boolean setSection(String sectionName)
    {

        if(sectionNames == null) {
            System.err.println(errMsg);
            return (false);
        }

        int sectionIndex = 0;

        Enumeration e = sectionNames.elements();

        boolean done = false;
        while(!done && e.hasMoreElements()) {
            String thisName = (String) e.nextElement();
            if(sectionName.equalsIgnoreCase(thisName))
                done = true;
            else
                sectionIndex++;
        }

        if(!done)
            return (false);

        currentSection = sectionIndex;

        return (true);
    }
    //************************************************************************

    /**
     * @param args
     */
    public static void main(String args[])
    {
        boolean dbgFlag = true;
        iniFile inf = null;

        switch (args.length) {
        case 1:
            inf = new iniFile(null, args[0], dbgFlag);
            break;

        case 2:
            inf = new iniFile(args[0], args[1], dbgFlag);
            break;

        case 3:
            if(args[2].equalsIgnoreCase("false"))
                dbgFlag = false;
            inf = new iniFile(args[0], args[1], dbgFlag);
            break;

        default:
            System.err.println("Usage: test_iniFile [path] filename.ini [false]");
            System.exit(1);
        }

        inf.printProps(System.out);

    }

}


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

import javax.swing.*;

public class ProjectionParamPanel extends JPanel {
    String name = "ProjectionParamPanel";

    public String toString() {
      return name;
    }

  ProjectionParamPanel() {
      setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
      //setLayout(new FlowLayout());
      //setProjection("ucar.unidata.geoloc.projection.Stereographic");
    }

    // construct input fields based on Projection Class
    public void setProjection(ProjectionManager.ProjectionClass pc)  {
      // clear out any fields
      removeAll();

      for (ProjectionManager.ProjectionParam pp : pc.paramList) {
        // construct the label
        JPanel thisPanel = new JPanel();
        thisPanel.add(new JLabel(pp.name + ": "));

        // text input field
        JTextField tf = new JTextField();
        pp.setTextField(tf);
        tf.setColumns(12);
        thisPanel.add(tf);
        add(thisPanel);
      }
      revalidate();
    }

  }


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

package opendap.servlet.www;

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

import opendap.dap.*;
import opendap.util.dasTools;

/**
 */
public class wwwOutPut {

    private static boolean _Debug = false;

    private int _attrRows;
    private int _attrCols;
    private PrintWriter pWrt;

    public wwwOutPut(PrintWriter sink, int rows, int cols) {
        _attrRows = rows;
        _attrCols = cols;
        pWrt = sink;
    }

    public wwwOutPut(PrintWriter sink) {
        this(sink, 5, 70);
    }

     static final String legal_javascript_id_chars = "_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    // Changed 4/28/10 dmh: legal dods names are not same as legal javascript names, so translate.
    public static String nameForJsCode(String dodsName) {
        StringBuilder buf = new StringBuilder(dodsName);
        for(int i=0;i<buf.length();i++) {
            char c = buf.charAt(i);
            if(c == '-') buf.replace(i,i+1,"_");
            else if(legal_javascript_id_chars.indexOf(c) < 0) {
                String s = "_"+String.valueOf((int)c)+"_";
                buf.replace(i,i+1,s);
            }
        }
        return "dods_" + buf.toString();
    }

    public void writeDisposition(String requestURL) {
        // To get the size to be a function of the image window size, you need to
        // use some JavaScript code to generate the HTML. C++ --> JS --> HTML.
        // 4/8/99 jhrg

        pWrt.print("
"
                + "\n"
                + "
Action:
\n"
                + "<input type=\"button\" value=\"Get ASCII\" onclick=\"ascii_button()\">\n"
                + "<input type=\"button\" value=\"Get Binary\" onclick=\"binary_button()\">\n"
//            + "<input type=\"button\" value=\"Send to Program\" onclick=\"program_button()\">\n" // removed 9/17/01 ndp
                + "<input type=\"button\" value=\"Show Help\" onclick=\"help_button()\">\n"
                + "
\n"
                + "\n"
                + "
Data URL:
"
                + "<input name=\"url\" type=\"text\" size="
                + _attrCols
                + " value=\""
                + requestURL + "\">\n"
        );
    }

    public void writeGlobalAttributes(DAS das, DDS dds) {

        pWrt.print("
\n"
                + "\n"
                + "
Global Attributes:
\n"
                + "<textarea name=\"global_attr\" rows="
                + _attrRows
                + " cols="
                + _attrCols
                + ">\n"
        );

        Enumeration edas = das.getNames();

        while (edas.hasMoreElements()) {
            String name = (String) edas.nextElement();

            if (!dasTools.nameInKillFile(name) &&
                    (dasTools.nameIsGlobal(name) || !dasTools.nameInDDS(name, dds))) {

                try {
                    AttributeTable attr = das.getAttributeTable(name);

                    writeAttributes(attr);
                } catch (NoSuchAttributeException nsae) {
                }
            }
        }

        pWrt.print("</textarea>
\n\n");
    }

    public void writeAttributes(AttributeTable aTbl) {
        writeAttributes(aTbl, "");
    }

    public void writeAttributes(AttributeTable aTbl, String indent) {

        if (aTbl != null) {

            Enumeration e = aTbl.getNames();

            while (e.hasMoreElements()) {
                String aName = (String) e.nextElement();

                Attribute a = aTbl.getAttribute(aName);
                if (a != null) {

                    if (a.isContainer()) {
                        pWrt.print(indent + aName + ":\n");
                        try {
                            writeAttributes(a.getContainer(), indent + "  ");
                        } catch (NoSuchAttributeException nsae) {
                        }
                    } else {
                        pWrt.print(indent + aName + ": ");

                        if (_Debug) {
                            System.out.println("Getting attribute value enumeration for \"" + aName + "\"...");
                        }
                        try {

                            Enumeration es = a.getValues();

                            if (_Debug) {
                                System.out.println("Attribute Values enumeration: " + es);
                            }
                            int i = 0;
                            while (es.hasMoreElements()) {
                                String val = (String) es.nextElement();
                                if (_Debug) {
                                    System.out.println("Value " + i + ": " + val);
                                }

                                pWrt.print(val);

                                if (es.hasMoreElements())
                                    pWrt.print(", ");

                                i++;
                            }
                        } catch (NoSuchAttributeException nsae) {
                        }

                        pWrt.println("");
                    }
                }

            }
        }
    }

    /*
    void
    WWWOutput::write_variable_entries(DAS &das, DDS &dds)
    {
        // This writes the text `Variables:' and then sets up the table so that
        // the first variable's section is written into column two.
        _os << \
    "

Variables:

    ";

        for (Pix p = dds.first_var(); p; dds.next_var(p)) {
	    dds.var(p)->print_val(_os);
	    write_variable_attributes(dds.var(p), das);
	    _os << "\n

\n\n";		// End the current var's section
	    _os << "
\n\n";	// Start the next var in column two
        }
    }
    */
    public void writeVariableEntries(DAS das, DDS dds) {
        // This writes the text `Variables:' and then sets up the table
        // so that the first variable's section is written into column two.

        pWrt.print("
\n"
                + "\n"
                + "
Variables:
\n"
                + "
\n"
        );

        Enumeration e = dds.getVariables();

        while (e.hasMoreElements()) {
            BaseType bt = (BaseType) e.nextElement();

            ((BrowserForm) bt).printBrowserForm(pWrt, das);

            writeVariableAttributes(bt, das);

            pWrt.print("\n

\n\n"); // End the current var's section
            pWrt.print("
\n\n"); // Start the next var in column two
        }

    }

    /*
    void
    WWWOutput::write_variable_attributes(BaseType *btp, DAS &das)
    {
        AttrTable *attr = das.get_table(btp->name());
        // Don't write anything if there are no attributes.
        if (!attr)
	    return;

        _os << "<textarea name=\"" << btp->name() << "_attr" << "\" rows="
	    << _attr_rows << " cols=" << _attr_cols << ">\n";
        write_attributes(attr);
        _os << "</textarea>\n\n";
    }
    */
    public void writeVariableAttributes(BaseType bt, DAS das) {

        try {

            AttributeTable attr = das.getAttributeTable(bt.getEncodedName());

            if (attr != null) {

                pWrt.print("<textarea name=\""
                        + bt.getLongName().replace('.', '_')
                        + "_attr"
                        + "\" rows="
                        + _attrRows
                        + " cols="
                        + _attrCols
                        + ">\n"
                );

                writeAttributes(attr);
                pWrt.print("</textarea>\n\n");

            }
        } catch (NoSuchAttributeException nsae) {
        }
    }

    public void writeSimpleVar(PrintWriter pw, BaseType bt) {

        String name = bt.getLongName().replace('.', '_');
        String type = dasTools.fancyTypeName(bt);

        pWrt.print(
                "\n"
        );

        pWrt.print(
                ""
                        + "<input type=\"checkbox\" name=\"get_"
                        + nameForJsCode(name) + "\"\n"
                        + "onclick=\""
                        + nameForJsCode(name) + ".handle_projection_change(get_"
                        + nameForJsCode(name) + ")\">\n"
                        + ""
                        + bt.getEncodedName() + "" // this is the Bold faced name of the variable in the form
                        + ": " + type + "
\n\n"
        );

        pWrt.print(

                bt.getEncodedName() //this name is the one used when choosing a constraint relation (=, <, >, etc.)
                        + " <select name=\"" + nameForJsCode(name) + "_operator\""
                        + " onfocus=\"describe_operator()\""
                        + " onchange=\"DODS_URL.update_url()\">\n"
                        + "<option value=\"=\" selected>=\n"
                        + "<option value=\"!=\">!=\n"
                        + "<option value=\"<\"><\n"
                        + "<option value=\"<=\"><=\n"
                        + "<option value=\">\">>\n"
                        + "<option value=\">=\">>=\n"
                        + "<option value=\"-\">--\n"
                        + "</select>\n"
        );

        pWrt.print(
                "<input type=\"text\" name=\"" + nameForJsCode(name) + "_selection"
                        + "\" size=12 onFocus=\"describe_selection()\" "
                        + "onChange=\"DODS_URL.update_url()\">\n"
        );

        pWrt.print("
\n\n");

    }

}


package ucar.nc2.ogc.spatialsampling;

import net.opengis.gml.x32.PointDocument;
import net.opengis.samplingSpatial.x20.ShapeType;
import ucar.nc2.ft.StationTimeSeriesFeature;
import ucar.nc2.ogc.gml.NcPointType;

/**
 * Created by cwardgar on 2014/02/28.
 */
public abstract class NcShapeType {
    // wml2:Collection/wml2:observationMember/om:OM_Observation/om:featureOfInterest/wml2:MonitoringPoint/sams:shape
    public static ShapeType initShape(ShapeType shape, StationTimeSeriesFeature stationFeat) {
        // gml:Point
        PointDocument pointDoc = PointDocument.Factory.newInstance();
        NcPointType.initPoint(pointDoc.addNewPoint(), stationFeat);
        shape.set(pointDoc);

        return shape;
    }

    private NcShapeType() { }
}


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

package ucar.util.prefs.ui;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;

/** helper class for PrefPanel */
class LayoutM implements LayoutManager2 {
    private String name;
    private Map<Component,Object> constraintMap = new HashMap<Component,Object>();
    private Rectangle globalBounds = null;

    private boolean debug = false, debugLayout = false;

   /**
     * Constructs a new SpringLayout.
     */
    public LayoutM(String name) {this.name = name; }

    // LayoutManager2

    /**
     * Adds the specified component to the layout, using the specified
     * constraint object.
     * @param comp the component to be added
     * @param constraint  where/how the component is added to the layout.
     */
    public void addLayoutComponent(Component comp, Object constraint) {
      if (debug) System.out.println(name+ " addLayoutComponent= "+ comp.getClass().getName()+" "+comp.hashCode()+" "+constraint);
      if (!(constraint instanceof Constraint))
        throw new IllegalArgumentException( "MySpringLayout must be Constraint");
      constraintMap.put( comp, constraint);
      globalBounds = null;
    }

    /**
     * Calculates the maximum size dimensions for the specified container,
     * given the components it contains.
     * @see java.awt.Component#getMaximumSize
     * @see LayoutManager
     */
    public Dimension maximumLayoutSize(Container parent){
      if (debug) System.out.println("maximumLayoutSize 2");
      if (globalBounds == null) layoutContainer(parent);
      return globalBounds.getSize();
    }

    /**
     * Returns the alignment along the x axis.  This specifies how
     * the component would like to be aligned relative to other
     * components.  The value should be a number between 0 and 1
     * where 0 represents alignment along the origin, 1 is aligned
     * the furthest away from the origin, 0.5 is centered, etc.
     */
    public float getLayoutAlignmentX(Container target){
      if (debug) System.out.println("getLayoutAlignmentX 2");
      return 0.5f;
    }

    /**
     * Returns the alignment along the y axis.  This specifies how
     * the component would like to be aligned relative to other
     * components.  The value should be a number between 0 and 1
     * where 0 represents alignment along the origin, 1 is aligned
     * the furthest away from the origin, 0.5 is centered, etc.
     */
    public float getLayoutAlignmentY(Container target){
      if (debug) System.out.println("getLayoutAlignmentY 2");
      return 0.5f;
    }

    /**
     * Invalidates the layout, indicating that if the layout manager
     * has cached information it should be discarded.
     */
    public void invalidateLayout(Container target){
      if (debug) System.out.println(name+" invalidateLayout ");
      globalBounds = null;

      // this probably need to be scheduled later ??
      // layoutContainer( target);
    }

    // LayoutManager

    /**
     * not used
     */
    public void addLayoutComponent(String name, Component comp){
      if (debug) System.out.println("addLayoutComponent ");
    }

    /**
     * Removes the specified component from the layout.
     * @param comp the component to be removed
     */
    public void removeLayoutComponent(Component comp){
      if (debug) System.out.println("removeLayoutComponent");
      constraintMap.remove(comp);
      globalBounds = null;
    }

    /**
     * Calculates the preferred size dimensions for the specified
     * container, given the components it contains.
     * @param parent the container to be laid out
     *
     * @see #minimumLayoutSize
     */
    public Dimension preferredLayoutSize(Container parent){
      if (globalBounds == null) layoutContainer(parent);
      if (debug) System.out.println(name+" preferredLayoutSize "+globalBounds.getSize()+" "+parent.getInsets());
      return globalBounds.getSize();
    }

    /**
     * Calculates the minimum size dimensions for the specified
     * container, given the components it contains.
     * @param parent the component to be laid out
     * @see #preferredLayoutSize
     */
    public Dimension minimumLayoutSize(Container parent){
      if (debug) System.out.println("minimumLayoutSize");
      if (globalBounds == null) layoutContainer(parent);
      return globalBounds.getSize();
    }

    /**
     * Lays out the specified container.
     * @param target the container to be laid out
     */
    public void layoutContainer(Container target) {
      synchronized (target.getTreeLock()) {
        if (debug) System.out.println(name+ " layoutContainer ");

        // first layout any nested LayoutM components
        // it seems that generally Swing laysout from outer to inner ???
        int n = target.getComponentCount();
        for (int i=0 ; i<n ; i++) {
          Component comp = target.getComponent(i);
          if (comp instanceof Container) {
            Container c = (Container) comp;
            LayoutManager m = c.getLayout();
            if (m instanceof LayoutM)
              m.layoutContainer( c);
          }
        }

        // now layout this container
        reset( target);
        globalBounds = new Rectangle(0,0,0,0);
        while ( !layoutPass( target))
          target.setPreferredSize( globalBounds.getSize()); // ??
      }
    }

    private boolean layoutPass(Container parent) {
      if (debugLayout) System.out.println("layout "+name);
      boolean gotAll = true;
      double x,  y;
      Rectangle bounds = new Rectangle();

      int n = parent.getComponentCount();
      for (int i = 0 ; i < n ; i++) {
        Component comp = parent.getComponent(i);
        Constraint cs =  (Constraint) constraintMap.get( comp);

        if (cs.c == null) { // absolute position
          Dimension size = comp.getPreferredSize();
          Rectangle cB = comp.getBounds();

          // negetive means cs.xspace is the right edge
          if (cs.xspace < 0)
            x = -(size.getWidth() + cs.xspace);
          else // otherwise its the left edge
            x = cs.xspace;

          // negative means cs.yspace is the bottom edge
          if (cs.yspace < 0)
            y = -(size.getHeight() + cs.yspace);
          else // otherwise its the top edge
            y = cs.yspace;

          // set the bounds
          bounds.setRect(x, y, size.getWidth(), size.getHeight());
          comp.setBounds( bounds);
          cs.laidout = true;

          globalBounds = globalBounds.union( bounds);
          if (debugLayout) {
            System.out.println("  "+name+" SET (absolute) "+comp.getClass().getName()+" bounds= "+cB+" prefrredSize= "+size);
            System.out.println("  new bounds= "+bounds);
          }

        } else { // position relative to cs.c
          Constraint cs2 =  (Constraint) constraintMap.get( cs.c);
          if (cs2.laidout) {
            Rectangle b2 = cs.c.getBounds();
            Dimension size = comp.getPreferredSize();
            Rectangle cB = comp.getBounds();

            if (cs.xspace == 0) // 0 means align
              x = b2.getX();
            else if (cs.xspace < 0) // negative means cs.xspace is the right edge, absolute
              x = -(size.getWidth() + cs.xspace);
            else // otherwise its reletive to right edge of cs.c
              x = b2.getX() + b2.getWidth() + cs.xspace;

            if (cs.yspace == 0) // 0 means align
              y = b2.getY();
            else if (cs.yspace < 0) // negative means cs.yspace is the bottom edge, absolute
              y = -(size.getHeight() + cs.yspace);
            else // otherwise its reletive to bottom edge of cs.c
              y = b2.getY() + b2.getHeight() + cs.yspace;

            // set bounds
            bounds.setRect(x, y, size.getWidth(), size.getHeight());
            comp.setBounds( bounds);
            cs.laidout = true;

            globalBounds = globalBounds.union( bounds);
            if (debugLayout) {
              System.out.println("  "+name+" SET (reletive) "+comp.getClass().getName()+" bounds= "+cB+" prefrredSize= "+size);
              System.out.println("  new bounds= "+bounds);
            }

          } else {
            gotAll = false;
            if (debugLayout) System.out.println("  "+name+" Missed "+comp.getClass().getName());
          } // if laidout

        } // cs.c == null
      } // loop over components

      return gotAll;
    }

    private void reset(Container parent) {
      int n = parent.getComponentCount();
      for (int i = 0 ; i < n ; i++) {
        Component c = parent.getComponent(i);
        Constraint cs =  (Constraint) constraintMap.get( c);
        cs.laidout = false;
      }
    }

  static class Constraint {
    Component c;
    int xspace, yspace;
    boolean laidout = false;

    public Constraint( Component c, int xspace, int yspace) {
      this.c = c;
      this.xspace = xspace;
      this.yspace = yspace;
    }

    public String toString() {
      String cname = (c == null) ? "null" : (c instanceof JLabel) ? ((JLabel) c).getText() : c.getClass().getName();
      return xspace +" "+yspace + " <"+ cname+">";
    }

  }

  public static void main(String args[]) {

    JFrame frame = new JFrame("Test LayoutM");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {System.exit(0);}
    });

    JPanel main = new JPanel(new LayoutM("test"));
    frame.getContentPane().add(main);
    main.setPreferredSize(new Dimension(200, 200));

    JLabel lab1 = new JLabel( "test1:");
    main.add( lab1, new Constraint(null, 10, 10));
    JTextField f1 = new JTextField( "why dont you just");
    main.add( f1, new Constraint(lab1, 6, 0));

    JLabel lab2 = new JLabel( "test2:");
    main.add( lab2, new Constraint(lab1, 0, 10));
    JTextField f2 = new JTextField( "fade away?");
    main.add( f2, new Constraint(lab2, 6, 0));

    frame.pack();
    frame.setLocation(300, 300);
    frame.setVisible(true);

    Dimension r = f2.getSize();
    System.out.println("getPreferredSize "+ r);
    r.setSize( (int) (r.getWidth() + 50), (int) r.getHeight());
    f2.setPreferredSize( r);
    System.out.println("setPreferredSize "+ r);
    f2.revalidate();
  }
}


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

// $Id:RadialCoordSys.java 51 2006-07-12 17:13:13Z caron $

package ucar.nc2.dt.radial;

import ucar.nc2.units.DateUnit;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.dataset.*;
import ucar.ma2.Array;
import ucar.ma2.MAMath;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.Earth;
import ucar.unidata.geoloc.LatLonPointImpl;

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

/**
 * A radial CoordinateSystem has azimuth, elevation and radial axes.
 *
 * @author caron
 */

public class RadialCoordSys {

  /**
   * Determine if this CoordinateSystem can be made into a RadialCoordSys.
   * @param parseInfo put debug information into this Formatter; may be null.
   * @param cs the CoordinateSystem to test
   * @return true if it can be made into a RadialCoordSys.
   */
  public static boolean isRadialCoordSys( Formatter parseInfo, CoordinateSystem cs) {
    return (cs.getAzimuthAxis() != null) && (cs.getRadialAxis() != null) && (cs.getElevationAxis() != null);
  }

  /**
   * Determine if the CoordinateSystem cs can be made into a GridCoordSys for the Variable v.
   * @param parseInfo put debug information into this StringBuffer; may be null.
   * @param cs CoordinateSystem to check.
   * @param v Variable to check.
   * @return the RadialCoordSys made from cs, else null.
   */
  public static RadialCoordSys makeRadialCoordSys( Formatter parseInfo, CoordinateSystem cs, VariableEnhanced v) {
    if (parseInfo != null) {
      parseInfo.format(" ");
      v.getNameAndDimensions(parseInfo, true, false);
      parseInfo.format(" check CS " + cs.getName());
    }
    if (isRadialCoordSys( parseInfo, cs)) {
      RadialCoordSys rcs = new RadialCoordSys( cs);
      if (cs.isComplete( v)) {
        if (parseInfo != null) parseInfo.format(" OK%n");
        return rcs;
      } else {
        if (parseInfo != null) parseInfo.format(" NOT complete%n");
      }
    }

    return null;
  }

  /////////////////////////////////////////////////////////////////////////////
  private CoordinateAxis aziAxis, elevAxis, radialAxis, timeAxis;
  private Array aziData, elevData, radialData, timeData;

  private String name;
  private ArrayList coordAxes = new ArrayList();
  private ucar.nc2.units.DateUnit dateUnit;
  private ucar.unidata.geoloc.EarthLocation origin;
  private LatLonRect bb;
  private double maxRadial;

  /** Create a RadialCoordSys from an existing Coordinate System.
   */
  public RadialCoordSys( CoordinateSystem cs) {
    super();

    aziAxis = cs.getAzimuthAxis();
    radialAxis = cs.getRadialAxis();
    elevAxis = cs.getElevationAxis();
    timeAxis = cs.getTaxis();

    coordAxes.add( aziAxis);
    coordAxes.add( radialAxis);
    coordAxes.add( elevAxis);

    // make name based on coordinate
    Collections.sort( coordAxes, new CoordinateAxis.AxisComparator()); // canonical ordering of axes
    this.name = CoordinateSystem.makeName( coordAxes);

  }

  public String getName() { return name; }
  public List getCoordAxes() { return coordAxes; }

  /** get the Azimuth axis */
  public CoordinateAxis getAzimuthAxis() { return aziAxis; }
  /** get the Elevation axis  */
  public CoordinateAxis getElevationAxis() { return elevAxis; }
  /** get the Radial axis */
  public CoordinateAxis getRadialAxis() { return radialAxis; }
  /** get the Time axis */
  public CoordinateAxis getTimeAxis() { return timeAxis; }

    /** get the Azimuth axis data. Calling this will force the data to be cached. */
  public Array getAzimuthAxisDataCached() throws IOException {
    if (aziData == null)
      aziData = aziAxis.read();
    return aziData;
  }

    /** get the Elevation axis data. Calling this will force the data to be cached. */
  public Array getElevationAxisDataCached() throws IOException {
    if (elevData == null)
      elevData = elevAxis.read();
    return elevData;
  }

    /** get the Radial axis data. Calling this will force the data to be cached. */
  public Array getRadialAxisDataCached() throws IOException {
    if (radialData == null)
      radialData = radialAxis.read();
    return radialData;
  }

    /** get the Time axis data. Calling this will force the data to be cached. */
  public Array getTimeAxisDataCached() throws IOException {
    if (timeData == null)
      timeData = timeAxis.read();
    return timeData;
  }

    /** Origin of the coordinate system */
  public ucar.unidata.geoloc.EarthLocation getOrigin() {
    return origin;
  }

  public void setOrigin(ucar.unidata.geoloc.EarthLocation origin) {
    this.origin = origin;
  }

  /**
   * Get the maximum radial distance, in km.
   */
  public double getMaximumRadial() {
    if (maxRadial == 0.0) {
      try {
        Array radialData = getRadialAxisDataCached();
        maxRadial = MAMath.getMaximum( radialData);

        String units = getRadialAxis().getUnitsString();
        SimpleUnit radialUnit = SimpleUnit.factory(units);
        maxRadial = radialUnit.convertTo(maxRadial, SimpleUnit.kmUnit); // convert to km

      } catch (IOException e) {
        e.printStackTrace();
      } catch (IllegalArgumentException e) {
        e.printStackTrace();
      }
    }
    return maxRadial;
  }

  public LatLonRect getBoundingBox() {
    if (bb != null)
      return bb;
    if (origin == null)
      return null;

    double dLat = Math.toDegrees( getMaximumRadial() / 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);

    return bb;
  }

  /** Get the units of Calendar time.
   *  To get a Date, from a time value, call DateUnit.getStandardDate(double value).
   *  To get units as a String, call DateUnit.getUnitsString().
   */
  public ucar.nc2.units.DateUnit getTimeUnits() throws Exception {
    if (null == dateUnit) {
      dateUnit = new DateUnit( timeAxis.getUnitsString());
    }
    return dateUnit;
  }

  /** debug */
  static public void main( String[] args) {
    System.out.println("1 Deg="+Math.toDegrees( 1000 * 111.0 / Earth.getRadius()));
  }

}


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

package ucar.nc2.dt.grid;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import ucar.ma2.Array;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis1DTime;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDatatype;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.geoloc.EarthLocation;
import ucar.unidata.geoloc.LatLonPoint;

/**
 * Add Point operations to a GridDataset.
 *
 * @author caron
 */
public class GridAsPointDataset {
  private List<CalendarDate> dates;

  public GridAsPointDataset( List<GridDatatype> grids) {
    //HashSet<Date> dateHash = new HashSet<Date>(); ????
    HashSet<CalendarDate> dateHash = new HashSet<>();
    List<CoordinateAxis1DTime> timeAxes = new ArrayList<>();

    for (GridDatatype grid : grids) {
      GridCoordSystem gcs = grid.getCoordinateSystem();
      CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
      if ((timeAxis != null) && !timeAxes.contains(timeAxis)) {
        timeAxes.add(timeAxis);

        //Date[] timeDates = timeAxis.getTimeDates();
        List<CalendarDate> timeDates = timeAxis.getCalendarDates();
        //for (Date timeDate : timeDates)
        for (CalendarDate timeDate : timeDates)
          //dateHash.add( CalendarDate.of( timeDate) );
        	dateHash.add( timeDate );
      }
    }

    // -->if dateHash is HashSet<Date> hashSet.toArray returns Date[], not CalendarDate[] ???
    dates = Arrays.asList( dateHash.toArray(new CalendarDate[dateHash.size()]));
    Collections.sort(dates);
  }

  public List<CalendarDate> getDates() { return dates; }

  public boolean hasTime( GridDatatype grid, CalendarDate date) {
    GridCoordSystem gcs = grid.getCoordinateSystem();
    CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
    return (timeAxis != null) && timeAxis.hasCalendarDate( date);
  }

  public double getMissingValue(GridDatatype grid) {
    return Double.NaN;
    //VariableEnhanced ve = grid.getVariable();
    //return ve.getValidMax(); // LOOK bogus
  }

  public Point readData(GridDatatype grid, CalendarDate date, double lat, double lon)  throws java.io.IOException {
    GridCoordSystem gcs = grid.getCoordinateSystem();

    //CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
    //int tidx = timeAxis.findTimeIndexFromCalendarDate(date);

    int tidx =-1;
    //Date may be null if the grid does not have time axis
    if(date != null)
    	tidx = findTimeIndexForCalendarDate(gcs, date);

    //int[] xy = gcs.findXYindexFromLatLonBounded(lat, lon, null);
    int[] xy = gcs.findXYindexFromLatLon(lat, lon, null);

    Array data  = grid.readDataSlice(tidx, -1, xy[1], xy[0]);

    // use actual grid midpoint
    LatLonPoint latlon = gcs.getLatLon(xy[0], xy[1]);

    Point p = new Point();
    p.lat = latlon.getLatitude();
    p.lon = latlon.getLongitude();
    p.dataValue = data.getDouble( data.getIndex());
    return p;
  }

  public boolean hasVert( GridDatatype grid, double zCoord) {
    GridCoordSystem gcs = grid.getCoordinateSystem();
    CoordinateAxis1D zAxis = gcs.getVerticalAxis();
    if (zAxis == null) return false;
    return (zAxis.findCoordElement( zCoord) >= 0);
  }

  public Point readData(GridDatatype grid, CalendarDate date, double zCoord, double lat, double lon)  throws java.io.IOException {
    GridCoordSystem gcs = grid.getCoordinateSystem();

    //CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
    //int tidx = timeAxis.findTimeIndexFromCalendarDate(date);
    int tidx = -1;
    //Date may be null if the grid does not have time axis
    if(date != null)
    	tidx= findTimeIndexForCalendarDate(gcs, date);

    CoordinateAxis1D zAxis = gcs.getVerticalAxis();
    int zidx = zAxis.findCoordElement( zCoord);

    int[] xy = gcs.findXYindexFromLatLon(lat, lon, null);

    Array data  = grid.readDataSlice(tidx, zidx, xy[1], xy[0]);

    // use actual grid midpoint
    LatLonPoint latlon = gcs.getLatLon(xy[0], xy[1]);

    Point p = new Point();
    p.lat = latlon.getLatitude();
    p.lon = latlon.getLongitude();
    p.z = zAxis.getCoordValue( zidx);
    p.dataValue = data.getDouble( data.getIndex());
    return p;
  }

  /**
   *
   * Reads one single data for one point.
   * Takes the ensemble and elevation coordinates allowing them to be < 0 and in that case they'll be ignored.
   *
   * @param grid
   * @param date
   * @param ensCoord
   * @param zCoord
   * @param lat
   * @param lon
   * @return Point matching lat/lon for this grid
   * @throws java.io.IOException
   */
  public Point readData(GridDatatype grid, CalendarDate date, double ensCoord, double zCoord, double lat, double lon)  throws java.io.IOException {
	    GridCoordSystem gcs = grid.getCoordinateSystem();

	    //CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
	    //int tidx = timeAxis.findTimeIndexFromCalendarDate(date);
	    int tidx = -1;
	    //Date may be null if the grid does not have time axis
	    if(date != null)
	    	tidx= findTimeIndexForCalendarDate(gcs, date);

	    Point p = new Point();
	    int zidx = -1;
	    //if(zCoord != -1){ LOOK!! --> zCoord may be -1 (ocean sigma levels usually go from 0 to -1)!!!
	    CoordinateAxis1D zAxis = gcs.getVerticalAxis();
	    if( zAxis !=null ){
	    	zidx = zAxis.findCoordElement( zCoord);
	    	if(zidx != -1)
	    		p.z = zAxis.getCoordValue(zidx);
	    }
	    //}

	    int eidx =-1;
	    if(ensCoord != -1){
	    	CoordinateAxis1D ensAxis = gcs.getEnsembleAxis();
	    	eidx = ensAxis.findCoordElement( ensCoord);
	    	p.ens = ensAxis.getCoordValue(eidx);
	    }

	    int[] xy = gcs.findXYindexFromLatLon(lat, lon, null);

	    //Array data  = grid.readDataSlice(tidx, zidx, xy[1], xy[0]);
	    Array data  = grid.readDataSlice(0, eidx, tidx, zidx, xy[1], xy[0]);

	    // use actual grid midpoint
	    LatLonPoint latlon = gcs.getLatLon(xy[0], xy[1]);

	    p.lat = latlon.getLatitude();
	    p.lon = latlon.getLongitude();

	    p.dataValue = data.getDouble( data.getIndex());
	    return p;
	  }

  /**
   * Reads data for the given point (earthlocation) and if bounded is true returns data for the closest point within the grid, for points outside of the grid
   *
   * @param grid read data from here
   * @param date at this time
   * @param location EarthLocation, if altitude is NaN assume that is 2D point
   * @param bounded if bounded, location must be in grid cell; otherwise get nearest grid point to location
   * @return the location and data value
   * @throws java.io.IOException on bad stuff
   */
  public Point readData(GridDatatype grid, CalendarDate date, EarthLocation location, boolean bounded)  throws java.io.IOException {

	  if(!bounded){
		  if( Double.isNaN(location.getAltitude()) ){
			  return readData(grid, date, location.getLatitude(), location.getLongitude());
		  }else{
			  return readData(grid, date, location.getAltitude(), location.getLatitude(), location.getLongitude());
		  }
	  }

	  //Bounded --> Read closest data
	  GridCoordSystem gcs = grid.getCoordinateSystem();
	  int tidx = findTimeIndexForCalendarDate(gcs, date);
	  int[] xy = gcs.findXYindexFromLatLonBounded(location.getLatitude(), location.getLongitude(), null);

	  LatLonPoint latlon = gcs.getLatLon(xy[0], xy[1]);
	  Point p = new Point();
	  p.lat = latlon.getLatitude();
	  p.lon = latlon.getLongitude();

	  int zidx=-1;
	  if( !Double.isNaN(location.getAltitude())){
		    CoordinateAxis1D zAxis = gcs.getVerticalAxis();
		    zidx = zAxis.findCoordElement( location.getAltitude());
		    p.z = zAxis.getCoordValue( zidx);
	  }

	  Array data = grid.readDataSlice(tidx, zidx, xy[1], xy[0]);
	  p.dataValue = data.getDouble( data.getIndex());

	  return p;
  }

  private int findTimeIndexForCalendarDate(GridCoordSystem gcs, CalendarDate date){

	  CoordinateAxis1DTime timeAxis = gcs.getTimeAxis1D();
	  return timeAxis.findTimeIndexFromCalendarDate(date);
  }

  public static class Point {
    public double lat,lon,z,ens,dataValue;
  }

  public static void main(String[] args) throws IOException {
    GridDataset gds = ucar.nc2.dt.grid.GridDataset.open("Q:/cdmUnitTest/transforms/Eumetsat.VerticalPerspective.grb");
    GridDatatype grid = gds.findGridDatatype( "Pixel_scene_type");
    GridCoordSystem gcs = grid.getCoordinateSystem();

    double lat = 8.0;
    double lon = 21.0;

    // find the x,y point for a specific lat/lon position
    int[] xy = gcs.findXYindexFromLatLon(lat, lon, null); // xy[0] = x, xy[1] = y

    // read the data at that lat, lon a specific t and z value
    Array data  = grid.readDataSlice(0, 0, xy[1], xy[0]); // note t, z, y, x
    double val = data.getDouble(0);
    System.out.printf("Value at %f %f == %f%n", lat, lon, val);
  }

}