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

package ucar.httpservices;

import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static ucar.httpservices.HTTPSession.Prop;

/**
 * HTTPMethod is the encapsulation of specific
 * kind of server request: GET, HEAD, POST, etc.
 * The general processing sequence is as follows.
 *

 *
 Create an HTTPMethod object using one of the
 * methods of HTTPFactory (e.g. HTTPFactory.Get()).
 *

 *
 Set parameters and headers of the returned HTTPMethod instance.
 *

 *
 Invoke the execute() method to actually make
 * the request.
 *

 *
 Extract response headers.
 *

 *
 Extract any body of the response in one of several forms:
 * an Inputstream, a byte array, or a String.
 *

 *
 Close the method.
 *

 * HTTPMethod is designed, at the moment, to be executed only once.
 * Note also that HTTPMethod is not thread safe but since it cannot be
 * executed multiple times, this should be irrelevant.
 *

 * The arguments to the factory method are as follows.
 *

 *
 An HTTPSession instance (optional).
 *
 A URL.
 *

 * An HTTPMethod instance is assumed to be operating in the context
 * of an HTTPSession instance as specified by the session argument.
 * If not present, the HTTPMethod instance will create a session
 * that will be reclaimed when the method is closed
 * (see the discussion about one-shot operation below).
 *

 * Method URLs may be specified in any of three ways.
 *

 *
 It may be inherited from the URL specified when
 * the session was created.
 *

 *
 It may be specified as part of the HTTPMethod
 * constructor (via the factory). If none is specified,
 * then the session URL is used.
 *

 *

 * Legal url arguments to HTTPMethod are constrained by the URL
 * specified in creating the HTTPSession instance.  If
 * the session was constructed with a specified URL, then any
 * url specified to HTTMethod (via the factory)
 * must be "compatible" with the session URL.
 * The term "compatible" basically means that the session url's
 * host+port is the same as that of the specified method url.
 * This maintains the semantics of the Session but allows
 * flexibility in accessing data from the server.
 *

 * One-Shot Operation:
 * A reasonably common use case is when a client
 * wants to create a method, execute it, get the response,
 * and close the method. For this use case, creating a session
 * and making sure it gets closed can be a tedious proposition.
 * To support this use case, HTTPMethod supports what amounts
 * to a one-shot use. The steps are as follows:
 *

 *
 HTTPMethod method = HTTPFactory.Get(<url string>); note
 * that this implicitly creates a session internal to the
 * method instance.
 *

 *
 Set any session parameters or headers using method.getSession().setXXX
 *

 *
 Set any parameters and headers on method
 *

 *
 method.execute();
 *

 *
 Get any response method headers
 *

 *
 InputStream stream = method.getResponseBodyAsStream()
 *

 *
 process the stream
 *

 *
 stream.close()
 *

 * There are several things to note.
 *

 *
 Closing the method (directly or through stream.close())
 * will close the one-shot session created by the method.
 *
 Closing the stream will close the underlying method, so it is not
 * necessary to call method.close().
 * However, if you, for example, get the response body using getResponseBodyAsString(),
 * then you need to explicitly call method.close().
 * The reason is that the stream is likely to be passed out of the scope in which the
 * method was created, hence method.close() is not easily accessible. In the second case,
 * however, this will occur in the same scope as the method and so method.close()
 * is accessible.
 *

 *

 * For testing purposes, and to allow use of Spring Servlet Mocking,
 * it is possible to set a special execution action (see executeRaw).
 */

@NotThreadSafe
public class HTTPMethod implements Closeable, Comparable<HTTPMethod>
{
    static final boolean DEBUG = false;

    //////////////////////////////////////////////////
    // Type Decl

    static public interface Executor
    {
        public HttpResponse execute(HttpRequestBase rq) throws IOException;
    }

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

    public static boolean TESTING = false;

    /* External tests can Set this to true if they are using Spring Servlet Mocking */
    public static Executor MOCKEXECUTOR = null;

    //////////////////////////////////////////////////
    // Instance fields

    protected HTTPSession session = null;
    protected boolean localsession = false;
    protected URI methodurl = null;
    protected String userinfo = null;
    protected HttpEntity content = null;
    protected HTTPSession.Methods methodkind = null;
    protected HTTPMethodStream methodstream = null; // wrapper for strm
    protected HttpRequestBase lastrequest = null;
    protected HttpResponse lastresponse = null;
    protected long[] range = null;
    // State tracking
    protected boolean closed = false;
    protected boolean executed = false;

    protected Map<String, String> headers = new HashMap<String, String>();

    // At the point of execution, the settings of the parent session are
    // captured as immutable

    protected Map<Prop, Object> settings = null;

    // For debugging
    protected RequestConfig debugconfig = null;

    //////////////////////////////////////////////////
    // Constructor(s)
    // These are package scope to prevent public instantiation

    protected HTTPMethod()
            throws HTTPException
    {
    }

    HTTPMethod(HTTPSession.Methods m, String url)
            throws HTTPException
    {
        this(m, null, url);
    }

    HTTPMethod(HTTPSession.Methods m, HTTPSession session)
            throws HTTPException
    {
        this(m, session, null);
    }

    HTTPMethod(HTTPSession.Methods m, HTTPSession session, String url)
            throws HTTPException
    {
        if(HTTPSession.TESTING) HTTPMethod.TESTING = true;

        boolean escapeUrl = Boolean.valueOf(System.getProperty("httpservices.urlencode", "true"));

        url = HTTPUtil.nullify(url);
        if(url == null && session != null)
            url = session.getSessionURI();
        if(url == null)
            throw new HTTPException("HTTPMethod: cannot find usable url");

        try {
            this.methodurl = escapeUrl ? new URI(Escape.escapeURL(url)) : new URI(url); /// validate
        } catch (URISyntaxException mue) {
            throw new HTTPException("Malformed URL: " + url, mue);
        }

        // Check method and url compatibiltiy

        if(session == null) {
            session = HTTPFactory.newSession(url);
            localsession = true;
        }
        this.session = session;
        this.userinfo = HTTPUtil.nullify(this.methodurl.getUserInfo());
        if(this.userinfo != null) {
            if(!TESTING)
                this.methodurl = HTTPUtil.uriExclude(this.methodurl, HTTPUtil.URIPart.USERINFO);
            // convert userinfo to credentials
            this.session.setCredentials(
                    new UsernamePasswordCredentials(this.userinfo));
        }
        this.session.addMethod(this);
        this.methodkind = m;
    }

    public int compareTo(HTTPMethod o)
    {
        if(o == this) return 0;
        if(o == null) return -1;
        return (this.hashCode() - o.hashCode());
    }

    protected void
    setcontent(RequestBuilder rb)
    {
        switch (this.methodkind) {
        case Put:
            if(this.content != null)
                rb.setEntity(this.content);
            break;
        case Post:
            if(this.content != null)
                rb.setEntity(this.content);
            break;
        case Head:
        case Get:
        case Options:
        default:
            break;
        }
        this.content = null; // do not reuse
    }

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

    /**
     * Calling close will force the method to close, and will
     * force any open stream to terminate. If the session is local,
     * Then that too will be closed.
     */
    public synchronized void
    close()
    {
        if(closed)
            return; // recursive calls ok
        closed = true; // mark as closed to prevent recursive calls
        if(methodstream != null) {
            try {
                this.methodstream.close(); // May recursr
            } catch (IOException ioe) {/*failure is ok*/}
            this.methodstream = null;
        }
        // Force release underlying connection back to the connection manager
        if(this.lastresponse != null) {
            if(false) {
                try {
                    try {
                        // Attempt to keep connection alive by consuming its remaining content
                        EntityUtils.consume(this.lastresponse.getEntity());
                    } finally {
                        HttpClientUtils.closeQuietly(this.lastresponse); // Paranoia
                    }
                } catch (IOException ignore) {/*ignore*/}
            } else
                HttpClientUtils.closeQuietly(this.lastresponse);
            this.lastresponse = null;
        }
        if(session != null) {
            session.removeMethod(this);
            if(localsession) {
                session.close();
                session = null;
            }
        }
        this.lastrequest = null;
    }

    //////////////////////////////////////////////////
    // Execution support

    /**
     * Create a request, add headers, and content,
     * then send to HTTPSession to do the bulk of the work.
     *
     * @return statuscode
     */

    public int execute()
            throws HTTPException
    {
        HttpResponse res = executeRaw();
        if(res != null)
            return res.getStatusLine().getStatusCode();
        else
            throw new HTTPException("HTTPMethod.execute: null response");
    }

    /**
     * Create a request, add headers, and content,
     * then send to HTTPSession to do the bulk of the work.
     *
     * @return statuscode
     */
    // debug only
    public HttpResponse
    executeRaw()
            throws HTTPException
    {
        if(this.closed)
            throw new IllegalStateException("HTTPMethod: attempt to execute closed method");
        if(this.executed)
            throw new IllegalStateException("HTTPMethod: attempt to re-execute method");
        this.executed = true;
        if(this.methodurl == null)
            throw new HTTPException("HTTPMethod: no url specified");
        if(!localsession && !sessionCompatible(this.methodurl))
            throw new HTTPException("HTTPMethod: session incompatible url: " + this.methodurl);

        // Capture the current state of the parent HTTPSession; never to be modified in this class
        this.settings = session.mergedSettings();

        try {
            // add range header
            if(this.range != null) {
                this.headers.put("Range", "bytes=" + range[0] + "-" + range[1]);
                range = null;
            }
            RequestBuilder rb = getRequestBuilder();
            setcontent(rb);
            setheaders(rb, this.headers);
            this.lastrequest = buildRequest(rb, this.settings);
            AuthScope methodscope = HTTPAuthUtil.uriToAuthScope(this.methodurl);
            AuthScope target = HTTPAuthUtil.authscopeUpgrade(session.getSessionScope(), methodscope);
            // AFAIK, targethost, httpclient, rb, and session
            // contain non-overlapping info => we cannot derive one
            // from any of the others.
            HttpHost targethost = HTTPAuthUtil.authscopeToHost(target);
            HttpClientBuilder cb = HttpClients.custom();
            configClient(cb, this.settings);
            session.setAuthenticationAndProxy(cb);
            HttpClient httpclient = cb.build();
            if(MOCKEXECUTOR != null) {
                URI uri = this.lastrequest.getURI();
                this.lastresponse = MOCKEXECUTOR.execute(this.lastrequest);
            } else {
                this.lastresponse = httpclient.execute(targethost, this.lastrequest, session.getContext());
            }
            if(this.lastresponse == null)
                throw new HTTPException("HTTPMethod.execute: Response was null");
            return this.lastresponse;
        } catch (IOException ioe) {
            throw new HTTPException(ioe);
        }
    }

