/*
 * Copyright (c) 1998 - 2010. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "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 UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*****************************************/
/* Note: Much of the naming is here to keep the
   netcdf/libncdap3 ce parser parallel to this one
*/

package opendap.servers.parsers;

import opendap.dap.parsers.ParseException;
import ucar.nc2.util.EscapeStrings;

import static opendap.servers.parsers.CeParser.*;

import java.io.*;

class Celex implements Lexer, ExprParserConstants
{

    /* Define 1 and > 1st legal characters */
    static final String wordchars1 =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-+_/%\\";
    static final String wordcharsn =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-+_/%\\ ";

    /* Number characters */
    static final String numchars1 = "+-0123456789";
    static final String numcharsn = "Ee.+-0123456789";

    /* Hack to separate numbers from identifiers */
    static final String wordornumberchars1 =
        wordchars1 + ".";
    static final String wordornumbercharsn =
        wordcharsn + ".";

    static String worddelims = "{}[]:;=,&";

    /**
     * **********************************************
     */
    /* Per-lexer state */

    Ceparse parsestate = null;
    Reader stream = null;
    StringBuilder input = null;
    StringBuilder yytext = null;
    int charno = 0;
    Object lval = null;
    StringBuilder lookahead = null;

    String url = null;
    String constraint = null;

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

    /* Constructor(s) */
    public Celex(Ceparse state)
    {
        reset(state, null);
    }

    public void reset(Ceparse state, String constraint)
    {
        this.parsestate = state;
        input = new StringBuilder(); /* InputStream so far */
        yytext = new StringBuilder();
        lookahead = new StringBuilder();
        lval = null;
        charno = 0;
        this.constraint = constraint;
        this.stream = (this.constraint == null ? null : new StringReader(this.constraint));
    }

    /* Get/Set */

    public String getInput()
    {
        return input.toString();
    }

    int
    peek() throws IOException
    {
        int c = read();
        pushback(c);
        return c;
    }

    void
    pushback(int c)
    {
        lookahead.insert(0, (char) c);
        charno--;
    }

    int
    read() throws IOException
    {
        int c;
        if(lookahead.length() == 0) {
            c = stream.read();
            if(c < 0) c = 0;
            charno++;
        } else {
            c = lookahead.charAt(0);
            lookahead.deleteCharAt(0);
        }
        return c;
    }

    /* This is part of the Lexer interface */

    public int
    yylex()
        throws ParseException
    {
        int token;
        int c;
        token = 0;
        yytext.setLength(0);
        /* invariant: p always points to current char */

        try {
            token = -1;
            while(token < 0) {
                if((c = read()) <= 0) break;
                if(c == '\n') {
                } else if(c <= ' ' || c == '\177') {
                    /* whitespace: ignore */
                } else if(worddelims.indexOf(c) >= 0) {
                    /* don't put in yytext to avoid memory leak */
                    token = c;
                } else if(c == '"') {
                    boolean more = true;
                    /* We have a string token; will be reported as SCAN_STRINGCONST */
                    while(more && (c = read()) > 0) {
                        if(c == '"')
                            more = false;
                        else if(c == '\\') {
                            c = read();
                            if(c < 0) more = false;
                        }
                        if(more) yytext.append((char) c);
                    }
                    token = SCAN_STRINGCONST;
                } else if(false && numchars1.indexOf(c) >= 0) {
                    // we might have a SCAN_NUMBERCONST
                    boolean isnumber = false;
                    yytext.append((char) c);
                    while((c = read()) > 0) {
                        if(numcharsn.indexOf(c) < 0) {
                            pushback(c);
                            break;
                        }
                        yytext.append((char) c);
                    }
                    removetrailingblanks();
                    //See if this is a number
                    try {
                        Double number = new Double(yytext.toString());
                        isnumber = true;
                    } catch (NumberFormatException nfe) {
                        isnumber = false;
                    }
                    //A number followed by an id char is assumed to just be a funny id
                    if(isnumber) {
                        c = read();
                        if(wordcharsn.indexOf(c) >= 0) { // this is apparently just a funny id
                            token = SCAN_WORD;
                        } else {  // its really a number
                            token = SCAN_NUMBERCONST;
                            if(c != '\0') pushback(c);
                        }
                    } else {// !isNumber
                        /* Now, if the funny word has a "." in it,
                           we have to back up to that dot */
                        int dotpoint = yytext.toString().indexOf('.');
                        if(dotpoint >= 0) {
                            for(int i = 0;i < dotpoint;i++) {
                                pushback(yytext.charAt(i));
                            }
                            yytext.setLength(dotpoint);
                        }
                        token = SCAN_WORD;
                    }
                } else if(wordornumberchars1.indexOf(c) >= 0) {
                    boolean isnumber = false;
                    /* we have a WORD or a number*/
                    yytext.append((char) c);
                    while((c = read()) > 0) {
                        if(wordornumbercharsn.indexOf(c) < 0) {
                            pushback(c);
                            break;
                        }
                        yytext.append((char) c);
                    }
                    removetrailingblanks();
                    /* If this looks like a number, then treat it as such.*/
                    try {
                        new Double(yytext.toString());
                        isnumber = true;
                    } catch (NumberFormatException nfe) {
                        isnumber = false;
                    }
                    if(isnumber)
                        token = SCAN_NUMBERCONST;
                    else {
                        token = SCAN_WORD;
                        /* If this is a mistaken number, then we need to
                           backup to the last occurrence of a dot '.'
                           because all other number characters are legitmate
                           identifier characters. Special case occurs when
                           we are left with a single dot.
                         */
                        int dotpoint = yytext.toString().indexOf('.');
                        if(dotpoint >= 0) {
                            // pushback the whole of yytext (in reverse order)
                            for(int i = yytext.length() - 1;i >= 0;i--)
                                pushback(yytext.charAt(i));
                            yytext.setLength(0);
                            if(dotpoint == 0) {// single character delimiter
                                token = '.';
                                yytext.append((char) (c = read()));
                            } else {
                                // Recollect up to but not including the first dot.
                                for(int i = 0;i < dotpoint;i++)
                                    yytext.append((char) (c = read()));
                            }
                        }
                    }
                } else {
                    /* we have a single char token */
                    token = c;
                }
            }
            if(token < 0) {
                token = 0;
                lval = null;
            } else {
                // We have to apply DAP2 %xx escaping if this is a SCAN_WORD
                String text = yytext.toString();
                if(token == SCAN_WORD)
                    text = EscapeStrings.unescapeDAPIdentifier(text);
                lval = (text.length() == 0 ? (String) null : text);
            }
            if(parsestate.getDebugLevel() > 0) dumptoken(token, (String) lval);
            return token;       /* Return the type of the token.  */

        } catch (IOException ioe) {
            throw new ParseException(ioe);
        }
    }

    void
    dumptoken(int token, String lval)
        throws ParseException
    {
        switch (token) {
        case SCAN_STRINGCONST:
            System.out.printf("TOKEN = |\"%s\"|%n", lval);
            break;
        case SCAN_WORD:
        case SCAN_NUMBERCONST:
            System.out.printf("TOKEN = |%s|%n", lval);
            break;
        default:
            System.out.printf("TOKEN = |%c|%n", (char) token);
            break;
        }
        System.err.flush();
    }

    static int
    tohex(int c)
        throws ParseException
    {
        if(c >= 'a' && c <= 'f') return (c - 'a') + 0xa;
        if(c >= 'A' && c <= 'F') return (c - 'A') + 0xa;
        if(c >= '0' && c <= '9') return (c - '0');
        return -1;
    }

    /**************************************************/
    /* Lexer Interface */

    /**
     * Method to retrieve the semantic value of the last scanned token.
     *
     * @return the semantic value of the last scanned token.
     */
    public Object getLVal()
    {
        return this.lval;
    }

    /**
     * Entry point for the scanner.	 Returns the token identifier corresponding
     * to the next token and prepares to return the semantic value
     * of the token.
     * @return the token identifier corresponding to the next token. */
    // int yylex() throws ParseException
    // Defined above

    /**
     * Entry point for error reporting.  Emits an error
     * in a user-defined way.
     *
     * @param s The string for the error message.
     */
    public void yyerror(String s)
    {
        Ceparse.log.error("yyerror: constraint parse error:" + s + "; char " + charno);
        if(yytext.length() > 0)
            Ceparse.log.error(" near |" + yytext + "|");
        // Add extra info
        if(parsestate.getURL() != null) Ceparse.log.error("\turl=" + parsestate.getURL());
        Ceparse.log.error("\tconstraint=" + (constraint == null ? "none" : constraint));
    }

    public void lexerror(String msg)
    {
        StringBuilder nextline = new StringBuilder();
        int c;
        try {
            while((c = read()) != -1) {
                if(c == '\n') break;
                nextline.append((char) c);
            }
        } catch (IOException ioe) {
        }
        ;
        System.out.printf("Lex error: %s; charno: %d: %s^%s%n", msg, charno, yytext, nextline);
    }

    void removetrailingblanks()
    {
        /* If the last characters were blank, then push them back */
        if(yytext.charAt(yytext.length() - 1) == ' ') {
            while(yytext.charAt(yytext.length() - 1) == ' ') {
                yytext.setLength(yytext.length() - 1);
            }
            pushback(' ');
        }
    }
}


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

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.Serializable;

public class MyMouseAdapter extends java.awt.event.MouseAdapter implements Serializable {
  private int startx, starty;
  private int minMove = 4;
  private boolean debugEvent = false;

  public void mousePressed( MouseEvent e) {
    startx = e.getX();
    starty = e.getY();

    if (debugEvent) System.out.println( "mousePressed "+startx+" "+starty);
  }

  public void mouseReleased( MouseEvent e) {
    int deltax = e.getX() - startx;
    int deltay = e.getY() - starty;
    int total = Math.abs(deltax) + Math.abs(deltay);
    if (total <= minMove)
      click(e);
    else
      drag(e, deltax, deltay);

    if (debugEvent) {
      System.out.println( "mouseReleased "+e.getX()+" "+e.getY());
      if ((deltax > 0) || (deltay > 0))
        System.out.println( "  MOVED "+deltax+" "+deltay);
    }
  }

  public void setMinMove( int minMove) { this.minMove = minMove; }

    /// subclasses should override
  public void click(MouseEvent e) { } // System.out.println( "click"); }
  public void drag(MouseEvent e, int deltax, int deltay) { }
    // System.out.println( "drag: "+deltax+" "+deltay);}

