All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.nc2.internal.dataset.CoordSystemBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1998-2020 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.internal.dataset;

import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;
import javax.annotation.Nullable;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFiles;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateSystem;
import ucar.nc2.dataset.CoordinateTransform;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.StructureDS;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.dataset.spi.CoordSystemBuilderFactory;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.EscapeStrings;
import ucar.unidata.util.Parameter;

/**
 * Super class for implementing Convention-specific parsing of netCDF files.
 * This class processes the "_Coordinate conventions", see
 * https://www.unidata.ucar.edu/software/netcdf-java/current/reference/CoordinateAttributes.html
 *
 * A good strategy is for subclasses to add those attributes, and let this class construct the coordinate systems.
 */

/*
 * Implementation notes:
 *
 * Generally, subclasses should add the _Coordinate conventions, see
 * https://www.unidata.ucar.edu/software/netcdf-java/current/reference/CoordinateAttributes.html
 * Then let this class do the rest of the work.
 *
 * How to add Coordinate Transforms:
 * A.
 * 1) create a dummy Variable called the Coordinate Transform Variable.
 * This Coordinate Transform variable always has a name that identifies the transform,
 * and any attributes needed for the transformation.
 * 2) explicitly point to it by adding a _CoordinateTransform attribute to a Coordinate System Variable
 * _CoordinateTransforms = "LambertProjection HybridSigmaVerticalTransform"
 *
 * B. You could explicitly add it by overriding assignCoordinateTransforms()
 */
