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

package dap4.servlet;

import dap4.core.ce.CEConstraint;
import dap4.core.data.ChecksumMode;
import dap4.core.data.DSP;
import dap4.core.dmr.DapAttribute;
import dap4.core.dmr.DapDataset;
import dap4.core.dmr.DapType;
import dap4.core.dmr.ErrorResponse;
import dap4.core.util.*;
import dap4.dap4lib.*;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.Map;

abstract public class DapController extends HttpServlet
{
    // Provide a way for test programs to pass info into the controller
    static public boolean TESTING = false;

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

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

    static public boolean PARSEDEBUG = false;

    static protected final String BIG_ENDIAN = "Big-Endian";
    static protected final String LITTLE_ENDIAN = "Little-Endian";

    // Is this machine big endian?
    static protected boolean IS_BIG_ENDIAN = (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN);

    static protected final String DMREXT = ".dmr";
    static protected final String DATAEXT = ".dap";
    static protected final String DSREXT = ".dsr";
    static protected final String[] ENDINGS = {DMREXT, DATAEXT, DSREXT};

    static protected final String FAVICON = "favicon.ico"; // relative to resource dir

    static public final long DEFAULTBINARYWRITELIMIT = 100 * 1000000; // in bytes

    //////////////////////////////////////////////////
    // static variables

    // We need a singleton instance of a DapCache in order
    // To avoid re-opening the same NetcdfFile instance.
    // Assume:
    // 1. This is subclassed only once and that class will fill in
    //    this DapCache instance.

    static protected DapCache cache = null;

    //////////////////////////////////////////////////
    // Static accessors

    static protected void
    setCache(DapCache cache)
    {
        DapController.cache = cache;
    }

    static protected DapCache
    getCache()
    {
        return DapController.cache;
    }

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

    protected boolean initialized = false; // Was initialize() called?

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

    public DapController()
    {
        ChunkWriter.DUMPDATA = DUMPDATA; // pass it on
    }

    //////////////////////////////////////////////////////////
    // Abstract methods

    /**
     * Process a favicon request.
     *
     * @param icopath The path to the icon
     * @param cxt     The dap context
     */

    abstract protected void doFavicon(String icopath, DapContext cxt) throws IOException;

    /**
     * Process a capabilities request.
     * Currently, does nothing (but see D4TSServlet.doCapabilities).
     *
     * @param cxt The dapontext
     */

    abstract protected void doCapabilities(DapRequest drq, DapContext cxt) throws IOException;

    /**
     * Convert a URL path into an absolute file path
     * Note that it is assumed than any leading servlet prefix has been removed.
     *
     * @param drq      dap request
     * @param location suffix of url path
     * @return
     * @throws IOException
     */

    abstract public String getResourcePath(DapRequest drq, String location) throws DapException;

    /**
     * Get the maximum # of bytes per request
     *
     * @return size
     */
    abstract public long getBinaryWriteLimit();

    /**
     * Get the servlet name (with no leading or trailing slashes)
     *
     * @return name
     */
    abstract public String getServletID();

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

