/////////////////////////////////////////////////////////////////////////////
// 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.dap;

import java.util.Enumeration;

import opendap.dap.parsers.DDSXMLParser;
import opendap.util.SortedTable;
import opendap.util.Debug;

import java.io.*;

/**
 * An AttributeTable stores a set of names and, for each name,
 * an Attribute object.  For more information on the types of
 * data which can be stored in an attribute, including aliases and other
 * AttributeTable objects, see the documentation for
 * Attribute.
 *

 * The attribute tables have a standard printed representation.  There is a
 * print method for writing this form and a parse
 * method for reading the printed form.
 *

 * An AttributeTable's print representation might look like:
 *

 *   String long_name "Weekly Means of Sea Surface Temperature";
 *

 * or
 *

 *   actual_range {
 *       Float64 min -1.8;
 *       Float64 max 35.09;
 *   }
 *

 * or
 *

 *   String Investigators "Cornillon", "Fleirl", "Watts";
 *

 * or
 *

 *   Alias New_Attribute Old_Attribute;
 *

 * Here, long_name and Investigators are
 * simple attributes, actual_range is a container attribute, and
 * New_Attribute is an alias pointing to Old_Attribute.
 *
 * @author jehamby
 * @version $Revision: 15901 $
 * @see DAS
 * @see Attribute
 *
 * Modified 1/9/2011 Dennis Heimbigner
 * - Make subclass of BaseType for uniformity
 */

public class AttributeTable extends DAPNode
{
    /**
     * A table of Attributes with their names as a key
     */
    private SortedTable _attr;

    /**
     * Create a new empty AttributeTable.
     *
     * @deprecated Use constructor that takes the name of the table.
     */
    public AttributeTable() {
        _attr = new SortedTable();
    }

    /**
     * Create a new empty AttributeTable.
     */
    public AttributeTable(String clearname) {
        super(clearname);
        _attr = new SortedTable();
    }

    /**
     *
     * @return the # of contained attributes
     */
    public int size()
    {
        return (_attr == null ? 0 : _attr.size());
    }

    /**
     * Returns an Enumeration of the attribute names in this
     * AttributeTable.
     * Use the getAttribute method to get the
     * Attribute for a given name.
     *
     * @return an Enumeration of String.
     * @see AttributeTable#getAttribute(String)
     */
    public final Enumeration getNames() {
        return _attr.keys();
    }

    /**
     * Returns the Attribute which matches name.
     *
     * @param clearname the name of the Attribute to return.
     * @return the Attribute with the specified name, or null
     *         if there is no matching Attribute.
     * @see Attribute
     */
    public final Attribute getAttribute(String clearname) { //throws NoSuchAttributeException {
        Attribute a = (Attribute) _attr.get(clearname);
        return (a);
    }

    /**
     * Returns the Attribute which matches name.
     *
     * @param clearname the name of the Attribute to return.
     * @return True if an Attribute with named 'name' exists, False otherwise.
     * @see Attribute
     */
    public final boolean hasAttribute(String clearname) {
        Attribute a = (Attribute) _attr.get(clearname);

        if (a == null) {
            return (false);
        }
        return (true);
    }

    /**
     * Adds an attribute to the table.  If the given name already
     * refers to an attribute, and the attribute has a vector value,
     * the given value is appended to the attribute vector.  Calling
     * this function repeatedly is the way to create an attribute
     * vector.
     *

     * The function throws an exception if the attribute is a
     * container, or if the type of the input value does not match the
     * existing attribute's type and the check parameter
     * is true.  Use the appendContainer method to add container
     * attributes.
     *
     * @param clearname  The name of the attribute to add or modify.
     * @param type  The type code of the attribute to add or modify.
     * @param value The value to add to the attribute table.
     * @param check Check the validity of the attribute's value?
     * @throws AttributeExistsException   thrown if an Attribute with the same
     *                                    name, but a different type was previously defined.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     * @see AttributeTable#appendContainer(String)
     */
    public final void appendAttribute(String clearname, int type, String value,
                                      boolean check) throws DASException {

        Attribute a = (Attribute) _attr.get(clearname);

        if (a != null && (type != a.getType())) {

            // type mismatch error
            throw new AttributeExistsException("The Attribute `" + clearname
                    + "' was previously defined with a different type.");

        } else if (a != null) {

            a.appendValue(value, check);

        } else {

            a = new Attribute(type, clearname, value, check);
            _attr.put(clearname, a);
        }
    }

    /**
     * Adds an attribute to the table.  If the given name already
     * refers to an attribute, and the attribute has a vector value,
     * the given value is appended to the attribute vector.  Calling
     * this function repeatedly is the way to create an attribute
     * vector.
     *

     * The function throws an exception if the attribute is a
     * container, or if the type of the input value does not match the
     * existing attribute's type.  Use the appendContainer
     * method to add container attributes.
     *
     * @param clearname  The name of the attribute to add or modify.
     * @param type  The type code of the attribute to add or modify.
     * @param value The value to add to the attribute table.
     * @throws AttributeExistsException   thrown if an Attribute with the same
     *                                    name, but a different type was previously defined.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     * @see AttributeTable#appendContainer(String)
     */
    public final void appendAttribute(String clearname, int type, String value)
            throws DASException {
        appendAttribute(clearname, type, value, true);
    }

    /**
     * Create and append an attribute container to the table.
     * A container is another AttributeTable object.
     *
     * @param clearname the name of the container to add.
     * @return A pointer to the new AttributeTable object, or null
     *         if a container by that name already exists.
     */
    public final AttributeTable appendContainer(String clearname) {
        // return null if clearname already exists
        // FIXME! THIS SHOULD RETURN AN EXCEPTION!
        if (_attr.get(clearname) != null)
            return null;

        AttributeTable at = new AttributeTable(clearname);
        Attribute a = new Attribute(clearname, at);
        _attr.put(clearname, a);
        return at;
    }

    /**
     * Create and append an attribute container to the table.
     * A container is another AttributeTable object.
     *
     * @param clearname the name of the container to add.
     *             if a container by that name already exists.
     */
    public final void addContainer(String clearname, AttributeTable at) throws AttributeExistsException {

        // return null if name already exists
        if (_attr.get(clearname) != null) {
            throw new AttributeExistsException("The Attribute '" + clearname +
                    "' already exists in the container '" +
                    getEncodedName() + "'");
        }

        Attribute a = new Attribute(clearname, at);
        _attr.put(clearname, a);
    }

    /**
     * Add an alias to the current table.
     * This method is used by the DAS parser to build Aliases
     * for the DAS. And the DDSXMLParser to add them to the DDX
     *

     * The new (9/26/02) DDS requires the use of
     * addAlias(String, String, String) and is the preffered
     * way of representing the DAS information.
     *
     * @param alias         The alias to insert into the attribute table.
     * @param attributeName The normalized name of the attribute to which
     *                      the alias will refer.
     * @throws NoSuchAttributeException thrown if the existing attribute
     *                                  could not be found.
     * @throws AttributeExistsException thrown if the new alias has the same
     *                                  name as an existing attribute.
     */
    public final void addAlias(String alias, String attributeName)
            throws NoSuchAttributeException, AttributeExistsException {

        // complain if alias name already exists in this AttributeTable.
        if (_attr.get(alias) != null) {
            throw new AttributeExistsException("Could not alias `" + alias +
                    "' to `" + attributeName + "'. " +
                    "It is a duplicat name in this AttributeTable");
        }
        if (Debug.isSet("AttributTable")) {
            log.debug("Adding alias '" + alias + "' to AttributeTable '" + getClearName() + "'");
        }

        Alias newAlias = new Alias(alias, attributeName);
        _attr.put(alias, newAlias);
    }

    /**
     * Delete the attribute named name.
     *
     * @param clearname The name of the attribute to delete.  This can be an
     *             attribute of any type, including containers.
     */
    public final void delAttribute(String clearname) {
        _attr.remove(clearname);
    }

    /**
     * Delete the attribute named name.  If the attribute has a
     * vector value, delete the i'th element of the vector.
     *
     * @param clearname The name of the attribute to delete.  This can be an
     *             attribute of any type, including containers.
     * @param i    If the named attribute is a vector, and i is
     *             non-negative, the i'th entry in the vector is deleted.
     *             If i equals -1, the entire attribute is deleted.
     * @see AttributeTable#delAttribute(String)
     */
    public final void delAttribute(String clearname, int i) throws DASException {

        if (i == -1) {  // delete the whole attribute

            _attr.remove(clearname);

        } else {

            Attribute a = (Attribute) _attr.get(clearname);

            if (a != null) {

                if (a.isContainer()) {

                    _attr.remove(clearname);  // delete the entire container

                } else {

                    a.deleteValueAt(i);

                }
            }
        }
    }