    protected RequestConfig
    buildRequestConfig(Map<Prop, Object> settings)
            throws HTTPException
    {
        // Configure the RequestConfig
        HttpRequestBase request;
        RequestConfig.Builder rcb = RequestConfig.custom();
        for(Prop key : settings.keySet()) {
            Object value = settings.get(key);
            boolean tf = (value instanceof Boolean ? (Boolean) value : false);
            if(key == Prop.ALLOW_CIRCULAR_REDIRECTS) {
                rcb.setCircularRedirectsAllowed(tf);
            } else if(key == Prop.HANDLE_REDIRECTS) {
                rcb.setRedirectsEnabled(tf);
                rcb.setRelativeRedirectsAllowed(tf);
            } else if(key == Prop.MAX_REDIRECTS) {
                rcb.setMaxRedirects((Integer) value);
            } else if(key == Prop.SO_TIMEOUT) {
                rcb.setSocketTimeout((Integer) value);
            } else if(key == Prop.CONN_TIMEOUT) {
                rcb.setConnectTimeout((Integer) value);
            } else if(key == Prop.CONN_REQ_TIMEOUT) {
                rcb.setConnectionRequestTimeout((Integer) value);
            } /* else ignore */
        }
        RequestConfig cfg = rcb.build();
        if(TESTING)
            this.debugconfig = cfg;
        return cfg;
    }

    protected void
    configClient(HttpClientBuilder cb, Map<Prop, Object> settings)
            throws HTTPException
    {
        cb.useSystemProperties();
        String agent = (String) settings.get(Prop.USER_AGENT);
        if(agent != null)
            cb.setUserAgent(agent);
        session.setInterceptors(cb);
        session.setContentDecoderRegistry(cb);
        session.setClientManager(cb, this);
        session.setRetryHandler(cb);
    }

    protected void
    setheaders(RequestBuilder rb, Map<String, String> headers)
    {
        // Add any defined headers
        if(headers.size() > 0) {
            for(HashMap.Entry<String, String> entry : headers.entrySet()) {
                rb.addHeader(entry.getKey(), entry.getValue());
            }
        }
    }

    protected RequestBuilder
    getRequestBuilder()
            throws HTTPException
    {
        if(this.methodurl == null)
            throw new HTTPException("Null url");
        RequestBuilder rb = null;
        switch (this.methodkind) {
        case Put:
            rb = RequestBuilder.put();
            break;
        case Post:
            rb = RequestBuilder.post();
            break;
        case Head:
            rb = RequestBuilder.head();
            break;
        case Options:
            rb = RequestBuilder.options();
            break;
        case Get:
        default:
            rb = RequestBuilder.get();
            break;
        }
        rb.setUri(this.methodurl);
        return rb;
    }

    protected HttpRequestBase
    buildRequest(RequestBuilder rb, Map<Prop, Object> settings)
            throws HTTPException
    {
        HttpRequestBase req;
        RequestConfig config = buildRequestConfig(settings);
        rb.setConfig(config);
        req = (HttpRequestBase) rb.build();
        if(req == null)
            throw new HTTPException("HTTPMethod.buildrequest: requestbuilder failed");
        return req;
    }

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

    public String
    getMethodKind()
    {
        return this.methodkind.name();
    }

    public int getStatusCode()
    {
        return (this.lastresponse == null) ? 0 : this.lastresponse.getStatusLine().getStatusCode();
    }

    public String getStatusLine()
    {
        return (this.lastresponse == null) ? null : this.lastresponse.getStatusLine().toString();
    }

    public String getRequestLine()
    {
        //fix: return (method == null ? null : request.getRequestLine().toString());
        throw new UnsupportedOperationException("getrequestline not implemented");
    }

    public String getPath()
    {
        return this.methodurl.toString();
    }

    public boolean canHoldContent()
    {
        return (this.methodkind == HTTPSession.Methods.Head);
    }

    public InputStream getResponseBodyAsStream()
    {
        return getResponseAsStream();
    }

    public InputStream getResponseAsStream()
    {
        if(closed)
            throw new IllegalStateException("HTTPMethod: method is closed");
        if(!executed)
            throw new IllegalStateException("HTTPMethod: method has not been executed");
        if(this.methodstream != null) { // duplicate: caller's problem
            HTTPSession.log.warn("HTTPRequest.getResponseBodyAsStream: Getting method stream multiple times");
        } else { // first time
            HTTPMethodStream stream = null;
            try {
                if(this.lastresponse == null) return null;
                stream = new HTTPMethodStream(this.lastresponse.getEntity().getContent(), this);
            } catch (Exception e) {
                stream = null;
            }
            this.methodstream = stream;
        }
        return this.methodstream;
    }

    public byte[] getResponseAsBytes(int maxbytes)
    {
        byte[] contents = getResponseAsBytes();
        if(contents != null && contents.length > maxbytes) {
            byte[] result = new byte[maxbytes];
            System.arraycopy(contents, 0, result, 0, maxbytes);
            contents = result;
        }
        return contents;
    }

    public byte[] getResponseAsBytes()
    {
        if(closed)
            throw new IllegalStateException("HTTPMethod: method is closed");
        byte[] content = null;
        if(this.lastresponse != null)
            try {
                content = EntityUtils.toByteArray(this.lastresponse.getEntity());
            } catch (Exception e) {/*ignore*/}
        return content;
    }

    public String getResponseAsString(String charset)
    {
        if(closed)
            throw new IllegalStateException("HTTPMethod: method is closed");
        String content = null;
        if(this.lastresponse != null)
            try {
                Charset cset = Charset.forName(charset);
                content = EntityUtils.toString(this.lastresponse.getEntity(), cset);
            } catch (Exception e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        close();//getting the response will disallow later stream
        return content;
    }

    public String getResponseAsString()
    {
        return getResponseAsString("UTF-8");
    }

    public Header getRequestHeader(String name)
    {
        Header[] hdrs = getRequestHeaders();
        if(hdrs != null)
            for(Header h : hdrs) {
                if(h.getName().equals(name))
                    return h;
            }
        return null;
    }

    public Header getResponseHeader(String name)
    {
        try {
            return this.lastresponse == null ? null : this.lastresponse.getFirstHeader(name);
        } catch (Exception e) {
            return null;
        }
    }

    public Header[] getResponseHeaders()
    {
        try {
            if(this.lastresponse == null)
                return null;
            Header[] hs = this.lastresponse.getAllHeaders();
            return hs;
        } catch (Exception e) {
            return null;
        }
    }

    public HTTPMethod setRequestContent(HttpEntity content)
    {
        if(DEBUG) try {
            InputStream stream = content.getContent();
            String report = HTTPUtil.readtextfile(stream);
            System.err.println("DEBUG: request content: "+report);
        } catch (IOException | UnsupportedOperationException e) {
             System.err.println("Cannot print content");
        }

        this.content = content;
        return this;
    }

    public String getCharSet()
    {
        return "UTF-8";
    }

    public URI getURI()
    {
        return this.methodurl;
    }

   /* public String getProtocolVersion()
    {
        String ver = null;
        if(request != null) {
            ver = request.getProtocolVersion().toString();
        }
        return ver;
    }  */

    public String getStatusText()
    {
        return getStatusLine();
    }

    // Convenience methods to minimize changes elsewhere

    public String getResponseCharSet()
    {
        return "UTF-8";
    }

    public HTTPSession
    getSession()
    {
        return this.session;
    }

    public boolean
    isSessionLocal()
    {
        return this.localsession;
    }

    public boolean hasStreamOpen()
    {
        return methodstream != null;
    }

    public boolean isClosed()
    {
        return this.closed;
    }

    public HTTPMethod
    setRange(long lo, long hi)
    {
        range = new long[]{lo, hi};
        return this;
    }

    public Header[] getRequestHeaders()
    {
        return this.lastrequest == null ? null : this.lastrequest.getAllHeaders();
    }

    //////////////////////////////////////////////////
    // Pass thru's to HTTPSession

    public HTTPMethod
    setCompression(String compressors)
    {
        this.session.setGlobalCompression(compressors);
        return this;
    }

    public HTTPMethod setFollowRedirects(boolean tf)
    {
        this.session.setFollowRedirects(tf);
        return this;
    }

    public HTTPMethod setMaxRedirects(int n)
    {
        this.session.setMaxRedirects(n);
        return this;
    }

    public HTTPMethod setSOTimeout(int n)
    {
        this.session.setSoTimeout(n);
        return this;
    }

    public HTTPMethod setUserAgent(String agent)
    {
        this.session.setUserAgent(agent);
        return this;
    }

    public HTTPMethod setUseSessions(boolean tf)
    {
        this.session.setUseSessions(tf);
        return this;
    }

    public HTTPMethod setCredentials(Credentials creds)
            throws HTTPException
    {
        this.session.setCredentials(creds);
        return this;
    }

    public HTTPMethod
    setCredentials(Credentials creds, AuthScope scope)
            throws HTTPException
    {
        this.session.setCredentials(creds, scope);
        return this;
    }

    //////////////////////////////////////////////////
    // Utilities

    /**
     * Test that the given url is "compatible" with the
     * session specified dataset. Wrapper around
     * HTTPAuthUtil.httphostCompatible().
     *
     * @param other to test for compatibility against this method's
     * @return true if compatible, false otherwise.
     */
    protected boolean
    sessionCompatible(AuthScope other)
    {
        return HTTPAuthUtil.authscopeCompatible(session.getAuthScope(), other);
    }

    protected boolean
    sessionCompatible(URI otheruri)
    {
        AuthScope other = HTTPAuthUtil.uriToAuthScope(otheruri);
        return sessionCompatible(other);
    }

    @Deprecated
    protected boolean sessionCompatible(HttpHost otherhost)
    {
        AuthScope other = HTTPAuthUtil.hostToAuthScope(otherhost);
        return sessionCompatible(other);
    }

    //////////////////////////////////////////////////
    // debug interface

    public RequestConfig
    getDebugConfig()
    {
        if(!TESTING) throw new UnsupportedOperationException();
        return this.debugconfig;
    }

    public HttpRequestBase debugRequest()
    {
        if(!TESTING) throw new UnsupportedOperationException();
        return (this.lastrequest);
    }

    public HttpResponse debugResponse()
    {
        if(!TESTING) throw new UnsupportedOperationException();
        return (this.lastresponse);
    }

    //////////////////////////////////////////////////
    // Deprecated but for back compatibility

    /**
     * Deprecated: use getMethodKind
     *
     * @return Name of the method: e.g. GET, HEAD, ...
     */
    @Deprecated
    public String
    getName()
    {
        return getMethodKind();
    }

    @Deprecated
    public HTTPMethod
    setMethodHeaders(List headers) throws HTTPException
    {
        try {
            for(Header h : headers) {
                this.headers.put(h.getName(), h.getValue());
            }
        } catch (Exception e) {
            throw new HTTPException(e);
        }
        return this;
    }

    @Deprecated
    public HTTPMethod
    setRequestHeader(String name, String value) throws HTTPException
    {
        return setRequestHeader(new BasicHeader(name, value));
    }

    @Deprecated
    protected HTTPMethod
    setRequestHeader(Header h) throws HTTPException
    {
        try {
            this.headers.put(h.getName(), h.getValue());
        } catch (Exception e) {
            throw new HTTPException("cause", e);
        }
        return this;
    }

}


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

package ucar.nc2.ft.point.standard.plug;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import ucar.ma2.DataType;
import ucar.nc2.Dimension;
import ucar.nc2.Structure;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.CoordSysBuilder;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.ft.point.standard.CoordSysEvaluator;
import ucar.nc2.ft.point.standard.Evaluator;
import ucar.nc2.ft.point.standard.JoinArray;
import ucar.nc2.ft.point.standard.Table;
import ucar.nc2.ft.point.standard.TableConfig;
import ucar.nc2.ft.point.standard.TableConfigurerImpl;

/**
 * CF "point obs" Convention.
 *
 * @author caron
 * @see "http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.6/cf-conventions.html#discrete-sampling-geometries"
 * @see "http://www.unidata.ucar.edu/software/netcdf-java/reference/FeatureDatasets/CFencodingTable.html"
 * @since Nov 3, 2008
 */
public class CFpointObs extends TableConfigurerImpl {