    public void init()
            throws ServletException
    {
        org.slf4j.Logger logServerStartup = org.slf4j.LoggerFactory.getLogger("serverStartup");
        logServerStartup.info(getClass().getName() + " initialization start");
        try {
            System.setProperty("file.encoding", "UTF-8");
            Field charset = Charset.class.getDeclaredField("defaultCharset");
            charset.setAccessible(true);
            charset.set(null, null);
            initialize();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    /**
     * Initialize servlet/controller
     */
    public void
    initialize()
    {
        this.initialized = true;
    }

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

    //////////////////////////////////////////////////////////
    // Primary Controller Entry Point

    public void
    handleRequest(HttpServletRequest req, HttpServletResponse res)
            throws IOException
    {
        DapLog.debug("doGet(): User-Agent = " + req.getHeader("User-Agent"));
        if(!this.initialized) initialize();
        DapRequest daprequest = getRequestState(req, res);
        String url = daprequest.getOriginalURL();
        StringBuilder info = new StringBuilder("doGet():");
        info.append(" dataset = ");
        info.append(" url = ");
        info.append(url);
        if(DEBUG) {
            System.err.println("DAP4 Servlet: processing url: " + daprequest.getOriginalURL());
        }
        DapContext dapcxt = new DapContext();
        // Add entries to the context
        dapcxt.put(HttpServletRequest.class, req);
        dapcxt.put(HttpServletResponse.class, res);
        dapcxt.put(DapRequest.class, daprequest);

        ByteOrder order = daprequest.getOrder();
        ChecksumMode checksummode = daprequest.getChecksumMode();
        dapcxt.put(Dap4Util.DAP4ENDIANTAG, order);
        dapcxt.put(Dap4Util.DAP4CSUMTAG, checksummode);
        // Transfer all other queries
        Map<String, String> queries = daprequest.getQueries();
        for(Map.Entry<String, String> entry : queries.entrySet()) {
            if(dapcxt.get(entry.getKey()) == null) {
                dapcxt.put(entry.getKey(), entry.getValue());
            }
        }

        if(url.endsWith(FAVICON)) {
            doFavicon(FAVICON, dapcxt);
            return;
        }

        String datasetpath = DapUtil.nullify(DapUtil.canonicalpath(daprequest.getDataset()));
        try {
            if(datasetpath == null) {
                // This is the case where a request was made without a dataset;
                // According to the spec, I think we should return the
                // services/capabilities document
                doCapabilities(daprequest, dapcxt);
            } else {
                RequestMode mode = daprequest.getMode();
                if(mode == null)
                    throw new DapException("Unrecognized request extension")
                            .setCode(HttpServletResponse.SC_BAD_REQUEST);
                switch (mode) {
                case DMR:
                    doDMR(daprequest, dapcxt);
                    break;
                case DAP:
                    doData(daprequest, dapcxt);
                    break;
                case DSR:
                    doDSR(daprequest, dapcxt);
                    break;
                default:
                    throw new DapException("Unrecognized request extension")
                            .setCode(HttpServletResponse.SC_BAD_REQUEST);
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
            int code = HttpServletResponse.SC_BAD_REQUEST;
            if(t instanceof DapException) {
                DapException e = (DapException) t;
                code = e.getCode();
                if(code <= 0)
                    code = DapCodes.SC_BAD_REQUEST;
                e.setCode(code);
            } else if(t instanceof FileNotFoundException)
                code = DapCodes.SC_NOT_FOUND;
            else if(t instanceof UnsupportedOperationException)
                code = DapCodes.SC_FORBIDDEN;
            else if(t instanceof MalformedURLException)
                code = DapCodes.SC_NOT_FOUND;
            else if(t instanceof IOException)
                code = DapCodes.SC_BAD_REQUEST;
            else
                code = DapCodes.SC_INTERNAL_SERVER_ERROR;
            senderror(daprequest, code, t);
        }//catch
    }

    //////////////////////////////////////////////////////////
    // Extension processors

    /**
     * Process a DSR request.
     * * @param cxt     The dap context
     */

    protected void
    doDSR(DapRequest drq, DapContext cxt)
            throws IOException
    {
        try {
            DapDSR dsrbuilder = new DapDSR();
            String dsr = dsrbuilder.generate(drq.getURL());
            OutputStream out = drq.getOutputStream();
            addCommonHeaders(drq);// Add relevant headers
            // Wrap the outputstream with a Chunk writer
            ByteOrder order = (ByteOrder) cxt.get(Dap4Util.DAP4ENDIANTAG);
            ChunkWriter cw = new ChunkWriter(out, RequestMode.DSR, order);
            cw.writeDSR(dsr);
            cw.close();
        } catch (IOException ioe) {
            throw new DapException("DSR generation error", ioe)
                    .setCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Process a DMR request.
     *
     * @param cxt The dap context
     */

    protected void
    doDMR(DapRequest drq, DapContext cxt)
            throws IOException
    {
        // Convert the url to an absolute path
        String realpath = getResourcePath(drq, drq.getDatasetPath());

        DSP dsp = DapCache.open(realpath, cxt);
        DapDataset dmr = dsp.getDMR();

        /* Annotate with our endianness */
        ByteOrder order = (ByteOrder) cxt.get(Dap4Util.DAP4ENDIANTAG);
        setEndianness(dmr, order);

        // Process any constraint view
        CEConstraint ce = null;
        String sce = drq.queryLookup(DapProtocol.CONSTRAINTTAG);
        ce = CEConstraint.compile(sce, dmr);
        setConstraint(dmr, ce);

        // Provide a PrintWriter for capturing the DMR.
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);

        // Get the DMR as a string
        DMRPrinter dapprinter = new DMRPrinter(dmr, ce, pw, drq.getFormat());
        if(cxt.get(Dap4Util.DAP4TESTTAG) != null)
            dapprinter.testprint();
        else
            dapprinter.print();
        pw.close();
        sw.close();

        String sdmr = sw.toString();
        if(DEBUG)
            System.err.println("Sending: DMR:\n" + sdmr);

        addCommonHeaders(drq);// Add relevant headers

        // Wrap the outputstream with a Chunk writer
        OutputStream out = drq.getOutputStream();
        ChunkWriter cw = new ChunkWriter(out, RequestMode.DMR, order);
        cw.cacheDMR(sdmr);
        cw.close();
    }

    /**
     * Process a DataDMR request.
     * Note that if this throws an exception,
     * then it has not yet started to output
     * a response. It a response had been initiated,
     * then the exception would produce an error chunk.
     * <p>
     * * @param cxt     The dap context
     */

    protected void
    doData(DapRequest drq, DapContext cxt)
            throws IOException
    {
        // Convert the url to an absolute path
        String realpath = getResourcePath(drq, drq.getDatasetPath());

        DSP dsp = DapCache.open(realpath, cxt);
        if(dsp == null)
            throw new DapException("No such file: " + drq.getResourceRoot());
        DapDataset dmr = dsp.getDMR();
        if(DUMPDMR) {
            printDMR(dmr);
            System.err.println(printDMR(dmr));
            System.err.flush();
        }

        /* Annotate with our endianness */
        ByteOrder order = (ByteOrder) cxt.get(Dap4Util.DAP4ENDIANTAG);
        setEndianness(dmr, order);

        // Process any constraint
        CEConstraint ce = null;
        String sce = drq.queryLookup(DapProtocol.CONSTRAINTTAG);
        ce = CEConstraint.compile(sce, dmr);
        setConstraint(dmr, ce);

        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);

        // Get the DMR as a string
        DMRPrinter dapprinter = new DMRPrinter(dmr, ce, pw, drq.getFormat());
        dapprinter.print();
        pw.close();
        sw.close();

        String sdmr = sw.toString();
        if(DEBUG || DUMPDMR)
            System.err.println("Sending: Data DMR:\n" + sdmr);

        // Wrap the outputstream with a Chunk writer
        OutputStream out = drq.getOutputStream();
        ChunkWriter cw = new ChunkWriter(out, RequestMode.DAP, order);
        cw.setWriteLimit(getBinaryWriteLimit());
        cw.cacheDMR(sdmr);
        cw.flush();

        addCommonHeaders(drq);

        // Dump the databuffer part
        switch (drq.getFormat()) {
        case TEXT:
        case XML:
        case HTML:
            throw new IOException("Unsupported return format: " + drq.getFormat());
            /*
            sw = new StringWriter();
            DAPPrint dp = new DAPPrint(sw);
            dp.print(dsp.getDataset(), ce);
            break;
                */
        case NONE:
        default:
            DapSerializer writer = new DapSerializer(dsp, ce, cw, order, drq.getChecksumMode());
            writer.write(dsp.getDMR());
            cw.flush();
            cw.close();
            break;
        }
        // Should we dump data?
        if(DUMPDATA) {
            byte[] data = cw.getDump();
            if(data != null)
                DapDump.dumpbytestream(data, cw.getWriteOrder(), "ChunkWriter.write");
        }
    }

    //////////////////////////////////////////////////////////
    // Utility Methods

    protected void
    addCommonHeaders(DapRequest drq)
            throws IOException
    {
        // Add relevant headers
        ResponseFormat format = drq.getFormat();
        if(format == null)
            format = ResponseFormat.NONE;
        DapProtocol.ContentType contentheaders = DapProtocol.contenttypes.get(drq.getMode());
        String header = contentheaders.getFormat(format);
        if(header != null) {
            header = header + "; charset=utf-8";
            drq.setResponseHeader("Content-Type", header);
        } else
            DapLog.error("Cannot determine response Content-Type");

        // Not sure what this should be yet
        //setHeader("Content-Description","?");

        // Again, not sure what value to use
        //setHeader("Content-Disposition","?");

        //not legal drq.setResponseHeader("Content-Encoding", IS_BIG_ENDIAN ? BIG_ENDIAN : LITTLE_ENDIAN);
    }

    /**
     * Merge the servlet inputs into a single object
     * for easier transport as well as adding value.
     *
     * @param rq  A Servlet request object
     * @param rsp A Servlet response object
     * @return the union of the
     * servlet request and servlet response arguments
     * from the servlet engine.
     */

    protected DapRequest
    getRequestState(HttpServletRequest rq, HttpServletResponse rsp)
            throws IOException
    {
        return new DapRequest(this, rq, rsp);
    }

    //////////////////////////////////////////////////////////
    // Error Methods

    /* Note that these error returns are assumed to be before
       any DAP4 response has been generated. So they will
       set the header return code and an Error Response as body.
       Error chunks are handled elsewhere.
     */

    /**
     * Generate an error based on the parameters
     *
     * @param drq      DapRequest
     * @param httpcode 0=>no code specified
     * @param t        exception that caused the error; may be null
     * @throws IOException
     */
    protected void
    senderror(DapRequest drq, int httpcode, Throwable t)
            throws IOException
    {
        if(httpcode == 0) httpcode = HttpServletResponse.SC_BAD_REQUEST;
        ErrorResponse err = new ErrorResponse();
        err.setCode(httpcode);
        if(t == null) {
            err.setMessage("Servlet error: " + drq.getURL());
        } else {
            StringWriter sw = new StringWriter();
            PrintWriter p = new PrintWriter(sw);
            t.printStackTrace(p);
            p.close();
            sw.close();
            err.setMessage(sw.toString());
        }
        err.setContext(drq.getURL());
        String errormsg = err.buildXML();
        drq.getResponse().sendError(httpcode, errormsg);
    }

    /**
     * Set special attribute: endianness : overwrite exiting value
     *
     * @param dmr
     * @param order
     * @throws DapException
     */
    void
    setEndianness(DapDataset dmr, ByteOrder order)
            throws DapException
    {
        DapAttribute a = dmr.findAttribute(DapUtil.LITTLEENDIANATTRNAME);
        if(a == null) {
            a = new DapAttribute(DapUtil.LITTLEENDIANATTRNAME, DapType.UINT8);
            dmr.addAttribute(a);
        }
        //ByteOrder order = (ByteOrder) cxt.get(Dap4Util.DAP4ENDIANTAG);
        String oz = (order == ByteOrder.BIG_ENDIAN ? "0" : "1");
        a.setValues(new String[]{oz});
    }

    /**
     * Set special attribute: constraint : overwrite exiting value
     *
     * @param dmr dmr to annotate
     * @param ce  the new constraint
     * @throws DapException
     */
    void
    setConstraint(DapDataset dmr, CEConstraint ce)
            throws DapException
    {
        if(ce == null) return;
        if(ce.isUniversal()) return;
        DapAttribute a = dmr.findAttribute(DapUtil.CEATTRNAME);
        if(a == null) {
            a = new DapAttribute(DapUtil.CEATTRNAME, DapType.STRING);
            dmr.addAttribute(a);
        }
        String sce = ce.toConstraintString();
        a.setValues(new String[]{sce});
    }

    static public String
    printDMR(DapDataset dmr)
    {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        DMRPrinter printer = new DMRPrinter(dmr, pw);
        try {
            printer.print();
            pw.close();
            sw.close();
        } catch (IOException e) {
        }
        return sw.toString();
    }

}


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

import java.awt.*;
import java.awt.event.*;
import java.io.Serializable;

/**
 * Implements XOR rubberbanding.
 * @author David M. Geary
 * @author John Caron
 */
abstract public class Rubberband implements Serializable {
  protected Point anchorPt    = new Point(0,0);
  protected Point stretchedPt = new Point(0,0);
  protected Point lastPt      = new Point(0,0);
  protected Point endPt       = new Point(0,0);

  protected Component component;
  protected boolean   firstStretch = true;
  private boolean   active = false;

  // actual drawing done by subclass.
  abstract public void drawLast(Graphics2D g);
  abstract public void drawNext(Graphics2D g);

  /**
   * Constructor. use if you want Rubberband to do the event listening.
   * @param c draw on top of this Component.
   */
  public Rubberband(Component c, boolean listen) {
     component = c;
     if (listen) setListeners();
  }

  /**
   * Set whether its in active mode. In active mode it listens for mouse drags and XOR draws.
   * @param b true if in active mode.
   */
  public void setActive(boolean b) { active = b; }
  public boolean isActive    () { return active;      }

  private void setListeners() {
    component.addMouseListener(new MouseAdapter() {
       public void mousePressed(MouseEvent event) {
        anchor(event.getPoint());
        setActive(true);
      }
      public void mouseReleased(MouseEvent event) {
        if (isActive()) end(event.getPoint());
        setActive(false);
      }
    });

    component.addMouseMotionListener( new MouseMotionAdapter() {
      public void mouseDragged(MouseEvent event) {
        if (isActive()) stretch(event.getPoint());
      }
    });
  }

  public Point   getAnchor   () { return anchorPt;    }
  public Point   getStretched() { return stretchedPt; }
  public Point   getLast     () { return lastPt;      }
  public Point   getEnd      () { return endPt;       }

  /**
   * Set the anchor point.
   */
  public boolean anchor(Point p) {
    firstStretch = true;
    anchorPt.x = p.x;
    anchorPt.y = p.y;

    stretchedPt.x = lastPt.x = anchorPt.x;
    stretchedPt.y = lastPt.y = anchorPt.y;

    return true;
  }

  /**
   * Erase the last rectangle and draw a new one from the anchor point to this point.
   */
  public void stretch(Point p) {
    lastPt.x      = stretchedPt.x;
    lastPt.y      = stretchedPt.y;
    stretchedPt.x = p.x;
    stretchedPt.y = p.y;

    Graphics2D g = (Graphics2D) component.getGraphics();
    if(g != null) {
     try {
       g.setXORMode(component.getBackground());
       if(firstStretch == true)
         firstStretch = false;
       else
         drawLast(g);
       drawNext(g);
     }
     finally {
       g.dispose();
     } // try
    } // if
  }

  /**
   * Last point, done with drawing.
   */
  public void end(Point p) {
    lastPt.x = endPt.x = p.x;
    lastPt.y = endPt.y = p.y;

    Graphics2D g = (Graphics2D) component.getGraphics();
    if(g != null) {
     try {
       g.setXORMode(component.getBackground());
       drawLast(g);
     }
     finally {
       g.dispose();
     }
    }
  }

  /**
   * Last point, done with drawing.
   */
  public void done() {
    Graphics2D g = (Graphics2D) component.getGraphics();
    if(g != null) {
     try {
       g.setXORMode(component.getBackground());
       drawLast(g);
     }
     finally {
       g.dispose();
     }
    }
  }

  /** Get current Bounds */
  public Rectangle getBounds() {
    return new Rectangle(stretchedPt.x < anchorPt.x ?
                         stretchedPt.x : anchorPt.x,
                         stretchedPt.y < anchorPt.y ?
                         stretchedPt.y : anchorPt.y,
                         Math.abs(stretchedPt.x - anchorPt.x),
                         Math.abs(stretchedPt.y - anchorPt.y));
  }

  /** Get previous Bounds */
  public Rectangle lastBounds() {
    return new Rectangle(
                lastPt.x < anchorPt.x ? lastPt.x : anchorPt.x,
                lastPt.y < anchorPt.y ? lastPt.y : anchorPt.y,
                Math.abs(lastPt.x - anchorPt.x),
                Math.abs(lastPt.y - anchorPt.y));
  }
}


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

package ucar.nc2.units;

import java.util.Date;

/**
 * Date parsing and formatting. Always uses GMT.
 * These are not thread-safe.
 * These use java.util.Date and java.text.SimpleDateFormat.
 *
 * @author caron
 */
public class DateFormatter {

  private java.text.SimpleDateFormat isoDateTimeFormat, isoDateNoSecsFormat, stdDateTimeFormat, stdDateNoSecsFormat, dateOnlyFormat;

  private void isoDateTimeFormat() {
    if (isoDateTimeFormat != null) return;
    isoDateTimeFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    isoDateTimeFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  }

  private void isoDateNoSecsFormat() {
    if (isoDateNoSecsFormat != null) return;
    isoDateNoSecsFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
    isoDateNoSecsFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  }

  private void stdDateTimeFormat() {
    if (stdDateTimeFormat != null) return;
    stdDateTimeFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    stdDateTimeFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  }

  private void stdDateNoSecsFormat() {
    if (stdDateNoSecsFormat != null) return;
    stdDateNoSecsFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm");
    stdDateNoSecsFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT")); // same as UTC
  }

  private void dateOnlyFormat() {
    if (dateOnlyFormat != null) return;
    dateOnlyFormat = new java.text.SimpleDateFormat("yyyy-MM-dd");
    dateOnlyFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  }

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

  /**
   * Parse the text in W3C profile of ISO 8601 format.
   * @param text parse this text
   * @return equivalent Date or null if failure
   * @see W3C profile of ISO 8601
   *
   * @deprecated As of netCDF-JAVA 4.3.10. Use {@link ucar.nc2.time.CalendarDateFormatter#isoStringToDate(String)  } instead
   *
   */
  @Deprecated
  public Date getISODate(String text) {
    Date result;

    // try "yyyy-MM-dd HH:mm:ss"
    try {
      result = stdDateTimeFormat(text);
      return result;
    } catch (java.text.ParseException e) {
    }

    // now try  "yyyy-MM-dd'T'HH:mm:ss"
    try {
      result = isoDateTimeFormat(text);
      return result;
    } catch (java.text.ParseException e) {
    }

    // now try "yyyy-MM-dd'T'HH:mm"
    try {
      result = isoDateNoSecsFormat(text);
      return result;
    } catch (java.text.ParseException e) {
    }

    // now try "yyyy-MM-dd HH:mm"
    try {
      result = stdDateNoSecsFormat(text);
      return result;
    } catch (java.text.ParseException e) {
    }

    // now try "yyyy-MM-dd"
    try {
      result = dateOnlyFormat(text);
      return result;
    } catch (java.text.ParseException e) {
    }

    return null;
  }

  /**
   * Parse text in the format "yyyy-MM-dd HH:mm:ss"
   * @param text parse this text
   * @return equivalent Date
   * @throws java.text.ParseException if not formatted correctly
   */
  private Date stdDateTimeFormat(String text) throws java.text.ParseException {
    text = (text == null) ? "" : text.trim();
    stdDateTimeFormat();
    return stdDateTimeFormat.parse(text);
  }

  /**
   * Parse text in the format "yyyy-MM-dd HH:mm"
   * @param text parse this text
   * @return equivalent Date
   * @throws java.text.ParseException if not formatted correctly
   */
  private Date stdDateNoSecsFormat(String text) throws java.text.ParseException {
    text = (text == null) ? "" : text.trim();
    stdDateNoSecsFormat();
    return stdDateNoSecsFormat.parse(text);
  }

  /**
   * Parse text in the format "yyyy-MM-dd'T'HH:mm:ss"
   * @param text parse this text
   * @return equivalent Date
   * @throws java.text.ParseException if not formatted correctly
   */
  private Date isoDateTimeFormat(String text) throws java.text.ParseException {
    text = (text == null) ? "" : text.trim();
    isoDateTimeFormat();
    return isoDateTimeFormat.parse(text);
  }

  /**
   * Parse text in the format "yyyy-MM-dd'T'HH:mm"
   * @param text parse this text
   * @return equivalent Date
   * @throws java.text.ParseException if not formatted correctly
   */
  private Date isoDateNoSecsFormat(String text) throws java.text.ParseException {
    text = (text == null) ? "" : text.trim();
    isoDateNoSecsFormat();
    return isoDateNoSecsFormat.parse(text);
  }

  /**
   * Parse text in the format "yyyy-MM-dd"
   * @param text parse this text
   * @return equivalent Date
   * @throws java.text.ParseException if not formatted correctly
   */
  private Date dateOnlyFormat(String text) throws java.text.ParseException {
    text = (text == null) ? "" : text.trim();
    dateOnlyFormat();
    return dateOnlyFormat.parse(text);
  }

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

  /** Return standard GMT date format; show date only, not time. Format = "yyyy-MM-dd"
   * @deprecated use toDateOnlyString */
  public String getStandardDateOnlyString(Date date) {
     return toDateOnlyString(date);
   }

  /** date only format= yyyy-MM-dd
   * @deprecated use toDateOnlyString */
  public String toDateString( Date date) {
    return toDateOnlyString( date);
  }

  /** date only format= yyyy-MM-dd
   * @param date format this date
   * @return date formatted as date only
   */
  public String toDateOnlyString( Date date) {
    dateOnlyFormat();
    return dateOnlyFormat.format( date);
  }

  /** Return standard formatted GMT date and time String. Format = "yyyy-MM-dd HH:mm:ss'Z'"
   *  @deprecated use toDateTimeString
   */
   public String getStandardDateString2(Date date) {
     return toDateTimeString(date);
   }

  /** "standard date format" = yyyy-MM-dd HH:mm:ssZ
   * @param date format this date
   * @return date formatted as date/time
   */
  public String toDateTimeString( Date date) {
    if (date == null) return "Unknown";
    stdDateTimeFormat();
    return stdDateTimeFormat.format( date) +"Z";
  }

  /** Return standard formatted GMT date and time String. Format = "yyyy-MM-dd'T'HH:mm:ss'Z'"
   *  @deprecated use toDateTimeStringISO
   */
   public String getStandardDateString(Date date) {
     return toDateTimeStringISO(date);
   }

  /** "ISO date format" = yyyy-MM-dd'T'HH:mm:ssZ
   *  @param date format this date
   * @return date formatted as ISO date string
   */
  public String toDateTimeStringISO( Date date) {
    isoDateTimeFormat();
    return isoDateTimeFormat.format( date) +"Z";
  }

  private static void test(String text) {
    DateFormatter formatter = new DateFormatter();
    Date date = formatter.getISODate(text);
    String text2 = formatter.toDateTimeStringISO( date);
    Date date2 = formatter.getISODate(text2);
    assert date.equals(date2);
    System.out.println(text+" == "+text2);
  }

  /** test */
  public static void main(String args[]) {
    test("2001-09-11T12:09:20");
    test("2001-09-11 12:10:12");
    test("2001-09-11T12:10");
    test("2001-09-11 12:01");
    test("2001-09-11");
  }

}


/*
 * 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.util.ArrayList;
import java.util.Collection;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ucar.ma2.Array;
import ucar.ma2.ArrayChar;
import ucar.ma2.ArraySequence;
import ucar.ma2.ArrayStructure;
import ucar.ma2.ArrayStructureMA;
import ucar.ma2.ArrayStructureW;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Section;
import ucar.ma2.StructureData;
import ucar.ma2.StructureDataIterator;
import ucar.ma2.StructureDataIteratorMediated;
import ucar.ma2.StructureDataMediator;
import ucar.ma2.StructureDataProxy;
import ucar.ma2.StructureDataW;
import ucar.ma2.StructureMembers;
import ucar.nc2.*;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.StructureDS;
import ucar.nc2.dataset.StructurePseudo2Dim;
import ucar.nc2.dataset.StructurePseudoDS;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.ft.point.StructureDataIteratorIndexed;
import ucar.nc2.ft.point.StructureDataIteratorLinked;

/**
 * A generalization of a Structure. Main function is to return a StructureDataIterator,
 * iterating over its table rows
 *
 * @author caron
 * @since Jan 20, 2009
 */
public abstract class Table {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Table.class);

  public enum CoordName {
    Lat, Lon, Elev, Time, TimeNominal, StnId, StnDesc, WmoId, StnAlt, FeatureId, MissingVar
  }

  public enum Type {
    ArrayStructure, Construct, Contiguous, LinkedList,
    MultidimInner, MultidimInner3D, MultidimInnerPsuedo, MultidimInnerPsuedo3D, MultidimStructure,
    NestedStructure, ParentId, ParentIndex, Singleton, Structure, Top
  }

  public static Table factory(NetcdfDataset ds, TableConfig config) {

    switch (config.type) {

      case ArrayStructure: // given array of StructureData, stored in config.as
        return new TableArrayStructure(ds, config);

      case Construct: // construct the table from its children - theres no seperate station table, stn info is duplicated in the obs structure.
        return new TableConstruct(ds, config);

      case Contiguous: // contiguous list of child record, using indexes
        return new TableContiguous(ds, config);

      case LinkedList: // linked list of child records, using indexes
        return new TableLinkedList(ds, config);

      case MultidimInner: // the inner struct of a 2D multdim(outer, inner) with unlimited dimension
        return new TableMultidimInner(ds, config);

      case MultidimInner3D: // the inner struct of a 3D multdim(outer, middle, inner) with unlimited dimension
        return new TableMultidimInner3D(ds, config);

      case MultidimStructure: // the outer struct of a multidim structure
        return new TableMultidimStructure(ds, config);

      case MultidimInnerPsuedo: // the inner struct of a 2D multdim(outer, inner) without the unlimited dimension
        // the middle struct of a 3D multdim(outer, middle, inner) without the unlimited dimension
        return new TableMultidimInnerPsuedo(ds, config);

      case MultidimInnerPsuedo3D: // the inner struct of a 3D multdim(outer, middle, inner) without the unlimited dimension
        return new TableMultidimInnerPsuedo3D(ds, config);

      case NestedStructure: // Structure or Sequence is nested in the parent
        return new TableNestedStructure(ds, config);

      case ParentId: // child record has an id for the parent.
        return new TableParentId(ds, config);

      case ParentIndex: // child record has the record index of the parent.
        return new TableParentIndex(ds, config);

      case Singleton: // singleton row, with given StructureData
        return new TableSingleton(ds, config);

      case Structure: // Structure or PsuedoStructure
        return new TableStructure(ds, config);

      case Top: // singleton consisting of top variables and constants
        return new TableTop(ds, config);
    }

    throw new IllegalStateException("Unimplemented Table type = " + config.type);
  }

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

  String name;
  FeatureType featureType;

  Table parent, child;
  List<Join> extraJoins;

  String lat, lon, elev, time, timeNominal;
  String stnId, stnDesc, stnNpts, stnWmoId, stnAlt, limit;
  String feature_id, missingVar;

  Map<String, VariableSimpleIF> cols = new HashMap<>();  // all variables
  Set<String> nondataVars = new HashSet<>();          // exclude these from the getDataVariables() list

  protected Table(NetcdfDataset ds, TableConfig config) {
    this.name = config.name;
    this.featureType = config.featureType;

    this.lat = config.lat;
    this.lon = config.lon;
    this.elev = config.elev;
    this.time = config.time;
    this.timeNominal = config.timeNominal;

    this.stnId = config.stnId;
    this.stnDesc = config.stnDesc;
    this.stnNpts = config.stnNpts;
    this.stnWmoId = config.stnWmoId;
    this.stnAlt = config.stnAlt;
    this.limit = config.limit;
    this.feature_id = config.feature_id;
    this.missingVar = config.missingVar;

    if (config.parent != null) {
      parent = Table.factory(ds, config.parent);
      parent.child = this;
    }

    this.extraJoins = config.extraJoin;

    // try to exclude coordinate vars and "structural data" from the list of data variables
    addNonDataVariable(config.time);
    addNonDataVariable(config.lat);
    addNonDataVariable(config.lon);
    addNonDataVariable(config.elev);
    addNonDataVariable(config.timeNominal);
    addNonDataVariable(config.stnId);
    addNonDataVariable(config.stnDesc);
    addNonDataVariable(config.stnWmoId);
    addNonDataVariable(config.stnAlt);
    addNonDataVariable(config.stnNpts);
    addNonDataVariable(config.limit);
    addNonDataVariable(config.feature_id);

    addNonDataVariable(config.parentIndex);
    addNonDataVariable(config.start);
    addNonDataVariable(config.next);
    addNonDataVariable(config.numRecords);
  }

  protected void addNonDataVariable(String name) {
    if (name != null)
      nondataVars.add(name);
  }

  // change shape of the data variables
  protected void replaceDataVars(StructureMembers sm) {
    for (StructureMembers.Member m : sm.getMembers()) {
      VariableSimpleIF org = this.cols.get(m.getName());
      int rank = org.getRank();
      List<Dimension> orgDims = org.getDimensions();
      // only keep the last n
      int n = m.getShape().length;
      List<Dimension> dims = orgDims.subList(rank-n, rank);
      VariableSimpleImpl result = new VariableSimpleImpl(org.getShortName(), org.getDescription(), org.getUnitsString(), org.getDataType(), dims);
      for (Attribute att : org.getAttributes()) result.add(att);
      this.cols.put(m.getName(), result);
    }
  }

  /**
   * Iterate over the rows of this table. Subclasses must implement this.
   *
   * @param cursor     state of comlpete iteration. Table implementations may not modify.
   * @return iterater over the rows of this table.
   * @throws IOException on read error
   */
  abstract public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException;

  String findCoordinateVariableName(CoordName coordName) {
    switch (coordName) {
      case Elev:
        return elev;
      case Lat:
        return lat;
      case Lon:
        return lon;
      case Time:
        return time;
      case TimeNominal:
        return timeNominal;

      case StnId:
        return stnId;
      case StnDesc:
        return stnDesc;
      case WmoId:
        return stnWmoId;
      case StnAlt:
        return stnAlt;

      case FeatureId:
        return feature_id;

      case MissingVar:
        return missingVar;

    }
    return null;
  }

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

  /**
   * A Structure, PsuedoStructure, or Sequence.
   *

   * Structure: defined by config.structName.
   * if config.vars if not null restricts to list of vars, must be members.
   *

   * PsuedoStructure: defined by variables with outer dimension = config.dim
   * So we find all Variables with signature v(outDim, ...) and make them into
   *

   * Structure {
   *   v1(...);
   *   v2(...);
   * } s
   *

   * config.vars if not null restricts to list of vars, must be members.
   */
  public static class TableStructure extends Table {
    StructureDS struct;
    Dimension dim, outer;
    TableConfig.StructureType stype;

    TableStructure(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.stype = config.structureType;

      switch (config.structureType) {

        case Structure:
          struct = (StructureDS) ds.findVariable(config.structName);
          if (struct == null)
            throw new IllegalStateException("Cant find Structure " + config.structName);

          dim = struct.getDimension(0);

          if (config.vars != null)
            struct = (StructureDS) struct.select(config.vars); // limit to list of vars
          break;

        case PsuedoStructure:
          this.dim = ds.findDimension(config.dimName);
          assert dim != null;
          String name = config.structName == null ? "anon" : config.structName;
          struct = new StructurePseudoDS(ds, dim.getGroup(), name, config.vars, this.dim);
          break;

        case PsuedoStructure2D:
          this.dim = ds.findDimension(config.dimName);
          this.outer = ds.findDimension(config.outerName);
          assert dim != null;
          assert config.outerName != null;
          struct = new StructurePseudo2Dim(ds, dim.getGroup(), config.structName, config.vars, this.dim, this.outer);
          break;
      }

      config.vars = new ArrayList<>();
      for (Variable v : struct.getVariables()) {
        // remove substructures
        if (v.getDataType() == DataType.STRUCTURE) {
          if (config.structureType == TableConfig.StructureType.PsuedoStructure)
            struct.removeMemberVariable(v);
        } else {
          this.cols.put(v.getShortName(), v);
          config.vars.add(v.getShortName());
        }
      }
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sstruct=%s, dim=%s type=%s%n", indent, struct.getNameAndDimensions(), dim.getShortName(), struct.getClass().getName());
    }

    @Override
    public VariableDS findVariable(String axisName) {
      String structPrefix = struct.getShortName() + ".";
      if (axisName.startsWith(structPrefix))
        axisName = axisName.substring(structPrefix.length());
      return (VariableDS) struct.findVariable(axisName);
    }

    @Override
    public String showDimension() {
      return dim.getShortName();
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      return new StructureDataIteratorMediated(struct.getStructureIterator(), new RestrictToColumns());
    }

    @Override
    public String getName() {
      return stype.toString() + "(" + struct.getShortName() + ")";
    }
  }

  private class RestrictToColumns implements StructureDataMediator {
    StructureMembers members;

    @Override
    public StructureData modify(StructureData sdata) {
      // make members restricted to column names
      if (members == null) {
        StructureMembers orgMembers = sdata.getStructureMembers();
        members = new StructureMembers(orgMembers.getName() + "RestrictToColumns");
        for (String colName : cols.keySet()) {
          StructureMembers.Member m = orgMembers.findMember(colName);
          if (m == null)
            throw new IllegalStateException("Cant find " + colName);
          members.addMember(m);
        }
      }
      return new StructureDataProxy(members, sdata);
    }
  }

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

  /**
   * ArrayStructure is passed in config.as
   * Used by
   * UnidataPointFeature: type StationProfile  (removed now)
   */
  public static class TableArrayStructure extends Table {
    ArrayStructure as;
    Dimension dim;

    TableArrayStructure(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      assert (config.as != null);
      this.as = config.as;
      this.dim = new Dimension(config.structName, (int) config.as.getSize(), false);

      for (StructureMembers.Member m : config.as.getStructureMembers().getMembers())
        cols.put(m.getName(), new VariableSimpleAdapter(m));
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sArrayStruct=%s, dim=%s%n", indent, new Section(as.getShape()), dim.getShortName());
    }

    @Override
    public String showDimension() {
      return dim.getShortName();
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      return as.getStructureDataIterator();
    }

    @Override
    public String getName() {
      return "ArrayStructure(" + name + ")";
    }
  }

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

  /**
   * When theres no seperate station table, but info is duplicated in the obs structure.
   * Must have a ParentId child table
   * No variables are added to cols.
   *

   * Used by:
   * BufrCdm StationProfile type
   */
  public static class TableConstruct extends Table {
    ArrayStructure as; // injected by TableParentId

    TableConstruct(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      return as.getStructureDataIterator();
    }

    @Override
    public String getName() {
      return "Constructed";
    }
  }

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

  /**
   * Contiguous children, using start and numRecords variables in the parent. This assumes column store.
   * TableContiguous is the children, config.struct describes the cols.
   *

   * Used by:
   * UnidataPointObs
   * CFPointObs
   */
  public static class TableContiguous extends TableStructure {
    private String startVarName; // variable name holding the starting index in parent
    private String numRecordsVarName; // variable name holding the number of children in parent
    private int[] startIndex, numRecords;
    private NetcdfDataset ds;
    private boolean isInit;

    TableContiguous(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.ds = ds;
      startVarName = config.getStart();
      numRecordsVarName = config.getNumRecords();

      addNonDataVariable(startVarName);
      addNonDataVariable(numRecordsVarName);
    }

    private void init() {
      if (startVarName == null) {  // read numRecords when startVar is not known  LOOK this should be deffered
        try {
          Variable v = ds.findVariable(numRecordsVarName);
          Array numRecords = v.read();
          int n = (int) numRecords.getSize();

          // construct the start variable
          this.numRecords = new int[n];
          this.startIndex = new int[n];
          int i = 0;
          int count = 0;
          while (numRecords.hasNext()) {
            this.startIndex[i] = count;
            this.numRecords[i] = numRecords.nextInt();
            count += this.numRecords[i];
            i++;
          }
          isInit = true;
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sstart=%s, numRecords=%s%n", indent, startVarName, numRecordsVarName);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      if (!isInit) init();

      int firstRecno, numrecs;
      StructureData parentStruct = cursor.getParentStructure();
      if (startIndex != null) {
        int parentIndex = cursor.getParentRecnum();
        firstRecno = startIndex[parentIndex];
        numrecs = numRecords[parentIndex];
      } else {
        firstRecno = parentStruct.getScalarInt(startVarName);
        numrecs = parentStruct.getScalarInt(numRecordsVarName);
      }
      return new StructureDataIteratorLinked(struct, firstRecno, numrecs, null);
    }

    @Override
    public String getName() {
      return "Contig(" + numRecordsVarName + ")";
    }
  }

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

  /**
   * The children have a field containing the index of the parent.
   * For efficiency, we scan this data and construct an IndexMap( parentIndex -> list of children),
   * i.e. we compute the inverse link, parent -> children.
   * TableParentIndex is the children, config.struct describes the cols.
   *

   * Used by:
   * CFPointObs
   */
  public static class TableParentIndex extends TableStructure {
    private Map<Integer, List<Integer>> indexMap;
    private String parentIndexName;

    TableParentIndex(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.parentIndexName = config.parentIndex;

      // construct the map
      try {
        Variable rpIndex = ds.findVariable(config.parentIndex);
        Array index = rpIndex.read();

        int childIndex = 0;
        this.indexMap = new HashMap<>((int) (2 * index.getSize()));
        while (index.hasNext()) {
          int parent = index.nextInt();
          List<Integer> list = indexMap.get(parent);
          if (list == null) {
            list = new ArrayList<>();
            indexMap.put(parent, list);
          }
          list.add(childIndex);
          childIndex++;
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }

      addNonDataVariable(config.parentIndex);
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sparentIndexName=%s, indexMap.size=%d%n", indent, parentIndexName, indexMap.size());
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      int parentIndex = cursor.getParentRecnum();
      List<Integer> index = indexMap.get(parentIndex);
      if (index == null) index = new ArrayList<>();
      return new StructureDataIteratorIndexed(struct, index);
    }

    @Override
    public String getName() {
      return "Indexed(" + parentIndexName + ")";
    }
  }

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

  /**
   * The children have a field containing the id of the parent.
   * For efficiency, we scan this data and construct an IndexMap( parentIndex -> list of children),
   * i.e. we compute the inverse link, parent -> children.
   * TableParentIndex is the children, config.struct describes the cols.
   *

   * Used by:
   * CFPointObs
   */
  public static class TableParentId extends TableStructure {
    private ParentInfo[] indexMap;
    private String parentIdName;

    TableParentId(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.parentIdName = config.parentIndex;

      // construct the hash of unique parents, based on the id variable
      Map<Object, ParentInfo> parentHash;
      try {
        Variable rpIndex = ds.findVariable(parentIdName);
        if (rpIndex == null)
          rpIndex = struct.findVariable(parentIdName);

        Array index = rpIndex.read();
        if (index instanceof ArrayChar)
          index = ((ArrayChar) index).make1DStringArray();

        parentHash = new HashMap<>((int) (2 * index.getSize()));

        int childIndex = 0;
        while (index.hasNext()) {
          Object parent = index.next();
          ParentInfo info = parentHash.get(parent);
          if (info == null) {
            info = new ParentInfo();
            parentHash.put(parent, info);
          }
          info.add(childIndex);
          childIndex++;
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }

      // construct the indexMap and the ArrayStructure containing StructureData for the parents
      Collection<ParentInfo> parents = parentHash.values();
      int n = parents.size();
      this.indexMap = new ParentInfo[n];
      StructureData[] parentData = new StructureData[n];
      int count = 0;
      for (ParentInfo info : parents) {
        this.indexMap[count] = info;
        parentData[count++] = info.sdata;
      }
      ArrayStructure as = new ArrayStructureW(struct.makeStructureMembers(), new int[]{n}, parentData);

      // find the parent TableConstruct and inject the ArrayStructure
      Table t = this;
      while (t.parent != null) {
        t = t.parent;
        if (t instanceof TableConstruct) {
          ((TableConstruct) t).as = as;
          break;
        }
      }
      addNonDataVariable(parentIdName);
    }

    private class ParentInfo {
      List<Integer> recnumList = new ArrayList<>();
      StructureData sdata;

      void add(int recnum) throws IOException {
        recnumList.add(recnum);
        if (sdata != null) return;
        try {
          sdata = struct.readStructure(recnum);
        } catch (ucar.ma2.InvalidRangeException e) {
          log.error("TableParentId read recno=" + recnum, e);
          throw new RuntimeException(e.getMessage());
        }
      }
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sparentIdName=%s, indexMap.size=%d%n", indent, parentIdName, indexMap.length);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      int parentIndex = cursor.getParentRecnum();
      ParentInfo info = indexMap[parentIndex];
      List<Integer> index = (info == null) ? new ArrayList<>() : info.recnumList;
      return new StructureDataIteratorIndexed(struct, index);
    }

    @Override
    public String getName() {
      return "ParentId(" + parentIdName + ")";
    }
  }

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

  /**
   * Linked list of children, using start variable in the parent, and next in the child.
   * TableLinkedList is the children, config.struct describes the cols.
   *

   * Used by:
   * UnidataPointObs
   */
  public static class TableLinkedList extends TableStructure {
    private String start; // variable name holding the starting index in parent
    private String next; // variable name holding the next index in child

    TableLinkedList(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.start = config.start;
      this.next = config.next;

      addNonDataVariable(config.start);
      addNonDataVariable(config.next);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      StructureData parentStruct = cursor.getParentStructure();
      int firstRecno = parentStruct.getScalarInt(start);
      return new StructureDataIteratorLinked(struct, firstRecno, -1, next);
    }

    @Override
    public String getName() {
      return "Linked(" + start + "->" + next + ")";
    }
  }

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

  // the inner struct of a 2D multdim(outer, inner) with unlimited dimension
  public static class TableMultidimInner extends Table {
    StructureMembers sm; // the inner structure members
    Dimension inner, outer;
    NetcdfDataset ds;

    TableMultidimInner(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.ds = ds;
      assert config.outerName != null;
      assert config.innerName != null;
      this.inner = ds.findDimension(config.innerName);
      this.outer = ds.findDimension(config.outerName);
      assert this.inner != null : config.innerName;
      assert this.outer != null : config.outerName;

      sm = new StructureMembers(config.name);
      if (config.vars != null) {
        for (String name : config.vars) {
          Variable v = ds.findVariable(name);
          if (v == null) continue;
          // cols.add(v);
          int rank = v.getRank();
          int[] shape = new int[rank - 2];
          System.arraycopy(v.getShape(), 2, shape, 0, rank - 2);
          sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
          this.cols.put(v.getShortName(), v);
        }

      } else {
        for (Variable v : ds.getVariables()) {
          if (v.getRank() < 2) continue;
          if (v.getDimension(0).equals(this.outer) && v.getDimension(1).equals(this.inner)) {
            // cols.add(v);
            int rank = v.getRank();
            int[] shape = new int[rank - 2];
            System.arraycopy(v.getShape(), 2, shape, 0, rank - 2);
            sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
            this.cols.put(v.getShortName(), v);
          }
        }
      }

      replaceDataVars(sm);
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sStructureMembers=%s, dim=%s,%s%n", indent, sm.getName(), outer.getShortName(), inner.getShortName());
    }

    @Override
    public String showDimension() {
      return inner.getShortName();
    }

    @Override
    public VariableDS findVariable(String axisName) {
      return (VariableDS) ds.findVariable(axisName);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      StructureData parentStruct = cursor.getParentStructure();
      if (parentStruct instanceof StructureDataProxy)
        parentStruct = ((StructureDataProxy) parentStruct).getOriginalStructureData(); // tricky dicky
      ArrayStructureMA asma = new ArrayStructureMA(sm, new int[]{inner.getLength()});
      for (String colName : cols.keySet()) {
        Array data = parentStruct.getArray(colName);
        StructureMembers.Member childm = sm.findMember(colName);
        childm.setDataArray(data);
      }
      return asma.getStructureDataIterator();
    }

    @Override
    public String getName() {
      return "Multidim(" + outer.getShortName() + "," + inner.getShortName() + ")";
    }
  }

  public static class TableMultidimInner3D extends Table {
    StructureMembers sm; // the inner structure members
    Dimension dim, inner, middle;
    NetcdfDataset ds;

    TableMultidimInner3D(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.ds = ds;
      assert config.dimName != null;
      assert config.outerName != null;
      assert config.innerName != null;
      this.dim = ds.findDimension(config.dimName);
      this.inner = ds.findDimension(config.innerName);
      this.middle = ds.findDimension(config.outerName);

      sm = new StructureMembers(config.name);
      if (config.vars != null) {
        for (String name : config.vars) {
          Variable v = ds.findVariable(name);
          if (v == null) continue;
          //cols.add(v);
          int rank = v.getRank();
          int[] shape = new int[rank - 3];
          System.arraycopy(v.getShape(), 3, shape, 0, rank - 3);
          sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
          this.cols.put(v.getShortName(), v);
        }

      } else {
        for (Variable v : ds.getVariables()) {
          if (v.getRank() < 3) continue;
          if (v.getDimension(0).equals(dim) && v.getDimension(1).equals(middle) && v.getDimension(2).equals(inner)) {
            //cols.add(v);
            int rank = v.getRank();
            int[] shape = new int[rank - 3];
            System.arraycopy(v.getShape(), 3, shape, 0, rank - 3);
            sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
            this.cols.put(v.getShortName(), v);
          }
        }
      }

      replaceDataVars(sm);
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sStructureMembers=%s, dim=%s%n", indent, sm.getName(), dim.getShortName());
    }

    @Override
    public String showDimension() {
      return dim.getShortName();
    }

    @Override
    public VariableDS findVariable(String axisName) {
      return (VariableDS) ds.findVariable(axisName);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      StructureData parentStruct = cursor.tableData[2];
      if (parentStruct instanceof StructureDataProxy)
        parentStruct = ((StructureDataProxy) parentStruct).getOriginalStructureData(); // tricky dicky
      int middleIndex = cursor.recnum[1];
      ArrayStructureMA asma = new ArrayStructureMA(sm, new int[]{inner.getLength()});
      for (String colName : cols.keySet()) {
        Array data = parentStruct.getArray(colName);
        Array myData = data.slice(0, middleIndex);
        StructureMembers.Member childm = sm.findMember(colName);
        childm.setDataArray(myData.copy());           // must make copy - ArrayStucture doesnt deal with logical views
      }
      return asma.getStructureDataIterator();
    }

    @Override
    public String getName() {
      return "Multidim(" + dim.getShortName() + "," + middle.getShortName() + "," + inner.getShortName() + ")";
    }

  }

  /**
   * Used for PsuedoStructure(station, time).
   * This is used for the inner table.
   * Need: config.inner, config.outer = config.dim, config.vars
   *

   * Used by:
   * CFpointObs
   */
  public static class TableMultidimInnerPsuedo extends Table.TableStructure {
    Dimension inner, outer;
    StructureMembers sm;

    TableMultidimInnerPsuedo(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      assert config.outerName != null;
      assert config.innerName != null;
      this.inner = ds.findDimension(config.innerName);
      this.outer = ds.findDimension(config.outerName);

      sm = new StructureMembers(config.name);
      for (Variable v : struct.getVariables()) {
        int rank = v.getRank();
        int[] shape = new int[rank - 1];
        System.arraycopy(v.getShape(), 1, shape, 0, rank - 1);
        sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
      }

      replaceDataVars(sm);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      int recnum = cursor.recnum[ cursor.currentIndex]; // LOOK
      try {
        StructureData parentStruct = struct.readStructure(recnum);
        ArrayStructureMA asma = new ArrayStructureMA(sm, new int[]{inner.getLength()});
        for (String colName : cols.keySet()) {
          Array data = parentStruct.getArray(colName);
          StructureMembers.Member childm = sm.findMember(colName);
          childm.setDataArray(data);
        }
        return asma.getStructureDataIterator();

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

    @Override
    public String getName() {
      return "MultidimPseudo(" + outer.getShortName() + "," + inner.getShortName() + ")";
    }

  }

  public static class TableMultidimInnerPsuedo3D extends Table.TableStructure {
    Dimension middle;
    Dimension inner;
    StructureMembers sm;

    TableMultidimInnerPsuedo3D(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      assert config.dimName != null;
      assert config.outerName != null; // middle
      assert config.innerName != null;
      this.dim = ds.findDimension(config.dimName);
      this.middle = ds.findDimension(config.outerName);
      this.inner = ds.findDimension(config.innerName);

      sm = new StructureMembers(config.name);
      for (Variable v : struct.getVariables()) {
        int rank = v.getRank();
        int[] shape = new int[rank - 1];
        System.arraycopy(v.getShape(), 1, shape, 0, rank - 1);
        sm.addMember(v.getShortName(), v.getDescription(), v.getUnitsString(), v.getDataType(), shape);
      }

      replaceDataVars(sm);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      int outerIndex = cursor.recnum[2];
      int middleIndex = cursor.recnum[1];
      try {
        Section s = new Section().appendRange(outerIndex, outerIndex).appendRange(middleIndex, middleIndex);
        ArrayStructure result = (ArrayStructure) struct.read(s);
        assert result.getSize() == 1;
        StructureData sdata = result.getStructureData(0); // should only be one
        ArrayStructureMA asma = new ArrayStructureMA(sm, new int[]{inner.getLength()});
        for (String colName : cols.keySet()) {
          Array data = sdata.getArray(colName);
          StructureMembers.Member childm = sm.findMember(colName);
          childm.setDataArray(data);
        }
        return asma.getStructureDataIterator();

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

    @Override
    public String getName() {
      return "MultidimPsuedo(" + dim.getShortName() + "," + middle.getShortName() + "," + inner.getShortName() + ")";
    }

  }

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

  /**
   * Used for Structure(station, time).
   * This is used for the inner table, where the station index gets set, and all the structures for that
   * styation are read in at once. Then we just iterate over that ArrayStructure.
   *

   * Used by:
   * GempakCdm
   */
  public static class TableMultidimStructure extends Table.TableStructure {

    TableMultidimStructure(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      int recnum = cursor.getParentRecnum();
      try {
        Section section = new Section().appendRange(recnum, recnum);
        int count = 1;
        while (count++ < struct.getRank()) // handles multidim case
          section.appendRange(null);
        ArrayStructure data = (ArrayStructure) struct.read(section); // read all the data for a fixed outer index
        return data.getStructureDataIterator();
      } catch (InvalidRangeException e) {
        throw new IllegalStateException(e);
      }
    }

    @Override
    public String getName() {
      return "MultidimStructure(" + struct.getFullName() + ")";
    }

  }

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

  /**
   * A Structure inside of a parent Structure.
   * Name of child member inside parent Structure is config.nestedTableName
   * obsTable.structName = obsStruct.getName();
   * obsTable.nestedTableName = obsStruct.getShortName();
   *

   * Used by:
   * BufrCdm
   */
  public static class TableNestedStructure extends Table {
    String nestedTableName; // short name of structure
    Structure struct;

    TableNestedStructure(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.nestedTableName = config.nestedTableName;
      struct = (Structure) ds.findVariable(config.structName);
      assert (struct != null) : config.structName;

      for (Variable v : struct.getVariables())
        cols.put(v.getShortName(), v);
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sstruct=%s, nestedTableName=%s%n", indent, struct.getNameAndDimensions(), nestedTableName);
    }

    @Override
    public VariableDS findVariable(String axisName) {
      return (VariableDS) struct.findVariable(axisName);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      StructureData parentStruct = cursor.getParentStructure();

      StructureMembers members = parentStruct.getStructureMembers();
      StructureMembers.Member m = members.findMember(nestedTableName);

      if (m.getDataType() == DataType.SEQUENCE) {
        ArraySequence seq = parentStruct.getArraySequence(m);
        return seq.getStructureDataIterator();
      } else if (m.getDataType() == DataType.STRUCTURE) {
        ArrayStructure as = parentStruct.getArrayStructure(m);
        return as.getStructureDataIterator();
      }

      throw new IllegalStateException("Cant find nested table member = " + nestedTableName);
    }

    @Override
    public String getName() {
      return "NestedStructure(" + nestedTableName + ")";
    }
  }

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

  /**
   * Table is a single StructureData, passed in as config.sdata.
   * Ok for sdata to be null
   *

   * Used by:
   * FslWindProfiler
   */
  public static class TableSingleton extends Table {
    StructureData sdata;

    TableSingleton(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.sdata = config.sdata;
      if (sdata == null) sdata = StructureData.EMPTY;

      for (StructureMembers.Member m : sdata.getStructureMembers().getMembers())
        cols.put(m.getName(), new VariableSimpleAdapter(m));
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
      f.format("%sStructureData=%s%n", indent, sdata);
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      return new SingletonStructureDataIterator(sdata);
    }

    @Override
    public String getName() {
      return "Singleton";
    }
  }

  /**
   * Table is a single StructureData, which is empty.
   * NestedTable looks for instance of this, and
   * 1) increments the nesting level
   * 2) looks for coordinate variables at the top level.
   *

   * Adds a table at top of the tree, consisting only of coordinate variables
   *

   * Used by:
   * CFpointObs
   * GempakCdm
   * Ndbc
   */
  public static class TableTop extends Table {
    NetcdfDataset ds;
    StructureDataTop sdata;

    TableTop(NetcdfDataset ds, TableConfig config) {
      super(ds, config);
      this.ds = ds;
    }

    @Override
    protected void showTableExtraInfo(String indent, Formatter f) {
    }

    @Override
    public StructureDataIterator getStructureDataIterator(Cursor cursor) throws IOException {
      // grab scalars, make sdata
      if (sdata == null) {
        sdata = new StructureDataTop();
        sdata.addVariableAsMember(ds, feature_id);
      }

      return new SingletonStructureDataIterator(sdata);
    }

    @Override
    public String getName() {
      return "TopScalars";
    }
  }

  private static class StructureDataTop extends StructureDataW {

    public StructureDataTop() {
      super(new StructureMembers("top"));
    }

    void addVariableAsMember(NetcdfDataset ds, String scalarVariableName) throws IOException {
      if (scalarVariableName == null) return;
      Variable v = ds.findVariable(scalarVariableName);
      if (v == null) return;
      StructureMembers.Member m = this.members.addMember(v.getFullName(), null, null, v.getDataType(), v.getShape());
      setMemberData(m, v.read());
    }
  }

  // not ok for sdata to be null
  private static class SingletonStructureDataIterator implements StructureDataIterator {
    private int count = 0;
    private StructureData sdata;

    SingletonStructureDataIterator(StructureData sdata) {
      this.sdata = sdata;
      assert sdata != null;
    }

    @Override
    public boolean hasNext() throws IOException {
      return (count == 0);
    }

    @Override
    public StructureData next() throws IOException {
      count++;
      return sdata;
    }

    @Override
    public StructureDataIterator reset() {
      count = 0;
      return this;
    }

    @Override
    public int getCurrentRecno() {
      return count - 1;
    }
  }

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

  public String getName() {
    return name;
  }

  public FeatureType getFeatureType() {
    return featureType;
  }

  // LOOK others should override
  public VariableDS findVariable(String axisName) {
    return null;
  }

  public String showDimension() {
    return "";
  }

  public String toString() {
    Formatter formatter = new Formatter();
    formatter.format(" Table %s on dimension %s type=%s%n", getName(), showDimension(), getClass().toString());
    formatter.format("  Coordinates=");
    formatter.format("%n  Data Variables= %d%n", cols.size());
    formatter.format("  Parent= %s%n", ((parent == null) ? "none" : parent.getName()));
    return formatter.toString();
  }

  public int show(Formatter f, int indent) {
    if (parent != null)
      indent = parent.show(f, indent);

    String s = indent(indent);
    String ftDesc = (featureType == null) ? "" : "featureType=" + featureType.toString();
    f.format("%n%sTable %s: type=%s %s%n", s, getName(), getClass().toString(), ftDesc);
    if (extraJoins != null) {
      f.format("  %sExtraJoins:%n", s);
      for (Join j : extraJoins)
        f.format("   %s  %s %n", s, j);
    }
    showTableExtraInfo(indent(indent + 2), f);
    showCoords(s, f);
    f.format("  %sVariables:%n", s);
    for (String colName : cols.keySet()) {
      f.format("   %s  %s %s%n", s, colName, getKind(colName));
    }
    return indent + 2;
  }

  String indent(int n) {
    StringBuilder sbuff = new StringBuilder();
    for (int i = 0; i < n; i++) sbuff.append(' ');
    return sbuff.toString();
  }

  protected abstract void showTableExtraInfo(String indent, Formatter f);

  String getKind(String v) {
    if (v.equals(lat)) return "[Lat]";
    if (v.equals(lon)) return "[Lon]";
    if (v.equals(elev)) return "[Elev]";
    if (v.equals(time)) return "[Time]";
    if (v.equals(timeNominal)) return "[timeNominal]";
    if (v.equals(stnId)) return "[stnId]";
    if (v.equals(stnDesc)) return "[stnDesc]";
    if (v.equals(stnNpts)) return "[stnNpts]";
    if (v.equals(stnWmoId)) return "[stnWmoId]";
    if (v.equals(stnAlt)) return "[stnAlt]";
    if (v.equals(limit)) return "[limit]";

    return "";
  }

  private void showCoords(String indent, Formatter out) {
    boolean gotSome = false;
    for (CoordName coord : CoordName.values()) {
      String varName = findCoordinateVariableName(coord);
      if (varName != null) {
        gotSome = true;
        out.format(" %s Coord %s [%s]%n", indent, varName, coord);
      }
    }
    if (gotSome) out.format("%n");
  }

}


/* This file is Copyright (c) 2005 Robert Alten Simons (info@cohort.com).
 * See the MIT/X-like license in LICENSE.txt.
 * For more information visit www.cohort.com or contact info@cohort.com.
 */
package ucar.nc2.ogc.erddap.util;

import java.text.MessageFormat;

/**
 * The Math2 class has several static Math-related methods.
 *

 *
 These are low level routines used by most other CoHort classes.
 *
 Many provide additional protection from run-time errors.
 *

 */
public class ErddapMath2 {
    /**
     * These are *not* final so EDStatic can replace them with translated Strings.
     * These are MessageFormat-style strings, so any single quote ' must be escaped as ''.
     */
    public static String memoryTooMuchData =
        "Your query produced too much data.  Try to request less data.";
    public static String memoryArraySize =
        "The request needs an array size ({0}) bigger than Java ever allows ({1}).";

    /**
     * Checks if the value is not NaN or +-Infinite.
     * This works for floats.
     *
     * @param d any double value or float value
     * @return true if d is is not NaN or +/-infinity.
     */
    public static boolean isFinite(double d) {
        return !Double.isNaN(d) && !Double.isInfinite(d);
    }

    /**
     * Even if JavaBits is 64, the limit on an array size is Integer.MAX_VALUE.
     *
     *
This is almost identical to EDStatic.ensureArraySizeOkay, but lacks tallying.
     *
     * @param tSize
     * @param attributeTo for a WARNING or ERROR message, this is the string
     *   to which this not-enough-memory issue should be attributed.
     */
    public static void ensureArraySizeOkay(long tSize, String attributeTo) {
        if (tSize >= Integer.MAX_VALUE)
            throw new RuntimeException(memoryTooMuchData + "  " +
                MessageFormat.format(memoryArraySize, "" + tSize, "" + Integer.MAX_VALUE) +
                (attributeTo == null || attributeTo.length() == 0? "" : " (" + attributeTo + ")"));
    }

    /**
     * Safely rounds a double to an int.
     * (Math.round but rounds to a long and not safely.)
     *
     * @param d any double
     * @return Integer.MAX_VALUE if d is too small, too big, or NaN;
     *   otherwise d, rounded to the nearest int.
     *   Undesirable: d.5 rounds up for positive numbers, down for negative.
     */
    public static int roundToInt(double d) {
        return d > Integer.MAX_VALUE || d <= Integer.MIN_VALUE - 0.5 || !isFinite(d)?
            Integer.MAX_VALUE :
            (int)Math.round(d); //safe since checked for larger values above
    }
}


/*
 * 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 ucar.nc2.grib.grib2.Grib2Parameter;
import ucar.nc2.util.TableParser;

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

/**
 * LocalTables from KMA (Japan)
 *
 * @author John
 * @since 6/22/11
 */
public class KmaLocalTables extends LocalTables {
  KmaLocalTables(Grib2TableConfig config) {
    super(config);
    initLocalTable();
  }

  // see http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc.shtml
  private void initLocalTable() {
    String tablePath = config.getPath();
    ClassLoader cl = KmaLocalTables.class.getClassLoader();
    try (InputStream is = cl.getResourceAsStream(tablePath)) {
    if (is == null) throw new IllegalStateException("Cant find "+tablePath);

    List<TableParser.Record> recs = TableParser.readTable(is, "41,112,124i,136i,148i,160", 1000);
    for (TableParser.Record record : recs) {
      String name = (String) record.get(0);
      int disc = (Integer) record.get(2);
      int cat = (Integer) record.get(3);
      int param = (Integer) record.get(4);
      String unit = (String) record.get(5);

      Grib2Parameter s = new Grib2Parameter(disc,cat,param,name,unit,null,null);
      local.put(makeParamId(disc, cat, param), s);
    }

    } catch (IOException ioe) {
      throw new RuntimeException(ioe);
    }
  }
/*
          1         2         3         4         5         6         7         8         9         10        11        12        13        14        15        16        17
012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
U_COMPNT_OF_WIND_AFTER_TIMESTEP                   56           0          65           2         128          33           0           2           2         m/s          UU
############################################################################################################################################################################
#                                      ||                      UM                      ||         GRIB1        ||               GRIB2              ||          ||          |
#            Parameter Name            ||     Field||Processing||     Level||     Stash||     Table|| Parameter||   Product|| Parameter|| Parameter||   Unit   ||   Grid   |
#                                      ||      Code||      Code||      Type||      Item||   Version||    Number|| Disciplin||  Category||    Number||          ||          |
#                                      ||  23(LBFC)||25(LBPROC)||  26(LBVC)||42(LBUSER)||  SEC 1(4)||  SEC 1(9)||  SEC 0(7)|| SEC 4(10)|| SEC 4(11)||          ||          |
#                  40                  ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    ||    12    |
############################################################################################################################################################################
          1         2         3         4         5         6         7         8         9         10        11        12        13        14        15        16        17
012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789

*/

}


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

package ucar.nc2.grib;

import ucar.ma2.DataType;
import ucar.unidata.io.RandomAccessFile;
import java.io.IOException;

/**
 * Utilities for reading and interpreting GRIB bytes.
 */
public final class GribNumbers {
  /**
   * If missing value is not defined use this value.
   */
  public static final int UNDEFINED = -9999;
  public static final double UNDEFINEDD = -9999.0;

  private static final int[] bitmask = {128, 64, 32, 16, 8, 4, 2, 1};

  /**
   * Test if the given gribBitNumber is set in the test value.
   * @param value test the 8 bits in this value .
   * @param gribBitNumber one based, starting from highest bit. Must be between 1-8.
   * @return true if the given gribBitNumber is set.
   */
  public static boolean testGribBitIsSet(int value, int gribBitNumber) {
    return (value & bitmask[gribBitNumber-1]) != 0;
  }

  /**
   * Test if the given bit is set in the test value.
   * @param value test the 8 bits in this value .
   * @param bit zero based, starting from highest bit. Must be between 0-7.
   * @return true if the given bit is set.
   */
  public static boolean testBitIsSet(int value, int bit) {
    return (value & bitmask[bit]) != 0;
  }

  /**
   * Grib uses this internally to mean missing
   */
  public static final int MISSING = 255;

  public static boolean isUndefined(double d) {
    return Double.compare(d, UNDEFINEDD) == 0;
  }

  /**
   * Convert 2 bytes into a signed integer.
   *
   * @param raf read from here
   * @return integer value
   * @throws IOException on read error
   */
  public static int int2(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();

    return int2(a, b);
  }

  /**
    * Convert unsigned bytes into an integer.
    *
    * @param raf read one byte from here
    * @return integer value
    * @throws IOException on read error
    */
   public static int uint(RandomAccessFile raf) throws IOException {
     int a = raf.read();
     return (int) DataType.unsignedByteToShort((byte) a);
   }

   /**
   * convert 2 bytes to a signed integer.
   *
   * @param a first byte
   * @param b second byte
   * @return int
   */
  public static int int2(int a, int b) {
    if (((a == 0xff) && (b == 0xff))) {  // all bits set to one
      return UNDEFINED;
    }

    return (1 - ((a & 128) >> 6)) * ((a & 127) << 8 | b);
  }

  /**
   * Convert 3 bytes into a signed integer.
   *
   * @param raf read from here
   * @return integer value
   * @throws IOException on read error
   */
  public static int int3(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();
    int c = raf.read();

    return int3(a, b, c);
  }

  /**
   * Convert 3 bytes to signed integer.
   *
   * @param a first byte
   * @param b second byte
   * @param c third byte
   * @return int
   */
  public static int int3(int a, int b, int c) {
    return (1 - ((a & 128) >> 6)) * ((a & 127) << 16 | b << 8 | c);
  }

  /**
   * Convert 4 bytes into a signed integer.
   *
   * @param raf read from here
   * @return integer value
   * @throws IOException on read error
   */
  public static int int4(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();
    int c = raf.read();
    int d = raf.read();

    return int4(a, b, c, d);
  }

  /**
   * Convert 4 bytes into a signed integer.
   *
   * @param a first byte
   * @param b second byte
   * @param c third byte
   * @param d fourth byte
   * @return int
   */
  public static int int4(int a, int b, int c, int d) {
    // all bits set to ones
    if ((a == 0xff) && (b == 0xff) && (c == 0xff) && (d == 0xff)) {
      return UNDEFINED;
    }

    return (1 - ((a & 128) >> 6))
            * ((a & 127) << 24 | b << 16 | c << 8 | d);
  }  // end int4

  /**
   * Convert 2 bytes into an unsigned integer.
   *
   * @param raf read from here
   * @return integer value
   * @throws IOException on read error
   */
  public static int uint2(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();

    return uint2(a, b);
  }

  /**
   * convert 2 bytes to an unsigned integer.
   *
   * @param a first byte
   * @param b second byte
   * @return unsigned int
   */
  public static int uint2(int a, int b) {
    return a << 8 | b;
  }

  /**
   * Convert 3 bytes into an unsigned integer.
   *
   * @param raf read from here
   * @return integer
   * @throws IOException on read error
   */
  public static int uint3(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();
    int c = raf.read();

    return uint3(a, b, c);
  }

  /**
   * Convert 3 bytes into an unsigned int.
   *
   * @param a first byte
   * @param b second byte
   * @param c third byte
   * @return unsigned integer
   */
  public static int uint3(int a, int b, int c) {
    return a << 16 | b << 8 | c;
  }

   /**
   * Convert 4 bytes into a float value.
   *
   * @param raf read from here
   * @return float value
   * @throws IOException on read error
   */
  public static float float4(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();
    int c = raf.read();
    int d = raf.read();

    return float4(a, b, c, d);
  }

  /**
   * Convert 4 bytes to a float.
   *
   * @param a first byte
   * @param b second byte
   * @param c third byte
   * @param d fourth byte
   * @return float
   */
  public static float float4(int a, int b, int c, int d) {
    int sgn, mant, exp;

    mant = b << 16 | c << 8 | d;
    if (mant == 0) {
      return 0.0f;
    }

    sgn = -(((a & 128) >> 6) - 1);
    exp = (a & 127) - 64;

    return (float) (sgn * Math.pow(16.0, exp - 6) * mant);
  }

  /**
   * Convert 8 bytes into a signed long.
   *
   * @param raf RandomAccessFile
   * @return long value
   * @throws IOException on read error
   */
  public static long int8(RandomAccessFile raf) throws IOException {
    int a = raf.read();
    int b = raf.read();
    int c = raf.read();
    int d = raf.read();
    int e = raf.read();
    int f = raf.read();
    int g = raf.read();
    int h = raf.read();

    return (1 - ((a & 128) >> 6))
            * ((long)(a & 127) << 56 | (long) b << 48 | (long) c << 40 | (long) d << 32 | e << 24
            | f << 16 | g << 8 | h);

  }

  /**
   * A signed byte has a sign bit then 1 15-bit value.
   * This is not twos complement (!)
   * @param v convert byte to signed int
   * @return signed int
   */
  public static int convertSignedByte(byte v) {
    int sign = ((v & 0x80) != 0) ? -1 : 1;
    int value = v & 0x7f;
    return sign * value;
  }

  public static int convertSignedByte2(byte v) {
    return (v >= 0) ? (int) v : -(128 + v);
  }

  // count number of bits on in bitmap
  public static int countBits(byte[] bitmap) {
    int bits = 0;
    for (byte b : bitmap) {
      short s = DataType.unsignedByteToShort(b);
      bits += Long.bitCount(s);
    }
    return bits;
  }

}