    /**
     * Print the attribute table on the given PrintWriter.
     *
     * @param os  the PrintWriter to use for output.
     * @param pad the number of spaces to indent each line.
     */
    public void print(PrintWriter os, String pad) {

        if (Debug.isSet("AttributTable")) os.println("Entered AttributeTable.print()");

        os.println(pad + getEncodedName() + " {");
        for (Enumeration e = getNames(); e.hasMoreElements();) {

            String name = (String) e.nextElement();
            Attribute a = getAttribute(name);
            if (a != null)
                a.print(os, pad + "    ");

        }
        os.println(pad + "}");
        if (Debug.isSet("AttributTable")) os.println("Leaving AttributeTable.print()");
        os.flush();
    }

    /**
     * Print the attribute table on the given OutputStream.
     *
     * @param os  the OutputStream to use for output.
     * @param pad the number of spaces to indent each line.
     */
    public final void print(OutputStream os, String pad) {
        print(new PrintWriter(new BufferedWriter(new OutputStreamWriter(os,Util.UTF8))), pad);
    }

    /**
     * Print the attribute table on the given PrintWriter with
     * four spaces of indentation.
     *
     * @param os the PrintWriter to use for output.
     */
    public final void print(PrintStream os) {
        print(os, "");
    }

    /**
     * Print the attribute table on the given PrintWriter with
     * four spaces of indentation.
     *
     * @param os the PrintWriter to use for output.
     */
    public final void print(PrintWriter os) {
        print(os, "");
    }

    /**
     * Print the attribute table on the given OutputStream with
     * four spaces of indentation.
     *
     * @param os the OutputStream to use for output.
     */
    public final void print(OutputStream os) {
        print(os, "");
    }

    /**
     *
     * @param os
     * @opendap.ddx.experimental
     */
    public void printXML(OutputStream os) {
        printXML(os, "");
    }

    /**
     *
     * @param os
     * @param pad
     * @opendap.ddx.experimental
     */
    public void printXML(OutputStream os, String pad) {
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os,Util.UTF8)));
        printXML(pw, pad);
        pw.flush();
    }

    /**
     *
     * @param pw
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw) {
        printXML(pw, "");
    }

    /**
     *
     * @param pw
     * @param pad
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw, String pad) {
        printXML(pw, pad, false);
    }

    /**
     *
     * @param pw
     * @param pad
     * @param constrained
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw, String pad, boolean constrained) {

        if (Debug.isSet("AttributTable")) pw.println("Entered AttributeTable.print()");

        pw.println(pad + "<Attribute name=\"" +
                DDSXMLParser.normalizeToXML(getEncodedName()) +
                "\" type=\"Container\">");

        Enumeration e = getNames();
        while (e.hasMoreElements()) {
            String name = (String) e.nextElement();
            Attribute a = getAttribute(name);
            if (a != null)
                a.printXML(pw, pad + "\t", constrained);

        }
        pw.println(pad + "</Attribute>");
        if (Debug.isSet("AttributTable")) pw.println("Leaving AttributeTable.print()");
        pw.flush();

    }

    /**
     * Returns a clone of this AttributeTable.
     * See DAPNode.cloneDag()
     *
     * @param map track previously cloned nodes
     * @return a clone of this Attribute.
     */
    public DAPNode cloneDAG(CloneMap map)
        throws CloneNotSupportedException
    {
            AttributeTable at = (AttributeTable) super.cloneDAG(map);
            at._attr = new SortedTable();
            for (int i = 0; i < _attr.size(); i++) {
                String key = (String) _attr.getKey(i);
                Attribute element = (Attribute) _attr.elementAt(i);
                // clone element (don't clone key because it's a read-only String)
                at._attr.put(key, (Attribute)cloneDAG(map,element));
            }
            return at;
    }

}

// $Log: AttributeTable.java,v $
// Revision 1.3  2003/09/02 17:49:34  ndp
// *** empty log message ***
//
// Revision 1.2  2003/09/02 15:06:25  ndp
// *** empty log message ***
//
// Revision 1.1  2003/08/12 23:51:25  ndp
// Mass check in to begin Java-OPeNDAP development work
//
// Revision 1.12 2011/01/09 Dennis Heimbigner
//  - Make subclass of BaseType for uniformity
//
// Revision 1.11  2003/04/07 22:12:32  jchamber
// added serialization
//
// Revision 1.10  2003/02/12 16:41:15  ndp
// *** empty log message ***
//
// Revision 1.9  2002/10/10 18:12:31  ndp
// Fixed bugs in DDS.getDAS(), Updated testDataset and sqlDataset
//
// Revision 1.8  2002/10/08 21:59:18  ndp
// Added XML functionality to the core. This includes the new DDS code (aka DDX)
// for parsing XML representations of the dataset description ( that's a DDX)
// Also BaseType has been modified to hold Attributes and methods added to DDS
// to ingest DAS's (inorder to add Attributes to variables) and to get the DAS
// object from the DDS. Geturl and DConnect hav been modified to provide client
// access to this new set of functionalites. ndp 10/8/2002
//
// Revision 1.7  2002/05/30 23:25:57  jimg
// I added methods that provide a way to add attribues without the usual
// type/value checking. See today's log in Attribute.java for more info.
//


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

package ucar.nc2.grib.grib1.tables;

import javax.annotation.Nullable;
import ucar.nc2.grib.GribNumbers;
import ucar.nc2.grib.coord.VertCoordType;
import ucar.nc2.grib.grib1.Grib1ParamLevel;
import ucar.nc2.grib.grib1.Grib1SectionProductDefinition;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Air Force Weather Center (center 57)
 *
 * @author caron
 * @since 7/31/13
 */
public class AfwaTables extends Grib1Customizer {
  private static Map<Integer, VertCoordType> levelTypesMap;  // shared by all instances
  private static Map<Integer, String> genProcessMap;  // shared by all instances
  private static Map<Integer, String> subcenterMap;  // shared by all instances

  AfwaTables(Grib1ParamTables tables) {
    super(57, tables);
  }

  // subcenters

  @Override
  public String getSubCenterName(int subcenter) {
    if (subcenterMap == null)
      subcenterMap = makeSubcenterMap();
    return subcenterMap.get(subcenter);
  }

  private static Map<Integer, String> makeSubcenterMap() {
    Map<Integer, String> result = new HashMap<>(100);

    result.put(0, "AFWA Primary Table");
    result.put(1, "AFWA Numerical Weather Prediction Models Products");
    result.put(2, "AFWA Special Environmental Models Products");
    result.put(3, "AFWA Space Products");
    result.put(4, "AFWA Ensemble Products");
    result.put(6, "AFWA Aerosol Products");
    result.put(7, "CDFS-II Aerosol Model");
    result.put(8, "AFWA Volcano Products");
    result.put(10, "14 WS (Formerly Air Force Combat Climatology Center (AFCCC))");
    result.put(11, "14 WS Numerical Weather Prediction Models Products");
    result.put(15, "15 OWS (Scott)");
    result.put(17, "17 OWS (Hickam)");
    result.put(20, "20 OWS (Yokota)");
    result.put(21, "21 OWS (Sembach)");
    result.put(25, "25 OWS (Davis-Monthan)");
    result.put(26, "26 OWS (Barksdale)");
    result.put(27, "27 OWS (Offutt)");
    result.put(28, "28 OWS (Shaw)");
    result.put(29, "Air Force Tactical Application Center (AFTAC) ");
    result.put(90, "AFWA 4DF 1");
    result.put(91, "AFWA 4DF 2");
    result.put(92, "AFWA 4DF 3");
    result.put(93, "AFWA 4DF 4");
    result.put(94, "AFWA 4DF 5");
    result.put(110, "Air Force Research Lab (AFRL) Office of Research and Applications");
    result.put(130, "Patrick AFB OWS Eastern Test Range");
    result.put(150, "IPDS Satellite");

    return Collections.unmodifiableMap(result);  // all at once - thread safe
  }

  // gen process
  @Override
  @Nullable
  public String getGeneratingProcessName(int genProcess) {
    if (genProcessMap == null) makeGenProcessMap();
    return genProcessMap.get(genProcess);
  }