  protected enum Encoding {
    single,          // single feature in files
    multidim,        // feature(s1, s2, ...)
    raggedContiguous,// row_size
    raggedIndex,     // stn_index
    flat,            // not used
   // multiStructure   // multiple structures (nc4)
  }

  public boolean isMine(FeatureType wantFeatureType, NetcdfDataset ds) {
    String conv = ds.findAttValueIgnoreCase(null, CDM.CONVENTIONS, null);
    if (conv == null) return false;

    List<String> names = CoordSysBuilder.breakupConventionNames(conv);
    for (String s : names) {
      if (s.startsWith("CF-1"))
        return true;
    }
    return false;
  }

  public TableConfig getConfig(FeatureType wantFeatureType, NetcdfDataset ds, Formatter errlog) throws IOException {
    EncodingInfo info = new EncodingInfo();

    // figure out the actual feature type of the dataset
    CF.FeatureType ftype = CF.FeatureType.getFeatureTypeFromGlobalAttribute(ds);
    if (ftype == null) ftype = CF.FeatureType.point;

    // make sure lat, lon, time coordinates exist
    if (!checkCoordinates(ds, info, errlog)) return null; // fail fast

    switch (ftype) {
      case point:
        return getPointConfig(ds, info, errlog);
      case timeSeries:
        return getStationConfig(ds, info, errlog);
      case profile:
        return getProfileConfig(ds, info, errlog);
      case trajectory:
        return getTrajectoryConfig(ds, info, errlog);
      case timeSeriesProfile:
        return getTimeSeriesProfileConfig(ds, info, errlog);
      case trajectoryProfile:
        return getSectionConfig(ds, info, errlog);
    }

    return null;
  }

  protected boolean checkCoordinates(NetcdfDataset ds, EncodingInfo info, Formatter errlog) {
    boolean ok = true;
    info.time = CoordSysEvaluator.findCoordByType(ds, AxisType.Time);
    if (info.time == null) {
      errlog.format("CFpointObs cant find a Time coordinate %n");
      ok = false;
    }

    // find lat coord
    info.lat = CoordSysEvaluator.findCoordByType(ds, AxisType.Lat);
    if (info.lat == null) {
      errlog.format("CFpointObs cant find a Latitude coordinate %n");
      ok = false;
    }

    // find lon coord
    info.lon = CoordSysEvaluator.findCoordByType(ds, AxisType.Lon);
    if (info.lon == null) {
      errlog.format("CFpointObs cant find a Longitude coordinate %n");
      ok = false;
    }

    if (!ok) return false;

    // dimensions must match
    List<Dimension> dimLat = info.lat.getDimensions();
    List<Dimension> dimLon = info.lon.getDimensions();
    if (!dimLat.equals(dimLon)) {
      errlog.format("CFpointObs Lat, Lon coordinate dimensions must match lat=%s lon=%s %n", info.lat.getNameAndDimensions(), info.lon.getNameAndDimensions());
      ok = false;
    }

    return ok;
  }

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

  protected TableConfig getPointConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (info.time.getRank() != 1) {
      errlog.format("CFpointObs type=point: coord time must have rank 1, coord var= %s %n", info.time.getNameAndDimensions());
      return null;
    }
    Dimension obsDim = info.time.getDimension(0);

    TableConfig obsTable = makeSingle(ds, obsDim, errlog);
    obsTable.featureType = FeatureType.POINT;
    return obsTable;
  }

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

  protected TableConfig getStationConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (!identifyEncodingStation(ds, info, CF.FeatureType.timeSeries, errlog))
      return null;

    // make station table
    TableConfig stnTable = makeStationTable(ds, FeatureType.STATION, info, errlog);
    if (stnTable == null) return null;

