/*
 * 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.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.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 "https://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.getRootGroup().findAttributeString(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) {
    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) {
    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) {
    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) {
    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) {
    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) {
    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
          assert z.getRank() != 1
              || !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) {
    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) {
    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) {
    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) {
    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) {
    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) {
    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) {
    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 (obsDim.equals(dim1))
            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) {
    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) {

    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) {
    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, Dimension outer) {
    Variable var = CoordSysEvaluator.findCoordByType(ds, type, 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();
        return (outer != null) && (parent.getRank() == 1) && (outer.equals(parent.getDimension(0)));
      }
      return false;
    });
    if (var == null)
      return null;
    return var.getFullName();
  }

}