  private static void makeGenProcessMap() {
    genProcessMap = new HashMap<>(100);
    genProcessMap.put(10, "Mesoscale Model 5 (MM5)");
    genProcessMap.put(11, "Weather Research and Forecasting Model (WRF)");
    genProcessMap.put(12, "Multivariate Optimum Interpolation Model (MVOI)");
    genProcessMap.put(13, "Three Dimensional Variate Model (3DVAR)");
    genProcessMap.put(14, "Weather Research and Forecasting Model Chemical & Aerosol (WRF-CHEM)");
    genProcessMap.put(25, "Snow Depth Model (SNODEP) (Manually modified)");
    genProcessMap.put(26, "Real Time Nephanalysis Model (RTNEPH)");
    genProcessMap.put(27, "Surface Temperature Model (SFCTMP)");
    genProcessMap.put(28, "Advect Cloud Model (ADVCLD)");
    genProcessMap.put(29, "Worldwide Merged Cloud Analysis (WWMCA) (Manually modified)");
    genProcessMap.put(30, "Short Range Cloud Forecast (SRCF)");
    genProcessMap.put(31, "Long Range Cloud Forecast (LRCF)");
    genProcessMap.put(32, "Worldwide Merged Cloud Analysis (WWMCA) (Not modified)");
    genProcessMap.put(33, "Diagnostic Cloud Forecast v. 3 (DCF3) 3-layer");
    genProcessMap.put(34, "Stochastic Cloud Forecast Model (SCFM)");
    genProcessMap.put(35, "Snow Depth Model (SNODEP) (Not modified)");
    genProcessMap.put(36, "Diagnostic Cloud Forecast v. 3 (DCF3) 5-layer");
    genProcessMap.put(39, "Diagnostic Cloud Forecast v. 3 (DCF3) P-layer");
    genProcessMap.put(40, "Diagnostic Cloud Forecast v. 4 (DCF4)");
    genProcessMap.put(50, "Dust Transport Application (DTA)");
    genProcessMap.put(55, "Aerosol Model (CDFS-II)");
    genProcessMap.put(75, "Advanced Climate Modeling and Environmental Simulations (ACMES)");
    genProcessMap.put(86, "Geostationary satellite-based precipitation model (GEO_PRECIP)");
    genProcessMap.put(87, "Agricultural Meteorology (AGRMET)");
    genProcessMap.put(88, "Land Information System (LIS)");
    genProcessMap.put(96, "Array-flipped (to NOGAPS grid structure) Global Forecast System (GFS)");
    genProcessMap.put(99, "Ensemble Post-Processor");
    genProcessMap.put(100, "Ionospheric Forecast Model (IFM)");
    genProcessMap.put(101, "Parameterized Real-Time Specification Model (PRISM)");
    genProcessMap.put(125, "Snow Depth Climatology");
    genProcessMap.put(127, "Unified Model (UM)");
    genProcessMap.put(200, "NOGAPS post-processed by AFWA");
    genProcessMap.put(201, "GFS post processed by AFWA");
    genProcessMap.put(204, "FNMOC NOGAPS geographically subsected by AFWA");
    genProcessMap.put(250, "Quality Control Display Process (SCIF only)");
  }

  /// levels
  @Override
  public Grib1ParamLevel getParamLevel(Grib1SectionProductDefinition pds) {
    int levelType = pds.getLevelType();
    int pds11 = pds.getLevelValue1();
    int pds12 = pds.getLevelValue2();
    int pds1112 = pds11 << 8 | pds12;

    switch (levelType) {
      case 210:
        return new Grib1ParamLevel(this, levelType, (float) pds1112, GribNumbers.MISSING);

      case 218:
         return new Grib1ParamLevel(this, levelType, (float) pds11 + 200, (float) pds12 + 200);

      case 246:
        return new Grib1ParamLevel(this, levelType, (float) pds1112, GribNumbers.MISSING);

       default:
        return new Grib1ParamLevel(this, pds);
    }
  }

  @Override
  protected VertCoordType getLevelType(int code) {
    if (levelTypesMap == null) makeLevelTypesMap();
    VertCoordType levelType = levelTypesMap.get(code);
    if (levelType != null) return levelType;
    return super.getLevelType(code);
  }

  private static void makeLevelTypesMap() {
    levelTypesMap = new HashMap<>(100);
    // (int code, String desc, String abbrev, String units, String datum, boolean isPositiveUp, boolean isLayer)
    levelTypesMap.put(21,  new VertCoordType(21,  "RTNEPH cloud layer", "RTNEPH", "", null, true, true));
    levelTypesMap.put(210, new VertCoordType(210, "Isobaric Surface", "ISBP", "", null, false, false));
    levelTypesMap.put(211, new VertCoordType(211, "Boundary layer cloud bottom level", "BCBL", "", null, false, false));
    levelTypesMap.put(212, new VertCoordType(212, "Boundary layer cloud top level", "BCTL", "", null, false, false));
    levelTypesMap.put(213, new VertCoordType(213, "Boundary layer cloud layer", "BCY", "", null, false, true));
    levelTypesMap.put(214, new VertCoordType(214, "Low cloud bottom level", "LCBL", "", null, false, false));
    levelTypesMap.put(215, new VertCoordType(215, "Low cloud top level", "LCTL", "", null, false, false));
    levelTypesMap.put(216, new VertCoordType(216, "Low cloud layer", "LCY", "", null, false, true));
    levelTypesMap.put(217, new VertCoordType(217, "Highest tropospheric freezing level", "HTFL", "K", null, false, false));
    levelTypesMap.put(218, new VertCoordType(218, "Layer between two temperature levels", "DEGY", "K", null, false, true));
    levelTypesMap.put(222, new VertCoordType(222, "Middle cloud bottom level", "MCBL", "", null, false, false));
    levelTypesMap.put(223, new VertCoordType(223, "Middle cloud top level", "MCTL", "", null, false, false));
    levelTypesMap.put(224, new VertCoordType(224, "Middle cloud layer", "MCY", "", null, false, true));
    levelTypesMap.put(232, new VertCoordType(232, "High cloud bottom level", "HCBL", "", null, false, false));
    levelTypesMap.put(233, new VertCoordType(233, "High cloud top level", "HCTL", "", null, false, false));
    levelTypesMap.put(234, new VertCoordType(234, "High cloud layer", "HCY", "", null, false, true));
    levelTypesMap.put(242, new VertCoordType(242, "Convective cloud bottom level", "CCBL", "", null, false, false));
    levelTypesMap.put(243, new VertCoordType(243, "Convective cloud top level", "CCTL", "", null, false, false));
    levelTypesMap.put(244, new VertCoordType(244, "Convective cloud layer", "CCY", "", null, false, true));
    levelTypesMap.put(246, new VertCoordType(246, "Specified height level above MSL", "HTIO", "km", "msl", false, false));
    levelTypesMap.put(251, new VertCoordType(251, "Layer between ground and 850 hPa level", "PTLR", "", null, false, true));
    levelTypesMap.put(252, new VertCoordType(252, "Layer between lowest soil layer (layer 112) and 800cm", "SBLR", "", null, false, true));
  }

}


package ucar.unidata.geoloc;

import ucar.nc2.util.Misc;

import java.util.Objects;

/**
 * Similar to {@link LatLonPoint}, but this class does not normalize its latitude and longitude, even for comparison.
 * That is, latitudes may lie outside of [-90, 90] and longitudes may lie outside of [-180, 180].
 *
 * @author cwardgar
 * @since 2018-03-08
 */
public class LatLonPointNoNormalize {
    /** East latitude in degrees, not necessarily in [-90, 90]. */
    private final double lat;

    /** North longitude in degrees, not necessarily in [-180, 180]. */
    private final double lon;

    public LatLonPointNoNormalize(double lat, double lon) {
        this.lat = lat;
        this.lon = lon;
    }

    /**
     * Returns the latitude, not necessarily in [-90, 90].
     *
     * @return latitude (degrees)
     */
    public double getLatitude() {
        return lat;
    }

    /**
     * Returns the longitude, not necessarily in [-180, 180].
     *
     * @return longitude (degrees)
     */
    public double getLongitude() {
        return lon;
    }

    /**
     * Returns the result of {@link #nearlyEquals(LatLonPointNoNormalize, double)}, with
     * {@link Misc#defaultMaxRelativeDiffDouble}.
     */
    public boolean nearlyEquals(LatLonPointNoNormalize that) {
        return nearlyEquals(that, Misc.defaultMaxRelativeDiffDouble);
    }

    /**
     * Returns {@code true} if this point is nearly equal to {@code that}. The "near equality" of points is determined
     * using {@link Misc#nearlyEquals(double, double, double)}, with the specified maxRelDiff.
     *
     * @param that    the other point to check.
     * @param maxRelDiff  the maximum {@link Misc#relativeDifference relative difference} the two points may have.
     * @return {@code true} if this point is nearly equal to {@code that}.
     */
    public boolean nearlyEquals(LatLonPointNoNormalize that, double maxRelDiff) {
        return Misc.nearlyEquals(this.getLatitude(), that.getLatitude(), maxRelDiff) &&
               Misc.nearlyEquals(this.getLongitude(), that.getLongitude(), maxRelDiff);
    }

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

        return Double.compare(this.getLatitude(),  that.getLatitude())  == 0 &&
               Double.compare(this.getLongitude(), that.getLongitude()) == 0;
    }

    @Override public int hashCode() {
        return Objects.hash(lat, lon);
    }

    @Override public String toString() {
        return String.format("(%.4f, %.4f)", lat, lon);
    }
}


package thredds.crawlabledataset;