public class CoordSystemBuilder {
  protected static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CoordSystemBuilder.class);
  private static boolean useMaximalCoordSys = true;
  private static final String CONVENTION_NAME = _Coordinate.Convention;

  public static class Factory implements CoordSystemBuilderFactory {
    @Override
    public String getConventionName() {
      return CONVENTION_NAME;
    }

    @Override
    public CoordSystemBuilder open(NetcdfDataset.Builder datasetBuilder) {
      return new CoordSystemBuilder(datasetBuilder);
    }
  }

  /**
   * Calculate if this is a classic coordinate variable: has same name as its first dimension.
   * If type char, must be 2D, else must be 1D.
   *
   * @return true if a coordinate variable.
   */
  public static boolean isCoordinateVariable(Variable.Builder vb) {
    // Structures and StructureMembers cant be coordinate variables
    if ((vb.dataType == DataType.STRUCTURE) || vb.getParentStructureBuilder() != null)
      return false;

    int rank = vb.getRank();
    if (rank == 1) {
      String firstd = vb.getFirstDimensionName();
      if (vb.shortName.equals(firstd)) {
        return true;
      }
    }
    if (rank == 2) { // two dimensional
      String firstd = vb.getFirstDimensionName();
      // must be char valued (then its really a String)
      return vb.shortName.equals(firstd) && (vb.dataType == DataType.CHAR);
    }

    return false;
  }

  public static int countDomainSize(Variable.Builder... axes) {
    Set domain = new HashSet<>();
    for (Variable.Builder axis : axes) {
      domain.addAll(axis.getDimensions());
    }
    return domain.size();
  }

  /**
   * Does this axis "fit" this variable. True if all of the dimensions in the axis also appear in
   * the variable. If char variable, last dimension is left out.
   *
   * @param axis check if this axis is ok for the given variable
   * @param vp the given variable
   * @return true if all of the dimensions in the axis also appear in the variable.
   */
  protected boolean isCoordinateAxisForVariable(CoordinateAxis.Builder axis, VarProcess vp) {
    ImmutableList varDims = vp.vb.getDimensions();
    ImmutableList axisDims = axis.getDimensions();

    // a CHAR variable must really be a STRING, so leave out the last (string length) dimension
    int checkDims = axisDims.size();
    if (axis.dataType == DataType.CHAR)
      checkDims--;

    for (int i = 0; i < checkDims; i++) {
      Dimension axisDim = axisDims.get(i);
      // added in 6, but causes the following tests to fail:
      // ucar.nc2.dataset.TestCoordSysCompareMore.compareCoordSysBuilders:
      // --> cdmUnitTest/formats/hdf4/ssec/CAL_LID_L1-Launch-V1-06.2006-07-07T21-20-40ZD.hdf
      // --> cdmUnitTest/formats/hdf5/SMAP_L4_SM_aup_20140115T030000_V05007_001.h5
      // if (!axisDim.isShared()) { // anon dimensions dont count. TODO does this work?
      // continue;
      // }
      if (!varDims.contains(axisDim)) {
        return false;
      }
    }
    return true;
  }

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

  protected NetcdfDataset.Builder datasetBuilder;
  protected Group.Builder rootGroup;
  protected CoordinatesHelper.Builder coords;

  protected List varList = new ArrayList<>();
  // coordinate variables for Dimension (full name)
  protected Multimap coordVarsForDimension = ArrayListMultimap.create();
  // default name of Convention, override in subclass
  protected String conventionName = _Coordinate.Convention;
  protected Formatter parseInfo = new Formatter();
  protected Formatter userAdvice = new Formatter();
  protected boolean debug;

  // Used when using NcML to provide convention attributes.
  protected CoordSystemBuilder(NetcdfDataset.Builder datasetBuilder) {
    this.datasetBuilder = datasetBuilder;
    this.rootGroup = datasetBuilder.rootGroup;
    this.coords = datasetBuilder.coords;
  }

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

  public String getConventionUsed() {
    return conventionName;
  }

  protected void addUserAdvice(String advice) {
    userAdvice.format("%s", advice);
  }

  public String getParseInfo() {
    return parseInfo.toString();
  }

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

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // subclasses can override any of these routines

  protected void augmentDataset(CancelTask cancelTask) throws IOException {}

  // All these steps may be overriden by subclasses.
  protected void buildCoordinateSystems() {
    // put status info into parseInfo that can be shown to someone trying to debug this process
    parseInfo.format("Parsing with Convention = %s%n", conventionName);

    // Bookkeeping info for each variable is kept in the VarProcess inner class
    addVariables(datasetBuilder.rootGroup);

    // identify which variables are coordinate axes
    identifyCoordinateAxes();
    // identify which variables are used to describe coordinate systems
    identifyCoordinateSystems();
    // identify which variables are used to describe coordinate transforms
    identifyCoordinateTransforms();

    // turn Variables into CoordinateAxis objects
    makeCoordinateAxes();

    // make Coordinate Systems for all Coordinate Systems Variables
    makeCoordinateSystems();

    // assign explicit CoordinateSystem objects to variables
    assignCoordinateSystemsExplicit();

    // assign implicit CoordinateSystem objects to variables
    makeCoordinateSystemsImplicit();

    // optionally assign implicit CoordinateSystem objects to variables that dont have one yet
    if (useMaximalCoordSys) {
      makeCoordinateSystemsMaximal();
    }

    // make Coordinate Transforms
    makeCoordinateTransforms();

    // assign Coordinate Transforms
    assignCoordinateTransforms();
  }

  private void addVariables(Group.Builder group) {
    for (Variable.Builder vb : group.vbuilders) {
      if (vb instanceof VariableDS.Builder) {
        varList.add(new VarProcess(group, (VariableDS.Builder) vb));
      } else if (vb instanceof StructureDS.Builder) {
        addStructure(group, (StructureDS.Builder) vb);
      }
    }

    for (Group.Builder nested : group.gbuilders) {
      addVariables(nested);
    }
  }

  private void addStructure(Group.Builder group, StructureDS.Builder structure) {
    List> nested = structure.vbuilders;
    for (Variable.Builder vb : nested) {
      if (vb instanceof VariableDS.Builder) {
        varList.add(new VarProcess(group, (VariableDS.Builder) vb));
      } else if (vb instanceof StructureDS.Builder) { // LOOK the actual Structure isnt in the VarProcess list.
        addStructure(group, (StructureDS.Builder) vb);
      }
    }
  }

  /** Everything named in the coordinateAxes or coordinates attribute are Coordinate axes. */
  protected void identifyCoordinateAxes() {
    for (VarProcess vp : varList) {
      if (vp.coordinateAxes != null) {
        identifyCoordinateAxes(vp, vp.coordinateAxes);
      }
      if (vp.coordinates != null) {
        identifyCoordinateAxes(vp, vp.coordinates);
      }
    }
  }

  // Mark named coordinates as "isCoordinateAxis"
  private void identifyCoordinateAxes(VarProcess vp, String coordinates) {
    StringTokenizer stoker = new StringTokenizer(coordinates);
    while (stoker.hasMoreTokens()) {
      String vname = stoker.nextToken();
      VarProcess ap = findVarProcess(vname, vp);
      if (ap == null) {
        Group.Builder gb = vp.vb.getParentGroupBuilder();
        Optional> vopt = gb.findVariableOrInParent(vname);
        if (vopt.isPresent()) {
          ap = findVarProcess(vopt.get().getFullName(), vp);
        } else {
          parseInfo.format("***Cant find coordAxis %s referenced from var= %s%n", vname, vp);
          userAdvice.format("***Cant find coordAxis %s referenced from var= %s%n", vname, vp);
        }
      }

      if (ap != null) {
        if (!ap.isCoordinateAxis) {
          parseInfo.format(" CoordinateAxis = %s added; referenced from var= %s%n", vname, vp);
        }
        ap.isCoordinateAxis = true;
      } else {
        parseInfo.format("***Cant find coordAxis %s referenced from var= %s%n", vname, vp);
        userAdvice.format("***Cant find coordAxis %s referenced from var= %s%n", vname, vp);
      }
    }
  }

  /** Identify coordinate systems, using _Coordinate.Systems attribute. */
  protected void identifyCoordinateSystems() {
    for (VarProcess vp : varList) {
      if (vp.coordinateSystems != null) {
        StringTokenizer stoker = new StringTokenizer(vp.coordinateSystems);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, vp);
          if (ap != null) {
            if (!ap.isCoordinateSystem) {
              parseInfo.format(" CoordinateSystem = %s added; referenced from var= %s%n", vname, vp);
            }
            ap.isCoordinateSystem = true;
          } else {
            parseInfo.format("***Cant find coordSystem %s referenced from var= %s%n", vname, vp);
            userAdvice.format("***Cant find coordSystem %s referenced from var= %s%n", vname, vp);
          }
        }
      }
    }
  }

  /** Identify coordinate transforms, using _CoordinateTransforms attribute. */
  protected void identifyCoordinateTransforms() {
    for (VarProcess vp : varList) {
      if (vp.coordinateTransforms != null) {
        StringTokenizer stoker = new StringTokenizer(vp.coordinateTransforms);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, vp);
          if (ap != null) {
            if (!ap.isCoordinateTransform) {
              parseInfo.format(" CoordinateTransform = %s added; referenced from var= %s%n", vname, vp);
            }
            ap.isCoordinateTransform = true;
          } else {
            parseInfo.format("***Cant find CoordinateTransform %s referenced from var= %s%n", vname, vp);
            userAdvice.format("***Cant find CoordinateTransform %s referenced from var= %s%n", vname, vp);
          }
        }
      }
    }
  }

  /**
   * Identify what kind of AxisType the named variable is. Only called for variables already
   * identified as Coordinate Axes. Default null - subclasses can override.
   *
   * @param vb a variable already identified as a Coordinate Axis
   * @return AxisType or null if unknown.
   */
  @Nullable
  protected AxisType getAxisType(VariableDS.Builder vb) {
    return null;
  }

  /**
   * Take previously identified Coordinate Axis and Coordinate Variables and make them into a
   * CoordinateAxis. Uses the getAxisType() method to figure out the type, if not already set.
   */
  protected void makeCoordinateAxes() {
    // The ones identified as coordinate variables or axes
    for (VarProcess vp : varList) {
      if (vp.isCoordinateAxis || vp.isCoordinateVariable) {
        if (vp.axisType == null) {
          vp.axisType = getAxisType(vp.vb);
        }
        if (vp.axisType == null) {
          userAdvice.format("Coordinate Axis %s does not have an assigned AxisType%n", vp);
        }
        vp.makeIntoCoordinateAxis();
      }
    }

    // The ones marked as Coordinate Systems, which will reference Coordinates
    for (VarProcess vp : varList) {
      if (vp.isCoordinateSystem) {
        vp.makeCoordinatesFromCoordinateSystem();
      }
    }
  }

  protected void makeCoordinateSystems() {
    // The ones marked as Coordinate Systems, which will reference Coordinates
    for (VarProcess vp : varList) {
      if (vp.isCoordinateSystem) {
        vp.makeCoordinateSystem();
      }
    }
  }

  /**
   * Assign explicit CoordinateSystem objects to variables.
   */
  protected void assignCoordinateSystemsExplicit() {

    // look for explicit references to coord sys variables
    for (VarProcess vp : varList) {
      if (vp.coordinateSystems != null && !vp.isCoordinateTransform) {
        StringTokenizer stoker = new StringTokenizer(vp.coordinateSystems);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, vp);
          if (ap == null) {
            parseInfo.format("***Cant find Coordinate System variable %s referenced from var= %s%n", vname, vp);
            userAdvice.format("***Cant find Coordinate System variable %s referenced from var= %s%n", vname, vp);
          } else if (ap.cs == null) {
            parseInfo.format("***Not a Coordinate System variable %s referenced from var= %s%n", vname, vp);
            userAdvice.format("***Not a Coordinate System variable %s referenced from var= %s%n", vname, vp);
          } else {
            String sysName = coords.makeCanonicalName(vp.vb, ap.cs.coordAxesNames);
            vp.vb.addCoordinateSystemName(sysName);
          }
        }
      }
    }

    // look for explicit references from coord sys variables to data variables
    for (VarProcess csVar : varList) {
      if (!csVar.isCoordinateSystem || (csVar.coordinateSystemsFor == null)) {
        continue;
      }

      // get list of dimensions from '_CoordinateSystemFor' attribute
      Set dimList = new HashSet<>();
      StringTokenizer stoker = new StringTokenizer(csVar.coordinateSystemsFor);
      while (stoker.hasMoreTokens()) {
        String dname = stoker.nextToken();
        Optional dimOpt = rootGroup.findDimension(dname);
        if (dimOpt.isPresent()) {
          dimList.add(dimOpt.get().getShortName());
        } else {
          parseInfo.format("***Cant find Dimension %s referenced from CoordSys var= %s%n", dname, csVar);
          userAdvice.format("***Cant find Dimension %s referenced from CoordSys var= %s%n", dname, csVar);
        }
      }

      // look for vars with those dimensions
      for (VarProcess vp : varList) {
        if (!vp.hasCoordinateSystem() && vp.isData() && (csVar.cs != null)) {
          if (CoordinateSystem.isSubset(dimList, vp.vb.getDimensionsAll())
              && CoordinateSystem.isSubset(vp.vb.getDimensionsAll(), dimList)) {
            vp.vb.addCoordinateSystemName(csVar.cs.coordAxesNames);
          }
        }
      }
    }

    // look for explicit listings of coordinate axes
    for (VarProcess vp : varList) {
      if (!vp.hasCoordinateSystem() && (vp.coordinateAxes != null) && vp.isData()) {
        String coordSysName = coords.makeCanonicalName(vp.vb, vp.coordinateAxes);
        Optional cso = coords.findCoordinateSystem(coordSysName);
        if (cso.isPresent()) {
          vp.vb.addCoordinateSystemName(coordSysName);
          parseInfo.format(" assigned explicit CoordSystem '%s' for var= %s%n", coordSysName, vp);
        } else {
          CoordinateSystem.Builder csnew = CoordinateSystem.builder().setCoordAxesNames(coordSysName);
          coords.addCoordinateSystem(csnew);
          vp.vb.addCoordinateSystemName(coordSysName);
          parseInfo.format(" created explicit CoordSystem '%s' for var= %s%n", coordSysName, vp);
        }
      }
    }
  }

  /**
   * Make implicit CoordinateSystem objects for variables that dont already have one, by using the
   * variables' list of coordinate axes, and any coordinateVariables for it. Must be at least 2
   * axes. All of a variable's _Coordinate Variables_ plus any variables listed in a
   * *__CoordinateAxes_* or *_coordinates_* attribute will be made into an *_implicit_* Coordinate
   * System. If there are at least two axes, and the coordinate system uses all of the variable's
   * dimensions, it will be assigned to the data variable.
   */
  protected void makeCoordinateSystemsImplicit() {
    for (VarProcess vp : varList) {
      if (!vp.hasCoordinateSystem() && vp.maybeData()) {
        List dataAxesList = vp.findCoordinateAxes(true);
        if (dataAxesList.size() < 2) {
          continue;
        }

        String csName = coords.makeCanonicalName(dataAxesList);
        Optional csOpt = coords.findCoordinateSystem(csName);
        if (csOpt.isPresent() && coords.isComplete(csOpt.get(), vp.vb)) {
          vp.vb.addCoordinateSystemName(csName);
          parseInfo.format(" assigned implicit CoordSystem '%s' for var= %s%n", csName, vp);
        } else {
          CoordinateSystem.Builder csnew = CoordinateSystem.builder().setCoordAxesNames(csName).setImplicit(true);
          if (coords.isComplete(csnew, vp.vb)) {
            vp.vb.addCoordinateSystemName(csName);
            coords.addCoordinateSystem(csnew);
            parseInfo.format(" created implicit CoordSystem '%s' for var= %s%n", csName, vp);
          }
        }
      }
    }
  }

  /**
   * If a variable still doesnt have a coordinate system, use hueristics to try to find one that was
   * probably forgotten. Examine existing CS. create a subset of axes that fits the variable. Choose
   * the one with highest rank. It must have X,Y or lat,lon. If so, add it.
   */
  private void makeCoordinateSystemsMaximal() {

    boolean requireCompleteCoordSys =
        !datasetBuilder.getEnhanceMode().contains(NetcdfDataset.Enhance.IncompleteCoordSystems);

    for (VarProcess vp : varList) {
      if (vp.hasCoordinateSystem() || !vp.isData()) {
        continue;
      }

      // look through all axes that fit
      List axisList = new ArrayList<>();
      for (CoordinateAxis.Builder axis : coords.coordAxes) {
        if (isCoordinateAxisForVariable(axis, vp)) {
          axisList.add(axis);
        }
      }

      if (axisList.size() < 2) {
        continue;
      }

      String csName = coords.makeCanonicalName(axisList);
      Optional csOpt = coords.findCoordinateSystem(csName);
      boolean okToBuild = false;

      // do coordinate systems need to be complete?
      // default enhance mode is yes, they must be complete
      if (requireCompleteCoordSys) {
        if (csOpt.isPresent()) {
          // only build if coordinate system is complete
          okToBuild = coords.isComplete(csOpt.get(), vp.vb);
        }
      } else {
        // coordinate system can be incomplete, so we're ok to build if we find something
        okToBuild = true;
      }

      if (csOpt.isPresent() && okToBuild) {
        vp.vb.addCoordinateSystemName(csName);
        parseInfo.format(" assigned maximal CoordSystem '%s' for var= %s%n", csName, vp);

      } else {
        CoordinateSystem.Builder csnew = CoordinateSystem.builder().setCoordAxesNames(csName);
        // again, do coordinate systems need to be complete?
        // default enhance mode is yes, they must be complete
        if (requireCompleteCoordSys) {
          // only build if new coordinate system is complete
          okToBuild = coords.isComplete(csnew, vp.vb);
        }
        if (okToBuild) {
          csnew.setImplicit(true);
          vp.vb.addCoordinateSystemName(csName);
          coords.addCoordinateSystem(csnew);
          parseInfo.format(" created maximal CoordSystem '%s' for var= %s%n", csnew.coordAxesNames, vp);
        }
      }
    }
  }

  /**
   * Take all previously identified Coordinate Transforms and create a CoordinateTransform object by
   * calling CoordTransBuilder.makeCoordinateTransform().
   */
  protected void makeCoordinateTransforms() {
    for (VarProcess vp : varList) {
      if (vp.isCoordinateTransform && vp.ct == null) { // TODO dont have ncd
        vp.ct = makeCoordinateTransform(vp.vb);
        if (vp.ct != null) {
          coords.addCoordinateTransform(vp.ct);
        }
      }
    }
  }

  protected CoordinateTransform.Builder makeCoordinateTransform(VariableDS.Builder vb) {
    return CoordinateTransform.builder().setName(vb.getFullName()).setAttributeContainer(vb.getAttributeContainer());
  }

  /** Assign CoordinateTransform objects to Variables and Coordinate Systems. */
  protected void assignCoordinateTransforms() {
    // look for explicit transform assignments on the coordinate systems
    for (VarProcess vp : varList) {
      if (vp.isCoordinateSystem && vp.coordinateTransforms != null) {
        StringTokenizer stoker = new StringTokenizer(vp.coordinateTransforms);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, vp);
          if (ap != null) {
            if (ap.ct != null) {
              vp.addCoordinateTransform(ap.ct);
              parseInfo.format(" assign explicit coordTransform %s to CoordSys= %s%n", ap.ct, vp.cs);
            } else {
              parseInfo.format("***Cant find coordTransform in %s referenced from var= %s%n", vname,
                  vp.vb.getFullName());
              userAdvice.format("***Cant find coordTransform in %s referenced from var= %s%n", vname,
                  vp.vb.getFullName());
            }
          } else {
            parseInfo.format("***Cant find coordTransform variable= %s referenced from var= %s%n", vname,
                vp.vb.getFullName());
            userAdvice.format("***Cant find coordTransform variable= %s referenced from var= %s%n", vname,
                vp.vb.getFullName());
          }
        }
      }
    }

    // look for explicit coordSys assignments on the coordinate transforms
    for (VarProcess vp : varList) {
      if (vp.isCoordinateTransform && (vp.ct != null) && (vp.coordinateSystems != null)) {
        StringTokenizer stoker = new StringTokenizer(vp.coordinateSystems);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess vcs = findVarProcess(vname, vp);
          if (vcs == null) {
            parseInfo.format("***Cant find coordSystem variable= %s referenced from var= %s%n", vname,
                vp.vb.getFullName());
            userAdvice.format("***Cant find coordSystem variable= %s referenced from var= %s%n", vname,
                vp.vb.getFullName());
          } else {
            vcs.addCoordinateTransform(vp.ct);
            parseInfo.format("***assign explicit coordTransform %s to CoordSys=  %s%n", vp.ct, vp.cs);
          }
        }
      }
    }

    // look for coordAxes assignments on the coordinate transforms
    for (VarProcess vp : varList) {
      if (vp.isCoordinateTransform && (vp.ct != null) && (vp.coordinateAxes != null)) {
        List dataAxesList = vp.findCoordinateAxes(false);
        if (!dataAxesList.isEmpty()) {
          for (CoordinateSystem.Builder cs : coords.coordSys) {
            if (coords.containsAxes(cs, dataAxesList)) {
              coords.addCoordinateTransform(vp.ct);
              cs.addCoordinateTransformByName(vp.ct.name);
              parseInfo.format("***assign (implicit coordAxes) coordTransform %s to CoordSys=  %s%n", vp.ct, cs);
            }
          }
        }
      }
    }

    // look for coordAxisType assignments on the coordinate transforms
    for (VarProcess vp : varList) {
      if (vp.isCoordinateTransform && (vp.ct != null) && (vp.coordAxisTypes != null)) {
        List axisTypesList = new ArrayList<>();
        StringTokenizer stoker = new StringTokenizer(vp.coordAxisTypes);
        while (stoker.hasMoreTokens()) {
          String name = stoker.nextToken();
          AxisType atype;
          if (null != (atype = AxisType.getType(name))) {
            axisTypesList.add(atype);
          }
        }
        if (!axisTypesList.isEmpty()) {
          for (CoordinateSystem.Builder cs : coords.coordSys) {
            if (coords.containsAxisTypes(cs, axisTypesList)) {
              cs.addCoordinateTransformByName(vp.ct.name);
              parseInfo.format("***assign (implicit coordAxisType) coordTransform %s to CoordSys=  %s%n", vp.ct, cs);
            }
          }
        }
      }
    }

  }

  protected VarProcess findVarProcess(String name, VarProcess from) {
    if (name == null) {
      return null;
    }

    // compare full name
    for (VarProcess vp : varList) {
      if (name.equals(vp.vb.getFullName())) {
        return vp;
      }
    }

    // prefer ones in the same group
    if (from != null) {
      for (VarProcess vp : varList) {
        if (vp.vb == null || vp.vb.getParentGroupBuilder() == null || from.vb == null) {
          continue;
        }
        if (name.equals(vp.vb.shortName) && vp.vb.getParentGroupBuilder().equals(from.vb.getParentGroupBuilder())) {
          return vp;
        }
      }
    }

    // WAEF, use short name
    for (VarProcess vp : varList) {
      if (name.equals(vp.vb.shortName)) {
        return vp;
      }
    }

    return null;
  }

  protected VarProcess findCoordinateAxis(String name) {
    if (name == null) {
      return null;
    }

    for (VarProcess vp : varList) {
      if (name.equals(vp.vb.getFullName()) && (vp.isCoordinateVariable || vp.isCoordinateAxis)) {
        return vp;
      }
    }
    return null;
  }

  /**
   * Create a "dummy" Coordinate Transform Variable based on the given CoordinateTransform.
   * This creates a scalar Variable with dummy data, and adds the Parameters of the CoordinateTransform
   * as attributes.
   *
   * @param ct based on the CoordinateTransform
   * @return the Coordinate Transform Variable. You must add it to the dataset.
   */
  protected VariableDS.Builder makeCoordinateTransformVariable(CoordinateTransform ct) {
    VariableDS.Builder v = VariableDS.builder().setName(ct.getName()).setDataType(DataType.CHAR);
    List params = ct.getParameters();
    for (Parameter p : params) {
      if (p.isString())
        v.addAttribute(new Attribute(p.getName(), p.getStringValue()));
      else {
        double[] data = p.getNumericValues();
        Array dataA = Array.factory(DataType.DOUBLE, new int[] {data.length}, data);
        v.addAttribute(new Attribute(p.getName(), dataA));
      }
    }
    v.addAttribute(new Attribute(_Coordinate.TransformType, ct.getTransformType().toString()));

    // fake data
    Array data = Array.factory(DataType.CHAR, new int[] {}, new char[] {' '});
    v.setCachedData(data, true);

    parseInfo.format("  made CoordinateTransformVariable: %s%n", ct.getName());
    return v;
  }

  @Nullable
  private String findDimFullName(Variable.Builder vb, String dimShortName) {
    String dimFullName = null;
    String dimName = EscapeStrings.backslashEscape(dimShortName, NetcdfFiles.reservedFullName);
    // In order to get the "full" dimension name, we need to know what group it lives in. We will start in the group
    // that the variable belongs to and work our way up the group tree. If we don't find the dimension, we get null.
    Group.Builder gb = vb.getParentGroupBuilder();
    while (gb != null) {
      Optional dim = gb.findDimensionLocal(dimName);
      if (dim.isPresent()) {
        dimFullName = makeDimFullName(gb, dimName);
        break;
      } else {
        gb = gb.getParentGroup();
      }
    }
    return dimFullName;
  }

  @Nullable
  private String makeDimFullName(Variable.Builder vb, Dimension d) {
    Preconditions.checkNotNull(d, "Cannot construct the full name of a null Dimension.");
    Group.Builder gb = vb.getParentGroupBuilder();
    return (gb != null) && gb.contains(d) ? makeDimFullName(gb, d.getShortName()) : null;
  }

  private String makeDimFullName(Group.Builder gb, String dimShortName) {
    Preconditions.checkNotNull(dimShortName, "Cannot construct the full name of a Dimension if its short name is null");
    String dimName = EscapeStrings.backslashEscape(dimShortName, NetcdfFiles.reservedFullName);
    return gb != null ? gb.makeFullName() + dimName : dimName;
  }

  /** Classifications of Variables into axis, systems and transforms */
  protected class VarProcess {
    public Group.Builder gb;
    public VariableDS.Builder vb;

    // attributes
    public String coordVarAlias; // _Coordinate.AliasForDimension
    public String positive; // _Coordinate.ZisPositive or CF.POSITIVE
    public String coordinateAxes; // _Coordinate.Axes
    public String coordinateSystems; // _Coordinate.Systems
    public String coordinateSystemsFor; // _Coordinate.SystemsFor
    public String coordinateTransforms; // _Coordinate.Transforms
    public String coordAxisTypes; // _Coordinate.AxisTypes
    public String coordTransformType; // _Coordinate.TransformType
    public String coordinates; // CF coordinates (set by subclasses)

    // coord axes
    public boolean isCoordinateVariable; // classic coordinate variable
    public boolean isCoordinateAxis;
    public AxisType axisType;
    public CoordinateAxis.Builder axis; // if its made into a Coordinate Axis, this is not null

    // coord systems
    public boolean isCoordinateSystem;
    public CoordinateSystem.Builder cs;

    // coord transform
    public boolean isCoordinateTransform;
    public CoordinateTransform.Builder ct;

    /**
     * Wrap the given variable. Identify Coordinate Variables. Process all _Coordinate attributes.
     *
     * @param v wrap this Variable
     */
    private VarProcess(Group.Builder gb, VariableDS.Builder v) {
      this.gb = gb;
      this.vb = v;
      isCoordinateVariable =
          isCoordinateVariable(v) || (null != v.getAttributeContainer().findAttribute(_Coordinate.AliasForDimension));
      if (isCoordinateVariable) {
        String dimFullName = makeDimFullName(v.getParentGroupBuilder(), v.getFirstDimensionName());
        coordVarsForDimension.put(dimFullName, this);
      }

      Attribute att = v.getAttributeContainer().findAttributeIgnoreCase(_Coordinate.AxisType);
      if (att != null) {
        String axisName = att.getStringValue();
        axisType = AxisType.getType(axisName);
        isCoordinateAxis = true;
        parseInfo.format(" Coordinate Axis added = %s type= %s%n", v.getFullName(), axisName);
      }

      coordVarAlias = v.getAttributeContainer().findAttributeString(_Coordinate.AliasForDimension, null);
      if (coordVarAlias != null) {
        coordVarAlias = coordVarAlias.trim();
        if (v.getRank() != 1) {
          parseInfo.format("**ERROR Coordinate Variable Alias %s has rank %d%n", v.getFullName(), v.getRank());
          userAdvice.format("**ERROR Coordinate Variable Alias %s has rank %d%n", v.getFullName(), v.getRank());
        } else {
          Optional coordDimOpt = gb.findDimension(coordVarAlias);
          coordDimOpt.ifPresent(coordDim -> {
            String vDim = v.getFirstDimensionName();
            if (!coordDim.getShortName().equals(vDim)) {
              parseInfo.format("**ERROR Coordinate Variable Alias %s names wrong dimension %s%n", v.getFullName(),
                  coordVarAlias);
              userAdvice.format("**ERROR Coordinate Variable Alias %s names wrong dimension %s%n", v.getFullName(),
                  coordVarAlias);
            } else {
              isCoordinateAxis = true;
              String dimFullName = makeDimFullName(v, coordDim);
              if (dimFullName != null) {
                coordVarsForDimension.put(dimFullName, this);
                parseInfo.format(" Coordinate Variable Alias added = %s for dimension= %s%n", v.getFullName(),
                    coordVarAlias);
              } else {
                parseInfo.format("Can't find dimension %s in a Group or parent Group of variable %s%n",
                    coordDim.getShortName(), vb);
                userAdvice.format("Can't find dimension %s in a Group or parent Group of variable %s%n",
                    coordDim.getShortName(), vb);
              }
            }
          });
        }
      }

      positive = v.getAttributeContainer().findAttributeString(_Coordinate.ZisPositive, null);
      if (positive == null) {
        positive = v.getAttributeContainer().findAttributeString(CF.POSITIVE, null);
      } else {
        isCoordinateAxis = true;
        positive = positive.trim();
        parseInfo.format(" Coordinate Axis added(from positive attribute ) = %s for dimension= %s%n", v.getFullName(),
            coordVarAlias);
      }

      coordinateAxes = v.getAttributeContainer().findAttributeString(_Coordinate.Axes, null);
      coordinateSystems = v.getAttributeContainer().findAttributeString(_Coordinate.Systems, null);
      coordinateSystemsFor = v.getAttributeContainer().findAttributeString(_Coordinate.SystemFor, null);
      coordinateTransforms = v.getAttributeContainer().findAttributeString(_Coordinate.Transforms, null);
      isCoordinateSystem = (coordinateTransforms != null) || (coordinateSystemsFor != null);

      coordAxisTypes = v.getAttributeContainer().findAttributeString(_Coordinate.AxisTypes, null);
      coordTransformType = v.getAttributeContainer().findAttributeString(_Coordinate.TransformType, null);
      isCoordinateTransform = (coordTransformType != null) || (coordAxisTypes != null);
    }

    protected boolean isData() {
      return !isCoordinateVariable && !isCoordinateAxis && !isCoordinateSystem && !isCoordinateTransform;
    }

    protected boolean maybeData() {
      return !isCoordinateVariable && !isCoordinateSystem && !isCoordinateTransform;
    }

    protected boolean hasCoordinateSystem() {
      return !vb.coordSysNames.isEmpty();
    }

    public String toString() {
      return vb.shortName;
    }

    /**
     * Turn the variable into a coordinate axis.
     * Add to the dataset, replacing variable if needed.
     *
     * @return coordinate axis
     */
    protected CoordinateAxis.Builder makeIntoCoordinateAxis() {
      if (axis != null) {
        return axis;
      }

      if (vb instanceof CoordinateAxis.Builder) {
        axis = (CoordinateAxis.Builder) vb;
      } else {
        // Create a CoordinateAxis out of this variable.
        vb = axis = CoordinateAxis.fromVariableDS(vb);
      }

      if (axisType != null) {
        axis.setAxisType(axisType);
        axis.addAttribute(new Attribute(_Coordinate.AxisType, axisType.toString()));

        if (((axisType == AxisType.Height) || (axisType == AxisType.Pressure) || (axisType == AxisType.GeoZ))
            && (positive != null)) {
          axis.setPositive(positive);
          axis.addAttribute(new Attribute(_Coordinate.ZisPositive, positive));
        }
      }
      coords.replaceCoordinateAxis(axis);
      if (axis.getParentStructureBuilder() != null) {
        axis.getParentStructureBuilder().replaceMemberVariable(axis);
      } else {
        gb.replaceVariable(axis);
      }
      return axis;
    }

    /** For any variable listed in a coordinateAxes attribute, make into a coordinate. */
    protected void makeCoordinatesFromCoordinateSystem() {
      // find referenced coordinate axes
      if (coordinateAxes != null) {
        StringTokenizer stoker = new StringTokenizer(coordinateAxes); // _CoordinateAxes attribute
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, this);
          if (ap != null) {
            ap.makeIntoCoordinateAxis();
          } else {
            parseInfo.format(" Cant find axes %s for Coordinate System %s%n", vname, vb);
            userAdvice.format(" Cant find axes %s for Coordinate System %s%n", vname, vb);
          }
        }
      }
    }

    /** For explicit coordinate system variables, make a CoordinateSystem. */
    protected void makeCoordinateSystem() {
      if (coordinateAxes != null) {
        String sysName = coords.makeCanonicalName(vb, coordinateAxes);
        this.cs = CoordinateSystem.builder().setCoordAxesNames(sysName);
        parseInfo.format(" Made Coordinate System '%s'", sysName);
        coords.addCoordinateSystem(this.cs);
      }
    }

    /**
     * Create a list of coordinate axes for this data variable. Use the list of names in axes or
     * coordinates field.
     *
     * @param addCoordVariables if true, add any coordinate variables that are missing.
     * @return list of coordinate axes for this data variable.
     */
    protected List findCoordinateAxes(boolean addCoordVariables) {
      List axesList = new ArrayList<>();

      if (coordinateAxes != null) { // explicit axes
        StringTokenizer stoker = new StringTokenizer(coordinateAxes);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, this);
          if (ap != null) {
            CoordinateAxis.Builder axis = ap.makeIntoCoordinateAxis();
            if (!axesList.contains(axis)) {
              axesList.add(axis);
            }
          }
        }
      } else if (coordinates != null) { // CF partial listing of axes
        StringTokenizer stoker = new StringTokenizer(coordinates);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess(vname, this);
          if (ap != null) {
            CoordinateAxis.Builder axis = ap.makeIntoCoordinateAxis(); // LOOK check if its legal
            if (!axesList.contains(axis)) {
              axesList.add(axis);
            }
          }
        }
      }

      if (addCoordVariables) {
        for (String d : vb.getDimensionsAll()) {
          String dimFullName = findDimFullName(vb, d);
          for (VarProcess vp : coordVarsForDimension.get(dimFullName)) {
            CoordinateAxis.Builder axis = vp.makeIntoCoordinateAxis();
            if (!axesList.contains(axis)) {
              axesList.add(axis);
            }
          }
        }
      }

      return axesList;
    }

    void addCoordinateTransform(CoordinateTransform.Builder ct) {
      if (cs == null) {
        parseInfo.format("  %s: no CoordinateSystem for CoordinateTransformVariable: %s%n", vb.getFullName(), ct.name);
        return;
      }
      cs.addCoordinateTransformByName(ct.name);
    }

  } // VarProcess

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy