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

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

import opendap.dap.*;
import opendap.util.dasTools;
import opendap.servers.*;
import opendap.dap.parsers.ParseException;

/**
 * Default handler for OPeNDAP info requests. This class is used
 * by AbstractServlet. This code exists as a seperate class in order to alleviate
 * code bloat in the AbstractServlet class. As such, it contains virtually no
 * state, just behaviors.
 *
 * @author Nathan David Potter
 */

public class GetInfoHandler {

    static final Charset UTF8 = Charset.forName("UTF-8");

    private static final boolean _Debug = false;

    private String infoDir = null;

    /**
     * ************************************************************************
     * Default handler for OPeNDAP info requests. Returns an html document
     * describing the contents of the servers datasets.
     *

     * The "INFOcache" <init-param> element in the web.xml file
     * specifies the designated location for:
     *

     *
".info" response override files.

     *
Server specific HTML* files.

     *
Dataset specific HTML* files .

     *

     *

     * The server specific HTML* files must be named #servlet#.html
     * where #servlet# is the name of the servlet that is running as
     * the OPeNDAP server in question. This name is determined at run time
     * by using the class called Class ( this.getClass().getName() ).
     *

     *
In the C++ code the analogy is the per-cgi file names.

     *

     *

     * The dataset specific HTML* files are located by catenating `.html'
     * to #name#, where #name# is the name of the dataset. If the filename part
     * of #name# is of the form [A-Za-z]+[0-9]*.* then this function also looks
     * for a file whose name is [A-Za-z].html For example, if #name# is
     * .../data/fnoc1.nc this function first looks for .../data/fnoc1.nc.html.
     * However, if that does not exist it will look for .../data/fnoc.html. This
     * allows one `per-dataset' file to be used for a collection of files with
     * the same root name.
     *

     *

     * NB: An HTML* file contains HTML without the ,  or  tags
     * (my own notation).
     *

     *
Look for the user supplied Server- and dataset-specific HTML* documents.

     *
     * @param pw  The PrintStream to which the output should be written.
     * @param gds The thread safe dataset.
     * @param rs  The ReqState object for theis client request.
     * @see GuardedDataset
     * @see ReqState
     */
    public void sendINFO(PrintWriter pw, GuardedDataset gds, ReqState rs) throws DAP2Exception, ParseException {

        if (_Debug) System.out.println("opendap.servlet.GetInfoHandler.sendINFO() reached.");

        String responseDoc = null;
        ServerDDS myDDS = null;
        DAS myDAS = null;

        myDDS = gds.getDDS();
        myDAS = gds.getDAS();

        infoDir = rs.getINFOCache(rs.getRootPath());

        responseDoc = loadOverrideDoc(infoDir, rs.getDataSet());

        if (responseDoc != null) {
            if (_Debug) System.out.println("override document: " + responseDoc);
            pw.print(responseDoc);
        } else {

            String user_html = get_user_supplied_docs(rs.getServerClassName(), rs.getDataSet());

            String global_attrs = buildGlobalAttributes(myDAS, myDDS);

            String variable_sum = buildVariableSummaries(myDAS, myDDS);

            // Send the document back to the client.
            pw.println("<title>Dataset Information</title>");
            pw.println("");
            pw.println("");
            pw.println("");

            if (global_attrs.length() > 0) {
                pw.println(global_attrs);
                pw.println("
");
            }

            pw.println(variable_sum);

            pw.println("
");

            pw.println(user_html);

            pw.println("");

            // Flush the output buffer.
            pw.flush();
        }

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

    /**
     * ************************************************************************
     * Checks the info directory for user supplied override documents for the
     * passed dataset name. If there are overridedocuments present then the
     * contents are read and returned to the caller as a string.
     *
     * @param dataSet The name of the dataset.
     */
    public String loadOverrideDoc(String infoDir, String dataSet) throws DAP2Exception {

        StringBuilder userDoc = new StringBuilder();
        String overrideFile = dataSet + ".ovr";

        //Try to open and read the override file for this dataset.
        try {
            File fin = new File(infoDir + overrideFile);
            try (
            BufferedReader svIn = new BufferedReader(new InputStreamReader(new FileInputStream(fin),Util.UTF8));
            ) {
                boolean done = false;
                while(!done) {
                    String line = svIn.readLine();
                    if(line == null) {
                        done = true;
                    } else {
                        userDoc.append(line);
                        userDoc.append("\n");
                    }
                }
            }
        } catch (FileNotFoundException fnfe) {
            userDoc.append("
No Could Not Open Override Document.

");
            return (null);
        } catch (IOException ioe) {
            throw(new DAP2Exception(opendap.dap.DAP2Exception.UNKNOWN_ERROR, ioe.getMessage()));
        }

        return (userDoc.toString());
    }
    /***************************************************************************/

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    /**
     */

    private String get_user_supplied_docs(String serverName, String dataSet) throws DAP2Exception {

        StringBuilder userDoc = new StringBuilder();

        //Try to open and read the Dataset specific information file.
        try {
            File fin = new File(infoDir + dataSet + ".html");
            try (FileInputStream fis = new FileInputStream(fin);
                BufferedReader svIn = new BufferedReader(new InputStreamReader(fis,UTF8));
            ) {
                boolean done = false;
                while(!done) {
                    String line = svIn.readLine();
                    if(line == null) {
                        done = true;
                    } else {
                        userDoc.append(line);
                        userDoc.append("\n");
                    }
                }
            }
        } catch (FileNotFoundException fnfe) {
            userDoc.append("
No Dataset Specific Information Available.

");
        } catch (IOException ioe) {
            throw(new DAP2Exception(opendap.dap.DAP2Exception.UNKNOWN_ERROR, ioe.getMessage()));
        }

        userDoc.append("
\n");

        //Try to open and read the server specific information file.
        try {
            String serverFile = infoDir + serverName + ".html";
            if (_Debug) System.out.println("Server Info File: " + serverFile);
            File fin = new File(serverFile);
            try (BufferedReader svIn = new BufferedReader(new InputStreamReader(new FileInputStream(fin),UTF8));) {

                boolean done = false;

                while(!done) {
                    String line = svIn.readLine();
                    if(line == null) {
                        done = true;
                    } else {
                        userDoc.append(line);
                        userDoc.append("\n");
                    }
                }
            }

        } catch (FileNotFoundException fnfe) {
            userDoc.append("
No Server Specific Information Available.

");
        } catch (IOException ioe) {
            throw(new DAP2Exception(opendap.dap.DAP2Exception.UNKNOWN_ERROR, ioe.getMessage()));
        }

        return (userDoc.toString());
    }
    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    private String buildGlobalAttributes(DAS das, ServerDDS dds) {

        boolean found = false;
        StringBuilder ga = new StringBuilder();

        ga.append("
Dataset Information
\n
\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);
		    if(attr != null) {
                    Enumeration e = attr.getNames();
                    while (e.hasMoreElements()) {
                        String aName = (String) e.nextElement();
                        Attribute a = attr.getAttribute(aName);

                        found = true;

                        ga.append("\n
");
                        ga.append(aName + ":	\n");
                        ga.append("");

                        Enumeration es = a.getValues();
                        while (es.hasMoreElements()) {
                            String val = (String) es.nextElement();
                            ga.append(val);
                            ga.append("
");
                        }

                        ga.append("
\n");

                    }
		  }
                } catch (NoSuchAttributeException nsae) {
                }

            }
        }
        ga.append("
\n
\n");
        if (!found)
            ga.setLength(0);
        return (ga.toString());
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    private String buildVariableSummaries(DAS das, ServerDDS dds) {

        StringBuilder vs = new StringBuilder();
        vs.append("
Variables in this Dataset
\n
\n");

        Enumeration e = dds.getVariables();

        while (e.hasMoreElements()) {

            BaseType bt = (BaseType) e.nextElement();

            vs.append("
");

            vs.append(summarizeVariable(bt, das));

            vs.append("
");

        }

        vs.append("
\n
\n");

        return (vs.toString());
    }

    private String summarizeAttributes(AttributeTable attr)  {
        StringBuilder vOut = new StringBuilder();

        if (attr != null) {

            Enumeration e = attr.getNames();
            while (e.hasMoreElements()) {
                String name = (String) e.nextElement();

                Attribute a = attr.getAttribute(name);
                if (a != null) {
                    if (a.isContainer()) {
                        vOut.append("
  ");
                        vOut.append(name);
                        vOut.append(":
\n");
                        vOut.append("
\n");

                        try {
                            vOut.append(summarizeAttributes(a.getContainer()));
                        } catch (NoSuchAttributeException nase) {
                        }

                        vOut.append("
\n");

                    } else {
                        vOut.append("
  ");
                        vOut.append(name);
                        vOut.append(":  ");
                        try {
                            Enumeration es = a.getValues();
                            while (es.hasMoreElements()) {
                                String val = (String) es.nextElement();
                                vOut.append(val);
                                if (es.hasMoreElements())
                                    vOut.append(", ");
                            }
                        } catch (NoSuchAttributeException nase) {
                        }
                        vOut.append("
\n");
                    }

                }
            }
        }

        return (vOut.toString());

    }

    private String summarizeVariable(BaseType bt, DAS das) {

        StringBuilder vOut = new StringBuilder();

        vOut.append("");
        vOut.append(bt.getEncodedName());
        vOut.append(":	\n");
        vOut.append("");
        vOut.append(dasTools.fancyTypeName(bt));

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

            // This will display the DAS variables (attributes) as a bulleted list.
            vOut.append("\n
\n");
            vOut.append(summarizeAttributes(attr));
            vOut.append("\n
\n");

            if (bt instanceof DConstructor) {
                vOut.append("
\n");

                DConstructor dc = (DConstructor) bt;

                Enumeration e = dc.getVariables();

                while (e.hasMoreElements()) {
                    BaseType bt2 = (BaseType) e.nextElement();
                    vOut.append("
\n");
                    vOut.append(summarizeVariable(bt2, das));
                    vOut.append("
\n");
                }
                vOut.append("
\n");

            } else if (bt instanceof DVector) {

                DVector da = (DVector) bt;
                PrimitiveVector pv = da.getPrimitiveVector();

                if (pv instanceof BaseTypePrimitiveVector) {
                    BaseType bt2 = pv.getTemplate();

                    if (bt2 instanceof DArray || bt2 instanceof DString) {
                    } else {
                        vOut.append("
\n");
                        vOut.append("
\n");
                        vOut.append(summarizeVariable(bt2, das));
                        vOut.append("
\n");
                        vOut.append("
\n");
                    }
                }

            } else {
/*
                In the C++ code the types are all checked here, and if an unknown
                type is recieved then an exception is thrown. I think it's not
                needed... James?

                ndp

                      default:
	                assert("Unknown type" && false);
                    }
                */

                // While fixing another problem I decided that this should be
                // moved to the ed of the method...
                //vOut.append("	\n";
            }
        } catch (NoSuchAttributeException nsae) {
        }

        // While fixing another problem I decided that this should be
        // here to close the opening tag  from the top of this method.
        vOut.append("	\n");

        return (vOut.toString());

    }

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

}


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

package ucar.ma2;

import ucar.nc2.Structure;
import ucar.nc2.iosp.IospHelper;

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

/**
 * Make a deep copy of an existing StructureData, so that all the data is contained in this object
 *
 * @author caron
 * @since 6/19/2014
 */
public class StructureDataDeep extends StructureDataA {

  /**
   * Make deep copy from sdata to another StructureData object whose data is self contained
   * @param sdata    original sdata
   * @param members  the StructureData members. a reference is kept to this object
   * @return StructureData with all data self contained
   */
  static public StructureDataDeep copy(StructureData sdata, StructureMembers members) {
    ArrayStructureBB abb = copyToArrayBB(sdata, members, ByteOrder.BIG_ENDIAN);
    return new StructureDataDeep(abb);
  }

  /**
   * Make deep copy from an ArrayStructure to a ArrayStructureBB whose data is contained in a ByteBuffer
   * @param as    original ArrayStructure
   * @param bo    what byte order to use ? (null for any)
   * @param canonical  packing must be canonical
   * @return ArrayStructureBB with all data self contained
   */
  static public ArrayStructureBB copyToArrayBB(ArrayStructure as, ByteOrder bo, boolean canonical) throws IOException {
    if (!canonical && as.getClass().equals(ArrayStructureBB.class)) { // no subclasses, LOOK detect already canonical later
      ArrayStructureBB abb = (ArrayStructureBB) as;
      ByteBuffer bb = abb.getByteBuffer();
      if (bo == null || bo.equals(bb.order()))
        return abb;
    }

    StructureMembers smo = as.getStructureMembers();
    StructureMembers sm = new StructureMembers(smo);
    ArrayStructureBB abb = new ArrayStructureBB(sm, as.getShape());
    ArrayStructureBB.setOffsets(sm);  // this makes the packing canonical
    if (bo != null) {
      ByteBuffer bb = abb.getByteBuffer();
      bb.order(bo);
    }

    try (StructureDataIterator iter = as.getStructureDataIterator()) {
      while (iter.hasNext())
        copyToArrayBB(iter.next(), abb);
    }
    return abb;
  }

  /**
   * Make deep copy to an ArrayStructureBB whose data is contained in a ByteBuffer.
   * Use the order of the members in the given Structure; skip copying any not in the Structure
   * @param s     list of structure members come from here; must be compatible with ArrayStructure's data
   * @param as    original ArrayStructure
   * @param bo    what byte order to use ? (null for default)
   * @return ArrayStructureBB with all data self contained
   */
  static public ArrayStructureBB copyToArrayBB(Structure s, ArrayStructure as, ByteOrder bo) throws IOException {
    StructureMembers sm = s.makeStructureMembers();
    ArrayStructureBB abb = new ArrayStructureBB(sm, as.getShape());
    ArrayStructureBB.setOffsets(sm);
    if (bo != null) {
      ByteBuffer bb = abb.getByteBuffer();
      bb.order(bo);
    }

    try (StructureDataIterator iter = as.getStructureDataIterator()) {
      while (iter.hasNext())
        copyToArrayBB(iter.next(), abb);
    }
    return abb;
  }

  /**
   * Make deep copy from a StructureData to a ArrayStructureBB whose data is contained in a ByteBuffer.
   * @param sdata  original ArrayStructure.
   * @return ArrayStructureBB with all data self contained
   */
  static public ArrayStructureBB copyToArrayBB(StructureData sdata) {
    return copyToArrayBB(sdata, new StructureMembers(sdata.getStructureMembers()), ByteOrder.BIG_ENDIAN);
  }

  /**
   * Make deep copy from a StructureData to a ArrayStructureBB whose data is contained in a ByteBuffer
   * @param sdata    original ArrayStructure
   * @param sm       the StructureData members. a reference is kept to this object
   * @param bo       Byte Order of the ByteBuffer
   * @return ArrayStructureBB with all data self contained
   */
  static public ArrayStructureBB copyToArrayBB(StructureData sdata, StructureMembers sm, ByteOrder bo) {
    int size = sm.getStructureSize();
    ByteBuffer bb = ByteBuffer.allocate(size); // default is big endian
    bb.order(bo);
    ArrayStructureBB abb = new ArrayStructureBB(sm, new int[]{1}, bb, 0);
    ArrayStructureBB.setOffsets(sm);
    copyToArrayBB(sdata, abb);
    return abb;
  }

  /**
   * Make deep copy from a StructureData into the given ArrayStructureBB
   * @param sdata    original data from here
   * @param abb      copy data into this ArrayStructureBB, starting from wherever the ByteBuffer current position is
   * @return number of bytes copied
   */
  static public int copyToArrayBB(StructureData sdata, ArrayStructureBB abb) {
    //StructureMembers sm = sdata.getStructureMembers();
    ByteBuffer bb = abb.getByteBuffer();
    int start = bb.limit();

    for (StructureMembers.Member wantMember : abb.getMembers()) {
      StructureMembers.Member m = sdata.findMember(wantMember.getName());
      assert m != null;
      assert m.getDataType() == wantMember.getDataType();

      DataType dtype = m.getDataType();
      //System.out.printf("do %s (%s) = %d%n", m.getName(), m.getDataType(), bb.position());
      if (m.isScalar()) {
        switch (dtype) {
          case STRING:
            bb.putInt(abb.addObjectToHeap(sdata.getScalarString(m)));
            break;
          case FLOAT:
            bb.putFloat(sdata.getScalarFloat(m));
            break;
          case DOUBLE:
            bb.putDouble(sdata.getScalarDouble(m));
            break;
          case INT:
          case UINT:
          case ENUM4:
            bb.putInt(sdata.getScalarInt(m));
            break;
          case SHORT:
          case USHORT:
          case ENUM2:
            bb.putShort(sdata.getScalarShort(m));
            break;
          case BYTE:
          case UBYTE:
          case ENUM1:
            bb.put(sdata.getScalarByte(m));
            break;
          case CHAR:
            bb.put((byte) sdata.getScalarChar(m));
            break;
          case LONG:
          case ULONG:
            bb.putLong(sdata.getScalarLong(m));
            break;
          case STRUCTURE:
            StructureData sd  = sdata.getScalarStructure(m);
            ArrayStructureBB out_abb = new ArrayStructureBB(sd.getStructureMembers(),
                    new int[]{1}, bb, 0);
            copyToArrayBB(sd, out_abb);
            break;
          default:
            throw new IllegalStateException("scalar " + dtype.toString());
            /* case BOOLEAN:
           break;
         case SEQUENCE:
           break;
         case OPAQUE:
           break; */
        }
      } else {
        int n = m.getSize();
        switch (dtype) {
          case STRING:
            String[] ss = sdata.getJavaArrayString(m);
            bb.putInt(abb.addObjectToHeap(ss)); // stored as String[] on the heap
            break;
          case FLOAT:
            float[] fdata = sdata.getJavaArrayFloat(m);
            for (int i = 0; i < n; i++)
              bb.putFloat(fdata[i]);
            break;
          case DOUBLE:
            double[] ddata = sdata.getJavaArrayDouble(m);
            for (int i = 0; i < n; i++)
              bb.putDouble(ddata[i]);
            break;
          case INT:
          case ENUM4:
            int[] idata = sdata.getJavaArrayInt(m);
            for (int i = 0; i < n; i++)
              bb.putInt(idata[i]);
            break;
          case SHORT:
          case ENUM2:
            short[] shdata = sdata.getJavaArrayShort(m);
            for (int i = 0; i < n; i++)
              bb.putShort(shdata[i]);
            break;
          case BYTE:
          case ENUM1:
            byte[] bdata = sdata.getJavaArrayByte(m);
            for (int i = 0; i < n; i++)
              bb.put(bdata[i]);
            break;
          case CHAR:
            char[] cdata = sdata.getJavaArrayChar(m);
            bb.put(IospHelper.convertCharToByte(cdata));
            break;
          case LONG:
            long[] ldata = sdata.getJavaArrayLong(m);
            for (int i = 0; i < n; i++)
              bb.putLong(ldata[i]);
            break;
          default:
            throw new IllegalStateException("array " + dtype.toString());
            /* case BOOLEAN:
          break;
         case OPAQUE:
          break;
        case STRUCTURE:
          break; // */
          case SEQUENCE:
            break; // skip
        }
      }
    }
    return bb.limit() - start;
  }

  //private ArrayStructureBB abb;
  private StructureDataDeep(ArrayStructureBB abb) {
    super(abb, 0);
    //this.abb = abb;
  }

  // public ByteBuffer getByteBuffer() { return abb.getByteBuffer(); }

}


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

import javax.annotation.Nullable;
import ucar.nc2.grib.coord.TimeCoordIntvDateValue;
import ucar.nc2.grib.grib2.Grib2Pds;
import ucar.nc2.grib.grib2.Grib2Record;
import ucar.nc2.grib.grib2.Grib2Utils;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarPeriod;

/**
 Center        = (7) US National Weather Service, National Centres for Environmental Prediction (NCEP)
 SubCenter     = (14) NWS Meteorological Development Laboratory
 Master Table  = 1
 Local Table   = 0
 *
 * @author caron
 * @since 1/28/2016.
 */
public class NwsMetDevTables extends NcepLocalTables {

  NwsMetDevTables(Grib2TableConfig grib2Table) {
    super(grib2Table);
  }

  @Override
  @Nullable
  public TimeCoordIntvDateValue getForecastTimeInterval(Grib2Record gr) {
    Grib2Pds pds = gr.getPDS();
    if (!pds.isTimeInterval()) return null;
    Grib2Pds.PdsInterval pdsIntv = (Grib2Pds.PdsInterval) gr.getPDS();

    // override here only if timeRangeUnit = 255
    boolean needOverride = false;
    for (Grib2Pds.TimeInterval ti : pdsIntv.getTimeIntervals()) {
      needOverride = (ti.timeRangeUnit == 255);
    }
    if (!needOverride)
      return super.getForecastTimeInterval(gr);

    CalendarDate intvEnd = pdsIntv.getIntervalTimeEnd();
    int ftime = pdsIntv.getForecastTime();
    int timeUnitOrg = pds.getTimeUnit();
    int timeUnitConvert = convertTimeUnit(timeUnitOrg);
    CalendarPeriod unitPeriod = Grib2Utils.getCalendarPeriod(timeUnitConvert);
    if (unitPeriod == null)
      throw new IllegalArgumentException("unknown CalendarPeriod " + timeUnitConvert + " org=" + timeUnitOrg);

    CalendarPeriod.Field fld = unitPeriod.getField();

    CalendarDate referenceDate = gr.getReferenceDate();
    CalendarDate intvStart = referenceDate.add(ftime, fld);

    return new TimeCoordIntvDateValue(intvStart, intvEnd);
  }

  /**
   * Only use in GribVariable to decide on variable identity when intvMerge = false.
   * By returning a constant, we dont intvMerge = false.
   * Problem is we cant reconstruct interval length without reference time, which is not in the pds.
   */
  @Override
  public double getForecastTimeIntervalSizeInHours(Grib2Pds pds) {
    Grib2Pds.PdsInterval pdsIntv = (Grib2Pds.PdsInterval) pds;

    // override here only if timeRangeUnit = 255
    boolean needOverride = false;
    for (Grib2Pds.TimeInterval ti : pdsIntv.getTimeIntervals()) {
      needOverride = (ti.timeRangeUnit == 255);
    }
    if (!needOverride)
      return super.getForecastTimeIntervalSizeInHours(pds);

    return 12.0;
  }

}


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

import ucar.ma2.*;
import ucar.nc2.Attribute;
import ucar.nc2.Group;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CF;
import ucar.nc2.util.NamedObject;
import ucar.unidata.util.Format;

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

/**
 * A 1-dimensional Coordinate Axis. Its values must be monotonic.
 *

 * If this is char valued, it will have rank 2, otherwise it will have rank 1.
 *

 * If string or char valued, only getCoordName() can be called.
 *

 * If the coordinates are regularly spaced, isRegular() is true, and the values are equal to
 * getStart() + i * getIncrement().
 *

 * This will also set "cell bounds" for this axis. By default, the cell bounds are midway between the coordinates values,
 * and are therefore contiguous, and can be accessed though getCoordEdge(i).
 * The only way the bounds can be set is if the coordinate variable has an attribute "bounds" that points to another variable
 * bounds(ncoords,2). These contain the cell bounds, and must be ascending or descending as the coordinate values are. In
 * this case isContiguous() is true when bounds1(i+1) == bounds2(i) for all i.
 *
 * @author john caron
 * @see CoordinateAxis#factory
 */

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

  private boolean wasRead = false; // have the data values been  read
  private boolean wasBoundsDone = false; // have we created the bounds arrays if exists ?
  private boolean isInterval = false; // is this an interval coordinates - then should use bounds
  private boolean isAscending;

  // read in on doRead()
  private double[] coords; // coordinate values, must be between edges
  private String[] names = null; // only set if String or char values

  // defer making until asked, use makeBounds()
  private double[] edge; // n+1 edges, edge[k] < midpoint[k] < edge[k+1]
  private double[] bound1, bound2; // may be contiguous or not

  private boolean wasCalcRegular = false; // have we checked if the data is regularly spaced ?
  private boolean isRegular = false;
  private double start, increment;

  /**
   * Create a 1D coordinate axis from an existing Variable
   *
   * @param ncd the containing dataset
   * @param vds wrap this VariableDS, which is not changed.
   */
  public CoordinateAxis1D(NetcdfDataset ncd, VariableDS vds) {
    super(ncd, vds);
    vds.setCaching(true);
  }

  /**
   * Copy constructor
   *
   * @param ncd ok to reparent
   * @param org copy from here
   */
  CoordinateAxis1D(NetcdfDataset ncd, CoordinateAxis1D org) {
    super(ncd, org);
    this.orgName = org.orgName;
    this.cache = new Variable.Cache(); // decouple cache
    org.setCaching(true);

    // copy rest of state
    this.increment = org.getIncrement();
    this.isAscending = org.isAscending;
    this.isInterval = org.isInterval();
    this.isRegular = org.isRegular();

    if (isNumeric()) {
      this.coords = org.getCoordValues();
      this.edge = org.getCoordEdges();
    }

    this.names = org.names;

    if (isInterval) {
      this.bound1 = org.getBound1();
      this.bound2 = org.getBound2();
    }

    this.wasBoundsDone = org.wasBoundsDone;
    this.wasCalcRegular = org.wasCalcRegular;
    this.wasRead = org.wasRead;
  }

  /**
   * Constructor when theres no underlying variable. You better set the values too!
   *
   * @param ds        the containing dataset.
   * @param group     the containing group; if null, use rootGroup
   * @param shortName axis name.
   * @param dataType  data type
   * @param dims      list of dimension names
   * @param units     units of coordinates, preferably udunit compatible.
   * @param desc      long name.
   */
  public CoordinateAxis1D(NetcdfDataset ds, Group group, String shortName,
                          DataType dataType, String dims, String units, String desc) {

    super(ds, group, shortName, dataType, dims, units, desc);
  }

  /**
   * Create a new CoordinateAxis1D as a section of this CoordinateAxis1D.
   *
   * @param r the section range
   * @return a new CoordinateAxis1D as a section of this CoordinateAxis1D
   * @throws InvalidRangeException if IllegalRange
   */
  public CoordinateAxis1D section(Range r) throws InvalidRangeException {
    Section section = new Section().appendRange(r);
    CoordinateAxis1D result = (CoordinateAxis1D) section(section);
    int len = r.length();

    // deal with the midpoints, bounds
    if (isNumeric()) {
      double[] new_mids = new double[len];
      for (int idx = 0; idx < len; idx++) {
        int old_idx = r.element(idx);
        new_mids[idx] = coords[old_idx];
      }
      result.coords = new_mids;

      if (isInterval) {
        double[] new_bound1 = new double[len];
        double[] new_bound2 = new double[len];
        double[] new_edge = new double[len + 1];
        for (int idx = 0; idx < len; idx++) {
          int old_idx = r.element(idx);
          new_bound1[idx] = bound1[old_idx];
          new_bound2[idx] = bound2[old_idx];
          new_edge[idx] = bound1[old_idx];
          new_edge[idx + 1] = bound2[old_idx]; // all but last are overwritten
        }
        result.bound1 = new_bound1;
        result.bound2 = new_bound2;
        result.edge = new_edge;

      } else {
        double[] new_edge = new double[len + 1];
        for (int idx = 0; idx < len; idx++) {
          int old_idx = r.element(idx);
          new_edge[idx] = edge[old_idx];
          new_edge[idx + 1] = edge[old_idx + 1]; // all but last are overwritten
        }
        result.edge = new_edge;
      }
    }

    if (names != null) {
      String[] new_names = new String[len];
      for (int idx = 0; idx < len; idx++) {
        int old_idx = r.element(idx);
        new_names[idx] = names[old_idx];
      }
      result.names = new_names;
    }

    result.wasCalcRegular = false;
    result.calcIsRegular();

    return result;
  }

  // for section and slice

  @Override
  protected Variable copy() {
    return new CoordinateAxis1D(this.ncd, this);
  }

  @Override
  public CoordinateAxis copyNoCache() {
    CoordinateAxis1D axis = new CoordinateAxis1D(ncd, getParentGroup(), getShortName(), getDataType(), getDimensionsString(),
            getUnitsString(), getDescription());

    // other state
    axis.axisType = this.axisType;
    axis.boundaryRef = this.boundaryRef;
    axis.isContiguous = this.isContiguous;
    axis.positive = this.positive;

    axis.cache = new Variable.Cache(); // decouple cache
    return axis;
  }

  /**
   * Get the list of names, to be used for user selection.
   * The ith one refers to the ith coordinate.
   *
   * @return List of ucar.nc2.util.NamedObject, or empty list.
   */
  public List<NamedObject> getNames() {
    int n = getDimension(0).getLength();
    List<NamedObject> names = new ArrayList<>(n);
    for (int i = 0; i < n; i++)
      names.add(new ucar.nc2.util.NamedAnything(getCoordName(i), getShortName() + " " + getUnitsString()));
    return names;
  }

  /**
   * The "name" of the ith coordinate. If nominal, this is all there is to a coordinate.
   * If numeric, this will return a String representation of the coordinate.
   *
   * @param index which one ?
   * @return the ith coordinate value as a String
   */
  public String getCoordName(int index) {
    if (!wasRead) doRead();
    if (isNumeric())
      return Format.d(getCoordValue(index), 5, 8);
    else
      return names[index];
  }

  /**
   * Get the ith coordinate value. This is the value of the coordinate axis at which
   * the data value is associated. These must be strictly monotonic.
   *
   * @param index which coordinate. Between 0 and getNumElements()-1 inclusive.
   * @return coordinate value.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double getCoordValue(int index) {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValue() on non-numeric");
    if (!wasRead) doRead();
    return coords[index];
  }

  @Override
  public double getMinValue() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValue() on non-numeric");
    if (!wasRead) doRead();

    return Math.min(coords[0], coords[coords.length - 1]);
  }

  @Override
  public double getMaxValue() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValue() on non-numeric");
    if (!wasRead) doRead();

    return Math.max(coords[0], coords[coords.length - 1]);
  }

  public double getMinEdgeValue() {
    if (edge == null) return getMinValue();
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValue() on non-numeric");
    if (!wasRead) doRead();

    return Math.min(edge[0], edge[edge.length - 1]);
  }

  public double getMaxEdgeValue() {
    if (edge == null) return getMaxValue();
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValue() on non-numeric");
    if (!wasRead) doRead();

    return Math.max(edge[0], edge[edge.length - 1]);
  }

  /**
   * Get the ith coordinate edge. Exact only if isContiguous() is true, otherwise use getBound1() and getBound2().
   * This is the value where the underlying grid element switches
   * from "belonging to" coordinate value i-1 to "belonging to" coordinate value i.
   * In some grids, this may not be well defined, and so should be considered an
   * approximation or a visualization hint.
   *

   *  Coordinate edges must be strictly monotonic:
   *    coordEdge(0) < coordValue(0) < coordEdge(1) < coordValue(1) ...
   *    ... coordEdge(i) < coordValue(i) < coordEdge(i+1) < coordValue(i+1) ...
   *    ... coordEdge(n-1) < coordValue(n-1) < coordEdge(n)
   *

   *
   * @param index which coordinate. Between 0 and getNumElements() inclusive.
   * @return coordinate edge.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double getCoordEdge(int index) {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordEdge() on non-numeric");
    if (!wasBoundsDone) makeBounds();
    return edge[index];
  }

  /**
   * Get the coordinate values as a double array.
   *
   * @return coordinate value.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double[] getCoordValues() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordValues() on non-numeric");
    if (!wasRead) doRead();
    return coords.clone();
  }

  /**
   * Get the coordinate edges as a double array.
   * Exact only if isContiguous() is true, otherwise use getBound1() and getBound2().
   *
   * @return coordinate edges.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double[] getCoordEdges() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getCoordEdges() on non-numeric");
    if (!wasBoundsDone) makeBounds();
    return edge.clone();
  }

  @Override
  public boolean isContiguous() {
    if (!wasBoundsDone) makeBounds();  // this sets isContiguous
    return isContiguous;
  }

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

  /**
   * If this coordinate has interval values.
   * If so, then one should use getBound1, getBound2, and not getCoordEdges()
   *
   * @return true if coordinate has interval values
   */
  public boolean isInterval() {
    if (!wasBoundsDone) makeBounds();      // this sets isInterval
    return isInterval;
  }

  /**
   * Get the coordinate bound1 as a double array.
   * bound1[i] # coordValue[i] # bound2[i], where # is < if increasing (bound1[i] < bound1[i+1])
   * else < if decreasing.
   *
   * @return coordinate bound1.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double[] getBound1() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getBound1() on non-numeric");
    if (!wasBoundsDone) makeBounds();
    if (bound1 == null) makeBoundsFromEdges();
    assert bound1 != null;
    return bound1.clone();
  }

  /**
   * Get the coordinate bound1 as a double array.
   * bound1[i] # coordValue[i] # bound2[i],  where # is < if increasing (bound1[i] < bound1[i+1])
   * else < if decreasing.
   *
   * @return coordinate bound2.
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double[] getBound2() {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis1D.getBound2() on non-numeric");
    if (!wasBoundsDone) makeBounds();
    if (bound2 == null) makeBoundsFromEdges();
    assert bound2 != null;
    return bound2.clone();
  }

  /**
   * Get the coordinate bounds for the ith coordinate.
   * Can use this for isContiguous() true or false.
   *
   * @param i coordinate index
   * @return double[2] edges for ith coordinate
   */
  public double[] getCoordBounds(int i) {
    if (!wasBoundsDone) makeBounds();

    double[] e = new double[2];
    if (isContiguous()) {
      e[0] = getCoordEdge(i);
      e[1] = getCoordEdge(i + 1);
    } else {
      e[0] = bound1[i];
      e[1] = bound2[i];
    }
    return e;
  }

  public double getCoordBoundsMidpoint(int i) {
    double[] bounds = getCoordBounds(i);
    return (bounds[0]+bounds[1])/2;
  }

  /**
   * Given a coordinate value, find what grid element contains it.
   * This means that
   *

   * edge[i] <= value < edge[i+1] (if values are ascending)
   * edge[i] > value >= edge[i+1] (if values are descending)
   *

   *
   * @param coordVal position in this coordinate system
   * @return index of grid point containing it, or -1 if outside grid area
   */
  public int findCoordElement(double coordVal) {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis.findCoordElement() on non-numeric");

    if (isRegular())
      return findCoordElementRegular(coordVal, false);
    if (isContiguous())
      return findCoordElementIrregular(coordVal, false);
    else
      return findCoordElementNonContiguous(coordVal, false);
  }

  /**
   * Given a coordinate position, find what grid element contains it, or is closest to it.
   *
   * @param coordVal position in this coordinate system
   * @return index of grid point containing it, or best estimate of closest grid interval.
   */
  public int findCoordElementBounded(double coordVal) {
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis.findCoordElementBounded() on non-numeric");

    // the scalar or len-1 case:
    if (this.getSize() == 1) return 0;

    if (isRegular())
      return findCoordElementRegular(coordVal, true);
    if (isContiguous())
      return findCoordElementIrregular(coordVal, true);
    else
      return findCoordElementNonContiguous(coordVal, true);
  }

  /**
   * @deprecated use findCoordElement(coordVal)
   */
  public int findCoordElement(double coordVal, int lastIndex) {
    return findCoordElement(coordVal);
  }

  //////////////////////////////////////////////////////////////////
  // following is from Jon Blower's ncWMS
  // faster routines for coordValue -> index search
  // significantly modified

  /**
   * Optimize the regular case
   * Gets the index of the given point. Uses index = (value - start) / stride,
   * hence this is faster than an exhaustive search.
   * from jon blower's ncWMS.
   *
   * @param coordValue The value along this coordinate axis
   * @param bounded    if false and not in range, return -1, else nearest index
   * @return the index that is nearest to this point, or -1 if the point is
   * out of range for the axis
   */
  private int findCoordElementRegular(double coordValue, boolean bounded) {
    int n = (int) this.getSize();

    // the scalar or len-1 case:
    if (this.getSize() == 1) {
      return 0;
    }

  /*  if (axisType == AxisType.Lon) {
      double maxValue = this.start + this.increment * n;
      if (betweenLon(coordValue, this.start, maxValue)) {
        double distance = LatLonPointImpl.getClockwiseDistanceTo(this.start, coordValue);
        double exactNumSteps = distance / this.increment;
        // This axis might wrap, so we make sure that the returned index is within range
        return ((int) Math.round(exactNumSteps)) % (int) this.getSize();

      } else if (coordValue < this.start) {
        return bounded ? 0 : -1;
      } else {
        return bounded ? n - 1 : -1;
      }
    } */

    double distance = coordValue - this.start;
    double exactNumSteps = distance / this.increment;
    int index = (int) Math.round(exactNumSteps);
    if (index < 0)
      return bounded ? 0 : -1;
    else if (index >= n)
      return bounded ? n - 1 : -1;
    return index;
  }

  private boolean betweenLon(double lon, double lonBeg, double lonEnd) {
    while (lon < lonBeg) lon += 360;
    return (lon >= lonBeg) && (lon <= lonEnd);
  }

  /**
   * Performs a binary search to find the index of the element of the array
   * whose value is contained in the interval, so must be contiguous.
   *
   * @param target  The value to search for
   * @param bounded if false, and not in range, return -1, else nearest index
   * @return the index of the element in values whose value is closest to target,
   * or -1 if the target is out of range
   */
  private int findCoordElementIrregular(double target, boolean bounded) {
    int n = (int) this.getSize();
    int low = 0;
    int high = n;

    if (isAscending) {
      // Check that the point is within range
      if (target < this.edge[low])
        return bounded ? 0 : -1;
      else if (target > this.edge[high])
        return bounded ? n - 1 : -1;

      // do a binary search to find the nearest index
      int mid = low;
      while (high > low + 1) {
        mid = (low + high) / 2;
        double midVal = this.edge[mid];
        if (midVal == target) return mid;
        else if (midVal < target) low = mid;
        else high = mid;
      }

      return low;

    } else {

      // Check that the point is within range
      if (target > this.edge[low])
        return bounded ? 0 : -1;
      else if (target < this.edge[high])
        return bounded ? n - 1 : -1;

      // do a binary search to find the nearest index
      int mid = low;
      while (high > low + 1) {
        mid = (low + high) / 2;
        double midVal = this.edge[mid];
        if (midVal == target) return mid;
        else if (midVal < target) high = mid;
        else low = mid;
      }

      return high - 1;
    }
  }

  /**
   * Given a coordinate position, find what grid element contains it.
   * Only use if isContiguous() == false
   * This algorithm does a linear search in the bound1[] amd bound2[] array.
   *

   * This means that
   *

   * edge[i] <= pos < edge[i+1] (if values are ascending)
   * edge[i] > pos >= edge[i+1] (if values are descending)
   *

   *
   * @param target  The value to search for
   * @param bounded if false, and not in range, return -1, else nearest index
   * @return the index of the element in values whose value is closest to target,
   * or -1 if the target is out of range
   */
  private int findCoordElementNonContiguous(double target, boolean bounded) {

    double[] bounds1 = getBound1();
    double[] bounds2 = getBound2();
    int n = bounds1.length;

    if (isAscending) {
      // Check that the point is within range
      if (target < bounds1[0])
        return bounded ? 0 : -1;
      else if (target > bounds2[n - 1])
        return bounded ? n - 1 : -1;

      int[] idx = findSingleHit(bounds1, bounds2, target);
      if (idx[0] == 0 && !bounded) return -1;  // no hits
      if (idx[0] == 1) return idx[1];          // one hit

      // multiple hits = choose closest to the midpoint i guess
      return findClosest(coords, target);

    } else {

      // Check that the point is within range
      if (target > bounds1[0])
        return bounded ? 0 : -1;
      else if (target < bounds2[n - 1])
        return bounded ? n - 1 : -1;

      int[] idx = findSingleHit(bounds2, bounds1, target);
      if (idx[0] == 0 && !bounded) return -1;  // no hits
      if (idx[0] == 1) return idx[1];

      // multiple hits = choose closest to the midpoint i guess
      return findClosest(getCoordValues(), target);
    }
  }

  // return index if only one match, else -1
  private int[] findSingleHit(double[] low, double[] high, double target) {
    int hits = 0;
    int idxFound = -1;
    int n = low.length;
    for (int i = 0; i < n; i++) {
      if ((low[i] <= target) && (target <= high[i])) {
        hits++;
        idxFound = i;
      }
    }
    return new int[] {hits, idxFound};
  }

  // return index of closest value to target
  private int findClosest(double[] values, double target) {
    double minDiff =  Double.MAX_VALUE;
    int idxFound = -1;
    int n = values.length;
    for (int i = 0; i < n; i++) {
      double diff =  Math.abs(values[i]-target);
      if (diff < minDiff) {
        minDiff = diff;
        idxFound = i;
      }
    }
    return idxFound;
  }

  ///////////////////////////////////////////////////////////////////////////////
  // check if Regular

  /**
   * Get starting value if isRegular()
   *
   * @return starting value if isRegular()
   */
  public double getStart() {
    calcIsRegular();
    return start;
  }

  /**
   * Get increment value if isRegular()
   *
   * @return increment value if isRegular()
   */
  public double getIncrement() {
    calcIsRegular();
    return increment;
  }

  /**
   * If true, then value(i) = getStart() + i * getIncrement().
   *
   * @return if evenly spaced.
   */
  public boolean isRegular() {
    calcIsRegular();
    return isRegular;
  }

  private void calcIsRegular() {
    if (wasCalcRegular) return;
    if (!wasRead) doRead();

    if (!isNumeric())
      isRegular = false;
    else if (getSize() < 2)
      isRegular = true;
    else {
      start = getCoordValue(0);
      int n = (int) getSize();
      increment = (getCoordValue(n - 1) - getCoordValue(0)) / (n - 1);
      isRegular = true;
      for (int i = 1; i < getSize(); i++)
        if (!ucar.nc2.util.Misc.nearlyEquals(getCoordValue(i) - getCoordValue(i - 1), increment, 5.0e-3)) {
          isRegular = false;
          break;
        }
    }
    wasCalcRegular = true;
  }

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

  private void doRead() {
    if (isNumeric()) {
      readValues();
      wasRead = true;

      if (getSize() < 2)
        isAscending = true;
      else
        isAscending = getCoordValue(0) < getCoordValue(1);
      //  calcIsRegular(); */
    } else if (getDataType() == DataType.STRING) {
      readStringValues();
      wasRead = true;
    } else {
      readCharValues();
      wasRead = true;
    }
  }

  // turns longitude coordinate into monotonic, dealing with possible wrap.
  public void correctLongitudeWrap() {
    // correct non-monotonic longitude coords
    if (axisType != AxisType.Lon) {
      return;
    }

    if (!wasRead) doRead();
    if (!wasBoundsDone) makeBounds();

    boolean monotonic = true;
    for (int i = 0; i < coords.length - 1; i++)
      monotonic &= isAscending ? coords[i] < coords[i + 1] : coords[i] > coords[i + 1];

    if (!monotonic) {
      boolean cross = false;
      if (isAscending) {
        for (int i = 0; i < coords.length; i++) {
          if (cross) coords[i] += 360;
          if (!cross && (i < coords.length - 1) && (coords[i] > coords[i + 1]))
            cross = true;
        }
      } else {
        for (int i = 0; i < coords.length; i++) {
          if (cross) coords[i] -= 360;
          if (!cross && (i < coords.length - 1) && (coords[i] < coords[i + 1]))
            cross = true;
        }
      }

      // LOOK - need to make sure we get stuff from the cache
      Array cachedData = Array.factory(DataType.DOUBLE, getShape(), coords);
      if (getDataType() != DataType.DOUBLE)
        cachedData = MAMath.convert(cachedData, getDataType());
      setCachedData(cachedData);

      if (!isInterval) {
        makeEdges();
      }
    }

  }

  // only used if String

  private void readStringValues() {
    int count = 0;
    Array data;
    try {
      data = read();
    } catch (IOException ioe) {
      log.error("Error reading string coordinate values ", ioe);
      throw new IllegalStateException(ioe);
    }

    names = new String[(int) data.getSize()];
    IndexIterator ii = data.getIndexIterator();
    while (ii.hasNext())
      names[count++] = (String) ii.getObjectNext();
  }

  private void readCharValues() {
    int count = 0;
    ArrayChar data;
    try {
      data = (ArrayChar) read();
    } catch (IOException ioe) {
      log.error("Error reading char coordinate values ", ioe);
      throw new IllegalStateException(ioe);
    }
    ArrayChar.StringIterator iter = data.getStringIterator();
    names = new String[iter.getNumElems()];
    while (iter.hasNext())
      names[count++] = iter.next();
  }

  private void readValues() {
    Array data;
    try {
      // setUseNaNs(false); // missing values not allowed LOOK not true for point data !!
      data = read();
      // if (!hasCachedData()) setCachedData(data, false); //cache data for subsequent reading
    } catch (IOException ioe) {
      log.error("Error reading coordinate values ", ioe);
      throw new IllegalStateException(ioe);
    }

    coords = (double []) data.get1DJavaArray(DataType.DOUBLE);
    //IndexIterator iter = data.getIndexIterator();
    //while (iter.hasNext())
    //  coords[count++] = iter.getDoubleNext();
  }

  /**
   * Calculate bounds, set isInterval, isContiguous
   */
  private void makeBounds() {
    if (!wasRead) doRead();
    if (isNumeric()) {
      if (!makeBoundsFromAux()) {
        makeEdges();
      }
    }
    wasBoundsDone = true;
  }

  private boolean makeBoundsFromAux() {
    Attribute boundsAtt = findAttributeIgnoreCase(CF.BOUNDS);
    if ((null == boundsAtt) || !boundsAtt.isString()) return false;
    String boundsVarName = boundsAtt.getStringValue();
    VariableDS boundsVar = (VariableDS) ncd.findVariable(getParentGroup(), boundsVarName);
    if (null == boundsVar) return false;
    if (2 != boundsVar.getRank()) return false;

    if (getDimension(0) != boundsVar.getDimension(0)) return false;
    if (2 != boundsVar.getDimension(1).getLength()) return false;

    Array data;
    try {
      boundsVar.removeEnhancement(NetcdfDataset.Enhance.ConvertMissing);  // Don't convert missing values to NaN.
      data = boundsVar.read();
    } catch (IOException e) {
      log.warn("CoordinateAxis1D.hasBounds read failed ", e);
      return false;
    }

    assert (data.getRank() == 2) && (data.getShape()[1] == 2) : "incorrect shape data for variable " + boundsVar;

    // extract the bounds
    int n = shape[0];
    double[] value1 = new double[n];
    double[] value2 = new double[n];
    Index ima = data.getIndex();
    for (int i = 0; i < n; i++) {
      ima.set0(i);
      value1[i] = data.getDouble(ima.set1(0));
      value2[i] = data.getDouble(ima.set1(1));
    }

    /* flip if needed
    boolean firstLower = true; // in the first interval, is lower < upper ?
    for (int i = 0; i < value1.length; i++) {
      if (Misc.nearlyEquals(value1[i], value2[i])) continue; // skip when lower == upper
      firstLower = value1[i] < value2[i];
      break;
    }
    // check first against last : lower, unless all lower equal then upper
    boolean goesUp = (n < 2) || value1[n - 1] > value1[0] || (Misc.nearlyEquals(value1[n - 1], value2[0]) && value2[n - 1] > value2[0]);
    if (goesUp != firstLower) {
      double[] temp = value1;
      value1 = value2;
      value2 = temp;
    } */

    // decide if they are contiguous
    boolean contig = true;
    for (int i = 0; i < n - 1; i++) {
      if (!ucar.nc2.util.Misc.nearlyEquals(value1[i + 1], value2[i]))
        contig = false;
    }

    if (contig) {
      edge = new double[n + 1];
      edge[0] = value1[0];
      for (int i = 1; i < n + 1; i++)
        edge[i] = value2[i - 1];
    } else {                           // what does edge mean when not contiguous ??
      edge = new double[n + 1];
      edge[0] = value1[0];
      for (int i = 1; i < n; i++)
        edge[i] = (value1[i] + value2[i - 1]) / 2;
      edge[n] = value2[n - 1];
      isContiguous = false;
    }

    bound1 = value1;
    bound2 = value2;
    isInterval = true;

    return true;
  }

  private void makeEdges() {
    int size = (int) getSize();
    edge = new double[size + 1];
    if (size < 1) return;
    for (int i = 1; i < size; i++)
      edge[i] = (coords[i - 1] + coords[i]) / 2;
    edge[0] = coords[0] - (edge[1] - coords[0]);
    edge[size] = coords[size - 1] + (coords[size - 1] - edge[size - 1]);
    isContiguous = true;
  }

  private void makeBoundsFromEdges() {
    int size = (int) getSize();
    if (size == 0) return;

    bound1 = new double[size];
    bound2 = new double[size];
    for (int i = 0; i < size; i++) {
      bound1[i] = edge[i];
      bound2[i] = edge[i + 1];
    }

    // flip if needed
    if (bound1[0] > bound2[0]) {
      double[] temp = bound1;
      bound1 = bound2;
      bound2 = temp;
    }
  }

}


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

package ucar.nc2.ui.widget;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.nc2.ui.table.ColumnWidthsResizer;
import ucar.nc2.ui.table.TableAligner;
import ucar.nc2.ui.util.SwingUtils;

import javax.swing.AbstractButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.MenuElement;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.filechooser.FileSystemView;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.LinkedList;
import java.util.List;

/**
 * A JFileChooser that displays the "Details" view by default. The table in that view has been tweaked to intelligently
 * resize columns' widths based on their contents and left-align all cells.
 *
 * An improved JFileChooser. It incorporates the following enhancements:
 * <ul>
 *   <li> The "Details" view is displayed by default.
 *   <li> The table in that view has been tweaked to intelligently resize columns' widths based on their contents.
 *   <li> All text in those cells is left-aligned.
 * </ul>
 *
 * @author cwardgar
 */
public class ImprovedFileChooser extends JFileChooser {
    private static final Logger logger = LoggerFactory.getLogger(ImprovedFileChooser.class);

    private static String osName = System.getProperty("os.name").toLowerCase();
    public static boolean isMacOs = osName.startsWith("mac os x");

    static {
        // Disable editable "Name" column in JFileChooser details table.
        // This setting is honored by sun.swing.FilePane.DetailsTableModel.
        UIManager.put("FileChooser.readOnly", Boolean.TRUE);
    }

    // This is a reference to the JDialog created by JFileChooser.createDialog().
    // JFileChooser also has this data member, but it's private. We need our own.
    private JDialog dialog = null;

    public ImprovedFileChooser() {
        this(null, null);
    }

    public ImprovedFileChooser(File currentDirectory) {
        this(currentDirectory, null);
    }

    public ImprovedFileChooser(File currentDirectory, FileSystemView fsv) {
        super(currentDirectory, fsv);

        AbstractButton detailsViewButton = getDetailsViewButton(this);
        if (detailsViewButton == null) {
            // mac osx does not have a details button. It is recommended to use
            // a java.awt.FileDialog box on MacOSX for the proper look and feel.
            // so, don't warn if on a mac.
            // https://developer.apple.com/library/mac/documentation/Java/Conceptual/Java14Development/07-NativePlatformIntegration/NativePlatformIntegration.html
            if (!isMacOs) {
                logger.warn("Couldn't find Details button!");
            }
            return;
        }

        detailsViewButton.doClick();    // Programmatically switch to the Details View.

        List<JTable> tables = SwingUtils.getDescendantsOfType(JTable.class, this);
        final JTable detailsTable;

        if (tables.size() != 1) {
            logger.warn("Expected to find 1 JTable in the file chooser, but found " + tables.size());
            return;
        } else if ((detailsTable = tables.get(0)) == null) {
            logger.warn("The details view table was null!");
            return;
        }

        // Set the preferred column widths so that they're big enough to display all data without truncation.
        ColumnWidthsResizer resizer = new ColumnWidthsResizer(detailsTable);
        detailsTable.getModel().addTableModelListener(resizer);
        detailsTable.getColumnModel().addColumnModelListener(resizer);

        // Left-align every cell, including header cells.
        TableAligner aligner = new TableAligner(detailsTable, SwingConstants.LEADING);
        detailsTable.getColumnModel().addColumnModelListener(aligner);

        // Every time the directory is changed in a JFileChooser dialog, a new TableColumnModel is created.
        // This is bad, because it discards the alignment decorators that we installed on the old model.
        // So, we 're going to listen for the creation of new TableColumnModels so that we can reinstall the decorators.
        detailsTable.addPropertyChangeListener(new NewColumnModelListener(detailsTable, SwingConstants.LEADING));

        // It's quite likely that the total width of the table is NOT EQUAL to the sum of the preferred column widths
        // that our TableModelListener calculated. In that case, resize all of the columns an equal percentage.
        // This will ensure that the relative ratios of the *actual* widths match those of the *preferred* widths.
        detailsTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
    }

    private static AbstractButton getDetailsViewButton(JFileChooser fileChooser) {
        AbstractButton detailsButton = SwingUtils.getDescendantOfType(
                AbstractButton.class, fileChooser, "Icon", UIManager.getIcon("FileChooser.detailsViewIcon"));
        if (detailsButton != null) {
            return detailsButton;
        }

        logger.debug("Couldn't find a Details View button in the File Chooser dialog. Searching in popup menu.");

        JComponent componentWithPopupMenu = SwingUtils.getDescendantOfType(
                JComponent.class, fileChooser, "ComponentPopupMenu", SwingUtils.NOT_NULL);
        if (componentWithPopupMenu == null) {
            return null;
        }

        JPopupMenu popupMenu = componentWithPopupMenu.getComponentPopupMenu();
        if (popupMenu == null) {
            return null;
        }

        for (JMenuItem menuItem : getAllMenuItems(popupMenu)) {
            if (menuItem.getText().equals("Details")) {
                return menuItem;
            }
        }

        return null;
    }

    private static List<JMenuItem> getAllMenuItems(JPopupMenu popupMenu) {
        List<JMenuItem> menuItems = new LinkedList<>();
        getAllMenuItems(popupMenu, menuItems);
        return menuItems;
    }

    private static void getAllMenuItems(MenuElement menuElem, List<JMenuItem> menuItems) {
        if (menuElem instanceof JMenuItem) {
            menuItems.add((JMenuItem) menuElem);
        }

        for (MenuElement subMenuElem : menuElem.getSubElements()) {
            getAllMenuItems(subMenuElem, menuItems);
        }
    }

    private static class NewColumnModelListener implements PropertyChangeListener {
        private final JTable table;
        private final int alignment;

        private NewColumnModelListener(JTable table, int alignment) {
            this.table = table;
            this.alignment = alignment;
        }

        @Override public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals("columnModel")) {
                // Left-align every cell, including header cells.
                TableAligner aligner = new TableAligner(table, alignment);
                table.getColumnModel().addColumnModelListener(aligner);
            }
        }
    }

    /////////////////////////////////////////////////// JFileChooser ///////////////////////////////////////////////////

    @Override public int showDialog(Component parent, String approveButtonText) throws HeadlessException {
        int returnValue = super.showDialog(parent, approveButtonText);
        this.dialog = null;  // dialog was disposed in super-method. Null out so we don't try to use it.
        return returnValue;
    }

    @Override protected JDialog createDialog(Component parent) throws HeadlessException {
        this.dialog = super.createDialog(parent);  // Grab our own local reference to the dialog.
        return this.dialog;
    }

    public static void main(String[] args) throws ClassNotFoundException, UnsupportedLookAndFeelException,
            InstantiationException, IllegalAccessException {
        // Switch to Nimbus Look and Feel, if it's available.
        if (isMacOs) {
            System.setProperty ("apple.laf.useScreenMenuBar", "true");
        } else {
            for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        }
        ImprovedFileChooser fileChooser = new ImprovedFileChooser();
        fileChooser.setPreferredSize(new Dimension(1000, 750));
        fileChooser.showDialog(null, "Choose");
    }
}


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

package ucar.nc2.ft.point.standard;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Structure;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.ft.FeatureDatasetFactoryManager;
import ucar.nc2.ft.point.standard.plug.BuoyShipSynop;
import ucar.nc2.ft.point.standard.plug.CFpointObs;
import ucar.nc2.ft.point.standard.plug.CFpointObsExt;
import ucar.nc2.ft.point.standard.plug.CdmDirect;
import ucar.nc2.ft.point.standard.plug.Cosmic;
import ucar.nc2.ft.point.standard.plug.FslRaob;
import ucar.nc2.ft.point.standard.plug.FslWindProfiler;
import ucar.nc2.ft.point.standard.plug.GempakCdm;
import ucar.nc2.ft.point.standard.plug.Iridl;
import ucar.nc2.ft.point.standard.plug.Jason;
import ucar.nc2.ft.point.standard.plug.Madis;
import ucar.nc2.ft.point.standard.plug.MadisAcars;
import ucar.nc2.ft.point.standard.plug.NdbcCoards;
import ucar.nc2.ft.point.standard.plug.Nldn;
import ucar.nc2.ft.point.standard.plug.RafNimbus;
import ucar.nc2.ft.point.standard.plug.SimpleTrajectory;
import ucar.nc2.ft.point.standard.plug.Suomi;
import ucar.nc2.ft.point.standard.plug.UnidataPointObs;

/**
 * Analyzes the coordinate systems of a dataset to try to identify the Feature Type and the
 *   structure of the data.
 * Used by PointDatasetStandardFactory.
 *
 * @author caron
 * @since Mar 20, 2008
 */
public class TableAnalyzer {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TableAnalyzer.class);

  static private final List<Configurator> conventionList = new ArrayList<>();
  static private boolean userMode = false;
  static private final boolean debug = false;

  // search in the order added
  static {

    registerAnalyzer("CDM", CdmDirect.class, null);
    registerAnalyzer(CDM.CF_EXTENDED, CFpointObsExt.class, null);

    registerAnalyzer("CF-1.", CFpointObs.class, new ConventionNameOk() {
      @Override
      public boolean isMatch(String convName, String wantName) {
        return convName.startsWith(wantName); //  && !convName.equals("CF-1.0"); // throw 1.0 to default analyser
      }
    });

    registerAnalyzer("GEMPAK/CDM", GempakCdm.class, null);
    registerAnalyzer("Unidata Observation Dataset v1.0", UnidataPointObs.class, null);

    registerAnalyzer("Cosmic", Cosmic.class, null);
    registerAnalyzer("Jason", Jason.class, null);
    registerAnalyzer("FslWindProfiler", FslWindProfiler.class, null);
    registerAnalyzer("MADIS-ACARS", MadisAcars.class, null); // must be before Madis
    registerAnalyzer("MADIS surface observations, v1.0", Madis.class, null);  // must be before FslRaob
    registerAnalyzer("FSL Raobs", FslRaob.class, null);  // must be before FslRaob

    registerAnalyzer("IRIDL", Iridl.class, null);
    registerAnalyzer("Ndbc", NdbcCoards.class, null);
    registerAnalyzer("Suomi-Station-CDM", Suomi.class, null);
    registerAnalyzer("BuoyShip-NetCDF", BuoyShipSynop.class, null);
    registerAnalyzer("NCAR-RAF/nimbus", RafNimbus.class, null);
    registerAnalyzer("NLDN-CDM", Nldn.class, null);
    registerAnalyzer("SimpleTrajectory", SimpleTrajectory.class, null);

    // further calls to registerConvention are by the user
    userMode = true;
  }

  static public void registerAnalyzer(String conventionName, Class c, ConventionNameOk match) {
    if (!(TableConfigurer.class.isAssignableFrom(c)))
      throw new IllegalArgumentException("Class " + c.getName() + " must implement TableConfigurer");

    // fail fast - check newInstance works
    TableConfigurer tc;
    try {
      tc = (TableConfigurer) c.newInstance();
    } catch (InstantiationException e) {
      throw new IllegalArgumentException("TableConfigurer Class " + c.getName() + " cannot instantiate, probably need default Constructor");
    } catch (IllegalAccessException e) {
      throw new IllegalArgumentException("TableConfigurer Class " + c.getName() + " is not accessible");
    }

    Configurator anal = new Configurator(conventionName, c, tc, match);
    if (userMode) // user stuff gets put at top
      conventionList.add(0, anal);
    else
      conventionList.add(anal);
  }

  static private interface ConventionNameOk {
    boolean isMatch(String convName, String wantName);
  }

  static private class Configurator {
    String convName;
    Class confClass;
    TableConfigurer confInstance;
    ConventionNameOk match;

    Configurator(String convName, Class confClass, TableConfigurer confInstance, ConventionNameOk match) {
      this.convName = convName;
      this.confClass = confClass;
      this.confInstance = confInstance;
      this.match = match;
    }
  }

  static private Configurator matchConfigurator(String convName) {
    for (Configurator anal : conventionList) {
      if ((anal.match == null) && anal.convName.equalsIgnoreCase(convName)) return anal;
      if ((anal.match != null) && anal.match.isMatch(convName, anal.convName)) return anal;
    }
    return null;
  }

  /**
   * Find a TableConfigurer for this dataset, if there is one.
   *
   * @param wantFeatureType want this FeatureType
   * @param ds for this dataset
   * @return TableConfigurer or null if not found
   * @throws IOException on read error
   */
  static public TableConfigurer getTableConfigurer(FeatureType wantFeatureType, NetcdfDataset ds) throws IOException {
    String convUsed = null;

    // search for the Conventions attribute
    String convName = ds.findAttValueIgnoreCase(null, CDM.CONVENTIONS, null);
    if (convName == null)
      convName = ds.findAttValueIgnoreCase(null, "Convention", null);

    // now search for TableConfigurer using that Convention
    Configurator anal = null;
    if (convName != null) {
      convName = convName.trim();

      // search for Convention parsing class
      anal = matchConfigurator(convName);
      if (anal != null) {
        convUsed = convName;
        if (debug) System.out.println("  TableConfigurer found using convName "+convName);
      }

      // now search for comma or semicolon or / delimited list
      if (anal == null) {
        List<String> names = new ArrayList<>();

        if ((convName.indexOf(',') > 0) || (convName.indexOf(';') > 0)) {
          StringTokenizer stoke = new StringTokenizer(convName, ",;");
          while (stoke.hasMoreTokens()) {
            String name = stoke.nextToken();
            names.add(name.trim());
          }
        } else if ((convName.indexOf('/') > 0)) {
          StringTokenizer stoke = new StringTokenizer(convName, "/");
          while (stoke.hasMoreTokens()) {
            String name = stoke.nextToken();
            names.add(name.trim());
          }
        }

        if (names.size() > 0) {
          // search the registered conventions, in order
          for (Configurator conv : conventionList) {
            for (String name : names) {
              if (name.equalsIgnoreCase(conv.convName)) {
                anal = conv;
                convUsed = name;
                if (debug) System.out.println("  TableConfigurer found using convName "+convName);
              }
            }
            if (anal != null) break;
          }
        }
      }
    }

    // search for ones that dont use Convention attribute, in order added.
    // call method isMine() using reflection.
    if (anal == null) {
      for (Configurator conv : conventionList) {
        Class c = conv.confClass;
        Method isMineMethod;

        try {
          isMineMethod = c.getMethod("isMine", new Class[]{FeatureType.class, NetcdfDataset.class});
        } catch (NoSuchMethodException ex) {
          continue;
        }

        try {
          Boolean result = (Boolean) isMineMethod.invoke(conv.confInstance, wantFeatureType, ds);
          if (debug) System.out.println("  TableConfigurer.isMine "+c.getName()+ " result = " + result);
          if (result) {
            anal = conv;
            convUsed = conv.convName;
            break;
          }
        } catch (Exception ex) {
          System.out.println("ERROR: Class " + c.getName() + " Exception invoking isMine method%n" + ex);
        }
      }
    }

    // Instantiate a new TableConfigurer object
    TableConfigurer tc = null;
    if (anal != null) {
      try {
        tc = (TableConfigurer) anal.confClass.newInstance();
        tc.setConvName( convName);
        tc.setConvUsed( convUsed);
      } catch (InstantiationException | IllegalAccessException e) {
        log.error("TableConfigurer create failed", e);
      }
    }

    return tc;
  }

  /**
   * Create a TableAnalyser for this dataset with the given TableConfigurer
   *
   * @param tc TableConfigurer, may be null.
   * @param wantFeatureType want this FeatureType
   * @param ds for this dataset
   * @return TableAnalyser
   * @throws IOException on read error
   */
  static public TableAnalyzer factory(TableConfigurer tc, FeatureType wantFeatureType, NetcdfDataset ds) throws IOException {

    // Create a TableAnalyzer with this TableConfigurer (may be null)
    TableAnalyzer analyzer = new TableAnalyzer(ds, tc);

    if (tc != null) {
      if (tc.getConvName() == null)
        analyzer.userAdvice.format(" No 'Conventions' global attribute.%n");
      else
        analyzer.userAdvice.format(" Conventions global attribute = %s %n", tc.getConvName());

      // add the convention name used
      if (tc.getConvUsed() != null) {
        analyzer.setConventionUsed(tc.getConvUsed());
        if (!tc.getConvUsed().equals(tc.getConvName()))
          analyzer.userAdvice.format(" TableConfigurer used = "+tc.getConvUsed()+".%n");
      }

    } else {
      analyzer.userAdvice.format(" No TableConfigurer found, using default analysis.%n");

    }

    // construct the nested table object
    analyzer.analyze(wantFeatureType);
    return analyzer;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////
  private TableConfigurer tc;
  private NetcdfDataset ds;
  private Map<String, TableConfig> tableFind = new HashMap<>();
  private Set<TableConfig> tableSet = new HashSet<>();
  private List<NestedTable> leaves = new ArrayList<>();
  private FeatureType ft;
  private TableConfig configResult;

  private TableAnalyzer(NetcdfDataset ds, TableConfigurer tc) {
    this.tc = tc;
    this.ds = ds;

    if (tc == null)
      userAdvice.format("Using default TableConfigurer.%n");
  }

  public List<NestedTable> getFlatTables() {
    return leaves;
  }

  public boolean featureTypeOk(FeatureType ftype, Formatter errlog) {
    for (NestedTable nt : leaves) {
      if (!nt.hasCoords()) {
        errlog.format("Table %s featureType %s: lat/lon/time coord not found%n", nt.getName(), nt.getFeatureType());
        try {
          writeConfigXML(errlog);
        } catch (IOException e) {
         log.error("featuretypeOk", e);
        }
      }

      if (!FeatureDatasetFactoryManager.featureTypeOk(ftype, nt.getFeatureType()))
        errlog.format("Table %s featureType %s doesnt match desired type %s%n", nt.getName(), nt.getFeatureType(), ftype);

      if (nt.hasCoords() && FeatureDatasetFactoryManager.featureTypeOk(ftype, nt.getFeatureType()))
        return true;
    }

    return false;
  }

  public String getName() {
    if (tc != null) return tc.getClass().getName();
    return "Default";
  }

  // for debugging messages
  public FeatureType getFirstFeatureType() {
    for (NestedTable nt : leaves) {
      if (nt.hasCoords())
        return nt.getFeatureType();
    }
    return null;
  }

  public NetcdfDataset getNetcdfDataset() {
    return ds;
  }

  private Formatter userAdvice = new Formatter();
  private Formatter errlog = new Formatter();

  public String getUserAdvice() {
    return userAdvice.toString();
  }

  public String getErrlog() {
    return errlog.toString();
  }

  private String conventionName;

  private void setConventionUsed(String convName) {
    this.conventionName = convName;
  }

  TableConfig getTableConfig() {
    return this.configResult;
  }

  TableConfigurer getTableConfigurer() {
    return tc;
  }

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

  /**
   * Make a NestedTable object for the dataset.
   * @param wantFeatureType want this FeatureType
   * @throws IOException on read error
   */
  private void analyze(FeatureType wantFeatureType) throws IOException {
    // for netcdf-3 files, convert record dimension to structure
    // LOOK may be problems when served via opendap
    boolean structAdded = (Boolean) ds.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);

    if (tc == null) {
      makeTablesDefault(structAdded);
      makeNestedTables();

    } else {
      configResult = tc.getConfig(wantFeatureType, ds, errlog);
      if (configResult != null)
        addTableRecurse( configResult); // kinda stupid
      else { // use default
        makeTablesDefault(structAdded);
        makeNestedTables();
      }
    }

    // find the leaves
    for (TableConfig config : tableSet) {
      if (config.children == null) { // its a leaf
        NestedTable flatTable = new NestedTable(ds, config, errlog);
        leaves.add(flatTable);
      }
    }

    if (PointDatasetStandardFactory.showTables)
      getDetailInfo( new Formatter( System.out));
  }

  private void addTable(TableConfig t) {
    tableFind.put(t.name, t);
    if (t.dimName != null)
      tableFind.put(t.dimName, t);
    tableSet.add(t);
  }

  private void addTableRecurse(TableConfig t) {
    addTable(t);
    if (t.children != null) {
      for (TableConfig child : t.children)
        addTableRecurse(child);
    }
  }

  ///////////////////////////////////////////////////////////
  // default analasis aka guessing

  // no TableConfig was passed in - gotta wing it
  private void makeTablesDefault(boolean structAdded) throws IOException {

    // make Structures into a table
    List<Variable> vars = new ArrayList<>(ds.getVariables());
    Iterator<Variable> iter = vars.iterator();
    while (iter.hasNext()) {
      Variable v = iter.next();
      if (v instanceof Structure) {  // handles Sequences too
        TableConfig st = new TableConfig(Table.Type.Structure, v.getFullName());
        CoordSysEvaluator.findCoords(st, ds, null);
        st.structName = v.getFullName();
        st.nestedTableName = v.getShortName();

        addTable(st);
        checkIfTrajectory(st);

        iter.remove();
        findNestedStructures((Structure) v, st); // look for nested structures

      } else if (structAdded && v.isUnlimited()) {
        iter.remove();
      }
    }

    if (tableSet.size() > 0) return;

    // search at dimensions that lat, lon, time coordinates use
    Set<Dimension> dimSet = new HashSet<>(10);
    for (CoordinateAxis axis : ds.getCoordinateAxes()) {
      if ((axis.getAxisType() == AxisType.Lat) || (axis.getAxisType() == AxisType.Lon)|| (axis.getAxisType() == AxisType.Time))
        for (Dimension dim : axis.getDimensions())
          dimSet.add(dim);
    }

    // lat, lon, time all use same dimension - use it
    if (dimSet.size() == 1) {
      final Dimension obsDim = (Dimension) dimSet.toArray()[0];
      TableConfig st = new TableConfig(Table.Type.Structure, obsDim.getShortName());
      st.structureType = obsDim.isUnlimited() ? TableConfig.StructureType.Structure : TableConfig.StructureType.PsuedoStructure;
      st.structName = obsDim.isUnlimited() ? "record" : obsDim.getShortName();
      st.dimName = obsDim.getShortName();
      CoordSysEvaluator.findCoords(st, ds, new CoordSysEvaluator.Predicate() {
        public boolean match(CoordinateAxis axis) {
          return obsDim.equals(axis.getDimension(0));
        }
      });

      CoordinateAxis time = CoordSysEvaluator.findCoordByType(ds, AxisType.Time);
      if ((time != null) && (time.getRank() == 0)) {
        st.addJoin(new JoinArray(time, JoinArray.Type.scalar, 0));
        st.time = time.getShortName();
      }
      addTable( st);
      checkIfTrajectory(st);
    }

    if (tableSet.size() > 0) return;

    // try the time dimension
    CoordinateAxis time = null;
    for (CoordinateAxis axis : ds.getCoordinateAxes()) {
      if ((axis.getAxisType() == AxisType.Time) && axis.isIndependentCoordinate()) {
        time = axis;
        break;
      }
    }
    if (time != null) {
      Dimension obsDim = time.getDimension(0);
      TableConfig st = new TableConfig(Table.Type.Structure, obsDim.getShortName());
      st.structureType = TableConfig.StructureType.PsuedoStructure;
      st.dimName = obsDim.getShortName();
      CoordSysEvaluator.findCoords(st, ds, null);

      addTable( st);
    }

  }

  private void checkIfTrajectory(TableConfig st) {
    // deal with possible trajectory - only do this if dataset has metadata
    FeatureType ft = FeatureDatasetFactoryManager.findFeatureType(ds);
    if (ft == FeatureType.TRAJECTORY) {
      st.featureType = FeatureType.TRAJECTORY;
      TableConfig pc = new TableConfig(Table.Type.Top, "single");
      st.parent = pc;
      pc.addChild(st);
    } else
      st.featureType = FeatureType.POINT;
  }

  private void findNestedStructures(Structure s, TableConfig parent) {
    for (Variable v : s.getVariables()) {
      if (v instanceof Structure) {  // handles Sequences too
        TableConfig nestedTable = new TableConfig(Table.Type.NestedStructure, v.getFullName());
        nestedTable.structName = v.getFullName();
        nestedTable.nestedTableName = v.getShortName();

        addTable(nestedTable);
        parent.addChild(nestedTable);

        // LOOK why not add the join(parent,child) here ?
        //nestedTable.join = new TableConfig.JoinConfig(Join.Type.NestedStructure);
        //joins.add(nestedTable.join);

        findNestedStructures((Structure) v, nestedTable); // search for nested structures
      }
    }
  }

  private void makeNestedTables() {
    // We search among all the possible Tables in a dataset for joins, and coordinate
    // variables. Based on those, we form "interesting" sets and make them into NestedTables.

    /* link the tables together with joins
    for (TableConfig.JoinConfig join : joins) {
      NestedTable.Table parent = join.parent;
      NestedTable.Table child = join.child;

      if (child.parent != null) throw new IllegalStateException("Multiple parents");
      child.parent = parent;
      child.join = join;

      if (parent.children == null) parent.children = new ArrayList<Join>();
      parent.children.add(join);
    } */
  }

  /////////////////////////////////////////////////////
  /* track station info

  private StationInfo stationInfo = new StationInfo();

  private StationInfo getStationInfo() {
    return stationInfo;
  }

  public class StationInfo {
    public String stationId, stationDesc, stationNpts;
    public int nstations;
    public String latName, lonName, elevName;
  }  */

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

  /* public void showCoordSys(java.util.Formatter sf) {
    sf.format("%nCoordinate Systems%n");
    for (CoordinateSystem cs : ds.getCoordinateSystems()) {
      sf.format(" %s%n", cs);
    }
  }

  public void showCoordAxes(java.util.Formatter sf) {
    sf.format("%nAxes%n");
    for (CoordinateAxis axis : ds.getCoordinateAxes()) {
      sf.format(" %s %s%n", axis.getAxisType(), axis.getNameAndDimensions());
    }
  } */

  /* public void showTables(java.util.Formatter sf) {
    sf.format("%nTables%n");
    for (NestedTable.Table t : tableSet)
      sf.format(" %s%n", t);

    sf.format("%nJoins%n");
    for (Join j : joins)
      sf.format(" %s%n", j);
  } */

  public void showNestedTables(java.util.Formatter sf) {
    for (NestedTable nt : leaves) {
      nt.show(sf);
    }
  }

  public String getImplementationName() {
    return (tc != null) ? tc.getClass().getSimpleName() : "defaultAnalyser";
  }

  public void getDetailInfo(java.util.Formatter sf) {
    sf.format("-----------------------------------------------------%nTableAnalyzer on Dataset %s%n", ds.getLocation());
    sf.format(" TableAnalyser = %s%n", getName());
    showNestedTables(sf);
    String errlogS = errlog.toString();
    if (errlogS.length() > 0)
      sf.format("%n Errlog=%n%s",errlogS);
    String userAdviceS = userAdvice.toString();
    if (userAdviceS.length() > 0)
      sf.format("%n userAdvice=%n%s%n",userAdviceS);

    try {
      writeConfigXML(sf);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private void writeConfigXML(java.util.Formatter sf) throws IOException {
    if (configResult != null) {
        PointConfigXML tcx = new PointConfigXML();
        tcx.writeConfigXML(configResult, getName(), sf);
        return;
    }
    XMLOutputter fmt = new XMLOutputter( Format.getPrettyFormat());
    sf.format("%s", fmt.outputString ( makeDocument()));
  }

  /** Create an XML document from this info
   * @return netcdfDatasetInfo XML document
   */
  private Document makeDocument() {
    Element rootElem = new Element("featureDataset");
    Document doc = new Document(rootElem);
    rootElem.setAttribute("location", ds.getLocation());
    rootElem.addContent( new Element("analyser").setAttribute("class", getName()));
    if (ft != null)
      rootElem.setAttribute("featureType", ft.toString());

    for (NestedTable nt : leaves) {
      writeTable( rootElem, nt.getLeaf());
    }

    return doc;
  }

  private Element writeTable( Element parent, Table table) {
    if (table.parent != null) {
      parent = writeTable( parent, table.parent);
    }
    Element tableElem = new Element("table");
    parent.addContent( tableElem);

    if (table.getName() != null)
      tableElem.setAttribute("name", table.getName());
    if (table.getFeatureType() != null)
      tableElem.setAttribute("featureType", table.getFeatureType().toString());
    tableElem.setAttribute("class", table.getClass().toString());

    addCoordinates(tableElem, table);
    for (String colName : table.cols.keySet()) {
      if (!table.nondataVars.contains(colName))
        tableElem.addContent( new Element("variable").addContent(colName));
    }

    if (table.extraJoins != null) {
      for (Join j : table.extraJoins) {
        if (j instanceof JoinArray)
          tableElem.addContent( writeJoinArray( (JoinArray)j));
        else if (j instanceof JoinMuiltdimStructure)
          tableElem.addContent( writeJoinMuiltdimStructure( (JoinMuiltdimStructure) j));
        else if (j instanceof JoinParentIndex)
          tableElem.addContent( writeJoinParentIndex( (JoinParentIndex) j));
      }
    }
    return tableElem;
  }

  private void addCoordinates( Element tableElem, Table table) {
    addCoord(tableElem, table.lat, "lat");
    addCoord(tableElem, table.lon, "lon");
    addCoord(tableElem, table.elev, "elev");
    addCoord(tableElem, table.time, "time");
    addCoord(tableElem, table.timeNominal, "timeNominal");
    addCoord(tableElem, table.stnId, "stnId");
    addCoord(tableElem, table.stnDesc, "stnDesc");
    addCoord(tableElem, table.stnNpts, "stnNpts");
    addCoord(tableElem, table.stnWmoId, "stnWmoId");
    addCoord(tableElem, table.stnAlt, "stnAlt");
    addCoord(tableElem, table.limit, "limit");
  }

  private void addCoord(Element tableElem, String name, String kind) {
    if (name != null) {
      Element elem = new Element("coordinate").setAttribute("kind", kind);
      elem.addContent(name);
      tableElem.addContent(elem);
    }
  }

  private Element writeJoinArray(JoinArray join) {
    Element joinElem = new Element("join");
    joinElem.setAttribute("class", join.getClass().toString());
    if (join.type != null)
      joinElem.setAttribute("type", join.type.toString());
    if (join.v != null)
      joinElem.addContent( new Element("variable").setAttribute("name", join.v.getFullName()));
    joinElem.addContent( new Element("param").setAttribute("value", Integer.toString(join.param)));
    return joinElem;
  }

  private Element writeJoinMuiltdimStructure(JoinMuiltdimStructure join) {
    Element joinElem = new Element("join");
    joinElem.setAttribute("class", join.getClass().toString());
    if (join.parentStructure != null)
      joinElem.addContent( new Element("parentStructure").setAttribute("name", join.parentStructure.getFullName()));
    joinElem.addContent( new Element("dimLength").setAttribute("value", Integer.toString(join.dimLength)));
    return joinElem;
  }

  private Element writeJoinParentIndex(JoinParentIndex join) {
    Element joinElem = new Element("join");
    joinElem.setAttribute("class", join.getClass().toString());
    if (join.parentStructure != null)
      joinElem.addContent( new Element("parentStructure").setAttribute("name", join.parentStructure.getFullName()));
    if (join.parentIndex != null)
      joinElem.addContent( new Element("parentIndex").setAttribute("name", join.parentIndex));
    return joinElem;
  }

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

  static void doit(String filename) throws IOException {
    System.out.println(filename);
    NetcdfDataset ncd = ucar.nc2.dataset.NetcdfDataset.openDataset(filename);
    TableAnalyzer csa = TableAnalyzer.factory(null, null, ncd);
    csa.getDetailInfo(new Formatter(System.out));
    System.out.println("%n-----------------");
  }

  static public void main(String args[]) throws IOException {
    /* doit("C:/data/dt2/station/Surface_METAR_20080205_0000.nc");
    doit("C:/data/bufr/edition3/idd/profiler/PROFILER_3.bufr");
    doit("C:/data/bufr/edition3/idd/profiler/PROFILER_2.bufr");
    doit("C:/data/profile/PROFILER_wind_01hr_20080410_2300.nc"); */
    //doit("C:/data/test/20070301.nc");
    //doit("C:/data/dt2/profile/PROFILER_3.bufr");

    //doit("C:/data/dt2/station/ndbc.nc");
    //doit("C:/data/dt2/station/madis2.sao");
    // doit("C:/data/metars/Surface_METAR_20070326_0000.nc");  // ok
    //doit("C:/data/dt2/station/Sean_multidim_20070301.nc"); // ok
    //doit("Q:/cdmUnitTest/formats/gempak/surface/20090521_sao.gem");
    doit("D:/datasets/metars/Surface_METAR_20070513_0000.nc");

    //doit("C:/data/profile/PROFILER_wind_01hr_20080410_2300.nc");
    //doit("C:/data/cadis/tempting");
  }
}


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

package ucar.nc2.iosp.mcidas;

import ucar.nc2.*;

import ucar.nc2.iosp.grid.*;
import ucar.nc2.util.CancelTask;

import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;

/**
 * An IOSP for McIDAS Grid data
 *
 * @author dmurray
 */
public class McIDASGridServiceProvider extends GridServiceProvider {

  /**
   * McIDAS file reader
   */
  protected McIDASGridReader mcGridReader;

  /**
   * Is this a valid file?
   *
   * @param raf RandomAccessFile to check
   * @return true if a valid McIDAS grid file
   * @throws IOException problem reading file
   */
  public boolean isValidFile(RandomAccessFile raf) throws IOException {
    mcGridReader = new McIDASGridReader();
    return mcGridReader.init(raf, false);
  }

  /**
   * Get the file type id
   *
   * @return the file type id
   */
  public String getFileTypeId() {
    return "McIDASGrid";
  }

  /**
   * Get the file type description
   *
   * @return the file type description
   */
  public String getFileTypeDescription() {
    return "McIDAS Grid file";
  }

  /**
   * Open the service provider for reading.
   *
   * @param raf        file to read from
   * @param ncfile     netCDF file we are writing to (memory)
   * @param cancelTask task for cancelling
   * @throws IOException problem reading file
   */
  public void open(RandomAccessFile raf, NetcdfFile ncfile, CancelTask cancelTask) throws IOException {
    //debugProj = true;
    super.open(raf, ncfile, cancelTask);
    long start = System.currentTimeMillis();
    if (mcGridReader == null) {
      mcGridReader = new McIDASGridReader();
    }
    mcGridReader.init(raf);
    GridIndex index = mcGridReader.getGridIndex();
    open(index, cancelTask);
    if (debugOpen) {
      System.out.println(" GridServiceProvider.open "
              + ncfile.getLocation() + " took "
              + (System.currentTimeMillis() - start));
    }
  }

  /**
   * Open the index and create the netCDF file from that
   *
   * @param index      GridIndex to use
   * @param cancelTask cancel task
   * @throws IOException problem reading the file
   */
  protected void open(GridIndex index, CancelTask cancelTask)
          throws IOException {
    McIDASLookup lookup = new McIDASLookup((McIDASGridRecord) index.getGridRecords().get(0));
    GridIndexToNC delegate = new GridIndexToNC(index.filename);
    //delegate.setUseDescriptionForVariableName(false);
    delegate.open(index, lookup, 4, ncfile, cancelTask);
    ncfile.finish();
  }

  /**
   * Sync and extend
   *
   * @return false
   */
  public boolean sync() {
    try {
      if (!mcGridReader.init()) {
        return false;
      }
      GridIndex index = mcGridReader.getGridIndex();
      // reconstruct the ncfile objects
      ncfile.empty();
      open(index, null);
      return true;

    } catch (IOException ioe) {
      return false;
    }
  }

  /**
   * Read the data for this GridRecord
   *
   * @param gr grid identifier
   * @return the data (or null)
   * @throws IOException problem reading the data
   */
  protected float[] _readData(GridRecord gr) throws IOException {
    return mcGridReader.readGrid((McIDASGridRecord) gr);
  }

}