public class CrawlableDatasetUtils {
  /**
   * Split a path into segments, handles null as empty string.
   *

   * Results:
   * Path []       splits into 1 segments: []
   * Path [a]      splits into 1 segments: [a]
   * Path [a/]     splits into 1 segments: [a]
   * Path [a/b]    splits into 2 segments: [a][b]
   * Path [a/b/c]  splits into 3 segments: [a][b][c]
   * Path [/]      splits into 0 segments:
   * Path [/a]     splits into 2 segments: [][a]
   * Path [/a/b]   splits into 3 segments: [][a][b]
   * Path [/a/b/c] splits into 4 segments: [][a][b][c]
   *

   * So, absolute paths are valid if length is >1 and first segment is "".
   * Relative paths are valid if length is >0 and first segment is not "".
   *
   * @param path the path to split.
   * @return String[] containing path segments, as per String.split("/").
   */
  public static String[] getPathSegments( String path) {
    return (path == null ? "" : path).split("/");
  }

  public static String getPath( String[] pathSegments ) {
    if ( pathSegments == null )
      return "";

    int start = 0;
    StringBuilder sb = new StringBuilder();
    if ( isValidAbsolutePath(pathSegments)) {
      if ( pathSegments.length == 0 )
        return "/";
      sb.append( "/").append( pathSegments[1]);
      start = 2;
    } else if ( isValidRelativePath( pathSegments)) {
      sb.append( pathSegments[0]);
      start = 1;
      if ( pathSegments.length == 1 ) return pathSegments[0];
    } else {
      throw new IllegalArgumentException( String.format( "Path segment array [%s] not valid path.", toStringForPathSegments( pathSegments)));
    }
    for ( int i = start; i < pathSegments.length; i++ ) {
      sb.append( "/").append( pathSegments[ i]);
    }
    return sb.toString();
  }

  public static boolean isValidAbsolutePath(String[] pathSegments) {
    if ( pathSegments == null )
      return false;
    if ( pathSegments.length == 0 )
      return true; // Root path ("/")
    return pathSegments.length > 1 && pathSegments[0].isEmpty();
  }

  public static boolean isValidRelativePath(String[] pathSegments) {
    return pathSegments != null && pathSegments.length > 0 && !pathSegments[0].isEmpty();
  }

  /**
   * Given a relative path as an array of path segments (see {@link #getPathSegments(String)},
   * return the path relative to the first path segment.
   *
   * I.e., drop the first segmentRejoin the given path segments after dropping the first path segment.
   *
   * @param pathSegments a string array of a relative path as returned by getPathSegments()
   * @return the path relative to the first path segment.
   */
  public static String stepDownRelativePath(String[] pathSegments) {
    if ( !CrawlableDatasetUtils.isValidRelativePath(pathSegments))
      throw new IllegalArgumentException( "Path segments not a valid relative path.");
    if ( pathSegments.length < 2 )
      throw new IllegalArgumentException( "Number of path segments must be > 1.");

    StringBuilder sb = new StringBuilder();
    for ( int i = 1; i < pathSegments.length - 1; i++ ) {
      sb.append( pathSegments[i]).append("/");
    }
    sb.append( pathSegments[pathSegments.length - 1]);
    return sb.toString();
  }

  public static String toStringForPathSegments( String[] pathSegments ) {
    StringBuilder sb = new StringBuilder();
    if ( isValidAbsolutePath( pathSegments)) {
      sb.append( "Absolute: ");
    } else if ( isValidRelativePath( pathSegments)) {
      sb.append( "Relative: ");
    } else {
      sb.append( "Invalid: ");
      if ( pathSegments == null )
        return sb.append( "null").toString();
    }
    for ( String curPathSegment : pathSegments ) {
      sb.append( "[").append( curPathSegment).append("]");
    }
    return sb.toString();
  }

  public static String toStringForPathSegments( String path) {
    String[] pathSegments = getPathSegments(path);
    StringBuilder sb = new StringBuilder();
    sb.append( String.format( "Path [%s] as %d segments: ", path, pathSegments.length));
    for ( String pathSegment : pathSegments) {
      sb.append("[").append( pathSegment).append("]");
    }
    sb.append( " - valid abs[").append( isValidAbsolutePath(pathSegments))
        .append("] rel[").append( isValidRelativePath(pathSegments)).append("]");
    return sb.toString();
  }

}


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

package dap4.core.data;

import dap4.core.util.DapContext;
import dap4.core.util.DapException;

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

/**
 */

public class DSPRegistry
{
    //////////////////////////////////////////////////
    // Constants

    // MNemonics
    static public final boolean LAST = true;
    static public final boolean FIRST = false;

    //////////////////////////////////////////////////
    // Type Decls

    static protected class Registration
    {
        Class<? extends DSP> dspclass;
        DSP matcher;

        public Registration(Class<? extends DSP> cl)
        {
            this.dspclass = cl;
            try {
                this.matcher = dspclass.newInstance();
            } catch (IllegalAccessException | InstantiationException e) {
                throw new IllegalArgumentException("DSPFactory: cannot create matching instance for Class: " + dspclass.getName());
            }
        }

        public String toString()
        {
            return dspclass.getName();
        }
    }

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

    static protected ClassLoader loader = DSPRegistry.class.getClassLoader();

    //////////////////////////////////////////////////
    // Instance Variables

    /**
     * Define a map of known DSP classes.
     * Must be ordered to allow control over
     * test order
     */
    protected List<Registration> registry = new ArrayList<>();

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

    public DSPRegistry()
    {
    }

    //////////////////////////////////////////////////
    // Accessors

    static public void setLoader(ClassLoader ldr)
    {
        loader = ldr;
    }

    //////////////////////////////////////////////////
    // API

    /**
     * Register a DSP, using its class string name.
     *
     * @param className Name of class that implements DSP.
     * @throws IllegalAccessException if class is not accessible.
     * @throws InstantiationException if class doesnt have a no-arg constructor.
     * @throws ClassNotFoundException if class not found.
     */
    synchronized public void register(String className, boolean last)
            throws DapException
    {
        try {
            Class<? extends DSP> klass = (Class<? extends DSP>) loader.loadClass(className);
            register(klass, last);
        } catch (ClassNotFoundException e) {
            throw new DapException(e);
        }
    }

    /**
     * Register a DSP class.
     *
     * @param klass Class that implements DSP.
     * @param last  true=>insert at the end of the list; otherwise front
     * @throws IllegalAccessException if class is not accessible.
     * @throws InstantiationException if class doesnt have a no-arg constructor.
     * @throws ClassCastException     if class doesnt implement DSP interface.
     */
    synchronized public void
    register(Class<? extends DSP> klass, boolean last)
    {
        // is this already defined?
        if(registered(klass)) return;
        if(last)
            registry.add(new Registration(klass));
        else
            registry.add(0, new Registration(klass));
    }

    /**
     * See if a specific DSP is registered
     *
     * @param klass Class for which to search
     */

    synchronized public boolean
    registered(Class<? extends DSP> klass)
    {
        for(Registration r : registry) {
            if(r.dspclass == klass) return true;
        }
        return false;
    }

    /**
     * Unregister dsp.
     *
     * @param klass Class for which to search
     */
    synchronized public void
    unregister(Class<? extends DSP> klass)
    {
        for(int i = 0; i < registry.size(); i++) {
            if(registry.get(i).dspclass == klass) {
                registry.remove(i);
                break;
            }
        }
    }

    /**
     * @param path
     * @return new DSP object that can process this path
     * @throws DapException
     */

    synchronized public DSP
    findMatchingDSP(String path, DapContext cxt)
            throws DapException
    {
        for(int i = 0; i < registry.size(); i++) {
            try {
                Registration tester = registry.get(i);
                boolean ismatch = (Boolean) tester.matcher.dspMatch(path, cxt);
                if(ismatch) {
                    DSP dsp = (DSP) tester.dspclass.newInstance();
                    return dsp;
                }
            } catch (Exception e) {
                throw new DapException(e);
            }
        }
        throw new IllegalArgumentException("Cannot open " + path);
    }

}


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

package ucar.nc2.grib.grib2.table;

import com.google.common.collect.ImmutableList;
import javax.annotation.Nullable;
import thredds.featurecollection.TimeUnitConverter;
import ucar.nc2.grib.*;
import ucar.nc2.grib.coord.TimeCoordIntvDateValue;
import ucar.nc2.grib.coord.TimeCoordIntvValue;
import ucar.nc2.grib.coord.VertCoordType;
import ucar.nc2.grib.grib2.Grib2Pds;
import ucar.nc2.grib.grib2.Grib2Record;
import ucar.nc2.grib.grib2.Grib2SectionIdentification;
import ucar.nc2.grib.grib2.Grib2Utils;
import ucar.nc2.grib.grib2.table.WmoCodeFlagTables.TableType;
import ucar.nc2.grib.grib2.table.WmoCodeFlagTables.WmoTable;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarPeriod;
import ucar.nc2.wmo.CommonCodeTable;