  public static void main(String args[]) {

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

    JLabel comp = new JLabel("test  sdfk sdf ks;dflk ;sdlkf ldsk lk");
    comp.setOpaque(true);
    comp.setBackground(Color.white);
    comp.setForeground(Color.black);

    comp.addMouseListener( new MyMouseAdapter());

    JPanel main = new JPanel(new FlowLayout());
    frame.getContentPane().add(main);
    main.setPreferredSize(new Dimension(200, 200));
    main.add( comp);

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


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

package dap4.core.dmr;

import dap4.core.util.DapException;

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

/**
 * DapStructure is normally used as a singleton
 * type for a variable, but for consistency,
 * we track it as a type rather than a variable
 * and create a separate variable whose basetype points
 * to it.
 * This complicates thigs because in effect a structure type
 * has two parents: 1) the group i which it is declared as a type
 * and 2) the group or structure in which the structure var
 * is declared.
 */

public class DapStructure extends DapType
{

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

    // Use list because field ordering can be important
    List<DapVariable> fields = new ArrayList<DapVariable>();

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

    public DapStructure(String name)
    {
        super(name, TypeSort.Structure);
    }

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

    public DapVariable
    findByName(String shortname)
    {
        for(DapVariable field : fields) {
            if(shortname.equals(field.getShortName()))
                return field;
        }
        return null;
    }

    public int
    indexByName(String shortname)
    {
        for(int i = 0; i < fields.size(); i++) {
            DapVariable field = fields.get(i);
            if(shortname.equals(field.getShortName()))
                return i;
        }
        return -1;
    }

    public int
    indexByField(DapVariable target)
    {
        for(int i = 0; i < fields.size(); i++) {
            DapVariable field = fields.get(i);
            if(field == target)
                return i;
        }
        return -1;
    }

    public DapVariable getField(int i)
    {
        return fields.get(i);
    }

    public List<DapVariable> getFields()
    {
        return fields;
    }

    /*public void setFields(DapVariable instance, List<? extends DapNode> fields)
            throws DapException
    {
        fields.clear();
        for(int i = 0; i < fields.size(); i++) {
            addField((DapVariable)fields.get(i),instance);
        }
    } */

    public boolean isLeaf()
    {
        return false;
    }

    public void addField(DapVariable newfield)
            throws DapException
    {
        DapStructure ds = this;
        for(DapVariable v : ds.fields) {
            if(v.getShortName().equals(newfield.getShortName()))
                throw new DapException("DapStructure: attempt to add duplicate field: " + newfield.getShortName());
        }
        ds.fields.add((DapVariable) newfield);
    }

} // class DapStructure


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

package ucar.nc2.ft.point.writer;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft.*;
import ucar.nc2.ft.point.CollectionInfo;
import ucar.nc2.ft.point.DsgCollectionHelper;
import ucar.nc2.ft.point.StationFeature;
import ucar.nc2.ncml.NcMLReader;
import ucar.nc2.ncml.NcMLWriter;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateFormatter;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarDateUnit;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.Station;

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

/**
 * generate capabilities XML for a FeatureDatasetPoint / StationTimeSeriesFeatureCollection
 *
 * @author caron
 * @since Aug 19, 2009
 */
public class FeatureDatasetCapabilitiesWriter {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FeatureDatasetCapabilitiesWriter.class);

  private FeatureDatasetPoint fdp;
  private String path;

  public FeatureDatasetCapabilitiesWriter(FeatureDatasetPoint fdp, String path) {
    this.fdp = fdp;
    this.path = path;
  }

  public String getCapabilities() {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    return fmt.outputString(getCapabilitiesDocument());
  }

  public void getCapabilities(OutputStream os) throws IOException {
    XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
    fmt.output(getCapabilitiesDocument(), os);
  }

  /**
   * Create an XML document for the stations in this dataset, possible subsetted by bb.
   * Must be a station dataset.
   *
   * @param bb    restrict stations to this bounding box, may be null
   * @param names restrict stations to these names, may be null
   * @return XML document for the stations
   * @throws IOException on read error
   */
  public Document makeStationCollectionDocument(LatLonRect bb, String[] names) throws IOException {

    List<DsgFeatureCollection> list = fdp.getPointFeatureCollectionList();
    DsgFeatureCollection fc = list.get(0); // LOOK maybe should pass in the dsg?

    if (!(fc instanceof StationTimeSeriesFeatureCollection)) {
      throw new UnsupportedOperationException(fc.getClass().getName() + " not a StationTimeSeriesFeatureCollection");
    }
    StationTimeSeriesFeatureCollection sobs = (StationTimeSeriesFeatureCollection) fc;

    Element rootElem = new Element("stationCollection");
    Document doc = new Document(rootElem);

    List<StationFeature> stations;
    if (bb != null)
      stations = sobs.getStationFeatures(bb);
    else if (names != null)
      stations = sobs.getStationFeatures(Arrays.asList(names));
    else
      stations = sobs.getStationFeatures();

    for (Station s : stations) {
      Element sElem = new Element("station");
      sElem.setAttribute("name", s.getName());
      if (s.getWmoId() != null)
        sElem.setAttribute("wmo_id", s.getWmoId());
      if ((s.getDescription() != null) && (s.getDescription().length() > 0))
        sElem.addContent(new Element("description").addContent(s.getDescription()));

      sElem.addContent(new Element("longitude").addContent(Double.toString(s.getLongitude())));
      sElem.addContent(new Element("latitide").addContent(Double.toString(s.getLatitude())));
      if (!Double.isNaN(s.getAltitude()))
        sElem.addContent(new Element("altitude").addContent(Double.toString(s.getAltitude())));
      rootElem.addContent(sElem);
    }

    return doc;
  }

  /**
   * Create the capabilities XML document for this dataset
   *
   * @return capabilities XML document
   */
  public Document getCapabilitiesDocument() {
    Element rootElem = new Element("capabilities");
    Document doc = new Document(rootElem);
    if (null != path) {
      rootElem.setAttribute("location", path);
      Element elem = new Element("featureDataset");
      FeatureType ft = fdp.getFeatureType();
      elem.setAttribute("type", ft.toString().toLowerCase());
      String url = path.replace("dataset.xml", ft.toString().toLowerCase() + ".xml");
      elem.setAttribute("url", url);
      rootElem.addContent(elem);
    }

    List<DsgFeatureCollection> list = fdp.getPointFeatureCollectionList();
    DsgFeatureCollection fc = list.get(0); // LOOK maybe should pass in the dsg?

    rootElem.addContent(writeTimeUnit(fc.getTimeUnit()));
    rootElem.addContent(new Element("AltitudeUnits").addContent(fc.getAltUnits()));

    // data variables
    List<? extends VariableSimpleIF> vars = fdp.getDataVariables();
    Collections.sort(vars);
    for (VariableSimpleIF v : vars) {
      rootElem.addContent(writeVariable(v));
    }

    /* CollectionInfo info;
    try {
      info = new DsgCollectionHelper(fc).calcBounds();
    } catch (IOException e) {
      throw new RuntimeException(e);
    } */

    LatLonRect bb = fc.getBoundingBox();
    if (bb != null)
      rootElem.addContent(writeBoundingBox(bb));

    // add date range
    CalendarDateRange dateRange = fc.getCalendarDateRange();
    if (dateRange != null) {
      Element drElem = new Element("TimeSpan"); // from KML
      drElem.addContent(new Element("begin").addContent(dateRange.getStart().toString()));
      drElem.addContent(new Element("end").addContent(dateRange.getEnd().toString()));
      if (dateRange.getResolution() != null)
        drElem.addContent(new Element("resolution").addContent(dateRange.getResolution().toString()));

      rootElem.addContent(drElem);
    }

    /* add accept list
    Element elem = new Element("AcceptList");
    //elem.addContent(new Element("accept").addContent("raw"));
    elem.addContent(new Element("accept").addContent("csv").setAttribute("displayName", "csv"));
    elem.addContent(new Element("accept").addContent("text/csv").setAttribute("displayName", "csv (file)"));
    elem.addContent(new Element("accept").addContent("xml").setAttribute("displayName", "xml"));
    elem.addContent(new Element("accept").addContent("text/xml").setAttribute("displayName", "xml (file)"));
    elem.addContent(new Element("accept").addContent("waterml2").setAttribute("displayName", "WaterML 2.0"));
    elem.addContent(new Element("accept").addContent("netcdf").setAttribute("displayName", "CF/NetCDF-3"));
    //elem.addContent(new Element("accept").addContent("ncstream"));
    rootElem.addContent(elem); */

    return doc;
  }

  private Element writeTimeUnit(CalendarDateUnit dateUnit) {
    Element elem = new Element("TimeUnit");
    elem.addContent(dateUnit.getUdUnit());
    elem.setAttribute("calendar", dateUnit.getCalendar().toString());
    return elem;
  }

  private Element writeBoundingBox(LatLonRect bb) {
    int decToKeep = 6;
    double bbExpand = Math.pow(10, -decToKeep);

    // extend the bbox to make sure the implicit rounding does not result in a bbox that does not contain
    //   any points (can happen when you have a single station with very precise lat/lon values)
    //   See https://github.com/Unidata/thredds/issues/470
    // This accounts for the implicit rounding errors that result from the use of
    //   ucar.unidata.util.Format.dfrac when writing out the lat/lon box on the NCSS for Points dataset.html
    //   page
    LatLonPointImpl extendNorthEast = new LatLonPointImpl(bb.getLatMax() + bbExpand, bb.getLonMax() + bbExpand);
    LatLonPointImpl extendSouthWest = new LatLonPointImpl(bb.getLatMin() - bbExpand, bb.getLonMin() - bbExpand);
    bb.extend(extendNorthEast);
    bb.extend(extendSouthWest);

    Element bbElem = new Element("LatLonBox"); // from KML

    bbElem.addContent(new Element("west").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMin(), decToKeep)));
    bbElem.addContent(new Element("east").addContent(ucar.unidata.util.Format.dfrac(bb.getLonMax(), decToKeep)));
    bbElem.addContent(new Element("south").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMin(), decToKeep)));
    bbElem.addContent(new Element("north").addContent(ucar.unidata.util.Format.dfrac(bb.getLatMax(), decToKeep)));
    return bbElem;
  }

  private Element writeVariable(VariableSimpleIF v) {
    NcMLWriter ncMLWriter = new NcMLWriter();
    Element varElem = new Element("variable");
    varElem.setAttribute("name", v.getShortName());

    ucar.ma2.DataType dt = v.getDataType();
    if (dt != null)
      varElem.setAttribute("type", dt.toString());

    // attributes
    for (Attribute att : v.getAttributes()) {
      varElem.addContent(ncMLWriter.makeAttributeElement(att));
    }

    return varElem;
  }

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

  public Document readCapabilitiesDocument(InputStream in) throws JDOMException, IOException {
    SAXBuilder builder = new SAXBuilder();
    return builder.build(in);
  }

  public static LatLonRect getSpatialExtent(Document doc) throws IOException {
    Element root = doc.getRootElement();
    Element latlonBox = root.getChild("LatLonBox");
    if (latlonBox == null) return null;

    String westS = latlonBox.getChildText("west");
    String eastS = latlonBox.getChildText("east");
    String northS = latlonBox.getChildText("north");
    String southS = latlonBox.getChildText("south");
    if ((westS == null) || (eastS == null) || (northS == null) || (southS == null)) return null;

    try {
      double west = Double.parseDouble(westS);
      double east = Double.parseDouble(eastS);
      double south = Double.parseDouble(southS);
      double north = Double.parseDouble(northS);
      return new LatLonRect(new LatLonPointImpl(south, east), new LatLonPointImpl(north, west));

    } catch (Exception e) {
      return null;
    }
  }

  public static CalendarDateRange getTimeSpan(Document doc) throws IOException {
    Element root = doc.getRootElement();
    Element timeSpan = root.getChild("TimeSpan");
    if (timeSpan == null) return null;

    String beginS = timeSpan.getChildText("begin");
    String endS = timeSpan.getChildText("end");
//    String resS = timeSpan.getChildText("resolution");
    if ((beginS == null) || (endS == null)) return null;

    try {
      CalendarDate start = CalendarDateFormatter.isoStringToCalendarDate(null, beginS);
      CalendarDate end = CalendarDateFormatter.isoStringToCalendarDate(null, endS);
      if ((start == null) || (end == null)) {
        return null;
      }

      CalendarDateRange dr = CalendarDateRange.of(start, end);

      // LOOK if (resS != null)
      //  dr.setResolution(new TimeDuration(resS));

      return dr;

    } catch (Exception e) {
      return null;
    }
  }

  public static CalendarDateUnit getTimeUnit(Document doc) {
    Element root = doc.getRootElement();
    Element timeUnitE = root.getChild("TimeUnit");
    if (timeUnitE == null) return null;

    String cal = timeUnitE.getAttributeValue("calendar");
    String timeUnitS = timeUnitE.getTextNormalize();

    try {
      return CalendarDateUnit.of(cal, timeUnitS);
    } catch (Exception e) {
      log.error("Illegal date unit {} in FeatureDatasetCapabilitiesXML", timeUnitS);
      return null;
    }
  }

  public static String getAltUnits(Document doc) throws IOException {
    Element root = doc.getRootElement();
    String altUnits = root.getChildText("AltitudeUnits");
    if (altUnits == null || altUnits.length() == 0) return null;
    return altUnits;
  }

  public static List<VariableSimpleIF> getDataVariables(Document doc) throws IOException {
    Element root = doc.getRootElement();

    List<VariableSimpleIF> dataVars = new ArrayList<>();
    List<Element> varElems = root.getChildren("variable");
    for (Element varElem : varElems) {
      dataVars.add(new VariableSimpleAdapter(varElem));
    }
    return dataVars;
  }

  private static class VariableSimpleAdapter implements VariableSimpleIF {
    String name, desc, units;
    DataType dt;
    List<Attribute> atts;

    VariableSimpleAdapter(Element velem) {
      name = velem.getAttributeValue("name");
      String type = velem.getAttributeValue("type");
      dt = DataType.getType(type);

      atts = new ArrayList<>();
      List<Element> attElems = velem.getChildren("attribute");
      for (Element attElem : attElems) {
        String attName = attElem.getAttributeValue("name");
        ucar.ma2.Array values = NcMLReader.readAttributeValues(attElem);
        atts.add(new Attribute(attName, values));
      }

      for (Attribute att : atts) {
        if (att.getShortName().equals(CDM.UNITS))
          units = att.getStringValue();
        if (att.getShortName().equals(CDM.LONG_NAME))
          desc = att.getStringValue();
        if ((desc == null) && att.getShortName().equals("description"))
          desc = att.getStringValue();
        if ((desc == null) && att.getShortName().equals("standard_name"))
          desc = att.getStringValue();
      }
    }

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

    @Override
    public String getFullName() {
      return name;
    }

    @Override
    public String getShortName() {
      return name;
    }

    @Override
    public String getDescription() {
      return desc;
    }

    @Override
    public String getUnitsString() {
      return units;
    }

    @Override
    public int getRank() {
      return 0;
    }

    @Override
    public int[] getShape() {
      return new int[0];
    }

    @Override
    public List<Dimension> getDimensions() {
      return null;
    }

    @Override
    public DataType getDataType() {
      return dt;
    }

    @Override
    public List<Attribute> getAttributes() {
      return atts;
    }

    @Override
    public Attribute findAttributeIgnoreCase(String name) {
      for (Attribute att : atts) {
        if (att.getShortName().equalsIgnoreCase(name))
          return att;
      }
      return null;
    }

    @Override
    public int compareTo(VariableSimpleIF o) {
      return name.compareTo(o.getShortName()); // ??
    }

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

      VariableSimpleAdapter that = (VariableSimpleAdapter) o;
      return name.equals(that.name);
    }

    @Override
    public int hashCode() {
      return name.hashCode();
    }
  }

}


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

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Concrete implementation of Array specialized for bytes.
 * Data storage is with 1D java array of bytes.
 * <p/>
 * issues: what should we do if a conversion loses accuracy? nothing ? Exception ?
 *
 * @author caron
 * @see Array
 */