    Dimension obsDim = info.childDim;
    TableConfig obsTable = null;
    switch (info.encoding) {
      case single:
        obsTable = makeSingle(ds, obsDim, errlog);
        break;

      case multidim:
        obsTable = makeMultidimInner(ds, stnTable, info.childDim, info, errlog);
        if (info.time.getRank() == 1) { // join time(time)
          obsTable.addJoin(new JoinArray(info.time, JoinArray.Type.raw, 0));
          obsTable.time = info.time.getFullName();
        }
        break;

      case raggedContiguous:
        stnTable.numRecords = info.ragged_rowSize.getFullName();
        obsTable = makeRaggedContiguousChildTable(ds, info.parentDim, info.childDim, info.childStruct, errlog);
        break;

      case raggedIndex:
        obsTable = makeRaggedIndexChildTable(ds, info.parentDim, info.childDim, info.ragged_parentIndex, errlog);
        break;

      case flat:
        info.set(Encoding.flat, obsDim);
        obsTable = makeStructTable(ds, FeatureType.STATION, info, errlog);
        obsTable.parentIndex = (info.instanceId == null) ? null : info.instanceId.getFullName();
        Variable stnIdVar = Evaluator.findVariableWithAttributeAndDimension(ds, CF.CF_ROLE, CF.STATION_ID, obsDim, errlog);
        if (stnIdVar == null)
          stnIdVar = Evaluator.findVariableWithAttributeAndDimension(ds, CF.STANDARD_NAME, CF.STATION_ID, obsDim, errlog);
        obsTable.stnId = (stnIdVar == null) ? null : stnIdVar.getFullName();
        obsTable.stnDesc = Evaluator.findNameOfVariableWithAttributeValue(ds, CF.STANDARD_NAME, CF.PLATFORM_NAME);
        if (obsTable.stnDesc == null)
          obsTable.stnDesc = Evaluator.findNameOfVariableWithAttributeValue(ds, CF.STANDARD_NAME, CF.STATION_DESC);
        obsTable.stnWmoId = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.STATION_WMOID, obsDim, errlog);
        obsTable.stnAlt = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.SURFACE_ALTITUDE, obsDim, errlog);
        if (obsTable.stnAlt == null)
          obsTable.stnAlt = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.STATION_ALTITUDE, obsDim, errlog);
        break;
    }
    if (obsTable == null) return null;

    stnTable.addChild(obsTable);
    return stnTable;
  }

  ////

  protected TableConfig getProfileConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (!identifyEncodingProfile(ds, info, errlog)) return null;

    TableConfig profileTable = makeStructTable(ds, FeatureType.PROFILE, info, errlog);
    if (profileTable == null) return null;
    profileTable.feature_id = identifyIdVariableName(ds, CF.FeatureType.profile);
    if (profileTable.feature_id == null) {
      errlog.format("CFpointObs getProfileConfig cant find a profile id %n");
    }

    // obs table
    VariableDS z = CoordSysEvaluator.findCoordByType(ds, AxisType.Height);
    if (z == null) z = CoordSysEvaluator.findCoordByType(ds, AxisType.Pressure);
    if (z == null) z = CoordSysEvaluator.findCoordByType(ds, AxisType.GeoZ);
    if (z == null) {
      errlog.format("CFpointObs getProfileConfig cant find a Height coordinate %n");
      return null;
    }
    if (info.childStruct == null)
      info.childStruct = z.getParentStructure();

    TableConfig obsTable = null;
    switch (info.encoding) {
      case single:
        obsTable = makeSingle(ds, info.childDim, errlog);
        break;
      case multidim:
        obsTable = makeMultidimInner(ds, profileTable, info.childDim, info, errlog);
        if (z.getRank() == 1) {// z(z)
          obsTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
          obsTable.elev = z.getFullName();
        }
        break;
      case raggedContiguous:
        profileTable.numRecords = info.ragged_rowSize.getFullName();
        obsTable = makeRaggedContiguousChildTable(ds, info.parentDim, info.childDim, info.childStruct, errlog);
        break;
      case raggedIndex:
        obsTable = makeRaggedIndexChildTable(ds, info.parentDim, info.childDim, info.ragged_parentIndex, errlog);
        break;
      case flat:
        throw new UnsupportedOperationException("CFpointObs: profile flat encoding");
    }
    if (obsTable == null) return null;

    profileTable.addChild(obsTable);
    return profileTable;
  }

  ////

  protected TableConfig getTrajectoryConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (!identifyEncodingTraj(ds, info, errlog)) return null;

    TableConfig trajTable = makeStructTable(ds, FeatureType.TRAJECTORY, info, errlog);
    if (trajTable == null) return null;
    trajTable.feature_id = identifyIdVariableName(ds, CF.FeatureType.trajectory);
    if (trajTable.feature_id == null) {
      errlog.format("CFpointObs getTrajectoryConfig cant find a trajectoy id %n");
    }

    // obs table
    //Dimension obsDim = time.getDimension(time.getRank() - 1); // may be time(time) or time(traj, obs)

    TableConfig obsConfig = null;
    switch (info.encoding) {
      case single:
        obsConfig = makeSingle(ds, info.childDim, errlog);
        break;
      case multidim:
        obsConfig = makeMultidimInner(ds, trajTable, info.childDim, info, errlog);
        if (info.time.getRank() == 1) { // join time(obs) or time(time)
          obsConfig.addJoin(new JoinArray(info.time, JoinArray.Type.raw, 0));
          obsConfig.time = info.time.getFullName();
        }
        break;
      case raggedContiguous:
        trajTable.numRecords = info.ragged_rowSize.getFullName();
        obsConfig = makeRaggedContiguousChildTable(ds, info.parentDim, info.childDim, info.childStruct, errlog);
        break;
      case raggedIndex:
        obsConfig = makeRaggedIndexChildTable(ds, info.parentDim, info.childDim, info.ragged_parentIndex, errlog);
        break;
      case flat:
        throw new UnsupportedOperationException("CFpointObs: trajectory flat encoding");
    }
    if (obsConfig == null) return null;

    trajTable.addChild(obsConfig);
    return trajTable;
  }

  ////

  protected TableConfig getTimeSeriesProfileConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (!identifyEncodingTimeSeriesProfile(ds, info, CF.FeatureType.timeSeriesProfile, errlog)) return null;

    VariableDS time = CoordSysEvaluator.findCoordByType(ds, AxisType.Time);
    if (time == null) return null;

    if (time.getRank() == 0 && time.getParentStructure() == null) {
      errlog.format("CFpointObs timeSeriesProfile cannot have a scalar time coordinate%n");  // why ?
      return null;
    }

    /* distinguish multidim from flat
    if ((info.encoding == Encoding.multidim) && (time.getRank() < 3) && (z.getRank() < 3)) {
      Variable parentId = identifyParent(ds, CF.FeatureType.timeSeriesProfile);
      if ((parentId != null) && (parentId.getRank() == 1) && (parentId.getDimension(0).equals(time.getDimension(0)))) {
        if (time.getRank() == 1) // multidim time must be 2 or 3 dim
          info = new EncodingInfo(Encoding.flat, parentId);
        else if (time.getRank() == 2) {
          Dimension zDim = z.getDimension(z.getRank() - 1); // may be z(z) or z(profile, z)
          if (zDim.equals(time.getDimension(1))) // flat 2D time will have time as inner dim
            info = new EncodingInfo(Encoding.flat, parentId);
        }
      }
    } */

    TableConfig stationTable = makeStationTable(ds, FeatureType.STATION_PROFILE, info, errlog);
    if (stationTable == null) return null;

    //Dimension stationDim = ds.findDimension(stationTable.dimName);
    //Dimension profileDim = null;
    //Dimension zDim = null;

    VariableDS z = info.alt;
    switch (info.encoding) {
      case single: {
        assert ((time.getRank() >= 1) && (time.getRank() <= 2)) : "time must be rank 1 or 2";
        assert ((z.getRank() >= 1) && (z.getRank() <= 2)) : "z must be rank 1 or 2";

        if (time.getRank() == 2) {
          if (z.getRank() == 2)  // 2d time, 2d z
            assert time.getDimensions().equals(z.getDimensions()) : "rank-2 time and z dimensions must be the same";
          else  // 2d time, 1d z
            assert time.getDimension(1).equals(z.getDimension(0)) : "rank-2 time must have z inner dimension";
          //profileDim = time.getDimension(0);
          //zDim = time.getDimension(1);

        } else { // 1d time
          if (z.getRank() == 2) { // 1d time, 2d z
            assert z.getDimension(0).equals(time.getDimension(0)) : "rank-2 z must have time outer dimension";
            //profileDim = z.getDimension(0);
            //zDim = z.getDimension(1);
          } else { // 1d time, 1d z
            assert !time.getDimension(0).equals(z.getDimension(0)) : "time and z dimensions must be different";
            //profileDim = time.getDimension(0);
            //zDim = z.getDimension(0);
          }
        }
        // make profile table
        TableConfig profileTable = makeStructTable(ds, FeatureType.PROFILE, new EncodingInfo().set(Encoding.multidim, info.childDim), errlog);
        if (profileTable == null) return null;
        if (time.getRank() == 1) {// join time(time)
          //profileTable.addJoin(new JoinArray(time, JoinArray.Type.raw, 0));
          profileTable.addJoin(new JoinArray(time, JoinArray.Type.level, 1));
          profileTable.time = time.getFullName();
        }

        stationTable.addChild(profileTable);

        // make the inner (z) table
        TableConfig zTable = makeMultidimInner(ds, profileTable, info.grandChildDim, info, errlog);
        if (z.getRank() == 1) { // join z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
          zTable.elev = z.getFullName();
        }
        profileTable.addChild(zTable);

        break;
      }

      case multidim: {
        assert ((time.getRank() >= 1) && (time.getRank() <= 3)) : "time must be rank 2 or 3";
        assert ((z.getRank() == 1) || (z.getRank() == 3)) : "z must be rank 1 or 3";

        if (time.getRank() == 3) {
          if (z.getRank() == 3)  // 3d time, 3d z
            assert time.getDimensions().equals(z.getDimensions()) : "rank-3 time and z dimensions must be the same";
          else  // 3d time, 1d z
            assert time.getDimension(2).equals(z.getDimension(0)) : "rank-3 time must have z inner dimension";
          //profileDim = time.getDimension(1);
          //zDim = time.getDimension(2);

        } else if (time.getRank() == 2) { // 2d time
          if (z.getRank() == 3) { // 2d time, 3d z
            assert z.getDimension(1).equals(time.getDimension(1)) : "rank-2 time must have time inner dimension";
            //profileDim = z.getDimension(1);
            //zDim = z.getDimension(2);
          } else { // 2d time, 1d z
            assert !time.getDimension(0).equals(z.getDimension(0)) : "time and z dimensions must be different";
            assert !time.getDimension(1).equals(z.getDimension(0)) : "time and z dimensions must be different";
            //profileDim = time.getDimension(1);
            //zDim = z.getDimension(0);
          }
        } else { // 1d time
          if (z.getRank() == 1) {
            assert !time.getDimension(0).equals(z.getDimension(0)) : "time and z dimensions must be different";
          }
        }

        TableConfig profileTable = makeMultidimInner(ds, stationTable, info.childDim, info, errlog);
        if (profileTable == null) return null;
        if (time.getRank() == 1) {// join time(time)
          profileTable.addJoin(new JoinArray(time, JoinArray.Type.level, 1));
          profileTable.time = time.getFullName();
        }
        stationTable.addChild(profileTable);

        // make the inner (z) table
        TableConfig zTable = makeMultidimInner3D(ds, stationTable, profileTable, info.grandChildDim, errlog);
        if (z.getRank() == 1)  { // join z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
          zTable.elev = z.getFullName();
        }
        profileTable.addChild(zTable);
        break;
      }

      case raggedIndex: {
        TableConfig profileTable = makeRaggedIndexChildTable(ds, info.parentDim, info.childDim, info.ragged_parentIndex, errlog);
        stationTable.addChild(profileTable);
        profileTable.numRecords = info.ragged_rowSize.getFullName();
        TableConfig obsTable = makeRaggedContiguousChildTable(ds, info.childDim, info.grandChildDim, info.grandChildStruct, errlog);
        profileTable.addChild(obsTable);
        break;
      }

      case raggedContiguous:   // NOT USED
        throw new UnsupportedOperationException("CFpointObs: timeSeriesProfile raggedContiguous encoding not allowed");

        /*
      case flat:
        //profileDim = time.getDimension(0); // may be time(profile) or time(profile, z)
        Variable parentId = identifyParent(ds, CF.FeatureType.timeSeriesProfile);

        TableConfig profileTable = makeStructTable(ds, FeatureType.PROFILE, info, errlog);
        profileTable.parentIndex = parentId.getName();
        profileTable.stnId = findNameVariableWithStandardNameAndDimension(ds, CF.STATION_ID, info.childDim, errlog);
        profileTable.stnDesc = findNameVariableWithStandardNameAndDimension(ds, CF.STATION_DESC, info.childDim, errlog);
        profileTable.stnWmoId = findNameVariableWithStandardNameAndDimension(ds, CF.STATION_WMOID, info.childDim, errlog);
        profileTable.stnAlt = findNameVariableWithStandardNameAndDimension(ds, CF.STATION_ALTITUDE, info.childDim, errlog);
        stationTable.addChild(profileTable);

        //zDim = z.getDimension(z.getRank() - 1); // may be z(z) or z(profile, z)
        TableConfig zTable = makeMultidimInner(ds, profileTable, info.grandChildDim, errlog);
        if (z.getRank() == 1) // z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
        profileTable.addChild(zTable);

        break; */
    }

    return stationTable;
  }

  protected TableConfig getSectionConfig(NetcdfDataset ds, EncodingInfo info, Formatter errlog) throws IOException {
    if (!identifyEncodingSection(ds, info, CF.FeatureType.trajectoryProfile, errlog)) return null;

    TableConfig parentTable = makeStructTable(ds, FeatureType.TRAJECTORY_PROFILE, info, errlog);
    if (parentTable == null) return null;
    parentTable.feature_id = identifyIdVariableName(ds, CF.FeatureType.trajectoryProfile);
    if (parentTable.feature_id == null) {
      errlog.format("CFpointObs:getSectionConfig cant find a section id %n");
    }

    //Dimension sectionDim = ds.findDimension(parentTable.dimName);
    //Dimension profileDim = null;
    //Dimension zDim = null;

    VariableDS time = info.time;
    VariableDS z = info.alt;

    switch (info.encoding) {
      case single: {
        assert ((time.getRank() >= 1) && (time.getRank() <= 2)) : "time must be rank 1 or 2";
        assert ((z.getRank() >= 1) && (z.getRank() <= 2)) : "z must be rank 1 or 2";

        if (time.getRank() == 2) {
          if (z.getRank() == 2)  // 2d time, 2d z
            assert time.getDimensions().equals(z.getDimensions()) : "rank-2 time and z dimensions must be the same";
          else  // 2d time, 1d z
            assert time.getDimension(1).equals(z.getDimension(0)) : "rank-2 time must have z inner dimension";
          //profileDim = time.getDimension(0);
          //zDim = time.getDimension(1);

        } else { // 1d time
          if (z.getRank() == 2) { // 1d time, 2d z
            assert z.getDimension(0).equals(time.getDimension(0)) : "rank-2 z must have time outer dimension";
            //profileDim = z.getDimension(0);
            //zDim = z.getDimension(1);
          } else { // 1d time, 1d z
            assert !time.getDimension(0).equals(z.getDimension(0)) : "time and z dimensions must be different";
            //profileDim = time.getDimension(0);
            //zDim = z.getDimension(0);
          }
        }
        // make profile table
        TableConfig profileTable = makeStructTable(ds, FeatureType.PROFILE, new EncodingInfo().set(Encoding.multidim, info.childDim), errlog);
        if (profileTable == null) return null;
        if (time.getRank() == 1) { // join time(time)
          profileTable.addJoin(new JoinArray(time, JoinArray.Type.raw, 0));
          profileTable.time = time.getFullName();
        }
          parentTable.addChild(profileTable);

        // make the inner (z) table
        TableConfig zTable = makeMultidimInner(ds, profileTable, info.grandChildDim, info, errlog);
        if (z.getRank() == 1) { // join z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
          zTable.elev = z.getFullName();
        }
        profileTable.addChild(zTable);

        break;
      }

      case multidim: {
        assert ((time.getRank() >= 2) && (time.getRank() <= 3)) : "time must be rank 2 or 3";
        assert ((z.getRank() == 1) || (z.getRank() == 3)) : "z must be rank 1 or 3";

        if (time.getRank() == 3) {
          if (z.getRank() == 3)  // 3d time, 3d z
            assert time.getDimensions().equals(z.getDimensions()) : "rank-3 time and z dimensions must be the same";
          else  // 3d time, 1d z
            assert time.getDimension(2).equals(z.getDimension(0)) : "rank-3 time must have z inner dimension";
          //profileDim = time.getDimension(1);
          //zDim = time.getDimension(2);

        } else { // 2d time
          if (z.getRank() == 3) { // 2d time, 3d z
            assert z.getDimension(1).equals(time.getDimension(1)) : "rank-2 time must have time inner dimension";
            // profileDim = z.getDimension(1);
            //zDim = z.getDimension(2);
          } else { // 2d time, 1d z
            assert !time.getDimension(0).equals(z.getDimension(0)) : "time and z dimensions must be different";
            assert !time.getDimension(1).equals(z.getDimension(0)) : "time and z dimensions must be different";
            //profileDim = time.getDimension(1);
            //zDim = z.getDimension(0);
          }
        }

        // make profile table
        //   private TableConfig makeMultidimInner(NetcdfDataset ds, TableConfig parentTable, Dimension obsDim, Formatter errlog) throws IOException {

        TableConfig profileTable = makeMultidimInner(ds, parentTable, info.childDim, info, errlog);
        if (profileTable == null) return null;
        profileTable.feature_id = identifyIdVariableName(ds, CF.FeatureType.profile);
        parentTable.addChild(profileTable);

        // make the inner (z) table
        TableConfig zTable = makeMultidimInner3D(ds, parentTable, profileTable, info.grandChildDim, errlog);
        if (z.getRank() == 1) { // join z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
          zTable.elev = z.getFullName();
        }
        profileTable.addChild(zTable);
        break;
      }

      case raggedIndex: {
        TableConfig profileTable = makeRaggedIndexChildTable(ds, info.parentDim, info.childDim, info.ragged_parentIndex, errlog);
        profileTable.feature_id = identifyIdVariableName(ds, CF.FeatureType.profile);
        if (profileTable.feature_id == null) {
           errlog.format("CFpointObs:getSectionConfig cant find a profile id %n");
         }
        parentTable.addChild(profileTable);
        profileTable.numRecords = info.ragged_rowSize.getFullName();
        TableConfig obsTable = makeRaggedContiguousChildTable(ds, info.childDim, info.grandChildDim, info.grandChildStruct, errlog);
        profileTable.addChild(obsTable);
        break;
      }

      case raggedContiguous: {
        throw new UnsupportedOperationException("CFpointObs: section raggedContiguous encoding " + info.encoding);
      }

      /*
      case flat:
        parentTable.type = Table.Type.Construct; // override default
        profileDim = time.getDimension(0); // may be time(profile) or time(profile, z)
        Variable parentId = identifyParent(ds, CF.FeatureType.trajectoryProfile);

        TableConfig profileTable = makeStructTable(ds, FeatureType.TRAJECTORY_PROFILE, info, errlog);
        profileTable.parentIndex = parentId.getName();
        profileTable.feature_id = identifyParentId(ds, CF.FeatureType.profile);
        parentTable.addChild(profileTable);

        zDim = z.getDimension(z.getRank() - 1); // may be z(z) or z(profile, z)
        TableConfig zTable = makeMultidimInner(ds, profileTable, zDim, errlog);
        if (z.getRank() == 1) // z(z)
          zTable.addJoin(new JoinArray(z, JoinArray.Type.raw, 0));
        profileTable.addChild(zTable);

        break;  */
    }

    return parentTable;
  }

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

  protected static class EncodingInfo {
    Encoding encoding;
    VariableDS lat, lon, alt, time;
    Dimension parentDim, childDim, grandChildDim;
    Variable instanceId;
    Variable ragged_parentIndex, ragged_rowSize;
    Structure parentStruct, childStruct, grandChildStruct;

    EncodingInfo set(Encoding encoding, Dimension parentDim) {
      this.encoding = encoding;
      this.parentDim = parentDim;
      return this;
    }

    EncodingInfo set(Encoding encoding, Dimension parentDim, Dimension childDim) {
      this.encoding = encoding;
      this.parentDim = parentDim;
      this.childDim = childDim;
      return this;
    }

    EncodingInfo set(Encoding encoding, Dimension parentDim, Dimension childDim, Dimension grandChildDim) {
      this.encoding = encoding;
      this.parentDim = parentDim;
      this.childDim = childDim;
      this.grandChildDim = grandChildDim;
      return this;
    }
  }

  /* given the feature type, figure out the encoding

  private EncodingInfo identifyEncoding(NetcdfDataset ds, CF.FeatureType ftype, Formatter errlog) {
    Variable ragged_rowSize = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_ROWSIZE);
    if (ragged_rowSize != null) {
      if (ftype == CF.FeatureType.trajectoryProfile) {
        Variable parentId = identifyIdVariable(ds, ftype);
        if (parentId == null) {
          errlog.format("Section ragged must have section_id variable%n");
          return null;
        }
        return new EncodingInfo(Encoding.raggedContiguous, parentId);
      }
      return new EncodingInfo(Encoding.raggedContiguous, ragged_rowSize);
    }

    Variable ragged_parentIndex = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_PARENTINDEX);
    if (ragged_parentIndex != null) {
      Variable ragged_parentId = identifyIdVariable(ds, ftype);
      return new EncodingInfo(Encoding.raggedIndex, ragged_parentId);
    }

    Variable lat = CoordSysEvaluator.findCoordByType(ds, AxisType.Lat);
    if (lat == null) {
      errlog.format("Must have a Latitude coordinate%n");
      return null;
    }

    switch (ftype) {
      case point:
        return new EncodingInfo(Encoding.multidim, (Dimension) null);

      case timeSeries:
      case profile:
      case timeSeriesProfile:
        if (lat.getRank() == 0)
          return new EncodingInfo(Encoding.single, (Dimension) null);
        else if (lat.getRank() == 1)
          return new EncodingInfo(Encoding.multidim, lat);

        errlog.format("CFpointObs %s Must have Lat/Lon coordinates of rank 0 or 1%n", ftype);
        return null;

      case trajectory:
      case trajectoryProfile:
        if (lat.getRank() == 1)
          return new EncodingInfo(Encoding.single, (Dimension) null);
        else if (lat.getRank() == 2)
          return new EncodingInfo(Encoding.multidim, lat);

        errlog.format("CFpointObs %s Must have Lat/Lon coordinates of rank 1 or 2%n", ftype);
        return null;
    }

    return null;
  } */

  // for stations, figure out the encoding
  protected boolean identifyEncodingStation(NetcdfDataset ds, EncodingInfo info, CF.FeatureType ftype, Formatter errlog) {
    // find the obs dimension
    Dimension obsDim = null;
    if (info.time.getRank() > 0)
      obsDim = info.time.getDimension(info.time.getRank() - 1); // may be time(time) or time(stn, obs)
    else if (info.time.getParentStructure() != null) {
      Structure parent = info.time.getParentStructure(); // if time axis is a structure member, try pulling dimension out of parent structure
      obsDim = parent.getDimension(parent.getRank() - 1);
    }
    if (obsDim == null) {
      errlog.format("CFpointObs: must have a non-scalar Time coordinate%n");
      return false;
    }

    // find the station dimension
    if (info.lat.getRank() == 0) {// scalar means single
      info.set(Encoding.single, null, obsDim);
      return true;
    }

    Dimension stnDim = info.lat.getDimension(0);
    if (obsDim == stnDim) {
      info.set(Encoding.flat, null, obsDim); // not used ?
      return true;
    }

    // the raggeds
    if (identifyRaggeds(ds, info, stnDim, obsDim, errlog))
      return true;

    // heres whats left
    if (info.lat.getRank() == 1) {
      //Encoding e = (info.time.getParentStructure() != null) ? Encoding.multiStructure : Encoding.multidim;
      info.set(Encoding.multidim, stnDim, obsDim);
      return true;
    }

    errlog.format("CFpointObs: %s Must have Lat/Lon coordinates of rank 0 or 1%n", ftype);
    return false;
  }

  /**
   * Identify ragged array representations for single nests (station, profile, trajectory)
   *
   * @param ds          in this dataset
   * @param info        put info here
   * @param instanceDim the instance dimension, null if not known yet
   * @param sampleDim   the sample dimension, null if not known yet
   * @param errlog      error go here
   * @return true if ragged array representations is found; side effect: set (info.ragged_rowSize, info.parentStruct) or (info.ragged_parentIndex, info.childStruct)
   */
  protected boolean identifyRaggeds(NetcdfDataset ds, EncodingInfo info, Dimension instanceDim, Dimension sampleDim, Formatter errlog) {

    // check for contiguous
    Evaluator.VarAtt varatt = Evaluator.findVariableWithAttribute(ds, CF.SAMPLE_DIMENSION);   // CF 1.6
    if (varatt == null) varatt = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_ROWSIZE);  // backwards compatibility
    if (varatt != null) {
      Variable ragged_rowSize = varatt.var;
      String sampleDimName = varatt.att.getStringValue();

      if (sampleDim != null && !sampleDimName.equals(sampleDim.getShortName())) {
        errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable has sample dimension %s must be %s%n", sampleDimName, sampleDim.getShortName());
        return false;
      }

      if (sampleDim == null) {
        sampleDim = ds.findDimension(sampleDimName);
        if (sampleDim == null) {
          errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable has invalid sample dimension %s%n", sampleDimName);
          return false;
        }
      }

      Dimension rrDim;
      if (ragged_rowSize.getRank() > 0)
        rrDim = ragged_rowSize.getDimension(0); // nobs(station)
      else if (ragged_rowSize.getParentStructure() != null) {
        Structure parent = ragged_rowSize.getParentStructure(); // if ragged_rowSize is a structure member, use dimension of parent structure
        rrDim = parent.getDimension(0);
      } else {
        errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable (%s) must have rank 1%n", ragged_rowSize);
        return false;
      }

      if (instanceDim != null && instanceDim != rrDim) {
          errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable has invalid instance dimension %s must be %s%n", rrDim, instanceDim);
          return false;
      }
      instanceDim = rrDim;

      if (ragged_rowSize.getDataType() != DataType.INT) {
        errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable must be of type integer%n");
        return false;
      }

      info.set(Encoding.raggedContiguous, instanceDim, sampleDim);
      info.ragged_rowSize = ragged_rowSize;
      info.parentStruct = ragged_rowSize.getParentStructure();
      return true;
    }  // rowsize was found

    varatt = Evaluator.findVariableWithAttribute(ds, CF.INSTANCE_DIMENSION);                       // CF 1.6
    if (varatt == null) varatt = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_PARENTINDEX);   // backwards compatibility
    if (varatt != null) {
      Variable ragged_parentIndex = varatt.var;
      String instanceDimName = varatt.att.getStringValue();

      if (instanceDim != null && !instanceDimName.equals(instanceDim.getShortName())) {
        errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable has instance dimension %s must be %s%n", instanceDimName, instanceDim.getShortName());
        return false;
      }

      if (instanceDim == null) {
        instanceDim = ds.findDimension(instanceDimName);
        if (instanceDim == null) {
          errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable has invalid instance dimension %s%n", instanceDimName);
          return false;
        }
      }

      if (ragged_parentIndex.getDataType() != DataType.INT) {
        errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable must be of type integer%n");
        return false;
      }

      // allow netcdf-4 structures, eg kunicki
      if (ragged_parentIndex.isMemberOfStructure()) {
        Structure s = ragged_parentIndex.getParentStructure();
        if (s.getRank() == 0 || !s.getDimension(0).equals(sampleDim)) {
          errlog.format("CFpointObs: Indexed ragged array representation (structure): parent_index variable must be of form Struct { %s }(%s) %n", ragged_parentIndex.getFullName(), sampleDim.getShortName());
          return false;
        }

      } else {
        if (ragged_parentIndex.getRank() != 1 || !ragged_parentIndex.getDimension(0).equals(sampleDim)) {
          errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable must be of form %s(%s) %n", ragged_parentIndex.getFullName(), sampleDim.getShortName());
          return false;
        }
      }
      info.set(Encoding.raggedIndex, instanceDim, sampleDim);
      info.ragged_parentIndex = ragged_parentIndex;
      info.childStruct = ragged_parentIndex.getParentStructure();
      return true;
    } // parent index was found

    /* kunicki 10/21/2011
    Variable ragged_parentIndex = Evaluator.getVariableWithAttributeValue(ds, CF.RAGGED_PARENTINDEX, parentDim.getShortName());
    if ((ragged_parentIndex == null) ||
            (!ragged_parentIndex.isMemberOfStructure() && (ragged_parentIndex.getRank() == 0 || ragged_parentIndex.getDimension(0).getShortName() != childDim.getShortName()) ||
                    (ragged_parentIndex.isMemberOfStructure() && (ragged_parentIndex.getParentStructure().getRank() == 0 || ragged_parentIndex.getParentStructure().getDimension(0).getShortName() != childDim.getShortName())))
            ) {
      // if ((null == ragged_parentIndex) || (ragged_parentIndex.getRank() == 0) || (ragged_parentIndex.getDimension(0).getShortName() != childDim.getShortName())) {
      errlog.format("there must be a ragged_parent_index variable with outer dimension that matches obs dimension %s%n", childDim.getShortName());
      return null;
    }  */

    return false;
  }

  /**
   * Identify ragged array representations for double nests (timeSeries profile, timeSeries trajectory)
   * <p/>
   * This uses the contiguous ragged array representation for each profile (9.5.43.3), and the indexed ragged array
   * representation to organise the profiles into time series (9.3.54). The canonical use case is when writing real-time
   * data streams that contain profiles from many stations, arriving randomly, with the data for each entire profile written all at once.
   *
   * @param ds          in this dataset
   * @param info        put info here
   * @param errlog      error go here
   * @return EncodingInfo if ragged array representations is found
   */
  protected boolean identifyDoubleRaggeds(NetcdfDataset ds, EncodingInfo info, Formatter errlog) {
    // the timeseries are stored as ragged index
    Evaluator.VarAtt varatt = Evaluator.findVariableWithAttribute(ds, CF.INSTANCE_DIMENSION);
    if (varatt == null) varatt = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_PARENTINDEX);
    if (varatt == null) return false;

    Variable ragged_parentIndex = varatt.var;
    String instanceDimName = varatt.att.getStringValue();
    Dimension stationDim = ds.findDimension(instanceDimName);

    if (stationDim == null) {
      errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable has illegal value for %s = %s%n", CF.INSTANCE_DIMENSION, instanceDimName);
      return false;
    }

    if (ragged_parentIndex.getDataType() != DataType.INT) {
      errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable must be of type integer%n");
      return false;
    }

    if (ragged_parentIndex.getRank() != 1 && info.childStruct == null) {
      errlog.format("CFpointObs: Indexed ragged array representation: parent_index variable %s must be 1D %n", ragged_parentIndex);
      return false;
    }
    Dimension profileDim = (info.childDim != null) ? info.childDim : ragged_parentIndex.getDimension(0);

    // onto the profiles, stored contiguously
    varatt = Evaluator.findVariableWithAttribute(ds, CF.SAMPLE_DIMENSION);
    if (varatt == null) varatt = Evaluator.findVariableWithAttribute(ds, CF.RAGGED_ROWSIZE);
    if (varatt == null) return false;

    Variable ragged_rowSize = varatt.var;
    String obsDimName = varatt.att.getStringValue();
    Dimension obsDim = ds.findDimension(obsDimName);

    if (obsDimName == null) {
      errlog.format("CFpointObs: Contiguous ragged array representation: parent_index variable has illegal value for %s = %s%n", CF.SAMPLE_DIMENSION, obsDimName);
      return false;
    }

    if (!obsDimName.equals(info.grandChildDim.getShortName())) {
      errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable has obs dimension %s must be %s%n", obsDimName, info.childDim);
      return false;
    }

    if (ragged_rowSize.getDataType() != DataType.INT) {
      errlog.format("CFpointObs: Contiguous ragged array representation: row_size variable must be of type integer%n");
      return false;
    }

    if (info.childDim == null) {  // nc4 ext
      Dimension profileDim2 = ragged_rowSize.getDimension(0);
      if (profileDim2 != profileDim) {
        errlog.format("CFpointObs: Double ragged array representation dimensions do not agree: %s != %s%n", profileDim2.getShortName(), profileDim.getShortName());
        return false;
      }
    }

    info.set(Encoding.raggedIndex, stationDim, profileDim, obsDim);
    info.ragged_parentIndex = ragged_parentIndex;
    info.ragged_rowSize = ragged_rowSize;
    return true;
  }

  protected boolean identifyEncodingProfile(NetcdfDataset ds, EncodingInfo info, Formatter errlog) {
    // find the obs dimension
    VariableDS z = CoordSysEvaluator.findCoordByType(ds, AxisType.Height);
    if (z == null) z = CoordSysEvaluator.findCoordByType(ds, AxisType.Pressure);
    if (z == null) z = CoordSysEvaluator.findCoordByType(ds, AxisType.GeoZ);
    if (z == null) {
      errlog.format("CFpointObs: Must have a Height coordinate%n");
      return false;
    }
    info.alt = z;
    Dimension obsDim = null;
    if (z.getRank() > 0)
      obsDim = z.getDimension(z.getRank() - 1); // may be z(z) or alt(profile, z)
    else if (z.getParentStructure() != null) {
      Structure parent = z.getParentStructure(); // if time axis is a structure member, try pulling dimension out of parent structure
      obsDim = parent.getDimension(parent.getRank() - 1);
    }
    if (obsDim == null) {
      errlog.format("CFpointObs: Must have a non-scalar Height coordinate%n");
      return false;
    }

    if (identifyRaggeds(ds, info, null, obsDim, errlog)) return true;

    // parent dimension
    Dimension parentDim;
    if (z.getRank() > 1) {
      parentDim = z.getDimension(0);
      info.set(Encoding.multidim, parentDim, obsDim);
      return true;
    }

    VariableDS time = CoordSysEvaluator.findCoordByType(ds, AxisType.Time);
    if (time == null) return false;

    if (time.getRank() == 0 || time.getDimension(0) == obsDim) {
      info.set(Encoding.single, null, obsDim);
      return true;
    }

    parentDim = time.getDimension(0);
    info.set(Encoding.multidim, parentDim, obsDim);
    return true;
  }

  protected boolean identifyEncodingTraj(NetcdfDataset ds, EncodingInfo info, Formatter errlog) {
    // find the obs dimension
    Dimension obsDim = null;
    if (info.time.getRank() > 0)
      obsDim = info.time.getDimension(info.time.getRank() - 1); // may be time(time) or time(traj, obs)
    else if (info.time.getParentStructure() != null) {
      Structure parent = info.time.getParentStructure(); // if time axis is a structure member, try pulling dimension out of parent structure
      obsDim = parent.getDimension(parent.getRank() - 1);
    }
    if (obsDim == null) {
      errlog.format("CFpointObs: Must have a non-scalar Time coordinate%n");
      return false;
    }

    if (identifyRaggeds(ds, info, null, obsDim, errlog)) return true;

    // parent dimension
    Dimension parentDim;
    if (info.time.getRank() > 1) {
      parentDim = info.time.getDimension(0);
      info.set(Encoding.multidim, parentDim, obsDim);
      return true;
    }

    if (info.lat.getRank() > 0) { // multidim case
      for (Dimension d : info.lat.getDimensions()) {
        if (!d.equals(obsDim)) {
          info.set(Encoding.multidim, d, obsDim);
          return true;
        }
      }
    }

    //otherwise its a single traj in the file
    info.set(Encoding.single, null, obsDim);
    return true;
  }

  protected boolean identifyEncodingSection(NetcdfDataset ds, EncodingInfo info, CF.FeatureType ftype, Formatter errlog) {
        // find the non-station altitude
    VariableDS z = findZAxisNotStationAlt(ds);
    if (z == null) {
      errlog.format("CFpointObs: section must have a z coordinate%n");
      return false;
    }
    if (z.getRank() == 0) {
      errlog.format("CFpointObs: section cannot have a scalar z coordinate%n");
      return false;
    }
    info.alt = z;

    Dimension obsDim = null;
    if (z.getRank() > 0)
      obsDim = z.getDimension(z.getRank() - 1); // may be z(z) or alt(profile, z)
    else if (z.getParentStructure() != null) {
      Structure parent = z.getParentStructure(); // if time axis is a structure member, try pulling dimension out of parent structure
      obsDim = parent.getDimension(parent.getRank() - 1);
    }
    if (obsDim == null) {
      errlog.format("CFpointObs: Must have a non-scalar Height coordinate%n");
      return false;
    }
    info.grandChildDim = obsDim;

    // parent dimension
    Dimension trajDim;
    Dimension profileDim;
    if (z.getRank() > 2) {
      trajDim = z.getDimension(0);
      profileDim = z.getDimension(1);
      info.set(Encoding.multidim, trajDim, profileDim, obsDim);
      return true;
    }

    if (identifyDoubleRaggeds(ds, info, errlog))
      return true;

    if (info.time.getRank() > 2) {
      trajDim = info.time.getDimension(0);
      profileDim = info.time.getDimension(1);
      info.set(Encoding.multidim, trajDim, profileDim, obsDim);
      return true;
    }

    if (info.lat.getRank() == 1) {
      profileDim = info.lat.getDimension(0);
      info.set(Encoding.single, null, profileDim, obsDim);
      return true;
    }

    if (info.lat.getRank() == 2) {
      trajDim = info.lat.getDimension(0);
      profileDim = info.lat.getDimension(0);
      info.set(Encoding.multidim, trajDim, profileDim, obsDim);
      return true;
    }

    // forget flat for now
    errlog.format("CFpointObs %s unrecognized form%n", ftype);
    return false;
  }

  protected boolean identifyEncodingTimeSeriesProfile(NetcdfDataset ds, EncodingInfo info, CF.FeatureType ftype, Formatter errlog) {
    // find the non-station altitude
    VariableDS z = findZAxisNotStationAlt(ds);
    if (z == null) {
      errlog.format("CFpointObs: timeSeriesProfile must have a z coordinate, not the station altitude%n");
      return false;
    }
    if (z.getRank() == 0) {
      errlog.format("CFpointObs: timeSeriesProfile cannot have a scalar z coordinate%n");
      return false;
    }

    Dimension obsDim = z.getDimension(z.getRank() - 1); // may be z(z) or alt(profile, z) or alt(sta,prof,z)
    info.alt = z;
    info.grandChildDim = obsDim;

    // alt(sta,prof,z)
    if (z.getRank() > 2) {
      Dimension stnDim = z.getDimension(0);
      Dimension profileDim = z.getDimension(1);
      info.set(Encoding.multidim, stnDim, profileDim, obsDim);
      return true;
    }

    // raggeds
    if (identifyDoubleRaggeds(ds, info, errlog))
      return true;

    Dimension profileDim;
    Dimension stnDim;

    // single
    if (info.lat.getRank() == 0) {
      profileDim = info.time.getDimension(0); // may be time(profile) or time(profile, z)
      info.set(Encoding.single, null, profileDim, obsDim);
      return true;

    } else { // lat must use the station dim
      stnDim = info.lat.getDimension(0);
    }

    // multidim from here on

    // time(profile) or time(profile, z)
    if ((info.time.getRank() == 1) || (info.time.getRank() == 2 && info.time.getDimension(1) == obsDim)) {
      profileDim = info.time.getDimension(0);
      info.set(Encoding.multidim, stnDim, profileDim, obsDim);
      return true;
    }

    // time(station, profile, z)
    if (info.time.getRank() > 2) {
      profileDim = info.time.getDimension(1);
      info.set(Encoding.multidim, stnDim, profileDim, obsDim);
      return true;
    }

    // time(station, profile)
    if (info.time.getRank() == 2 && info.time.getDimension(0) == stnDim) {
      profileDim = info.time.getDimension(1);
      info.set(Encoding.multidim, stnDim, profileDim, obsDim);
      return true;
    }

    errlog.format("CFpointObs %s unrecognized form%n", ftype);
    return false;
  }

 private String identifyIdVariableName(NetcdfDataset ds, CF.FeatureType ftype) {
    Variable v = identifyIdVariable(ds, ftype);
    return (v == null) ? null : v.getFullName();
  }

  private Variable identifyIdVariable(NetcdfDataset ds, CF.FeatureType ftype) {
    Variable result;

    switch (ftype) {
      case timeSeriesProfile:
      case timeSeries:
        result = Evaluator.findVariableWithAttributeValue(ds, CF.CF_ROLE, CF.TIMESERIES_ID);
        if (result != null) return result;
        return Evaluator.findVariableWithAttributeValue(ds, CF.STANDARD_NAME, CF.STATION_ID);  // old way for backwards compatibility

      case trajectory:
      case trajectoryProfile:
        result = Evaluator.findVariableWithAttributeValue(ds, CF.CF_ROLE, CF.TRAJECTORY_ID);
        if (result != null) return result;
        return Evaluator.findVariableWithAttributeValue(ds, CF.STANDARD_NAME, CF.TRAJECTORY_ID); // old way for backwards compatibility

      case profile:
        result = Evaluator.findVariableWithAttributeValue(ds, CF.CF_ROLE, CF.PROFILE_ID);
        if (result != null) return result;
        return Evaluator.findVariableWithAttributeValue(ds, CF.STANDARD_NAME, CF.PROFILE_ID);  // old way for backwards compatibility

      default:
        return null;
    }
  }

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

  // for station and stationProfile, not flat

  private TableConfig makeStationTable(NetcdfDataset ds, FeatureType ftype, EncodingInfo info, Formatter errlog) throws IOException {
    Variable lat = CoordSysEvaluator.findCoordByType(ds, AxisType.Lat);
    Variable lon = CoordSysEvaluator.findCoordByType(ds, AxisType.Lon);
    if (lat == null || lon == null) {
      errlog.format("CFpointObs: must have lat and lon coordinates%n");
      return null;
    }
    //Dimension stationDim = (info.encoding == Encoding.single) ? null : lat.getDimension(0); // assumes outer dim of lat is parent dimension, single = scalar

    Table.Type stationTableType = Table.Type.Structure;
    if (info.encoding == Encoding.single) stationTableType = Table.Type.Top;
    if (info.encoding == Encoding.flat) stationTableType = Table.Type.Construct;

    Dimension stationDim = (info.encoding == Encoding.flat) ? info.childDim : info.parentDim;
    String name = (stationDim == null) ? " single" : stationDim.getShortName();
    TableConfig stnTable = new TableConfig(stationTableType, name);
    stnTable.featureType = ftype;

    // stnId
    Variable stnIdVar = Evaluator.findVariableWithAttributeAndDimension(ds, CF.CF_ROLE, CF.TIMESERIES_ID, stationDim, errlog);
    if (stnIdVar == null)
      stnIdVar = Evaluator.findVariableWithAttributeAndDimension(ds, CF.STANDARD_NAME, CF.STATION_ID, stationDim, errlog);
    if (stnIdVar == null) {
      errlog.format("CFpointObs: must have a Station id variable with %s = %s%n", CF.CF_ROLE, CF.TIMESERIES_ID);
      return null;
    }
    stnTable.stnId = stnIdVar.getFullName();
    info.instanceId = stnIdVar;

    stnTable.stnDesc = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.PLATFORM_NAME, stationDim, errlog);
    if (stnTable.stnDesc == null)
      stnTable.stnDesc = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.STATION_DESC, stationDim, errlog);
    stnTable.stnWmoId = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.PLATFORM_ID, stationDim, errlog);
    if (stnTable.stnWmoId == null)
      stnTable.stnWmoId = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.STATION_WMOID, stationDim, errlog);
    stnTable.stnAlt = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.SURFACE_ALTITUDE, stationDim, errlog);
    if (stnTable.stnAlt == null)
      stnTable.stnAlt = Evaluator.findNameVariableWithStandardNameAndDimension(ds, CF.STATION_ALTITUDE, stationDim, errlog);
    stnTable.lat = lat.getFullName();
    stnTable.lon = lon.getFullName();

    if (info.encoding != Encoding.single && stationDim != null) {
      stnTable.dimName = stationDim.getShortName();
      makeStructureInfo(stnTable, ds, stnIdVar.getParentStructure(), stationDim);
    }

    // LOOK probably need a standard name here
    // optional alt coord - detect if its a station height or actually associated with the obs, eg for a profile
    if (stnTable.stnAlt == null) {
      Variable alt = CoordSysEvaluator.findCoordByType(ds, AxisType.Height);
      if (alt != null) {
        if ((info.encoding == Encoding.single) && alt.getRank() == 0)
          stnTable.stnAlt = alt.getFullName();

        if ((info.encoding != Encoding.single) && (lat.getRank() == alt.getRank()) && alt.getRank() > 0 && alt.getDimension(0).equals(stationDim))
          stnTable.stnAlt = alt.getFullName();
      }
    }

    return stnTable;
  }

  private void makeStructureInfo(TableConfig tableConfig, NetcdfDataset ds, Structure parent, Dimension dim) {
    tableConfig.dimName = dim.getShortName();
    if (parent != null) {
       tableConfig.structureType = TableConfig.StructureType.Structure;
       tableConfig.structName = parent.getShortName();
    } else {
      boolean hasNetcdf3Struct = Evaluator.hasNetcdf3RecordStructure(ds) && dim.isUnlimited();
      tableConfig.structureType = hasNetcdf3Struct ? TableConfig.StructureType.Structure : TableConfig.StructureType.PsuedoStructure;
      tableConfig.structName = hasNetcdf3Struct ? "record" : dim.getShortName();
    }

  }

  private TableConfig makeStructTable(NetcdfDataset ds, FeatureType ftype, EncodingInfo info, Formatter errlog) throws IOException {
    Table.Type tableType = Table.Type.Structure;
    if (info.encoding == Encoding.single) tableType = Table.Type.Top;
    if (info.encoding == Encoding.flat) tableType = Table.Type.ParentId;

    String name = (info.parentDim == null) ? " single" : info.parentDim.getShortName();
    TableConfig tableConfig = new TableConfig(tableType, name);
    tableConfig.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, info.parentDim);
    tableConfig.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, info.parentDim);
    tableConfig.elev = matchAxisTypeAndDimension(ds, AxisType.Height, info.parentDim);
    if (tableConfig.elev == null) tableConfig.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, info.parentDim);
    if (tableConfig.elev == null) tableConfig.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, info.parentDim);
    tableConfig.time = matchAxisTypeAndDimension(ds, AxisType.Time, info.parentDim);
    tableConfig.featureType = ftype;

    if (info.encoding != Encoding.single && info.parentDim != null) {
      tableConfig.dimName = name;
      Structure parent = info.parentStruct;
      if (parent == null) {
        switch (info.encoding) {
          case raggedContiguous:
            parent = info.ragged_rowSize.getParentStructure();
            break;
          case raggedIndex:
            parent = info.ragged_parentIndex.getParentStructure();
            break;
        }
      }
      makeStructureInfo(tableConfig, ds, parent, info.parentDim);
    }

    return tableConfig;
  }

  // test E:/work/signell/traj2D.ncml
  private TableConfig makeStructTableTestTraj(NetcdfDataset ds, FeatureType ftype, EncodingInfo info, Formatter errlog) throws IOException {
    Table.Type tableType = Table.Type.Structure;
    if (info.encoding == Encoding.single) tableType = Table.Type.Top;
    if (info.encoding == Encoding.flat) tableType = Table.Type.ParentId;

    String name = (info.parentDim == null) ? " single" : info.parentDim.getShortName();
    TableConfig tableConfig = new TableConfig(tableType, name);
    tableConfig.lat = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Lat);
    tableConfig.lon = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Lon);
    tableConfig.elev = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Height);
    if (tableConfig.elev == null) tableConfig.elev = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Pressure);
    if (tableConfig.elev == null) tableConfig.elev = CoordSysEvaluator.findCoordNameByType(ds, AxisType.GeoZ);
    tableConfig.time = CoordSysEvaluator.findCoordNameByType(ds, AxisType.Time);
    tableConfig.featureType = ftype;

    if (info.encoding != Encoding.single && info.parentDim != null) {
      tableConfig.dimName = name;
      makeStructureInfo(tableConfig, ds, null, info.parentDim);
    }

    return tableConfig;
  }

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

  private TableConfig makeRaggedContiguousChildTable(NetcdfDataset ds, Dimension parentDim, Dimension childDim, Structure childStruct, Formatter errlog) throws IOException {
    TableConfig childTable = new TableConfig(Table.Type.Contiguous, childDim.getShortName());
    childTable.dimName = childDim.getShortName();

    childTable.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, childDim);
    childTable.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, childDim);
    childTable.elev = matchAxisTypeAndDimension(ds, AxisType.Height, childDim);
    if (childTable.elev == null) childTable.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, childDim);
    if (childTable.elev == null) childTable.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, childDim);
    childTable.time = matchAxisTypeAndDimension(ds, AxisType.Time, childDim);

    makeStructureInfo(childTable, ds, childStruct, childDim);

    return childTable;
  }

  private TableConfig makeRaggedIndexChildTable(NetcdfDataset ds, Dimension parentDim, Dimension childDim, Variable ragged_parentIndex, Formatter errlog) throws IOException {
    TableConfig childTable = new TableConfig(Table.Type.ParentIndex, childDim.getShortName());
    childTable.dimName = childDim.getShortName();

    childTable.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, childDim);
    childTable.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, childDim);
    childTable.elev = matchAxisTypeAndDimension(ds, AxisType.Height, childDim);
    if (childTable.elev == null) childTable.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, childDim);
    if (childTable.elev == null) childTable.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, childDim);
    childTable.time = matchAxisTypeAndDimension(ds, AxisType.Time, childDim);

    makeStructureInfo(childTable, ds, ragged_parentIndex.getParentStructure(), childDim);
    childTable.parentIndex = ragged_parentIndex.getFullName();

    return childTable;
  }

  // the inner table of Structure(outer, inner) and middle table of Structure(outer, middle, inner)

  private TableConfig makeMultidimInner(NetcdfDataset ds, TableConfig parentTable, Dimension obsDim, EncodingInfo info, Formatter errlog) throws IOException {
    Dimension parentDim = ds.findDimension(parentTable.dimName);

    Table.Type obsTableType = (parentTable.structureType == TableConfig.StructureType.PsuedoStructure) ? Table.Type.MultidimInnerPsuedo : Table.Type.MultidimInner;
    // if (info.time.isMemberOfStructure()) obsTableType = Table.Type.Structure;

    TableConfig obsTable = new TableConfig(obsTableType, obsDim.getShortName());

    obsTable.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, parentDim, obsDim);
    obsTable.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, parentDim, obsDim);
    obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Height, parentDim, obsDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, parentDim, obsDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, parentDim, obsDim);
    obsTable.time = matchAxisTypeAndDimension(ds, AxisType.Time, parentDim, obsDim);

    // divide up the variables between the parent and the obs
    List<String> obsVars;
    List<Variable> vars = ds.getVariables();
    List<String> parentVars = new ArrayList<>(vars.size());
    obsVars = new ArrayList<>(vars.size());
    for (Variable orgV : vars) {
      if (orgV instanceof Structure) continue;

      Dimension dim0 = orgV.getDimension(0);
      if ((dim0 != null) && dim0.equals(parentDim)) {
        if ((orgV.getRank() == 1) || ((orgV.getRank() == 2) && orgV.getDataType() == DataType.CHAR)) {
          parentVars.add(orgV.getShortName());
        } else {
          Dimension dim1 = orgV.getDimension(1);
          if ((dim1 != null) && dim1.equals(obsDim))
            obsVars.add(orgV.getShortName());
        }
      }
    }
    parentTable.vars = parentVars;
    // parentTable.vars = parentTable.isPsuedoStructure ? parentVars : null; // restrict to these if psuedoStruct

    obsTable.structureType = parentTable.structureType;
    obsTable.outerName = parentDim.getShortName();
    obsTable.innerName = obsDim.getShortName();
    obsTable.dimName = (parentTable.structureType == TableConfig.StructureType.PsuedoStructure) ? obsTable.outerName : obsTable.innerName;
    obsTable.structName = obsDim.getShortName();
    obsTable.vars = obsVars;

    return obsTable;
  }

  // the inner table of Structure(outer, middle, inner)

  private TableConfig makeMultidimInner3D(NetcdfDataset ds, TableConfig outerTable, TableConfig middleTable, Dimension innerDim, Formatter errlog) throws IOException {
    Dimension outerDim = ds.findDimension(outerTable.dimName);
    Dimension middleDim = ds.findDimension(middleTable.innerName);

    Table.Type obsTableType = (outerTable.structureType == TableConfig.StructureType.PsuedoStructure) ? Table.Type.MultidimInnerPsuedo3D : Table.Type.MultidimInner3D;
    TableConfig obsTable = new TableConfig(obsTableType, innerDim.getShortName());
    obsTable.structureType = TableConfig.StructureType.PsuedoStructure2D;
    obsTable.dimName = outerTable.dimName;
    obsTable.outerName = middleTable.innerName;
    obsTable.innerName = innerDim.getShortName();
    obsTable.structName = innerDim.getShortName();

    obsTable.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, outerDim, middleDim, innerDim);
    obsTable.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, outerDim, middleDim, innerDim);
    obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Height, outerDim, middleDim, innerDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, middleDim, innerDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, middleDim, innerDim);
    obsTable.time = matchAxisTypeAndDimension(ds, AxisType.Time, outerDim, middleDim, innerDim);

    // divide up the variables between the 3 tables
    List<Variable> vars = ds.getVariables();
    List<String> outerVars = new ArrayList<>(vars.size());
    List<String> middleVars = new ArrayList<>(vars.size());
    List<String> innerVars = new ArrayList<>(vars.size());
    for (Variable orgV : vars) {
      if (orgV instanceof Structure) continue;

      if ((orgV.getRank() == 1) || ((orgV.getRank() == 2) && orgV.getDataType() == DataType.CHAR)) {
        if (outerDim.equals(orgV.getDimension(0)))
          outerVars.add(orgV.getShortName());

      } else if (orgV.getRank() == 2) {
        if (outerDim.equals(orgV.getDimension(0)) && middleDim.equals(orgV.getDimension(1)))
          middleVars.add(orgV.getShortName());

      } else if (orgV.getRank() == 3) {
        if (outerDim.equals(orgV.getDimension(0)) && middleDim.equals(orgV.getDimension(1)) && innerDim.equals(orgV.getDimension(2)))
          innerVars.add(orgV.getShortName());
      }
    }
    outerTable.vars = outerVars;
    middleTable.vars = middleVars;
    obsTable.vars = innerVars;

    return obsTable;
  }

  private TableConfig makeSingle(NetcdfDataset ds, Dimension obsDim, Formatter errlog) throws IOException {

    Table.Type obsTableType = Table.Type.Structure;
    TableConfig obsTable = new TableConfig(obsTableType, "single");
    obsTable.dimName = obsDim.getShortName();

    obsTable.lat = matchAxisTypeAndDimension(ds, AxisType.Lat, obsDim);
    obsTable.lon = matchAxisTypeAndDimension(ds, AxisType.Lon, obsDim);
    obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Height, obsDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.Pressure, obsDim);
    if (obsTable.elev == null) obsTable.elev = matchAxisTypeAndDimension(ds, AxisType.GeoZ, obsDim);
    obsTable.time = matchAxisTypeAndDimension(ds, AxisType.Time, obsDim);

    makeStructureInfo(obsTable, ds, null, obsDim);
    return obsTable;
  }

  private TableConfig makeMiddleTable(NetcdfDataset ds, TableConfig parentTable, Dimension obsDim, Formatter errlog) throws IOException {
    throw new UnsupportedOperationException("CFpointObs: middleTable encoding");
  }

  // Adds check for dimensions against parent structure if applicable...
  //
  // Note to John.  It may be that this implementation can be pushed into the super
  // class, I don't understand enough of the code base to anticipate implementation artifacts.

  protected String matchAxisTypeAndDimension(NetcdfDataset ds, AxisType type, final Dimension outer) {
    Variable var = CoordSysEvaluator.findCoordByType(ds, type, new CoordSysEvaluator.Predicate() {
      public boolean match(CoordinateAxis axis) {
        if ((outer == null) && (axis.getRank() == 0))
          return true;
        if ((outer != null) && (axis.getRank() == 1) && (outer.equals(axis.getDimension(0))))
          return true;

        // if axis is structure member, try pulling dimension out of parent structure
        if (axis.getParentStructure() != null) {
          Structure parent = axis.getParentStructure();
          if ((outer != null) && (parent.getRank() == 1) && (outer.equals(parent.getDimension(0))))
            return true;
        }
        return false;
      }
    });
    if (var == null) return null;
    return var.getFullName();
  }

}