import javax.annotation.concurrent.Immutable;
import java.util.*;

/**
 * Grib 2 Tables - allows local overrides and augmentation
 * This class serves the standard WMO tables, local tables are subclasses.
 *
 * @author caron
 * @since 4/3/11
 */
@Immutable
public class Grib2Tables implements ucar.nc2.grib.GribTables, TimeUnitConverter {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Grib2Tables.class);
  private static Map<Grib2TablesId, Grib2Tables> tables = new HashMap<>();
  private static Grib2Tables wmoStandardTable = null;

  public static Grib2Tables factory(Grib2Record gr) {
    Grib2SectionIdentification ids = gr.getId();
    Grib2Pds pds = gr.getPDS();
    return factory(ids.getCenter_id(), ids.getSubcenter_id(), ids.getMaster_table_version(), ids.getLocal_table_version(), pds.getGenProcessId());
  }

  // Lazy instantiation.
  public static Grib2Tables factory(int center, int subCenter, int masterVersion, int localVersion, int genProcessId) {
    Grib2TablesId id = new Grib2TablesId(center, subCenter, masterVersion, localVersion, genProcessId);
    Grib2Tables cust = tables.get(id);
    if (cust != null) return cust;

    // note that we match on id, so same Grib2Customizer may be mapped to multiple id's (eg match on -1)
    Grib2TableConfig config = Grib2TableConfig.matchTable(id);
    cust = build(config);

    tables.put(id, cust);
    return cust;
  }

  private static Grib2Tables build(Grib2TableConfig config) {
    switch (config.getType()) {
      case cfsr: return new CfsrLocalTables(config);
      case gempak: return new GempakLocalTables(config); // LOOK: not used
      case gsd: return new FslHrrrLocalTables(config);
      case kma: return new KmaLocalTables(config);
      case ncep: return new NcepLocalTables(config);
      case ndfd: return new NdfdLocalTables(config);
      case mrms: return new MrmsLocalTables(config);
      case nwsDev: return new NwsMetDevTables(config);
      default:
        if (wmoStandardTable == null) wmoStandardTable = new Grib2Tables(config);
        return wmoStandardTable;
    }
  }

  public static int makeParamId(int discipline, int category, int number) {
    return (discipline << 16) + (category << 8) + number;
  }

  public static int[] unmakeParamId(int code) {
    int number = code & 255;
    code = code >> 8;
    int category = code & 255;
    int discipline = code >> 8;
    return new int[] {discipline, category, number};
  }

  public static boolean isLocal(Parameter p) {
    return ((p.getDiscipline() > 191) || (p.getCategory() > 191) || (p.getNumber() > 191));
  }

  public static ImmutableList<Grib2Tables> getAllRegisteredTables() {
    ImmutableList.Builder<Grib2Tables> builder = ImmutableList.builder();
    for (Grib2TableConfig config : Grib2TableConfig.getTables()) {
      builder.add(build(config));
    }
    return builder.build();
  }

  ///////////////////////////////////////////////////////////////
  protected final Grib2TableConfig config;
  private boolean timeUnitWarnWasSent;

  protected Grib2Tables(Grib2TableConfig config) {
    this.config = config;
  }

  public String getName() {
    return config.getName();
  }

  public String getPath() {
    return config.getPath();
  }

  public Grib2TablesId getConfigId() {
    return config.getConfigId();
  }

  public Grib2TablesId.Type getType() {
    return config.getType();
  }

  public String getVariableName(Grib2Record gr) {
    return getVariableName(gr.getDiscipline(), gr.getPDS().getParameterCategory(), gr.getPDS().getParameterNumber());
  }

  public String getVariableName(int discipline, int category, int parameter) {
    String s = WmoParamTable.getParameterName(discipline, category, parameter);
    if (s == null)
      s = "U" + discipline + "-" + category + "-" + parameter;
    return s;
  }

  @Nullable
  public String getTableValue(String tableName, int code) {
    WmoCodeTable codeTable = WmoCodeFlagTables.getInstance().getCodeTable(tableName);
    Grib2CodeTableInterface.Entry entry = (codeTable == null) ? null : codeTable.getEntry(code);
    return (entry == null) ? null : entry.getName();
  }

  @Nullable
  public GribTables.Parameter getParameter(int discipline, int category, int number) {
    return WmoParamTable.getParameter(discipline, category, number);
  }

  @Override
  @Nullable
  public String getSubCenterName(int center_id, int subcenter_id) {
    return CommonCodeTable.getSubCenterName(center_id, subcenter_id);
  }

  @Nullable
  public String getGeneratingProcessName(int genProcess) {
    return null;
  }

  @Nullable
  public String getGeneratingProcessTypeName(int genProcess) {
    return getTableValue("4.3", genProcess);
  }

  @Nullable
  public String getCategory(int discipline, int category) {
    WmoCodeTable catTable = WmoCodeFlagTables.getInstance().getCodeTable("4.1." + discipline);
    Grib2CodeTableInterface.Entry entry = (catTable == null) ? null : catTable.getEntry(category);
    return (entry == null) ? null : entry.getName();
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  // Time

  private TimeUnitConverter timeUnitConverter;  // LOOK not really immutable

  public void setTimeUnitConverter(TimeUnitConverter timeUnitConverter) {
    if (this.timeUnitConverter != null) throw new RuntimeException("Cant modify timeUnitConverter once its been set");
    this.timeUnitConverter = timeUnitConverter;
  }

  @Override
  public int convertTimeUnit(int timeUnit) {
    if (timeUnitConverter == null) return timeUnit;
    return timeUnitConverter.convertTimeUnit(timeUnit);
  }

  @Nullable
  public CalendarDate getForecastDate(Grib2Record gr) {
    Grib2Pds pds = gr.getPDS();
    if (pds.isTimeInterval()) {
      TimeCoordIntvDateValue intv = getForecastTimeInterval(gr);
      return intv == null ? null : intv.getEnd();

    } else {
      int val = pds.getForecastTime();
      CalendarPeriod period = Grib2Utils.getCalendarPeriod(convertTimeUnit(pds.getTimeUnit()));
      if (period == null) return null;
      return gr.getReferenceDate().add(period.multiply(val));
    }
  }

  /*
  Code Table Code table 4.11 - Type of time intervals (4.11)
    0: Reserved
    1: Successive times processed have same forecast time, start time of forecast is incremented
    2: Successive times processed have same start time of forecast, forecast time is incremented
    3: Successive times processed have start time of forecast incremented and forecast time decremented so that valid time remains constant
    4: Successive times processed have start time of forecast decremented and forecast time incremented so that valid time remains constant
    5: Floating subinterval of time between forecast time and end of overall time interval

  public static class TimeInterval {
    public int statProcessType; // (code table 4.10) Statistical process used to calculate the processed field from the field at each time increment during the time range
    public int timeIncrementType;  // (code table 4.11) Type of time increment between successive fields used in the statistical processing
    public int timeRangeUnit;  // (code table 4.4) Indicator of unit of time for time range over which statistical processing is done
    public int timeRangeLength; // Length of the time range over which statistical processing is done, in units defined by the previous octet
    public int timeIncrementUnit; // (code table 4.4) Indicator of unit of time for the increment between the successive fields used
    public int timeIncrement; // Time increment between successive fields, in units defined by the previous octet

    from NDFD site:
    timeRangeUnit:    8-14 Day Outlooks = 2 (days); Monthly and Seasonal Outlooks = 3 (months)
    timeRangeLength:  8-14 Day Outlooks = 6 (days); Monthly Outlooks = 1 (month); Seasonal Outlooks = 3 (months)
   */

  /**
   * Get the time interval in units of gr.getPDS().getTimeUnit()
   *
   * @param gr Grib record, must have pds that is a  time interval.
   * @return time interval in units of gr.getPDS().getTimeUnit()
   */
  @Nullable
  public TimeCoordIntvDateValue getForecastTimeInterval(Grib2Record gr) {
    // note  from Arthur Taylor (degrib):
    /* If there was a range I used:

    End of interval (EI) = (bytes 36-42 show an "end of overall time interval")
    C1) End of Interval = EI;
    Begin of Interval = EI - range

    and if there was no interval then I used:
    C2) End of Interval = Begin of Interval = Ref + ForeT.
    */
    if (!gr.getPDS().isTimeInterval()) return null;
    Grib2Pds.PdsInterval pdsIntv = (Grib2Pds.PdsInterval) gr.getPDS();
    int timeUnitOrg = gr.getPDS().getTimeUnit();

    // calculate total "range"
    int range = 0;
    for (Grib2Pds.TimeInterval ti : pdsIntv.getTimeIntervals()) {
      if (ti.timeRangeUnit == 255)
        continue;
      if ((ti.timeRangeUnit != timeUnitOrg) || (ti.timeIncrementUnit != timeUnitOrg && ti.timeIncrementUnit != 255 && ti.timeIncrement != 0)) {
        if (!timeUnitWarnWasSent) {
          logger.warn("TimeInterval has different units timeUnit org=" + timeUnitOrg + " TimeInterval=" + ti.timeIncrementUnit);
          timeUnitWarnWasSent = true;
          // throw new RuntimeException("TimeInterval(2) has different units");
        }
      }

      range += ti.timeRangeLength;
      if (ti.timeIncrementUnit != 255) range += ti.timeIncrement;
    }

    CalendarPeriod unitPeriod = Grib2Utils.getCalendarPeriod(convertTimeUnit(timeUnitOrg));
    if (unitPeriod == null) return null;
    CalendarPeriod period = unitPeriod.multiply(range);

    // End of Interval as date
    CalendarDate EI = pdsIntv.getIntervalTimeEnd();
    if (EI == CalendarDate.UNKNOWN) {  // all values were set to zero   LOOK guessing!
      return new TimeCoordIntvDateValue(gr.getReferenceDate(), period);
    } else {
      return new TimeCoordIntvDateValue(period, EI);
    }
  }

  /**
   * Get interval size in units of hours.
   * Only use in GribVariable to decide on variable identity when intvMerge = false.
   * @param pds must be a Grib2Pds.PdsInterval
   * @return interval size in units of hours
   */
  public double getForecastTimeIntervalSizeInHours(Grib2Pds pds) {
    Grib2Pds.PdsInterval pdsIntv = (Grib2Pds.PdsInterval) pds;
    int timeUnitOrg = pds.getTimeUnit();

    // calculate total "range" in units of timeUnit
    int range = 0;
    for (Grib2Pds.TimeInterval ti : pdsIntv.getTimeIntervals()) {
      if (ti.timeRangeUnit == 255)
        continue;
      if ((ti.timeRangeUnit != timeUnitOrg) || (ti.timeIncrementUnit != timeUnitOrg && ti.timeIncrementUnit != 255 && ti.timeIncrement != 0)) {
        logger.warn("TimeInterval(2) has different units timeUnit org=" + timeUnitOrg + " TimeInterval=" + ti.timeIncrementUnit);
        throw new RuntimeException("TimeInterval(2) has different units");
      }

      range += ti.timeRangeLength;
      if (ti.timeIncrementUnit != 255) range += ti.timeIncrement;
    }

    // now convert that range to units of the requested period.
    CalendarPeriod timeUnitPeriod = Grib2Utils.getCalendarPeriod(convertTimeUnit(timeUnitOrg));
    if (timeUnitPeriod == null) return GribNumbers.UNDEFINEDD;
    if (timeUnitPeriod.equals(CalendarPeriod.Hour)) return range;

    double fac;
    if (timeUnitPeriod.getField() == CalendarPeriod.Field.Month) {
      fac = 30.0 * 24.0;  // nominal hours in a month
    } else if (timeUnitPeriod.getField() == CalendarPeriod.Field.Year) {
      fac = 365.0 * 24.0; // nominal hours in a year
    } else {
      fac = CalendarPeriod.Hour.getConvertFactor(timeUnitPeriod);
    }
    return fac * range;
  }

  /**
   * If this has a time interval coordinate, get time interval
   *
   * @param gr from this record
   * @return time interval in units of pds.getTimeUnit(), or null if not a time interval
   */
  @Nullable
  public int[] getForecastTimeIntervalOffset(Grib2Record gr) {
    TimeCoordIntvDateValue tinvd = getForecastTimeInterval(gr);
    if (tinvd == null) return null;

    Grib2Pds pds = gr.getPDS();
    int unit = convertTimeUnit(pds.getTimeUnit());
    TimeCoordIntvValue tinv = tinvd.convertReferenceDate(gr.getReferenceDate(), Grib2Utils.getCalendarPeriod(unit));
    if (tinv == null) return null;
    int[] result = new int[2];
    result[0] = tinv.getBounds1();
    result[1] = tinv.getBounds2();
    return result;
  }

  public String getStatisticName(int id) {
    String result = getTableValue("4.10", id); // WMO
    if (result == null)
      result = getStatisticNameShort(id);
    return result;
  }

  public String getStatisticNameShort(int id) {
    GribStatType stat = GribStatType.getStatTypeFromGrib2(id);
    return (stat == null) ? "UnknownStatType-" + id : stat.toString();
  }

  @Override
  @Nullable
  public GribStatType getStatType(int grib2StatCode) {
    return GribStatType.getStatTypeFromGrib2(grib2StatCode);
  }

   /*
Code Table Code table 4.7 - Derived forecast (4.7)
    0: Unweighted mean of all members
    1: Weighted mean of all members
    2: Standard deviation with respect to cluster mean
    3: Standard deviation with respect to cluster mean, normalized
    4: Spread of all members
    5: Large anomaly index of all members
    6: Unweighted mean of the cluster members
    7: Interquartile range (range between the 25th and 75th quantile)
    8: Minimum of all ensemble members
    9: Maximum of all ensemble members
   -1: Reserved
   -1: Reserved for local use
  255: Missing
  */

  public String getProbabilityNameShort(int id) {
    switch (id) {
      case 0:
        return "unweightedMean";
      case 1:
        return "weightedMean";
      case 2:
        return "stdDev";
      case 3:
        return "stdDevNormalized";
      case 4:
        return "spread";
      case 5:
        return "largeAnomalyIndex";
      case 6:
        return "unweightedMeanCluster";
      case 7:
        return "interquartileRange";
      case 8:
        return "minimumEnsemble";
      case 9:
        return "maximumEnsemble";
      default:
        return "UnknownProbType" + id;
    }
  }

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

  /**
   * Unit of vertical coordinate.
   * from Grib2 code table 4.5.
   * Only levels with units get a dimension added
   *
   * @param code code from table 4.5
   * @return level unit, default is empty unit string
   */
  @Override
  public VertCoordType getVertUnit(int code) {
    //     VertCoordType(int code, String desc, String abbrev, String units, String datum, boolean isPositiveUp, boolean isLayer)
    switch (code) {

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

      case 20:
        return new VertCoordType(code, "K", null, false);

      case 100:
        return new VertCoordType(code, "Pa", null, false);

      case 102:
        return new VertCoordType(code, "m", "mean sea level", true);

      case 103:
        return new VertCoordType(code, "m", "ground", true);

      case 104:
      case 105:
        return new VertCoordType(code, "sigma", null, false); // positive?

      case 106:
        return new VertCoordType(code, "m", "land surface", false);

      case 107:
        return new VertCoordType(code, "K", null, true); // positive?

      case 108:
        return new VertCoordType(code, "Pa", "ground", true);

      case 109:
        return new VertCoordType(code, "K m2 kg-1 s-1", null, true); // positive?

      case 114:
        return new VertCoordType(code, "numeric", null, false);

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

      case 119:
        return new VertCoordType(code, "Pa", null, false); // ??

      case 160:
        return new VertCoordType(code, "m", "sea level", false);

      case 161:
        return new VertCoordType(code, "m", "water surface", false);

      // LOOK NCEP specific
      case 235:
        return new VertCoordType(code, "0.1 C", null, true);

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

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

      default:
        return new VertCoordType(code, null, null, true);
    }
  }

  public boolean isLevelUsed(int code) {
    VertCoordType vunit = getVertUnit(code);
    return vunit.isVerticalCoordinate();
  }

  @Nullable
  public String getLevelName(int id) {
    return getTableValue("4.5", id);
  }

  public boolean isLayer(Grib2Pds pds) {
    return !(pds.getLevelType2() == 255 || pds.getLevelType2() == 0);
  }

  // Table 4.5
  @Override
  public String getLevelNameShort(int id) {

    switch (id) {
      case 1:
        return "surface";
      case 2:
        return "cloud_base";
      case 3:
        return "cloud_tops";
      case 4:
        return "zeroDegC_isotherm";
      case 5:
        return "adiabatic_condensation_lifted";
      case 6:
        return "maximum_wind";
      case 7:
        return "tropopause";
      case 8:
        return "atmosphere_top";
      case 9:
        return "sea_bottom";
      case 10:
        return "entire_atmosphere";
      case 11:
        return "cumulonimbus_base";
      case 12:
        return "cumulonimbus_top";
      case 20:
        return "isotherm";
      case 100:
        return "isobaric";
      case 101:
        return "msl";
      case 102:
        return "altitude_above_msl";
      case 103:
        return "height_above_ground";
      case 104:
        return "sigma";
      case 105:
        return "hybrid";
      case 106:
        return "depth_below_surface";
      case 107:
        return "isentrope";
      case 108:
        return "pressure_difference";
      case 109:
        return "potential_vorticity_surface";
      case 111:
        return "eta";
      case 113:
        return "log_hybrid";
      case 117:
        return "mixed_layer_depth";
      case 118:
        return "hybrid_height";
      case 119:
        return "hybrid_pressure";
      case 120:
        return "pressure_thickness";
      case 160:
        return "depth_below_sea";
      case GribNumbers.UNDEFINED:
        return "none";
      default:
        return "UnknownLevelType-" + id;
    }
  }

  /////////////////////////////////////////////////////
  // debugging

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

  public String getTablePath(int discipline, int category, int number) {
    return WmoCodeFlagTables.standard.getResourceName();
  }

  public List<GribTables.Parameter> getParameters() {
    List<GribTables.Parameter> allParams = new ArrayList<>(3000);
    for (WmoTable wmoTable : WmoCodeFlagTables.getInstance().getWmoTables()) {
      if (wmoTable.getType() == TableType.param) {
        WmoParamTable paramTable = new WmoParamTable(wmoTable);
        allParams.addAll(paramTable.getParameters());
      }
    }
    return allParams;
  }

  public void lookForProblems(Formatter f) {
  }

  public void showSpecialPdsInfo(Grib2Record pds, Formatter f) {
  }
}


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