public class ArrayByte extends Array {

  // package private. use Array.factory() */
  static ArrayByte factory(Index index, boolean isUnsigned) {
    return ArrayByte.factory(index, isUnsigned, null);
  }

  /* create new ArrayByte with given Index and backing store.
   * @param index use this Index
   * @param storage use this storage. if null, allocate.
   * @return new ArrayByte.D<rank> or ArrayByte object.
   */
  static ArrayByte factory( Index index, boolean isUnsigned, byte [] storage) {
    if (index instanceof Index0D) {
      return new ArrayByte.D0(index, isUnsigned, storage);
    } else if (index instanceof Index1D) {
      return new ArrayByte.D1(index, isUnsigned, storage);
    } else if (index instanceof Index2D) {
      return new ArrayByte.D2(index, isUnsigned, storage);
    } else if (index instanceof Index3D) {
      return new ArrayByte.D3(index, isUnsigned, storage);
    } else if (index instanceof Index4D) {
      return new ArrayByte.D4(index, isUnsigned, storage);
    } else if (index instanceof Index5D) {
      return new ArrayByte.D5(index, isUnsigned, storage);
    } else if (index instanceof Index6D) {
      return new ArrayByte.D6(index, isUnsigned, storage);
    } else if (index instanceof Index7D) {
      return new ArrayByte.D7(index, isUnsigned, storage);
    } else {
      return new ArrayByte(index, isUnsigned, storage);
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  protected byte[] storage;

  /**
   * Create a new Array of type byte and the given shape.
   * dimensions.length determines the rank of the new Array.
   *
   * @param dimensions the shape of the Array.
   */
  public ArrayByte(int[] dimensions, boolean isUnsigned) {
    super(isUnsigned ? DataType.UBYTE : DataType.BYTE, dimensions);
    storage = new byte[(int) indexCalc.getSize()];
  }

  /**
   * Create a new Array using the given IndexArray and backing store.
   * used for sections. Trusted package private.
   *
   * @param ima  use this IndexArray as the index
   * @param data use this as the backing store
   */
  ArrayByte(Index ima, boolean isUnsigned, byte[] data) {
    super(isUnsigned ? DataType.UBYTE : DataType.BYTE, ima);
    /* replace by something better
    if (ima.getSize() != data.length)
      throw new IllegalArgumentException("bad data length");  */
    if (data != null)
      storage = data;
    else
      storage = new byte[(int) ima.getSize()];
  }

  protected Array createView(Index index) {
    return ArrayByte.factory(index, isUnsigned(), storage);
  }

  public Object getStorage() {
    return storage;
  }

  // copy from javaArray to storage using the iterator: used by factory( Object);
  protected void copyFrom1DJavaArray(IndexIterator iter, Object javaArray) {
    byte[] ja = (byte[]) javaArray;
    for (byte aJa : ja) iter.setByteNext(aJa);
  }

  // copy to javaArray from storage using the iterator: used by copyToNDJavaArray;
  protected void copyTo1DJavaArray(IndexIterator iter, Object javaArray) {
    byte[] ja = (byte[]) javaArray;
    for (int i = 0; i < ja.length; i++)
      ja[i] = iter.getByteNext();
  }

  @Override
  public ByteBuffer getDataAsByteBuffer() {return getDataAsByteBuffer(null);}

  public ByteBuffer getDataAsByteBuffer(ByteOrder order) {// order irrelevant here
    return ByteBuffer.wrap((byte[]) get1DJavaArray(getDataType()));
  }

  /**
   * Return the element class type
   */
  public Class getElementType() {
    return byte.class;
  }

  /**
   * get the value at the specified index.
   *
   * @param i index
   * @return byte value
   */
  public byte get(Index i) {
    return storage[i.currentElement()];
  }

  /**
   * set the value at the sepcified index.
   *
   * @param i     index
   * @param value set to this value
   */
  public void set(Index i, byte value) {
    storage[i.currentElement()] = value;
  }

  public double getDouble(Index i) {
    byte val = storage[i.currentElement()];
    return (double) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setDouble(Index i, double value) {
    storage[i.currentElement()] = (byte) value;
  }

  public float getFloat(Index i) {
    byte val = storage[i.currentElement()];
    return (float) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setFloat(Index i, float value) {
    storage[i.currentElement()] = (byte) value;
  }

  public long getLong(Index i) {
    byte val = storage[i.currentElement()];
    return (long) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setLong(Index i, long value) {
    storage[i.currentElement()] = (byte) value;
  }

  public int getInt(Index i) {
    byte val = storage[i.currentElement()];
    return (int) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setInt(Index i, int value) {
    storage[i.currentElement()] = (byte) value;
  }

  public short getShort(Index i) {
    byte val = storage[i.currentElement()];
    return (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setShort(Index i, short value) {
    storage[i.currentElement()] = (byte) value;
  }

  public byte getByte(Index i) {
    return storage[i.currentElement()];
  }

  public void setByte(Index i, byte value) {
    storage[i.currentElement()] = value;
  }

  public char getChar(Index i) {
    byte val = storage[i.currentElement()];
    return (char) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setChar(Index i, char value) {
    storage[i.currentElement()] = (byte) value;
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public boolean getBoolean(Index i) {
    throw new ForbiddenConversionException();
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public void setBoolean(Index i, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(Index i) {
    return storage[i.currentElement()];
  }

  public void setObject(Index i, Object value) {
    storage[i.currentElement()] = ((Number) value).byteValue();
  }

  // package private : mostly for iterators
  public double getDouble(int index) {
    byte val = storage[index];
    return (double) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setDouble(int index, double value) {
    storage[index] = (byte) value;
  }

  public float getFloat(int index) {
    byte val = storage[index];
    return (float) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setFloat(int index, float value) {
    storage[index] = (byte) value;
  }

  public long getLong(int index) {
    byte val = storage[index];
    return (long) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setLong(int index, long value) {
    storage[index] = (byte) value;
  }

  public int getInt(int index) {
    byte val = storage[index];
    return (int) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setInt(int index, int value) {
    storage[index] = (byte) value;
  }

  public short getShort(int index) {
    byte val = storage[index];
    return (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setShort(int index, short value) {
    storage[index] = (byte) value;
  }

  public byte getByte(int index) {
    return storage[index];
  }

  public void setByte(int index, byte value) {
    storage[index] = value;
  }

  public char getChar(int index) {
    byte val = storage[index];
    return (char) (isUnsigned() ? DataType.unsignedByteToShort(val) : val);
  }

  public void setChar(int index, char value) {
    storage[index] = (byte) value;
  }

  public boolean getBoolean(int index) {
    throw new ForbiddenConversionException();
  }

  public void setBoolean(int index, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(int index) {
    return getByte(index);
  }

  public void setObject(int index, Object value) {
    storage[index] = ((Number) value).byteValue();
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 0.
   */
  public static class D0 extends ArrayByte {
    private Index0D ix;

    public D0(boolean isUnsigned) {
      super(new int[]{}, isUnsigned);
      ix = (Index0D) indexCalc;
    }

    private D0(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index0D) indexCalc;
    }

    public byte get() {
      return storage[ix.currentElement()];
    }

    public void set(byte value) {
      storage[ix.currentElement()] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 1.
   */
  public static class D1 extends ArrayByte {
    private Index1D ix;

    public D1(int len0, boolean isUnsigned) {
      super(new int[]{len0}, isUnsigned);
      ix = (Index1D) indexCalc;
    }

    private D1(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index1D) indexCalc;
    }

    public byte get(int i) {
      return storage[ix.setDirect(i)];
    }

    public void set(int i, byte value) {
      storage[ix.setDirect(i)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 2.
   */
  public static class D2 extends ArrayByte {
    private Index2D ix;

    public D2(int len0, int len1, boolean isUnsigned) {
      super(new int[]{len0, len1}, isUnsigned);
      ix = (Index2D) indexCalc;
    }

    private D2(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index2D) indexCalc;
    }

    public byte get(int i, int j) {
      return storage[ix.setDirect(i, j)];
    }

    public void set(int i, int j, byte value) {
      storage[ix.setDirect(i, j)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 3.
   */
  public static class D3 extends ArrayByte {
    private Index3D ix;

    public D3(int len0, int len1, int len2, boolean isUnsigned) {
      super(new int[]{len0, len1, len2}, isUnsigned);
      ix = (Index3D) indexCalc;
    }

    private D3(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index3D) indexCalc;
    }

    public byte get(int i, int j, int k) {
      return storage[ix.setDirect(i, j, k)];
    }

    public void set(int i, int j, int k, byte value) {
      storage[ix.setDirect(i, j, k)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 4.
   */
  public static class D4 extends ArrayByte {
    private Index4D ix;

    public D4(int len0, int len1, int len2, int len3, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3}, isUnsigned);
      ix = (Index4D) indexCalc;
    }

    private D4(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index4D) indexCalc;
    }

    public byte get(int i, int j, int k, int l) {
      return storage[ix.setDirect(i, j, k, l)];
    }

    public void set(int i, int j, int k, int l, byte value) {
      storage[ix.setDirect(i, j, k, l)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 5.
   */
  public static class D5 extends ArrayByte {
    private Index5D ix;

    public D5(int len0, int len1, int len2, int len3, int len4, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4}, isUnsigned);
      ix = (Index5D) indexCalc;
    }

    private D5(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index5D) indexCalc;
    }

    public byte get(int i, int j, int k, int l, int m) {
      return storage[ix.setDirect(i, j, k, l, m)];
    }

    public void set(int i, int j, int k, int l, int m, byte value) {
      storage[ix.setDirect(i, j, k, l, m)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 6.
   */
  public static class D6 extends ArrayByte {
    private Index6D ix;

    public D6(int len0, int len1, int len2, int len3, int len4, int len5, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4, len5}, isUnsigned);
      ix = (Index6D) indexCalc;
    }

    private D6(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index6D) indexCalc;
    }

    public byte get(int i, int j, int k, int l, int m, int n) {
      return storage[ix.setDirect(i, j, k, l, m, n)];
    }

    public void set(int i, int j, int k, int l, int m, int n, byte value) {
      storage[ix.setDirect(i, j, k, l, m, n)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for byte, rank 7.
   */
  public static class D7 extends ArrayByte {
    protected Index7D ix;

    public D7(int len0, int len1, int len2, int len3, int len4, int len5, int len6, boolean isUnsigned) {
      super(new int[]{len0, len1, len2, len3, len4, len5, len6}, isUnsigned);
      ix = (Index7D) indexCalc;
    }

    private D7(Index i, boolean isUnsigned, byte[] store) {
      super(i, isUnsigned, store);
      ix = (Index7D) indexCalc;
    }

    public byte get(int i, int j, int k, int l, int m, int n, int o) {
      return storage[ix.setDirect(i, j, k, l, m, n, o)];
    }

    public void set(int i, int j, int k, int l, int m, int n, int o, byte value) {
      storage[ix.setDirect(i, j, k, l, m, n, o)] = value;
    }
  }

}


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

package ucar.nc2.grib.collection;

import com.google.common.base.MoreObjects;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thredds.featurecollection.FeatureCollectionConfig;
import thredds.inventory.MFile;
import ucar.nc2.Attribute;
import ucar.nc2.AttributeContainer;
import ucar.nc2.AttributeContainerHelper;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.grib.*;
import ucar.nc2.grib.coord.Coordinate;
import ucar.nc2.grib.coord.CoordinateEns;
import ucar.nc2.grib.coord.CoordinateRuntime;
import ucar.nc2.grib.coord.CoordinateTime2D;
import ucar.nc2.grib.coord.CoordinateTimeAbstract;
import ucar.nc2.grib.coord.SparseArray;
import ucar.nc2.grib.coord.TimeCoordIntvValue;
import ucar.nc2.grib.coord.VertCoordValue;
import ucar.nc2.grib.grib1.Grib1Variable;
import ucar.nc2.grib.grib1.tables.Grib1Customizer;
import ucar.nc2.grib.grib2.table.Grib2Tables;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.util.cache.FileCacheIF;
import ucar.nc2.util.cache.FileCacheable;
import ucar.nc2.wmo.CommonCodeTable;
import ucar.unidata.io.RandomAccessFile;

import javax.annotation.concurrent.Immutable;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

/**
 * An Immutable GribCollection, corresponds to one index (ncx) file.
 * The index file has already been read; it is opened and the closed when a variable is first accessed to read in the record array (sa).
 * <p>
 * Could use the Proto equivalents, and eliminate GribCollectionMutable ?
 *
 * @author caron
 * @since 11/10/2014
 */
@Immutable
public abstract class GribCollectionImmutable implements Closeable, FileCacheable {
  private static final Logger logger = LoggerFactory.getLogger(GribCollectionImmutable.class);
  public static int countGC; // debug

  public enum Type {    // must match with GribCollectionProto.Dataset.Type
    SRC,               // GC: Single Runtime Collection                [ntimes]
    MRC,              // GC: Multiple Runtime Collection              [nruns, ntimes]
    // MRSTC,             // GC: Multiple Runtime Single Time Collection  [nruns, 1]
    MRUTC,             // GC: Multiple Runtime Unique Time Collection  [ntimes]

    // MRSTP,            // PC: Multiple Runtime Single Time Partition   [nruns, 1]
    TwoD,            // PC: TwoD time partition                      [nruns, ntimes]
    Best,             // PC: Best time partition                      [ntimes]
    BestComplete,     // PC: Best complete time partition (not done)  [ntimes]
    MRUTP;            // PC: Multiple Runtime Unique Time Partition   [ntimes]

    public boolean isSingleRuntime() {
      return this == SRC;
    }

    public boolean isUniqueTime() {
      return this == MRUTC || this == MRUTP || this == SRC;
    }

    public boolean isTwoD() {
      return this == MRC || this == TwoD;
    }
  }

  ////////////////////////////////////////////////////////////////
  protected final String name; // collection name; index filename must be directory/name.ncx2
  protected final File directory;
  protected final FeatureCollectionConfig config;
  public final boolean isGrib1;
  protected final Info info;

  protected final List<Dataset> datasets;
  protected final CoordinateRuntime masterRuntime;
  protected final CalendarDateRange dateRange;

  protected final Map<Integer, MFile> fileMap; // all the files used in the GC; key is the index in original collection, GC has subset of them
  protected final GribTables cust;
  protected final String indexFilename;       // full path of index Filename

  private FileCacheIF objCache = null;  // optional object cache - used in the TDS

  GribCollectionImmutable(GribCollectionMutable gc) {
    countGC++;

    this.config = gc.config;
    this.name = gc.name;
    this.directory = gc.directory;
    this.isGrib1 = gc.isGrib1;
    this.info = new Info(gc);

    List<Dataset> work = new ArrayList<>(gc.datasets.size());
    for (GribCollectionMutable.Dataset gcDataset : gc.datasets) {
      work.add(new Dataset(gcDataset.gctype, gcDataset.groups));
    }
    this.datasets = Collections.unmodifiableList(work);

    // this.horizCS = Collections.unmodifiableList(gc.horizCS);
    this.masterRuntime = gc.masterRuntime;
    this.fileMap = gc.fileMap;
    this.cust = gc.cust;
    this.dateRange = gc.dateRange;

    if (gc.indexFilename != null) {
      indexFilename = gc.indexFilename;

    } else {
      File indexFile = GribCdmIndex.makeIndexFile(name, directory);
      File indexFileInCache = GribIndexCache.getExistingFileOrCache(indexFile.getPath());
      if (indexFileInCache == null)
        throw new IllegalStateException(indexFile.getPath() + " does not exist, nor in cache");
      indexFilename = indexFileInCache.getPath();
    }
  }

  // overridden in PartitionCollection
  protected VariableIndex makeVariableIndex(GroupGC group, GribCollectionMutable.VariableIndex mutableVar) {
    return new VariableIndex(group, mutableVar);
  }

  public List<Dataset> getDatasets() {
    return datasets;
  }

  public Dataset getDataset(int idx) {
    return datasets.get(idx);
  }

  public Dataset getDatasetCanonical() {
    for (Dataset ds : datasets) {
      if (ds.gctype != GribCollectionImmutable.Type.Best) return ds;
    }
    throw new IllegalStateException("GC.getDatasetCanonical failed on=" + name);
  }

  public String getName() {
    return name;
  }

  public File getDirectory() {
    return directory;
  }

  public CoordinateRuntime getMasterRuntime() {
    return masterRuntime;
  }

  public CalendarDate getMasterFirstDate() {
    return masterRuntime.getFirstDate();
  }

  public int getVersion() {
    return info.version;
  }

  public int getCenter() {
    return info.center;
  }

  public int getSubcenter() {
    return info.subcenter;
  }

  public int getMaster() {
    return info.master;
  }

  public int getLocal() {
    return info.local;
  }

  public int getGenProcessType() {
    return info.genProcessType;
  }

  public int getGenProcessId() {
    return info.genProcessId;
  }

  public int getBackProcessId() {
    return info.backProcessId;
  }

  /*
    String val = CommonCodeTable.getCenterName(gribCollection.getCenter(), 2);
    ncfile.addAttribute(null, new Attribute(GribUtils.CENTER, val == null ? Integer.toString(gribCollection.getCenter()) : val));
    val = gribTable.getSubCenterName(gribCollection.getCenter(), gribCollection.getSubcenter());
    ncfile.addAttribute(null, new Attribute(GribUtils.SUBCENTER, val == null ? Integer.toString(gribCollection.getSubcenter()) : val));
    ncfile.addAttribute(null, new Attribute(GribUtils.TABLE_VERSION, gribCollection.getMaster() + "," + gribCollection.getLocal())); // LOOK

    addGlobalAttributes(ncfile);

    ncfile.addAttribute(null, new Attribute(CDM.CONVENTIONS, "CF-1.6"));
    ncfile.addAttribute(null, new Attribute(CDM.HISTORY, "Read using CDM IOSP GribCollection v3"));
    ncfile.addAttribute(null, new Attribute(CF.FEATURE_TYPE, FeatureType.GRID.name()));
    ncfile.addAttribute(null, new Attribute(CDM.FILE_FORMAT, getFileTypeId()));
   */

  public AttributeContainerHelper getGlobalAttributes() {
    AttributeContainerHelper result = new AttributeContainerHelper(name);
    String val = CommonCodeTable.getCenterName(getCenter(), 2);
    result.addAttribute(new Attribute(GribUtils.CENTER, val == null ? Integer.toString(getCenter()) : val));
    val = cust.getSubCenterName(getCenter(), getSubcenter());
    result.addAttribute(new Attribute(GribUtils.SUBCENTER, val == null ? Integer.toString(getSubcenter()) : val));
    result.addAttribute(new Attribute(GribUtils.TABLE_VERSION, getMaster() + "," + getLocal())); // LOOK

    addGlobalAttributes(result);  // add subclass atts

    result.addAttribute(new Attribute(CDM.CONVENTIONS, "CF-1.6"));
    result.addAttribute(new Attribute(CDM.HISTORY, "Read using CDM IOSP GribCollection v3"));
    result.addAttribute(new Attribute(CF.FEATURE_TYPE, FeatureType.GRID.name()));

    return result;
  }

  public abstract void addGlobalAttributes(AttributeContainer result);

  public abstract void addVariableAttributes(AttributeContainer v, GribCollectionImmutable.VariableIndex vindex);

  protected abstract String makeVariableId(VariableIndex v);

  public static class Info {
    final int version; // the ncx version
    final int center, subcenter, master, local;  // GRIB 1 uses "local" for table version
    final int genProcessType, genProcessId, backProcessId;

    public Info(int version, int center, int subcenter, int master, int local, int genProcessType, int genProcessId, int backProcessId) {
      this.version = version;
      this.center = center;
      this.subcenter = subcenter;
      this.master = master;
      this.local = local;
      this.genProcessType = genProcessType;
      this.genProcessId = genProcessId;
      this.backProcessId = backProcessId;
    }

    public Info(GribCollectionMutable gc) {
      this.version = gc.version;
      this.center = gc.center;
      this.subcenter = gc.subcenter;
      this.master = gc.master;
      this.local = gc.local;
      this.genProcessType = gc.genProcessType;
      this.genProcessId = gc.genProcessId;
      this.backProcessId = gc.backProcessId;
    }

    @Override
    public String toString() {
      return "Info{" +
          "version=" + version +
          ", center=" + center +
          ", subcenter=" + subcenter +
          ", master=" + master +
          ", local=" + local +
          ", genProcessType=" + genProcessType +
          ", genProcessId=" + genProcessId +
          ", backProcessId=" + backProcessId +
          '}';
    }
  }

  @Immutable
  public class Dataset {
    final Type gctype;
    final List<GroupGC> groups;  // must be kept in order, because PartitionForVariable2D has index into it

    public Dataset(Type gctype, List<GribCollectionMutable.GroupGC> groups) {
      this.gctype = gctype;
      List<GroupGC> work = new ArrayList<>(groups.size());
      for (GribCollectionMutable.GroupGC gcGroup : groups) {
        work.add(new GroupGC(this, gcGroup));
      }
      this.groups = Collections.unmodifiableList(work);
    }

    public Iterable<GroupGC> getGroups() {
      return groups;
    }

    public int getGroupsSize() {
      return groups.size();
    }

    public Type getType() {
      return gctype;
    }

    public GroupGC getGroup(int index) {
      return groups.get(index);
    }

    @Nullable
    public GroupGC findGroupById(String id) {
      for (GroupGC g : getGroups()) {
        if (g.getId().equals(id))
          return g;
      }
      return null;
    }
  }

  @Immutable
  public class GroupGC {
    final Dataset ds;
    public final GribHorizCoordSystem horizCoordSys;
    final List<VariableIndex> variList;
    final List<Coordinate> coords;      // shared coordinates
    final int[] filenose;               // key for GC.fileMap
    private final Map<VariableIndex, VariableIndex> varMap;

    public GroupGC(Dataset ds, GribCollectionMutable.GroupGC gc) {
      this.ds = ds;
      this.horizCoordSys = gc.horizCoordSys;
      this.coords = gc.coords;
      this.filenose = gc.filenose;
      this.varMap = new HashMap<>(gc.variList.size() * 2);

      List<GribCollectionMutable.VariableIndex> gcVars = gc.variList;
      List<VariableIndex> work = new ArrayList<>(gcVars.size());
      for (GribCollectionMutable.VariableIndex gcVar : gcVars) {
        VariableIndex vi = makeVariableIndex(this, gcVar);
        work.add(vi);
        varMap.put(vi, vi);
      }
      this.variList = Collections.unmodifiableList(work);
    }

    public Type getType() {
      return ds.gctype;
    }

    public String getId() {
      return horizCoordSys.getId();
    }

    public Object getGdsHash() {
      return horizCoordSys.getGdsHash();
    }

    public GribCollectionImmutable getGribCollection() {
      return GribCollectionImmutable.this;
    }

    // human readable
    public String getDescription() {
      return horizCoordSys.getDescription();
    }

    public GribHorizCoordSystem getGribHorizCoordSys() {
      return horizCoordSys;
    }

    public GdsHorizCoordSys getGdsHorizCoordSys() {
      return horizCoordSys.getHcs();
    }

    VariableIndex findVariableByHash(VariableIndex vi) {
      return varMap.get(vi);
    }

    public Optional<Coordinate> findCoordinate(String name) {
      return coords.stream().filter(x -> x.getName().equals(name)).findFirst();
    }

    public List<VariableIndex> getVariables() {
      return variList;
    }

    public List<Coordinate> getCoordinates() {
      return coords;
    }

    public int getNruntimes() {
      return masterRuntime.getSize();
    }

    public int getNFiles() {
      if (filenose == null) return 0;
      return filenose.length;
    }

    public List<MFile> getFiles() {
      List<MFile> result = new ArrayList<>();
      if (filenose == null) return result;
      for (int fileno : filenose)
        result.add(fileMap.get(fileno));
      Collections.sort(result);
      return result;
    }

    public CalendarDateRange makeCalendarDateRange() {
      CalendarDateRange result = null;
      for (Coordinate coord : coords) {
        switch (coord.getType()) {
          case time:
          case timeIntv:
          case time2D:
            CoordinateTimeAbstract time = (CoordinateTimeAbstract) coord;
            CalendarDateRange range = time.makeCalendarDateRange(null);
            if (result == null) result = range;
            else result = result.extend(range);
        }
      }

      return result;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("horizCoordSys", horizCoordSys.getDescription())
          .toString();
    }

    public void show(Formatter f) {
      f.format("Group %s (%d) type=%s%n", horizCoordSys.getId(), horizCoordSys.getGdsHash().hashCode(), ds.gctype);
      f.format(" nfiles %d%n", filenose == null ? 0 : filenose.length);
      f.format(" hcs = %s%n", horizCoordSys.getHcs());
    }
  }

  @Immutable      // except for sa
  public class VariableIndex {
    final GroupGC group;     // belongs to this group
    final VariableIndex.Info info;
    final Object gribVariable;

    final List<Integer> coordIndex;  // indexes into group.coords
    final long recordsPos;    // where the records array is stored in the index. 0 means no records
    final int recordsLen;

    // stats
    final int ndups, nrecords, nmissing;

    // read in on demand
    private SparseArray<Record> sa;   // for GC only; lazily read; same array shape as variable, minus x and y

    protected VariableIndex(GroupGC g, GribCollectionMutable.VariableIndex gcVar) {
      this.group = g;
      this.info = new Info(gcVar);
      this.gribVariable = gcVar.gribVariable;

      this.coordIndex = gcVar.coordIndex;
      this.recordsPos = gcVar.recordsPos;
      this.recordsLen = gcVar.recordsLen;

      this.ndups = gcVar.ndups;
      this.nrecords = gcVar.nrecords;
      this.nmissing = gcVar.nmissing;
    }

    public synchronized void readRecords() throws IOException {
      if (this.sa != null) return;

      if (recordsLen == 0)
        return;
      byte[] b = new byte[recordsLen];

      try (RandomAccessFile indexRaf = RandomAccessFile.acquire(indexFilename)) {

        indexRaf.seek(recordsPos);
        indexRaf.readFully(b);

        /*
        message SparseArray {
          repeated uint32 size = 2 [packed=true];     // multidim sizes = shape[]
          repeated uint32 track = 3 [packed=true];    // 1-based index into record list, 0 == missing
          repeated Record records = 4;                // List<Record>
          uint32 ndups = 5;                           // duplicates found when creating
        }
       */
        GribCollectionProto.SparseArray proto = GribCollectionProto.SparseArray.parseFrom(b);

        int nsizes = proto.getSizeCount();
        int[] size = new int[nsizes];
        for (int i = 0; i < nsizes; i++)
          size[i] = proto.getSize(i);

        int ntrack = proto.getTrackCount();
        int[] track = new int[ntrack];
        for (int i = 0; i < ntrack; i++)
          track[i] = proto.getTrack(i);

        int n = proto.getRecordsCount();
        List<Record> records = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
          GribCollectionProto.Record pr = proto.getRecords(i);
          records.add(new Record(pr.getFileno(), pr.getStartPos(), pr.getBmsOffset(), pr.getDrsOffset()));
        }
        int ndups = proto.getNdups();
        this.sa = new SparseArray<>(size, track, records, ndups);

      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
        logger.error(" file={} recordsLen={} recordPos={}", indexFilename, recordsLen, recordsPos);
        throw e;
      }
    }

    synchronized Record getRecordAt(int sourceIndex) {
      return sa.getContent(sourceIndex);
    }

    synchronized Record getRecordAt(int[] sourceIndex) {
      return sa.getContent(sourceIndex);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // coord based record finding. note only one record at a time
    @Nullable
    synchronized Record getRecordAt(SubsetParams coords) {
      int[] want = new int[getRank()];
      int count = 0;
      int runIdx = -1;
      for (Coordinate coord : getCoordinates()) {
        int idx = -1;
        switch (coord.getType()) {
          case runtime:
            CalendarDate runtimeCooord = coords.getRunTime();
            idx = coord.getIndex(runtimeCooord);
            runIdx = idx;
            break;

          case timeIntv:
            double[] timeIntv = coords.getTimeOffsetIntv();
            idx = coord.getIndex(new TimeCoordIntvValue((int) timeIntv[0], (int) timeIntv[1]));
            break;

          case time:
            Double timeOffset = coords.getTimeOffset(); // Double
            int coordInt = timeOffset.intValue();
            idx = coord.getIndex(coordInt);
            break;

          case time2D:
            timeIntv = coords.getTimeOffsetIntv();
            if (timeIntv != null) {
              TimeCoordIntvValue coordTinv = new TimeCoordIntvValue((int) timeIntv[0], (int) timeIntv[1]);
              idx = ((CoordinateTime2D) coord).findTimeIndexFromVal(runIdx, coordTinv); // LOOK can only use if orthogonal
              break;
            }
            Double timeCoord = coords.getTimeOffset();
            if (timeCoord != null) {
              coordInt = timeCoord.intValue();
              idx = ((CoordinateTime2D) coord).findTimeIndexFromVal(runIdx, coordInt);
              break;
            }

          // the OneTime case
          CoordinateTime2D coord2D = (CoordinateTime2D) coord;
          if (coord2D.getNtimes() == 1) {
            idx = 0;
            break;
          }
          throw new IllegalStateException("time2D must have timeOffset ot timeOffsetIntv coordinare");

          case vert:
            double[] vertIntv = coords.getVertCoordIntv();
            if (vertIntv != null) {
              VertCoordValue coordVert = new VertCoordValue(vertIntv[0], vertIntv[1]);
              idx = coord.getIndex(coordVert);
              break;
            }
            Double vertCoord = coords.getVertCoord();
            if (vertCoord != null) {
              VertCoordValue coordVert = new VertCoordValue(vertCoord);
              idx = coord.getIndex(coordVert);
            }
            break;

          case ens:
            Double ensVal = coords.getEnsCoord();
            idx = ((CoordinateEns) coord).getIndexByMember(ensVal);
            break;

          default:
            logger.warn("GribCollectionImmutable: missing CoordVal for {}%n", coord.getName());
        }

        if (idx < 0) {
          logger.debug("Cant find index for value in axis {} in variable {}", coord.getName(), name);
          return null;
        }

        want[count++] = idx;
      }
      return sa.getContent(want);
    }

    public List<Coordinate> getCoordinates() {
      List<Coordinate> result = new ArrayList<>(coordIndex.size());
      for (int idx : coordIndex)
        result.add(group.coords.get(idx));
      return result;
    }

    @Nullable
    public Coordinate getCoordinate(Coordinate.Type want) {
      for (int idx : coordIndex)
        if (group.coords.get(idx).getType() == want)
          return group.coords.get(idx);
      return null;
    }

    @Nullable
    CoordinateTimeAbstract getCoordinateTime() {
      for (int idx : coordIndex)
        if (group.coords.get(idx) instanceof CoordinateTimeAbstract)
          return (CoordinateTimeAbstract) group.coords.get(idx);
      return null;
    }

    // get the ith coordinate
    public Coordinate getCoordinate(int index) {
      int grpIndex = coordIndex.get(index);
      return group.coords.get(grpIndex);
    }

    public Iterable<Integer> getCoordinateIndex() {
      return coordIndex;
    }

    public SparseArray<Record> getSparseArray() {
      return sa;
    }

    public int getNRecords() {
      return sa == null ? -1 : sa.countNotMissing();
    }

    public int getTableVersion() {
      return info.tableVersion;
    }

    public int getDiscipline() {
      return info.discipline;
    }

    public int getCategory() {
      return info.category;
    }

    public int getParameter() {
      return info.parameter;
    }

    public int getLevelType() {
      return info.levelType;
    }

    public int getIntvType() {
      return info.intvType;
    }

    public int getSpatialStatisticalProcessType() {
      return info.spatialStatType;
    }

    public int getEnsDerivedType() {
      return info.ensDerivedType;
    }

    public int getProbType() {
      return info.probType;
    }

    public String getIntvName() {
      return info.intvName;
    }

    public String getProbabilityName() {
      return info.probabilityName;
    }

    public boolean isLayer() {
      return info.isLayer;
    }

    public boolean isEnsemble() {
      return info.isEnsemble;
    }

    public int getGenProcessType() {
      return info.genProcessType;
    }

    public int getNdups() {
      return ndups;
    }

    public GroupGC getGroup() {
      return group;
    }

    public int getNmissing() {
      return nmissing;
    }

    public int getNrecords() {
      return nrecords;
    }

    public int getSize() {
      int size = 1;
      for (int idx : coordIndex) {
        Coordinate c = group.coords.get(idx);
        int csize = (c instanceof CoordinateTime2D) ? ((CoordinateTime2D) c).getNtimes() : c.getSize();
        size *= csize;
      }
      return size;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("info", info)
          .toString();
    }

    public int getRank() {
      return coordIndex.size();
    }

    public String toStringFrom() {
      try (Formatter sb = new Formatter()) {
        sb.format("Variable {%d-%d-%d", info.discipline, info.category, info.parameter);
        sb.format(", levelType=%d", info.levelType);
        sb.format(", intvType=%d", info.intvType);
        sb.format(", nrecords=%d", nrecords);
        sb.format(", ndups=%d", ndups);
        sb.format(", nmiss=%d}", nmissing);
        return sb.toString();
      }
    }

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

      VariableIndex that = (VariableIndex) o;
      return gribVariable.equals(that.gribVariable);
    }

    @Override
    public int hashCode() {
      return gribVariable.hashCode();
    }

    public String makeVariableName() {
      if (isGrib1)
        return ((Grib1Variable) gribVariable).makeVariableName(config.gribConfig);
      else
        return Grib2Iosp.makeVariableNameFromTable((Grib2Tables) cust, GribCollectionImmutable.this, this, config.gribConfig.useGenType);
    }

    public String makeVariableUnits() {
      if (isGrib1)
        return Grib1Iosp.makeVariableUnits((Grib1Customizer) cust, GribCollectionImmutable.this, this);
      else
        return Grib2Iosp.makeVariableUnits((Grib2Tables) cust, this);
    }

    public String makeVariableDescription() {
      if (isGrib1)
        return Grib1Iosp.makeVariableLongName((Grib1Customizer) cust, getCenter(), getSubcenter(), getTableVersion(), getParameter(),
                getLevelType(), isLayer(), getIntvType(), getIntvName(), getProbabilityName());
      else
        return Grib2Iosp.makeVariableLongName((Grib2Tables) cust, this, config.gribConfig.useGenType);
    }

    public GribTables.Parameter getGribParameter() {
      if (isGrib1)
        return ((Grib1Customizer) cust).getParameter(getCenter(), getSubcenter(), getVersion(), getParameter());
      else
        return ((Grib2Tables) cust).getParameter(getDiscipline(), getCategory(), getParameter());
    }

    public GribStatType getStatType() {
      return cust.getStatType(getIntvType());
    }

    @Immutable
    public final class Info {
      final int tableVersion;   // grib1 only : can vary by variable
      final int discipline;     // grib2 only

      // derived from pds
      final int category, parameter, levelType, intvType, ensDerivedType, probType;
      @Nullable final String intvName;  // eg "mixed intervals, 3 Hour, etc"
      final String probabilityName;
      final boolean isLayer, isEnsemble;
      final int genProcessType;
      final int spatialStatType;

      public Info(GribCollectionMutable.VariableIndex gcVar) {
        this.tableVersion = gcVar.tableVersion;
        this.discipline = gcVar.discipline;
        this.category = gcVar.category;
        this.parameter = gcVar.parameter;
        this.levelType = gcVar.levelType;
        this.intvType = gcVar.intvType;
        this.ensDerivedType = gcVar.ensDerivedType;
        this.probType = gcVar.probType;
        this.intvName = gcVar.getTimeIntvName();
        this.probabilityName = gcVar.probabilityName;
        this.isLayer = gcVar.isLayer;
        this.isEnsemble = gcVar.isEnsemble;
        this.genProcessType = gcVar.genProcessType;
        this.spatialStatType = gcVar.spatialStatType;
      }

      @Override
      public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("discipline", discipline)
            .add("category", category)
            .add("parameter", parameter)
            .toString();
      }
    }

  }

  @Immutable
  public static class Record {
    public final int fileno;    // which file
    public final long pos;      // offset on file where message starts
    public final int bmsOffset;   // if non-zero, offset where bms starts (grib2)
    public final int drsOffset;   // if non-zero, offset where drs starts  (grib2)

    public Record(int fileno, long pos, int bmsOffset, int drsOffset) { // }, int scanMode) {
      this.fileno = fileno;
      this.pos = pos;
      this.bmsOffset = bmsOffset;
      this.drsOffset = drsOffset;
    }

    @Override
    public String toString() {
      return "Record{" +
          "fileno=" + fileno +
          ", pos=" + pos +
          ", bmsOffset=" + bmsOffset +
          ", drsOffset=" + drsOffset +
          '}';
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // stuff for FileCacheable

  public synchronized void close() throws java.io.IOException {
    if (objCache != null) {
      objCache.release(this);
    }
  }

  // release any resources like file handles
  public void release() {
  }

  // reacquire any resources like file handles
  public void reacquire() {
  }

  @Override
  public String getLocation() {
    return indexFilename;
  }

  @Override
  public long getLastModified() {
    File indexFile = new File(indexFilename);
    return indexFile.lastModified();
  }

  @Override
  public synchronized void setFileCache(FileCacheIF fileCache) {
    this.objCache = fileCache;
  }

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

  public void showStatus(Formatter f) {
    showIndexFile(f);
    f.format("Class (%s)%n", getClass().getName());
    f.format("%s%n%n", info.toString());

    f.format("masterRuntime: size=%d%n", masterRuntime.getSize());
    if (masterRuntime.getSize() < 20)
      masterRuntime.showCoords(f);

    for (Dataset ds : datasets) {
      f.format("%nDataset %s%n", ds.getType());
      for (GroupGC g : ds.groups) {
        int nrecords = 0, ndups = 0, nmissing = 0;
        f.format(" Group %s%n", g.horizCoordSys.getId());
        for (VariableIndex v : g.variList) {
          f.format("  %s%n", v.toStringFrom());
          nrecords += v.nrecords;
          ndups += v.ndups;
          nmissing += v.nmissing;
        }
        f.format(" Group total nrecords=%d", nrecords);
        f.format(", ndups=%d", ndups);
        f.format(", nmiss=%d%n", nmissing);

        int nruntimes = 0, ntimes = 0, ntimes2D = 0, ntimeIntvs = 0;
        for (Coordinate coord : g.getCoordinates()) {
          if (coord.getType() == Coordinate.Type.runtime) nruntimes++;
          if (coord.getType() == Coordinate.Type.time) ntimes++;
          if (coord.getType() == Coordinate.Type.timeIntv) ntimeIntvs++;
          if (coord.getType() == Coordinate.Type.time2D) ntimes2D++;
        }
        f.format(" Group nruntimes=%d ntimes=%d ntimeIntvs=%d ntimes2D=%d%n", nruntimes, ntimes, ntimeIntvs, ntimes2D);
      }
    }
    if (fileMap == null) {
      f.format("%nFiles empty%n");
    } else {
      f.format("%nFiles count = %d%n", fileMap.size());
      for (int index : fileMap.keySet())
        f.format("  %d: %s%n", index, fileMap.get(index));
      f.format("%n");
    }
  }

  public void showStatusSummary(Formatter f, String type) {
    Dataset ds = getDatasetCanonical();
    if (ds == null) return;

    if (type.equalsIgnoreCase("csv")) {
      for (GroupGC g : ds.groups) {
        int nrecords = 0, ndups = 0, nmissing = 0;
        for (VariableIndex v : g.variList) {
          nrecords += v.nrecords;
          ndups += v.ndups;
          nmissing += v.nmissing;
        }
        if (nrecords == 0) nrecords = 1;
        f.format("%s, %s, %s, %s, %d, %d, %f, %d, %f%n", name, config.type, ds.getType(), g.getDescription(), nrecords, ndups, ((float) ndups / nrecords), nmissing, ((float) nmissing / nrecords));
      }
    } else {
      for (GroupGC g : ds.groups) {
        int nrecords = 0, ndups = 0, nmissing = 0;
        for (VariableIndex v : g.variList) {
          nrecords += v.nrecords;
          ndups += v.ndups;
          nmissing += v.nmissing;
        }
        f.format(" Group %s (%s) total nrecords=%d", g.getDescription(), ds.getType(), nrecords);
        if (nrecords == 0) nrecords = 1;
        f.format(", ndups=%d (%f)", ndups, ((float) ndups / nrecords));
        f.format(", nmiss=%d (%f)%n", nmissing, ((float) nmissing / nrecords));
      }
    }

  }

  private void showIndexFile(Formatter f) {
    if (indexFilename == null) return;
    f.format("indexFile=%s%n", indexFilename);
    try {
      Path indexFile = Paths.get(indexFilename);
      BasicFileAttributes attr = Files.readAttributes(indexFile, BasicFileAttributes.class);
      f.format("  size=%d lastModifiedTime=%s lastAccessTime=%s creationTime=%s%n", attr.size(), attr.lastModifiedTime(), attr.lastAccessTime(), attr.creationTime());
    } catch (IOException e) {
      e.printStackTrace();
    }
    f.format("%n");
  }

  public void showIndex(Formatter f) {
    showIndexFile(f);
    f.format("Class (%s)%n", getClass().getName());
    f.format(" version %d%n", info.version);
    f.format("%s%n%n", toString());
    f.format("%s%n%n", info.toString());

    f.format("masterRuntime: size=%d%n", masterRuntime.getSize());
    if (masterRuntime.getSize() < 200)
      masterRuntime.showCoords(f);

    for (Dataset ds : datasets) {
      f.format("%nDataset %s%n", ds.getType());
      for (GroupGC g : ds.groups) {
        f.format(" Group %s%n", g.horizCoordSys.getId());
        for (VariableIndex v : g.variList) {
          f.format("  %s%n", v.toStringFrom());
        }
      }
    }

    f.format("%n");
    if (fileMap == null) {
      f.format("Files empty%n");
    } else {
      f.format("Files count = %d%n", fileMap.size());
    }
  }

  @Override
  public String toString() {
    return "GribCollectionImmutable{" +
        "name='" + name + '\'' +
        ", directory=" + directory +
        ", config=" + config +
        ", isGrib1=" + isGrib1 +
        ", info=" + info +
        ", masterRuntime=" + masterRuntime.getName() +
        ", dateRange=" + dateRange +
        ", indexFilename='" + indexFilename + '\'' +
        '}';
  }

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

  public long getIndexFileSize() {
    File indexFile = new File(indexFilename);
    return indexFile.length();
  }

  public MFile getFile(int fileno) {
    return fileMap.get(fileno);
  }

  public String getFilename(int fileno) {
    return fileMap.get(fileno).getPath();
  }

  public Collection<MFile> getFiles() {
    return fileMap.values();
  }

  @Nullable
  public MFile findMFileByName(String filename) {
    for (MFile file : fileMap.values())
      if (file.getName().equals(filename))
        return file;
    return null;
  }

  RandomAccessFile getDataRaf(int fileno) throws IOException {
    // absolute location
    MFile mfile = fileMap.get(fileno);
    String filename = mfile.getPath();
    File dataFile = new File(filename);

    // if data file does not exist, check reletive location - eg may be /upc/share instead of Q:
    if (!dataFile.exists()) {
      if (fileMap.size() == 1) {
        dataFile = new File(directory, name); // single file case
      } else {
        dataFile = new File(directory, dataFile.getName()); // must be in same directory as the ncx file
      }
    }

    // data file not here
    if (!dataFile.exists()) {
      throw new FileNotFoundException("data file not found = " + dataFile.getPath());
    }

    RandomAccessFile want = RandomAccessFile.acquire(dataFile.getPath());
    want.order(RandomAccessFile.BIG_ENDIAN);
    return want;
  }

  String getDataRafFilename(int fileno) {
    MFile mfile = fileMap.get(fileno);
    return mfile.getPath();
  }

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

  // stuff needed by InvDatasetFcGrib
  @Nullable
  public abstract ucar.nc2.dataset.NetcdfDataset getNetcdfDataset(Dataset ds, GroupGC group, String filename,
                                                                  FeatureCollectionConfig gribConfig, Formatter errlog, org.slf4j.Logger logger) throws IOException;

  @Nullable
  public abstract ucar.nc2.dt.grid.GridDataset getGridDataset(Dataset ds, GroupGC group, String filename,
                                                              FeatureCollectionConfig gribConfig, Formatter errlog, org.slf4j.Logger logger) throws IOException;

  @Nullable
  public abstract CoverageCollection getGridCoverage(Dataset ds, GroupGC group, String filename,
                                                     FeatureCollectionConfig gribConfig, Formatter errlog, org.slf4j.Logger logger) throws IOException;

}


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

package dap4.core.dmr;

public class DMRFactory
{
    /////////////////////////////////////////////////

    static public boolean DEBUG = false;

    //////////////////////////////////////////////////
    // Constructor
    public DMRFactory()
    {
    }

    //////////////////////////////////////////////////
    // DMRFactory API

    public DapAttribute
    newAttribute(String name, DapType basetype)
    {
        DapAttribute node = new DapAttribute(name, basetype);
        return node;
    }

    public DapAttributeSet
    newAttributeSet(String name)
    {
        DapAttributeSet node = new DapAttributeSet(name);
        if(DEBUG) debug(node);
        return node;
    }

    public DapOtherXML
    newOtherXML(String name)
    {
        DapOtherXML node = new DapOtherXML(name);
        if(DEBUG) debug(node);
        return node;
    }

    public DapDimension
    newDimension(String name, long size)
    {
        DapDimension node = new DapDimension(name, size);
        if(DEBUG) debug(node);
        return node;
    }

    public DapMap
    newMap(DapVariable target)
    {
        DapMap node = new DapMap(target);
        if(DEBUG) debug(node);
        return node;
    }

    public DapVariable
    newVariable(String name, DapType t)
    {
        DapVariable node = new DapVariable(name, t);
        if(DEBUG) debug(node);
        return node;
    }

    public DapGroup
    newGroup(String name)
    {
        DapGroup node = new DapGroup(name);
        if(DEBUG) debug(node);
        return node;
    }

    public DapDataset
    newDataset(String name)
    {
        DapDataset node = new DapDataset(name);
        if(DEBUG) debug(node);
        return node;
    }

    public DapEnumeration
    newEnumeration(String name, DapType basetype)
    {
        DapEnumeration node = new DapEnumeration(name, basetype);
        if(DEBUG) debug(node);
        return node;
    }

    public DapEnumConst
    newEnumConst(String name, long value)
    {
        DapEnumConst node = new DapEnumConst(name, value);
        if(DEBUG) debug(node);
        return node;
    }

    public DapStructure
    newStructure(String name)
    {
        DapStructure node = new DapStructure(name);
        if(DEBUG) debug(node);
        return node;
    }

    public DapSequence
    newSequence(String name)
    {
        DapSequence node = new DapSequence(name);
        if(DEBUG) debug(node);
        return node;
    }

    protected void
    debug(DapNode node)
    {
        System.err.printf("NEW: %s%n", node.toString());
    }

}


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

package ucar.util.prefs.ui;

import ucar.nc2.ui.widget.IndependentDialog;
import ucar.util.prefs.PersistenceManager;
import ucar.util.prefs.PreferencesExt;

import java.util.*;
import java.util.List;
import java.io.IOException;
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;

/**
 * A simple extension to JComboBox, which persists the n latest values.
 * The JComboBox is editable; user can add a new String, then if acceptable,
 * the calling routine should transform it to correct object type and call addItem().
 * When item is added, it is placed on the top of the list, so it is more likely to be saved.
 *
 The items in the list can be any Object type with these caveats:
 *

 *
 item.toString() used as display name
 *
 item.equals() used for object equality
 *
 prefs.putBeanObject() used for storage, so XMLEncoder used, so object must have no-arg Constructor.
 *

 *
 * When listening for change events, generally key on type comboBoxChanged, and you
 *  must explicitly decide to save it in the list:
 *
 *

 *  cb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("comboBoxChanged")) {
          Object select = cb.getSelectedItem());
          if (isOK( select))
            cb.addItem( select);
        }
      }
    });

 * @author John Caron
 */

public class ComboBox extends JComboBox {
  private static final String LIST = "ComboBoxList";
  private boolean deleting = false;

  private PersistenceManager prefs;
  private int nkeep = 20;

  public ComboBox() {
    this( null, 20);
  }

  /**
   * Constructor.
   * @param prefs get/put list here; may be null.
   */
  public ComboBox(PersistenceManager prefs) {
    this( prefs, 20);
  }

  /**
   * Constructor.
   * @param prefs get/put list here; may be null.
   * @param nkeep keep this many when you save.
   */
  public ComboBox(PersistenceManager prefs, int nkeep) {
    super();
    this.prefs = prefs;
    this.nkeep = nkeep;
    setEditable(true);
    setPreferences(prefs);
    addContextMenu();
  }

  public void setPreferences(PersistenceManager prefs) {
    this.prefs = prefs;
    if (prefs != null) {
      ArrayList list = (ArrayList) prefs.getList(LIST, null);
      setItemList(list);
    }
  }

  public JComponent getDeepEditComponent() {
    return (JComponent) getEditor().getEditorComponent();
  }

  private JPopupMenu popupMenu;
  public void addContextMenu() {
    Component editComp = getEditor().getEditorComponent();
    popupMenu = new JPopupMenu();
    editComp.addMouseListener( new PopupTriggerListener() {
      public void showPopup(java.awt.event.MouseEvent e) {
        popupMenu.show(ComboBox.this, e.getX(), e.getY());
      }
    });

    AbstractAction deleteAction = new AbstractAction() {
      public void actionPerformed( java.awt.event.ActionEvent e) {
        final JList delComp= new JList();
        delComp.setModel( getModel());
        delComp.addListSelectionListener(e2 -> {
            int index = delComp.getSelectedIndex();
            deleting = true;
            if (index >= 0) {
              removeItemAt( index);
            }
            deleting = false;
        });

        IndependentDialog iw = new IndependentDialog(null, true, "delete items", delComp);
        iw.show();
      }
    };
    deleteAction.putValue( Action.NAME, "Delete");
    popupMenu.add( deleteAction);

    AbstractAction deleteAllAction = new AbstractAction() {
      public void actionPerformed( java.awt.event.ActionEvent e) {
        setItemList( new ArrayList());
      }
    };
    deleteAllAction.putValue( Action.NAME, "Delete All");
    popupMenu.add( deleteAllAction);

  }

  private static abstract class PopupTriggerListener extends MouseAdapter {
    public void mouseReleased (MouseEvent e) { if(e.isPopupTrigger()) showPopup(e); }
    public abstract void showPopup(MouseEvent e);
  }

  protected void fireActionEvent() {
    if (deleting) return; // no events while deleting
    super.fireActionEvent();
  }

  /**
   * Add the item to the top of the list. If it already exists, move it to the top.
   * @param item to be added.
   */
  public void addItem( Object item) {
    if (item == null) return;
    for (int i=0; i<getItemCount(); i++) {
      if (item.equals( getItemAt(i))) {
        if (i == 0) {
          setSelectedIndex(0);
          return; // already there
        }
        removeItemAt(i);
      }
    }

    // add as first in the list
    insertItemAt( item, 0);
    setSelectedIndex(0);
  }

  /** Save the last n items to PreferencesExt. */
  public void save() {
    if (prefs != null)
      prefs.putList(LIST, getItemList());
  }

  /**
   * Use this to obtain the list of items.
   * @return ArrayList of items, may be any Object type.
   */
  public List<Object> getItemList() {
    ArrayList<Object> list = new ArrayList<Object>();
    for (int i=0; i< getItemCount() && i < nkeep; i++)
      list.add( getItemAt(i));
    return list;
  }

  /**
   * Use this to set the list of items.
   * @param list of items, may be any Object type.
   */
  public void setItemList(Collection<Object> list) {
    if (list == null) return;
    setModel( new DefaultComboBoxModel( list.toArray()));

    if (list.size() > 0)
      setSelectedIndex(0);
  }

  /** Set the number of items to keep */
  public void setNkeep( int nkeep) { this.nkeep = nkeep; }
  /** Get the number of items to keep */
  public int getNkeep() { return nkeep; }

  /** Get value from Store, will be an ArrayList or null */
  protected Object getStoreValue(Object defValue) {
    if (prefs == null)
      return defValue;
    return ((PreferencesExt)prefs).getBean(LIST, defValue);
  }

  /** Put new value into Store, must be a List of Strings */
  protected void setStoreValue(List newValue) {
    if (prefs != null)
      prefs.putList(LIST, newValue);
  }

  // debug
  private static long lastEvent;
  public static void main(String args[]) throws IOException {

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

    final ComboBox cb = new ComboBox( null);
    cb.addActionListener(e -> {
        System.out.println("**** cb event="+e);
        if (e.getActionCommand().equals("comboBoxChanged")) {
          //System.out.println("cb.getSelectedItem="+cb.getSelectedItem());
          cb.addItem( cb.getSelectedItem());
        }
    });
    cb.getEditor().getEditorComponent().setForeground(Color.red);

    /* JButton butt = new JButton("accept");
   butt.addActionListener( new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("butt accept");
        cb.accept();
     }
   }); */

    JPanel main = new JPanel();
    main.add(cb);
    // main.add(butt);

    frame.getContentPane().add(main);
    // cb.setPreferredSize(new java.awt.Dimension(500, 200));

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

}


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

package ucar.unidata.geoloc.vertical;

import ucar.nc2.Variable;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Dimension;
import ucar.ma2.*;
import ucar.ma2.ArrayDouble.D1;
import ucar.unidata.util.Parameter;

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

/**
 * Implement CF vertical coordinate "atmosphere_ln_pressure_coordinate"
 * pressure(z) = p0 * exp(-lev(k))" .
 *
 * Theres a problem here, since its not 3D, we dont know what the 2D extent is.
 * DO NOT USE: see CF1Convention.makeAtmLnCoordinate()

 * @author caron
 * @see http://cf-pcmdi.llnl.gov/
 * @since May 6, 2008
 */
public class AtmosLnPressure extends VerticalTransformImpl {
  public static final String P0 = "ReferencePressureVariableName";
  public static final String LEV = "VerticalCoordinateVariableName";

  private final Array pressure;

  /**
   * Create a new vertical transform for Ocean S coordinates
   *
   * @param ds      dataset
   * @param timeDim time dimension
   * @param params  list of transformation Parameters
   */
  public AtmosLnPressure(NetcdfFile ds, Dimension timeDim, List<Parameter> params) {
    super(timeDim);

    String p0name = getParameterStringValue(params, P0);
    Variable p0var = ds.findVariable(p0name);
    double p0;
    try {
      p0 = p0var.readScalarDouble();
    } catch (IOException e) {
      throw new IllegalArgumentException("AtmosLnPressure failed to read "
          + p0name + " err= " + e.getMessage());
    }

    String levName = getParameterStringValue(params, LEV);
    Variable levVar = ds.findVariable(levName);

    try {
      Array lev = levVar.read();
      assert lev.getRank() == 1;
      pressure = new ArrayDouble.D1( (int) lev.getSize());
      IndexIterator ii = pressure.getIndexIterator();
      while (lev.hasNext()) {
        double result = p0 * Math.exp(-lev.nextDouble());
        ii.setDoubleNext( result );
      }

    } catch (IOException e) {
      throw new IllegalArgumentException("AtmosLnPressure failed to read " + levName + " err= " + e.getMessage());
    }

    units = p0var.getUnitsString();
  }

  /**
   * Get the 3D vertical coordinate array for this time step.
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @return vertical coordinate array
   * @throws IOException           problem reading data
   * @throws ucar.ma2.InvalidRangeException _more_
   */
  public ArrayDouble.D3 getCoordinateArray(int timeIndex) throws IOException, InvalidRangeException {

    int nz = (int) pressure.getSize();
    int[] shape2D = pressure.getShape();
    int ny = shape2D[0];
    int nx = shape2D[1];

    ArrayDouble.D3 result = new ArrayDouble.D3(nz, ny, nx);

    IndexIterator ii = pressure.getIndexIterator();
    for (int z = 0; z < nz; z++) {
      double p = ii.getDoubleNext();
      for (int y = 0; y < ny; y++) {
        for (int x = 0; x < nx; x++) {
          result.set(z, y, x, p);
        }
      }
    }

    return result;
  }

  /**
   * Get the 1D vertical coordinate array for this time step and point
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @param xIndex    the x index
   * @param yIndex    the y index
   * @return vertical coordinate array
   * @throws java.io.IOException problem reading data
   * @throws ucar.ma2.InvalidRangeException _more_
   */
  public D1 getCoordinateArray1D(int timeIndex, int xIndex, int yIndex)
  		throws IOException, InvalidRangeException {

	  throw new UnsupportedOperationException("1D subsetting is not implemented yet for this vertical tranformation");
  }

}