package dap4.core.dmr;

import dap4.core.ce.CEConstraint;
import dap4.core.util.*;

import java.util.*;

/**
 * This class defines a non-Gridd Grid:
 * i.e. one with an atomic type.
 */

public class DapDataset extends DapGroup
{

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

    // Do not, for now, allow maps to specify fields
    static private final boolean ALLOWFIELDMAPS = false;

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

    protected CEConstraint ce = null;

    protected List<DapNode> nodelist = new ArrayList<DapNode>(); // ordered by prefix order
    protected Map<String, DapNode> fqnmap = new HashMap<String, DapNode>();
    protected List<DapDimension> anonymousdims = new ArrayList<DapDimension>(); // Track separately

    protected List<DapNode> visiblenodes = null; // nodelist filtered by constraint

    // Dataset xml attribute values
    protected String dapversion = null;
    protected String dmrversion = null;
    protected String base = null;
    protected String ns = null;

    // Collect some (optionally constrained) subsets of the nodeset
    protected List<DapVariable> topvariables = null;
    protected List<DapVariable> allvariables = null;
    protected List<DapGroup> allgroups = null;
    protected List<DapEnumeration> allenums = null;
    protected List<DapStructure> allcompounds = null;
    protected List<DapDimension> alldimensions = null;

    protected boolean finished = false;

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

    public DapDataset()
    {
        super();
        addNode(this);
    }

    public DapDataset(String name)
    {
        super(name);
        addNode(this);
    }

    //////////////////////////////////////////////////
    // Compute inferred information

    public void finish()
    {
        if(this.finished)
            return;
        if(this.ce == null)
            this.visiblenodes = nodelist;
        else {
            this.visiblenodes = new ArrayList<DapNode>(nodelist.size());
            for(int i = 0; i < nodelist.size(); i++) {
                DapNode node = nodelist.get(i);
                if(ce.references(node))
                    visiblenodes.add(node);
            }
        }
        this.topvariables = new ArrayList<DapVariable>();
        this.allvariables = new ArrayList<DapVariable>();
        this.allgroups = new ArrayList<DapGroup>();
        this.allenums = new ArrayList<DapEnumeration>();
        this.allcompounds = new ArrayList<DapStructure>();
        this.alldimensions = new ArrayList<DapDimension>();
        finishR(this);
    }

    /**
     * Recursive helper
     *
     * @param node to walk
     */
    protected void
    finishR(DapNode node)
    {
        if(ce != null && !ce.references(node)) return;
        switch (node.getSort()) {
        case DIMENSION:
            this.alldimensions.add((DapDimension) node);
            break;
        case ENUMERATION:
            this.allenums.add((DapEnumeration) node);
            break;
        case SEQUENCE:
        case STRUCTURE:
            this.allcompounds.add((DapStructure)node);
            break;
        case VARIABLE:
            if(node.isTopLevel())
                this.topvariables.add((DapVariable) node);
            this.allvariables.add((DapVariable) node);
            break;
        case GROUP:
        case DATASET:
            DapGroup g = (DapGroup) node;
            this.allgroups.add(g);
            for(DapNode subnode : g.getDecls()) {
                finishR(subnode);
            }
            break;
        default: /*ignore*/
            break;
        }
    }

    //////////////////////////////////////////////////
    // Accessors

    public CEConstraint getConstraint()
    {
        return this.ce;
    }

    public DapDataset
    setConstraint(CEConstraint ce)
    {
        this.ce = ce;
        return this;
    }

    public String getDapVersion()
    {
        return dapversion;
    }

    public void setDapVersion(String value)
    {
        dapversion = value;
    }

    public String getDMRVersion()
    {
        return dmrversion;
    }

    public void setDMRVersion(String value)
    {
        dmrversion = value;
    }

    public String getNS()
    {
        return ns;
    }

    public void setNS(String value)
    {
        ns = value;
    }

    public String getBase()
    {
        return base;
    }

    public void setBase(String value)
    {
        base = value;
    }

    public Map<String, DapNode> getFQNMap()
    {
        return fqnmap;
    }

    public void setFQNMap(Map<String, DapNode> fqnmap)
    {
        this.fqnmap = fqnmap;
    }

    public List<DapNode> getNodeList()
    {
        return this.visiblenodes;
    }

    public List<DapVariable> getTopVariables() { return this.topvariables;}

    public void addNode(DapNode newnode)
    {
        if(nodelist == null)
            nodelist = new ArrayList<DapNode>();
        newnode.setIndex(nodelist.size());
        nodelist.add(newnode);
    }

    public DapDimension createAnonymous(long size)
            throws DapException
    {
        for(DapDimension dim : anonymousdims) {
            if(dim.getSize() == size) return dim;
        }
        DapDimension anon = new DapDimension();
        anon.setSize(size);
        anonymousdims.add(anon);
        this.addDecl(anon);
        return anon;
    }

    public DapIterator getIterator(EnumSet<DapSort> sortset)
    {
        return new DapIterator(visiblenodes, sortset);
    }

    //////////////////////////////////////////////////
    // Lookup Functions

    /**
     * Parse an FQN and use it to trace to a specific
     * object in a dataset. Note that this is quite
     * tricky in the face of backslash escapes.
     * Because of backslash escapes, we cannot
     * use the String.split function because it does appear to be
     * possible to write a regexp that handles preceding backslashes
     * correctly. Instead, we must parse character by character left to right.
     * Traversal into structs:
     * In theory, a map variable could point to a field of a structure.
     * However, in practice, this will not work because we would need a
     * specific instance of that field, which means including dimension
     * indices must be included starting from some top-level variable.
     * We choose, for now, to make that illegal until such time as there
     * is a well-defined meaning for this.
     * Note: this assumes the fqn is absolute (i.e. starts with '/').
     *
     * @param fqn     the fully qualified name
     * @param sortset the kind(s) of object we are looking for
     * @return the matching Dap Node or null if not found
     */
    public DapNode
    lookup(String fqn, DapSort... sortset)
            throws DapException
    {
        fqn = fqn.trim();
        if(fqn == null)
            return null;
        if("".equals(fqn) || "/".equals(fqn)) {
            return this;
        }
        if(fqn.charAt(0) == '/')
            fqn = fqn.substring(1); // remove leading /
        //Check first for an atomic type
        TypeSort ts = TypeSort.getTypeSort(fqn);
        if(ts != null && ts.isAtomic()) {
            // see if we are looking for an atomic type
            for(DapSort ds: sortset) {
                if(ds == DapSort.ATOMICTYPE)
                    return DapType.lookup(ts);
            }
        }

        // Do not use split to be able to look for escaped '/'
        // Warning: elements of path are unescaped
        List<String> path = DapUtil.backslashSplit(fqn, '/');
        DapGroup current = dataset;
        // Walk all but the last element to walk group path
        for(int i = 0; i < path.size() - 1; i++) {
            String groupname = Escape.backslashUnescape(path.get(i));
            DapGroup g = (DapGroup)current.findInGroup(groupname, DapSort.GROUP);
            if(g == null)
                return null;
            assert (g.getSort() == DapSort.GROUP);
            current = (DapGroup) g;
        }
        if(!ALLOWFIELDMAPS) {
            String targetname = Escape.backslashUnescape(path.get(path.size()-1));
            return current.findInGroup(targetname, sortset);
        } else { // ALLOWFIELDMAPS)
            // We need to handle the last segment of the group path
            // to deal with struct walking using '.'. We need to obtain the last segment
            // with escapes intact so we can spot '.' separators.
            // Locate the last element in the last group
            // Start by looking for any containing structure
            String varpart = path.get(path.size() - 1); // Note that this still has escapes
            // So that '.' parsing will be correct.
            List<String> structpath = DapUtil.backslashSplit(varpart, '.');
            String outer = Escape.backslashUnescape(structpath.get(0));
            if(structpath.size() == 1) {
                return current.findInGroup(outer, sortset);
            } else {// It is apparently a structure field
                // locate the outermost structure to start with
                DapStructure currentstruct = (DapStructure) current.findInGroup(outer, DapSort.STRUCTURE, DapSort.SEQUENCE);
                if(currentstruct == null)
                    return null; // does not exist
                // search for the innermost structure
                String fieldname;
                for(int i = 1; i < structpath.size() - 1; i++) {
                    fieldname = Escape.backslashUnescape(structpath.get(i));
                    DapVariable field = (DapVariable) currentstruct.findByName(fieldname);
                    if(field == null)
                        throw new DapException("No such field: " + fieldname);
                    if(!field.isCompound())
                        break;
                    currentstruct = (DapStructure) field.getBaseType();
                }
                fieldname = Escape.backslashUnescape(structpath.get(structpath.size() - 1));
                DapVariable field = currentstruct.findByName(fieldname);
                if(field == null)
                    throw new DapException("No such field: " + fieldname);
                if(field.getSort().oneof(sortset))
                    return (field);
            }
        }
        return null;
    }

    //////////////////////////////////////////////////
    // Dap4Action functions

    /**
     * Sort the nodelist into prefix left to right order
     */
    public void
    sort()
    {
        List<DapNode> sorted = new ArrayList<DapNode>();
        sortR(this, sorted);
        // Assign indices
        for(int i = 0; i < sorted.size(); i++) {
            sorted.get(i).setIndex(i);
        }
        this.nodelist = sorted;
    }

    /**
     * Sort helper
     *
     * @param node the current node we are traversing
     */
    public void
    sortR(DapNode node, List<DapNode> sortlist)
    {
        DapVariable var = null;
        Map<String, DapAttribute> attrs = null;
        sortlist.add(node);
        switch (node.getSort()) {
        case DATASET:
        case GROUP:
            // Walk the decls in this group in order
            // attributes, dimensions, enums, variables, groups
            DapGroup group = (DapGroup) node;
            attrs = group.getAttributes();
            for(Map.Entry<String, DapAttribute> entry : attrs.entrySet()) {
                sortR(entry.getValue(), sortlist);
            }
            List<DapDimension> dims = group.getDimensions();
            if(dims != null)
                for(int i = 0; i < dims.size(); i++) {
                    sortR(dims.get(i), sortlist);
                }
            List<DapEnumeration> enums = group.getEnums();
            if(enums != null)
                for(int i = 0; i < enums.size(); i++) {
                    sortR(enums.get(i), sortlist);
                }
            List<DapVariable> vars = group.getVariables();
            if(vars != null)
                for(int i = 0; i < vars.size(); i++) {
                    sortR(vars.get(i), sortlist);
                }
            List<DapGroup> groups = group.getGroups();
            if(groups != null)
                for(int i = 0; i < groups.size(); i++) {
                    sortR(groups.get(i), sortlist);
                }
            break;
        case VARIABLE:
            var = (DapVariable) node;
            attrs = var.getAttributes();
            if(attrs != null)
                for(Map.Entry<String, DapAttribute> entry : attrs.entrySet()) {
                    sortR(entry.getValue(), sortlist);
                }
            List<DapMap> maps = var.getMaps();
            if(maps != null)
                for(int i = 0; i < maps.size(); i++) {
                    sortR(maps.get(i), sortlist);
                }
            dims = var.getDimensions();
            if(dims != null)
                for(int i = 0; i < dims.size(); i++) {
                    sortR(dims.get(i), sortlist);
                }
            break;

        case ATTRIBUTE:
            attrs = node.getAttributes();
            if(attrs != null)
                for(String name : attrs.keySet()) {
                    sortR(attrs.get(name), sortlist);
                }
            break;

        default:
            break;
        }
    }

} // class DapDataset


package ucar.nc2.grib.grib2.tools;

import com.google.common.collect.ImmutableList;
import java.io.File;
import java.util.Formatter;
import ucar.nc2.grib.GribTables;
import ucar.nc2.grib.grib2.table.EcmwfParamTable;
import ucar.nc2.grib.grib2.table.Grib2ParamTableInterface;
import ucar.nc2.grib.grib2.table.WmoCodeFlagTables;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.wmo.Util;

/**
 * ECMWF param tables read from resources/grib2/ecmwf/tables/<latest>.
 * EcmwfParamTableCompare is used to compare with WMO.
 * No significant differences.
*/
public class EcmwfParamTableCompare {

  private static boolean verbose = false;

  // Compare 2 tables, print report.
  public static void compareTables(Grib2ParamTableInterface t1, Grib2ParamTableInterface t2, Formatter f) {
    int extra = 0;
    int udunits = 0;
    int conflict = 0;
    f.format("%s%n", t2.getName());
    for (GribTables.Parameter p1 : t1.getParameters()) {
      if (t1.getParameter(p1.getNumber()) == null) {
        f.format(" ERROR %s missing own parameter %d%n", t1.getShortName(), p1.getNumber());
      }
      GribTables.Parameter p2 = t2.getParameter(p1.getNumber());
      if (p2 == null) {
        extra++;
        if (verbose) {
          f.format("  %s missing %s%n", t2.getShortName(), p1);
        }
      } else {
        if (!Util.equivilantName(p1.getName(), p2.getName())) {
          f.format("  p1=%10s %s%n", p1.getId(), p1.getName());
          f.format("  p2=%10s %s%n", p2.getId(), p2.getName());
          conflict++;
        }

        if (!p1.getUnit().equalsIgnoreCase(p2.getUnit())) {
          String cu1 = Util.cleanUnit(p1.getUnit());
          String cu2 = Util.cleanUnit(p2.getUnit());

          // eliminate common non-udunits
          boolean isUnitless1 = Util.isUnitless(cu1);
          boolean isUnitless2 = Util.isUnitless(cu2);

          if (isUnitless1 != isUnitless2) {
            f.format("  unitless for %10s %s != %s%n", p1.getId(), cu1, cu2);
            udunits++;

          } else if (!isUnitless1) {

            try {
              SimpleUnit su1 = SimpleUnit.factoryWithExceptions(cu1);
              if (!su1.isCompatible(cu2)) {
                f.format("  incompatible for %10s %s (%s) != %s (org %s)%n", p1.getId(), cu1, su1,
                    cu2, p2.getUnit());
                udunits++;
              }
            } catch (Exception e) {
              f.format("  udunits cant parse=%10s %15s %15s%n", p1.getId(), cu1, cu2);
            }
          }

        }
      }
    }

    int missing = 0;
    for (GribTables.Parameter p2 : t2.getParameters()) {
      if (t2.getParameter(p2.getNumber()) == null) {
        f.format(" ERROR %s missing own parameter %d%n", t2.getShortName(), p2.getNumber());
      }
      GribTables.Parameter p1 = t1.getParameter(p2.getNumber());
      if (p1 == null) {
        missing++;
        f.format("  %s missing %s%n", t1.getShortName(), p2);
      }
    }
    if (conflict > 0 || udunits > 0 || missing > 0) {
      f.format(" ***Conflicts=%d extra=%d udunits=%d missing=%s%n", conflict, extra, udunits,
          missing);
    }
  }

  public static void readParamTable(int discipline, int category) {
    Grib2ParamTableInterface latest = WmoCodeFlagTables.getInstance().getParamTable(discipline, category);
    System.out.printf("================================================================%n");
    System.out.printf("%s%n", latest.getName());

    for (int version = 21; version >= 0; version--) {
      Grib2ParamTableInterface next = EcmwfParamTable.factory(version, discipline, category);
      if (next == null) {
        System.out.printf("Missing version %d%n", version);
      } else {
        Formatter out = new Formatter();
        EcmwfParamTableCompare.compareTables(latest, next, out);
        System.out.printf("%s", out);
        latest = next;
      }
      if (!verbose) break;
    }
  }

  public static void main(String[] args) {
    final String PATH = "/usr/local/google/home/jlcaron/github/thredds/grib/src/main/resources/resources/grib2/ecmwf/tables/21";
    System.out.printf("EcmwfParamTableCompare%n");
    System.out.printf("  ECMWF = %s%n", PATH);
    System.out.printf("  WMO   = %s%n", WmoCodeFlagTables.standard.getResourceName());

    File root = new File(PATH);
    ImmutableList<String> children = ImmutableList.copyOf(root.list());
    children.stream().sorted().forEach(filename -> {
      if (filename.startsWith("4.2.")) {
        String[] split = filename.split("\\.");
        int discipline = Integer.parseInt(split[2]);
        int category = Integer.parseInt(split[3]);
        readParamTable(discipline, category);
      }
    });
  }
}