// $Id: CoordSysBuilder.java,v 1.7 2004/12/10 17:04:16 caron Exp $
/*
 * Copyright 1997-2000 Unidata Program Center/University Corporation for
 * Atmospheric Research, P.O. Box 3000, Boulder, CO 80307,
 * support@unidata.ucar.edu.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or (at
 * your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package ucar.nc2.dataset;

import ucar.nc2.*;
import ucar.nc2.util.CancelTask;
import ucar.nc2.ncml.NcMLReader;
import ucar.nc2.dataset.*;
import ucar.nc2.dataset.conv.*;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;
import ucar.unidata.geoloc.vertical.*;
import ucar.unidata.util.Parameter;

import ucar.ma2.Array;
import ucar.ma2.IndexIterator;

import java.lang.reflect.Method;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.util.*;

/**
 * Abstract class for implementing Convention-specific parsing of netCDF files.
 *
 * You can use an NcML file alone (use registerNcML()) if file uses a convention attribute.
 * If not, you must implement a class that implements isMine() to identify your files, and
 *  call wrapNcML in the augmentDataset method (see eg ATDRadarConvention class).
 *
 * @author caron
 * @version $Revision: 1.7 $ $Date: 2004/12/10 17:04:16 $
 */

public class CoordSysBuilder {
  static private HashMap registeredConventions = new HashMap();
  static private HashMap registeredNcML = new HashMap();

  static {
    registerConvention("_Coordinates", CoordSysBuilder.class);

    registerConvention("ATDRadar", ATDRadarConvention.class);
    registerConvention("Zebra", ZebraConvention.class);
    registerConvention("GIEF/GIEF-F", GIEFConvention.class);

    registerConvention("COARDS", COARDSConvention.class);
    registerConvention("NCAR-CSM", CSMConvention.class);
    registerConvention("CF-1.0", CF1Convention.class);
    registerConvention("GDV", GDVConvention.class);

    // the 3 uglies
    registerConvention("NUWG", NUWGConvention.class);
    registerConvention("AWIPS", AWIPSConvention.class);
    registerConvention("WRF", WRFConvention.class);

    registerConvention("M3IO", M3IOConvention.class);
  }

  /**
   * Register an NcML file that implements a Convention.
   * @param conventionName name of Convention, must be in the "Conventions" global attribute.
   * @param ncmlLocation location of NcML file.
   */
  static public void registerNcML( String conventionName, String ncmlLocation) {
    registeredConventions.put( conventionName, ncmlLocation);
  }

  /**
   * Register a class that implements a Convention.
   * @param conventionName name of Convention.
   *   This name will be used to look in the "Conventions" global attribute.
   *   Otherwise, you must implement the isMine() static method.
   * @param c subclass of CoordSysBuilder that parses those kinds of netcdf files.
   */
  static public void registerConvention( String conventionName, Class c) {
    registeredConventions.put( conventionName, c);
  }

    /** subclasses override.
  public static boolean isMine( NetcdfFile ncfile) {
    return false;
  } */

  /**
   * Add Coordinate information to a NetcdfDataset using a registered Convention parsing class.
   * @param ds the NetcdfDataset to modify
   * @param cancelTask allow user to bail out.
   */
  static public void addCoordinateSystems( NetcdfDataset ds, CancelTask cancelTask) throws IOException {
    if (ds.getCoordSysWereAdded()) return; // ??

    // look for the Conventions attribute
    String name = ds.findAttValueIgnoreCase(null, "Conventions", null);
    if (name == null)
      name = ds.findAttValueIgnoreCase(null, "Convention", null);

    // look for ncml first
    String convNcML = null;
    if (name != null) {
      convNcML = (String) registeredNcML.get( name);
      if (convNcML != null) {
        CoordSysBuilder csb = new CoordSysBuilder();
        NcMLReader.wrapNcML( ds, convNcML, cancelTask);
        csb.buildCoordinateSystems( ds);
        return;
      }
    }

    // now look for Convention parsing class
    Class convClass = null;
    if (name != null) {
      convClass = (Class) registeredConventions.get( name);

      // look for comma delimited list
      if (convClass == null) {
        StringTokenizer stoke = new StringTokenizer(name, ", ");
        while (stoke.hasMoreTokens() && (convClass == null)) {
          String token = stoke.nextToken();

          convClass = (Class) registeredConventions.get( token);
          if (convClass != null) break; // take first one that matches
        }
      }
    }

    /* look for standard ones that dont use Convention attribute.
    if (convClass == null) {
      if (AWIPSConvention.isMine(ds))
        name = "AWIPS";
      else if (WRFConvention.isMine(ds))
        name = "WRF";
      else if (ATDRadarConvention.isMine(ds))
        name = "ATDRadar";
      else if (ZebraConvention.isMine(ds))
        name = "Zebra";
      else if (M3IOConvention.isMine(ds))
        name = "M3IO";

      if (name != null)
        convClass = (Class) registeredConventions.get(name);
    } */

    // look for registered (non-standard) ones that dont use Convention attribute.
    // call isMine() using reflection. note order is random
    if (convClass == null) {
      for (Iterator iter = registeredConventions.keySet().iterator(); iter.hasNext(); ) {
        String key = (String) iter.next();
        Class c = (Class) registeredConventions.get(key);
        Method m = null;

        try {
          m = c.getMethod("isMine", new Class[] { NetcdfFile.class });
        } catch (NoSuchMethodException ex) {
          //System.out.println("ERROR: Class "+c.getName()+" does not have isMine(NetcdfFile) method\n");
          continue;
        }

        try {
          Boolean result = (Boolean) m.invoke(null, new Object[] {ds});
          if (result.booleanValue()) {
            convClass = c;
            break;
          }
        } catch (Exception ex) {
          System.out.println("ERROR: Class "+c.getName()+" Exception invoking isMine method\n"+ex);
        }

      }

    }

    // no convention class found, GDV is the default
    if (convClass == null)
      convClass = GDVConvention.class;

    // get an instance of that class
    CoordSysBuilder conv = null;
    try {
      conv = (CoordSysBuilder) convClass.newInstance();
      if (conv == null) return;
    } catch (InstantiationException e) {
      return;
    } catch (IllegalAccessException e) {
      return;
    }

    // create the dataset to add info to
    conv.augmentDataset( ds, cancelTask);
    conv.buildCoordinateSystems( ds);
    return;
  }

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

  public String conventionName = "_Coordinates"; // name of Convention
  protected ArrayList varList = new ArrayList(); // varProcess objects
  protected StringBuffer parseInfo = new StringBuffer(); // debugging

  protected boolean debug = false, showRejects = false;

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

  /**
   * This is where subclasses make changes to the dataset, like adding new variables, attribuites, etc.
   *
   * @param ncDataset modify this dataset
   * @param cancelTask give user a chance to bail out
   * @throws IOException
   */
  protected void augmentDataset( NetcdfDataset ncDataset, CancelTask cancelTask) throws IOException { }

  /**
   * Identify what kind of AxisType the named variable is.
   * Only called for variables alreaddy identified as Coodinate Axes.
   * @param ncDataset for this dataset
   * @param v a variable alreaddy identified as a Coodinate Axis
   * @return AxisType or null if unknown.
   */
  protected AxisType getAxisType( NetcdfDataset ncDataset, VariableEnhanced v) {
    return null;
  }

  /**
   * Heres where the work is to identify coordinate axes and coordinate systems.
   * @param ncDataset modify this dataset
   */
  protected void buildCoordinateSystems( NetcdfDataset ncDataset) {
    //this.ncDataset = ncDataset;
    parseInfo.append("Parsing with Convention = "+conventionName+"\n");

    List vars = ncDataset.getVariables();
    for (int i = 0; i < vars.size(); i++) {
      VariableEnhanced v = (VariableEnhanced) vars.get(i);
      varList.add( new VarProcess(ncDataset, v));
    }

    findCoordinateAxes( ncDataset);
    // findCoordinateVariableAliases( ncDataset);
    findCoordinateSystems( ncDataset);
    findCoordinateTransforms( ncDataset);

    makeCoordinateAxes( ncDataset);

    makeCoordinateSystems( ncDataset);
    assignCoordinateSystems( ncDataset);
    makeCoordinateSystemsImplicit( ncDataset);

    makeCoordinateTransforms( ncDataset);
    assignCoordinateTransforms( ncDataset);

    ncDataset.setCoordSysWereAdded (true);
    ncDataset.setParseInfo( parseInfo.toString());
    if (debug) System.out.println("parseInfo = \n"+parseInfo.toString());
  }

  /**
   * Identify coordinate axes, set VarProcess.isCoordinateAxis = true.
   * Default is to look for those referenced by _CoordinateAxes attribute.
   * Note coordinate variables are already identified.
   */
  protected void findCoordinateAxes( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.coordAxes != null) {
        StringTokenizer stoker = new StringTokenizer( vp.coordAxes);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
          if (ap != null)
            ap.isCoordinateAxis = true;
          else
            parseInfo.append("***Cant find coordAxis "+vname+" referenced from var= "+vp.v.getName()+"\n");
        }
      }
    }
  }

  /*
   * Find any variables marked as Coordinate Aliases.
   * Better to do in the variable processing i think
   *
  protected void findCoordinateVariableAliases(NetcdfDataset ds) {
    String axes = ds.findAttValueIgnoreCase(null, "_CoordinateVariableAliases", null);
    if (axes == null)
      return;

    StringTokenizer stoker = new StringTokenizer( axes);
    while (stoker.hasMoreTokens()) {
      String vname = stoker.nextToken();
      Variable v = ds.findVariable(vname);
      if (v == null) {
        parseInfo.append(" cant find implicit coord axis " + vname + "\n");
        continue;
      }
      if (v.getRank() != 1) {
        parseInfo.append(" implicit coord axis must have rank 1" + vname + "\n");
        continue;
      }
      if (null != v.getCoordinateDimension()) {
        parseInfo.append(" implicit coord axis already assigned" + vname + "\n"); // LOOK ??
        continue;
      }
      Dimension dim = v.getDimension(0);
      if (dim.getCoordinateAxis() != null) {
        if (!dim.getCoordinateAxis().equals(v))
          parseInfo.append(" implicit coord axis dimension already assigned" + vname + "\n");
        continue;
      }

      // ok already do it
      dim.setCoordinateAxis(v);
      v.setIsCoordinateAxis( true);
      VarProcess vp = findVarProcess( vname);
      vp.isCoordinateVariable = true;
    }
  } */

  /**
   * Identify coordinate systems, set VarProcess.isCoordinateSystem = true.
   * Default is to look for those referenced by _CoordinateSystems attribute.
   */
  protected void findCoordinateSystems( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.coordSys != null) {
        StringTokenizer stoker = new StringTokenizer( vp.coordSys);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
          if (ap != null)
            ap.isCoordinateSystem = true;
          else
            parseInfo.append("***Cant find coordSystem "+vname+" referenced from var= "+vp.v.getName()+"\n");
        }
      }
    }
  }

  /**
   * Identify coordinate transforms, set VarProcess.isCoordinateTransform = true.
   * Default is to look for those referenced by _CoordinateTransforms attribute
   * ( or has a _CoordinateTransformType attribute, done in VarProcess constructor)
   */
  protected void findCoordinateTransforms( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.coordTransforms != null) {
        StringTokenizer stoker = new StringTokenizer( vp.coordTransforms);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
          if (ap != null)
            ap.isCoordinateTransform = true;
          else
            parseInfo.append("***Cant find coord Transform "+vname+" referenced from var= "+vp.v.getName()+"\n");
        }
      }
    }
  }

  /**
   * 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( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.isCoordinateAxis || vp.isCoordinateVariable) {
        if (vp.axisType == null)
          vp.axisType = getAxisType( ncDataset, (VariableEnhanced) vp.v);
        vp.makeIntoCoordinateAxis();
      }
    }
  }

   /**
   * Take all previously identified Coordinate Systems and create a
   * CoordinateSystem object.
   */
  protected void makeCoordinateSystems( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.isCoordinateSystem) {
        vp.makeCoordinateSystem();
      }
    }
  }

  /**
   * Assign CoordinateSystem objects to variables.
   */
  protected void assignCoordinateSystems( NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (vp.coordSys != null && !vp.isCoordinateTransform) {
        StringTokenizer stoker = new StringTokenizer( vp.coordSys);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
          if (ap == null) {
            parseInfo.append("***Cant find coord System "+vname+" referenced from var= "+vp.v.getName()+"\n");
            continue;
          }
          if (ap.cs == null) {
            parseInfo.append("***Cant find coord System "+vname+" referenced from var= "+vp.v.getName()+"\n");
            continue;
          }
          if ((ap != null) && (ap.cs != null)) {
            VariableEnhanced ve = (VariableEnhanced) vp.v;
            ve.addCoordinateSystem( ap.cs);
          }  else
            parseInfo.append("***Cant find coord System "+vname+" referenced from var= "+vp.v.getName()+"\n");

        }
      }
    }
  }

  /**
   * Make implicit CoordinateSystem objects for variables that dont already have one, by using the
   * variables' list of coordinate axes.
   * @param ncDataset
   */
  protected void makeCoordinateSystemsImplicit(NetcdfDataset ncDataset) {
    for (int i = 0; i < varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);

      if (vp.coordSys == null && vp.isData()) {
        List dataAxesList = vp.findCoordinateAxes( vp.coordAxes);
        if (dataAxesList.size() < 2)
          continue;

        VariableEnhanced ve = (VariableEnhanced) vp.v;
        String csName = CoordinateSystem.makeName(dataAxesList);
        CoordinateSystem cs = ncDataset.findCoordinateSystem(csName);
        if (cs != null) {
          ve.addCoordinateSystem(cs);
          parseInfo.append(" assigned implicit coord System " + cs.getName() + " for var= " + vp.v.getName() + "\n");
        } else {
          CoordinateSystem csnew = new CoordinateSystem(dataAxesList, null);
          csnew.setImplicit( true);
          //assignImplicitCoordinateTransforms( ncDataset, csnew);

          ve.addCoordinateSystem(csnew);
          ncDataset.addCoordinateSystem(csnew);
          parseInfo.append(" created implicit coord System " + csnew.getName() + " for var= " + vp.v.getName() + "\n");
        }
      }
    }
  }

  //protected void assignImplicitCoordinateTransforms( NetcdfDataset ncDataset, CoordinateSystem csnew) {
  //}

  /**
    * Take all previously identified Coordinate Transforms and create a
    * CoordinateTransform object by calling makeCoordinateTransform().
    */
   protected void makeCoordinateTransforms( NetcdfDataset ncDataset) {
     for (int i = 0; i < varList.size(); i++) {
       VarProcess vp = (VarProcess) varList.get(i);
       if (vp.isCoordinateTransform && vp.ct == null) {
         vp.ct = makeCoordinateTransform( vp.ds, vp.v);
       }
     }
   }

   /**
    * Assign CoordinateTransform objects to Coordinate Systems.
    */
   protected void assignCoordinateTransforms( NetcdfDataset ncDataset) {
     // look for explicit transform assignments on the coordinate systems
     for (int i = 0; i < varList.size(); i++) {
       VarProcess vp = (VarProcess) varList.get(i);
       if (vp.isCoordinateSystem && vp.coordTransforms != null) {
         StringTokenizer stoker = new StringTokenizer( vp.coordTransforms);
         while (stoker.hasMoreTokens()) {
           String vname = stoker.nextToken();
           VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
           if (ap != null) {
             if (ap.ct != null)
               vp.cs.addCoordinateTransform( ap.ct);
             else
               parseInfo.append("***Cant find coordTransform in "+vname+" referenced from var= "+vp.v.getName()+"\n");
           } else
             parseInfo.append("***Cant find coordTransform variable "+vname+" referenced from var= "+vp.v.getName()+"\n");
         }
       }
     }

     // look for explicit coordSys assignments on the coordinate transforms
     for (int i = 0; i < varList.size(); i++) {
       VarProcess vp = (VarProcess) varList.get(i);
       if (vp.isCoordinateTransform && (vp.ct != null) && (vp.coordSys != null)) {
         StringTokenizer stoker = new StringTokenizer( vp.coordSys);
         while (stoker.hasMoreTokens()) {
           String vname = stoker.nextToken();
           VarProcess vcs = findVarProcess( vname); // LOOK: full vs short name
           if (vcs == null)
             parseInfo.append("***Cant find coordSystem variable "+vname+" referenced from var= "+vp.v.getName()+"\n");
           else
             vcs.cs.addCoordinateTransform( vp.ct);
         }
       }
     }

     // look for coordAxes assignments on the coordinate transforms
     for (int i = 0; i < varList.size(); i++) {
       VarProcess vp = (VarProcess) varList.get(i);
       if (vp.isCoordinateTransform && (vp.ct != null) && (vp.coordAxes != null)) {
         List dataAxesList = vp.findCoordinateAxes( vp.coordAxes);

         if (dataAxesList.size() > 0) {
           List csList = ncDataset.getCoordinateSystems();
           for (int j = 0; j < csList.size(); j++) {
             CoordinateSystem cs = (CoordinateSystem) csList.get(j);
             if (cs.containsAxes( dataAxesList))
               cs.addCoordinateTransform(vp.ct);
           }
         } else {
           // try as a list of axes types
           List axisTypesList = new ArrayList();
           StringTokenizer stoker = new StringTokenizer( vp.coordAxes);
           while (stoker.hasMoreTokens()) {
             String name = stoker.nextToken();
             AxisType atype;
             if (null != (atype = AxisType.getType(name)))
               axisTypesList.add(atype);
           }
           if (axisTypesList.size() > 0) {
             List csList = ncDataset.getCoordinateSystems();
             for (int j = 0; j < csList.size(); j++) {
               CoordinateSystem cs = (CoordinateSystem) csList.get(j);
               if (cs.containsAxisTypes( axisTypesList))
                 cs.addCoordinateTransform(vp.ct);
             }
           }
         }
       }
     }

   }

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

    for (int i=0; i<varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (name.equals(vp.v.getName()))
        return vp;
    }
    return null;
  }

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

    for (int i=0; i<varList.size(); i++) {
      VarProcess vp = (VarProcess) varList.get(i);
      if (name.equals(vp.v.getName()) && (vp.isCoordinateVariable || vp.isCoordinateAxis))
        return vp;
    }
    return null;
  }

  public class VarProcess {
    public NetcdfDataset ds;
    public Variable v;

    // data variable
    // public ArrayList dataAxesList;

    // coord axes
    public boolean isCoordinateVariable;
    public boolean isCoordinateAxis;
    public AxisType axisType;
    public String coordAxes, coordSys, coordVarAlias, positive;

    // coord systems
    public boolean isCoordinateSystem;
    public ArrayList axesList;
    public String coordTransforms;
    public CoordinateSystem cs;

    // coord transform
    public boolean isCoordinateTransform;
    public String coordTransformType;
    public CoordinateTransform ct;

    /**
     * Wrap each variable in the dataset with a VarProcess object.
     * Identify coordinate Variables.
     * Identify all _Coordinate attributes.
     */
    private VarProcess( NetcdfDataset ds, VariableEnhanced ve) {
      this.ds = ds;
      this.v = (Variable) ve;
      isCoordinateVariable = v.getCoordinateDimension() != null;

      Attribute att = v.findAttributeIgnoreCase("_CoordinateAxisType");
      if (att != null) {
        String axisName = att.getStringValue();
        axisType = AxisType.getType( axisName);
        isCoordinateAxis = true;
        parseInfo.append(" found Coordinate Axis "+v.getName()+" type= "+axisName+"\n");
      }

      coordVarAlias = ds.findAttValueIgnoreCase(v, "_CoordinateAliasForDimension", null);
      if (coordVarAlias != null) {
        if (v.getRank() != 1) {
          parseInfo.append("**ERROR Coordinate Variable Alias "+v.getName()+" has rank "+v.getRank()+"\n");
        } else {
          Dimension coordDim = ds.findDimension( coordVarAlias);
          Dimension vDim = v.getDimension( 0);
          if (!coordDim.equals(vDim)) {
            parseInfo.append("**ERROR Coordinate Variable Alias "+v.getName()+" names wrong dimension "+coordVarAlias+"\n");
          } else {
            isCoordinateAxis = true;
            v.setIsCoordinateAxis( true);
            parseInfo.append(" found Coordinate Variable Alias "+v.getName()+" for dimension "+coordVarAlias+"\n");
          }
        }
      }
      coordAxes = ds.findAttValueIgnoreCase(v, "_CoordinateAxes", null);
      coordSys = ds.findAttValueIgnoreCase(v, "_CoordinateSystems", null);
      coordTransforms = ds.findAttValueIgnoreCase(v, "_CoordinateTransforms", null);
      isCoordinateSystem = (coordTransforms != null);

      coordTransformType = ds.findAttValueIgnoreCase(v, "_CoordinateTransformType", null);
      isCoordinateTransform = (coordTransformType != null);

      positive = ds.findAttValueIgnoreCase(v, "_CoordinateZIsPositive", null);
      if (positive == null)
        positive = ds.findAttValueIgnoreCase(v, "positive", null);
    }

    // fakeroo
    public VarProcess( NetcdfDataset ds) {
      this.ds = ds;
    }

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

    /**
     * Turn the variable into a coordinate axis.
     */
    public void makeIntoCoordinateAxis() {
      CoordinateAxis ca = null;
      if (v instanceof CoordinateAxis) {
        ca = (CoordinateAxis) v;
      } else {
        ca = CoordinateAxis.factory(ds, (VariableDS) v); // LOOK StructureDS cant be coord axis
        ds.addCoordinateAxis( ca);
        v = ca;
      }

      Dimension dim = ca.getCoordinateDimension();
      if (dim != null)
        dim.addCoordinateVariable(  ca); // remove old one

      if (axisType != null) {
        ca.setAxisType( axisType);
        ca.addAttribute( new Attribute("_CoordinateAxisType", axisType.toString()));

        if (((axisType == AxisType.Height) || (axisType == AxisType.Pressure) || (axisType == AxisType.GeoZ)) &&
            (positive != null)) {
          ca.setPositive( positive);
          ca.addAttribute( new Attribute("_CoordinateZisPositive", positive));
        }
      }

    }

    /**
     * Create a Coordinate System object, using list of coordinate axis names in axes field.
     * List of CoordinateTransform objects should already be in transformList.
     */
    public void makeCoordinateSystem () {

      // find referenced coordinate axes
      axesList = new ArrayList();
      StringTokenizer stoker = new StringTokenizer( coordAxes);
      while (stoker.hasMoreTokens()) {
        String vname = stoker.nextToken();
        VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
        if (ap != null)
          axesList.add( ap.v);
        else
          parseInfo.append(" Cant find axes "+vname+ " for Coordinate System "+v.getName()+"\n");
      }

      if (axesList.size() == 0) {
        parseInfo.append(" No axes found for Coordinate System "+v.getName()+"\n");
        return;
      }

      // create the coordinate system
      cs = new CoordinateSystem(axesList, null);
      ds.addCoordinateSystem( cs);

      parseInfo.append(" Made Coordinate System "+cs.getName()+"\n");
    }

    /**
     * Create a list of coordinate axes for this data variable.
     * Use the list of names in axes field. If axes is null, then use coordinate variables only.
     */
    public ArrayList findCoordinateAxes(String nameList) {
      ArrayList axesList = new ArrayList();

      if (nameList == null) { // implicit axes = coordinate variables
        List dims = v.getDimensions();
        for (int i = 0; i < dims.size(); i++) {
          Dimension d = (Dimension) dims.get(i);
          List coordVars = d.getCoordinateVariables();
          for (int j = 0; j < coordVars.size(); j++) {
            Variable cv =  (Variable) coordVars.get(j);
            parseInfo.append(" Add coordinate variable for "+v.getName()+" dimension= "+d.getName()+"\n");
            axesList.add( cv);
          }
        }
      } else { // explicit axes
        StringTokenizer stoker = new StringTokenizer( nameList);
        while (stoker.hasMoreTokens()) {
          String vname = stoker.nextToken();
          VarProcess ap = findVarProcess( vname); // LOOK: full vs short name
          if (ap != null)
            axesList.add( ap.v);
        }
      }

      return axesList;
    }

  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////////
  // Coordinate Transformations.
  // Default is to use CF conventions, so CF processing is here.

  /**
   * Create a CoordinateTransform if possible out of the information in this Variable.
   * Generally, this just passes the attrinutes of the variables as parameters of the CoordinateTransform.
   * Specifically, this routine currently handles
   * <ul>
   *   <li>CF lambert_conformal_conic
   * </ul>
   * @param ds for this dataset
   * @param ctv for this variable
   * @return CoordinateTransform
   */
  protected CoordinateTransform makeCoordinateTransform (NetcdfDataset ds, Variable ctv) {
    CoordinateTransform ct = null;

    // standard name
    String transform_name = ds.findAttValueIgnoreCase(ctv, "transform_name", null);
    if (null == transform_name)
      transform_name = ds.findAttValueIgnoreCase(ctv, "Projection_Name", null);

    // these are from CF
    if (null == transform_name)
      transform_name = ds.findAttValueIgnoreCase(ctv, "grid_mapping_name", null);
    if (null == transform_name)
      transform_name = ds.findAttValueIgnoreCase(ctv, "standard_name", null);

    if (null != transform_name) {

      // check projections
      if (transform_name.equalsIgnoreCase("lambert_conformal_conic"))
        ct = makeLCProjection(ds, ctv);
      else
        // otherwise it should be a vertical transformation
        ct = makeVerticalCoordinateTransform(ds, ctv);

      if (ct == null)
        parseInfo.append("**Failed to make Projection transform "+transform_name+" from "+ctv+": missing parameters\n");
      else {
        parseInfo.append(" Made Projection transform "+transform_name+" from "+ctv.getName()+": missing parameters\n");
        return ct;
      }

      return ct;
    }

    // otherwise
    ct = new CoordinateTransform(ctv.getName(), conventionName, null);
    List atts = ctv.getAttributes();
    for (int i = 0; i < atts.size(); i++) {
      Attribute att = (Attribute) atts.get(i);
      ct.addParameter( new Parameter(att.getName(), att.getStringValue())); // LOOK what about numeric
    }
    parseInfo.append(" Made unknown transform from: "+ctv+"\n");
    return ct;
  }

  protected CoordinateTransform makeLCProjection(NetcdfDataset ds, Variable ctv) {
    Attribute a;
    if (null == (a = ctv.findAttribute( "standard_parallel")))
      return null;
    if (a.isString())
      return null;
    double par1 = a.getNumericValue().doubleValue();
    double par2 = (a.getLength() > 1) ? a.getNumericValue(1).doubleValue() : par1;

    if (null == (a = ctv.findAttribute( "longitude_of_central_meridian")))
      return null;
    if (a.isString())
      return null;
    double lon0 = a.getNumericValue().doubleValue();

    if (null == (a = ctv.findAttribute( "latitude_of_projection_origin")))
      return null;
    if (a.isString())
      return null;
    double lat0 = a.getNumericValue().doubleValue();

    LambertConformal lc = new LambertConformal(lat0, lon0, par1, par2);
    CoordinateTransform rs = new ProjectionCT(ctv.getShortName(), "FGDC", lc);

    parseInfo.append("  made Lambert Conformal projection:\n");
    return rs;
  }

  protected VariableDS makeCoordinateTransformVariable(NetcdfDataset ds, CoordinateTransform ct) {
    VariableDS v = new VariableDS( ds, null, null, ct.getName(), DataType.CHAR, "", null, null);
    List params = ct.getParameters();
    for (int i = 0; i < params.size(); i++) {
      Parameter p = (Parameter) params.get(i);
      v.addAttribute( new Attribute(p.getName(), p.getStringValue()));
    }
    v.addAttribute( new Attribute("_CoordinateTransformType", ct.getTransformType().toString()));
    parseInfo.append("  made CoordinateTransformVariable:"+ct.getName()+"\n");

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

    return v;
  }

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

  protected CoordinateTransform makeVerticalCoordinateTransform(NetcdfDataset ds, Variable v) {
    String sname = ds.findAttValueIgnoreCase(v, "standard_name", null);
    if (null == sname) return null;

    String formula = ds.findAttValueIgnoreCase(v, "formula_terms", null);
    if (null == formula) return null;

    CoordinateTransform ct = null;

    if (sname.equalsIgnoreCase("atmosphere_sigma_coordinate")) {
      try {
        SigmaFormula f = new SigmaFormula(formula);
        ct = f.makeCT( v.getName(), ds);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make Sigma CoordinateTransform for "+v.getName()+"\n");
        return null;
      }
    }

    if (sname.equalsIgnoreCase("atmosphere_hybrid_sigma_pressure_coordinate")) {
      HybridSigmaPressureFormula f = new HybridSigmaPressureFormula(formula);
      try {
        ct = f.makeCT( v.getName(), ds);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make HybridSigmaPressure CoordinateTransform for "+v.getName()+"\n");
        return null;
      }
    }

   if (sname.equalsIgnoreCase("ocean_s_coordinate")) {
      try {
        OceanSFormula f = new OceanSFormula(formula);
        ct = f.makeCT( v.getName(), ds);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make OceanS CoordinateTransform for "+v.getName()+"\n");
        return null;
      }
    }

    parseInfo.append("  made "+sname+" CoordinateTransform for "+v.getName()+ " formula= "+formula+"\n");
    showFormula( ds, formula);
    return ct;
  }

  private void showFormula( NetcdfDataset ds, String formula) {
    StringTokenizer stoke = new StringTokenizer( formula);

    while (stoke.hasMoreTokens()) {
      String name = stoke.nextToken();
      String vname = stoke.nextToken();
      Variable v = ds.findVariable( vname);

      if (v == null) {
        parseInfo.append(" ***ERROR Cant find   "+vname+" in formula\n");
        return;
      }

      parseInfo.append("    "+name+" = ");
      v.getNameAndDimensions(parseInfo, false, false);
      parseInfo.append("\n");
    }
  }


  private double[] makeDataValues(NetcdfFile ds, String varName) throws IOException {
    Variable dataVar = ds.findVariable(varName);
    Array data = dataVar.read();
    double d[] = new double[ (int) data.getSize()];
    int count = 0;
    IndexIterator ii = data.getIndexIterator();
    while (ii.hasNext())
      d[count++] = ii.getDoubleNext();
    return d;
  }

  private class SigmaFormula {
    String sigma="", ps="", ptop="";

    SigmaFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("sigma:"))
          sigma = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ps:"))
          ps = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ptop:"))
          ptop = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds) throws IOException {
      CoordinateTransform rs = new VerticalCT(varName, conventionName, VerticalCT.Type.Sigma);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = ptop + sigma(z)*(surfacePressure(x,y)-ptop)"));
      rs.addParameter(new Parameter("surfacePressure variable name", ps));
      rs.addParameter(new Parameter("sigma", makeDataValues(ds, sigma)));
      rs.addParameter(new Parameter("ptop", makeDataValues(ds, ptop)));
      return rs;
    }

    public String toString() { return "Sigma "+sigma + ps + ptop; }
  }

 private class HybridSigmaPressureFormula {
    String a="", b="", ps="", p0="";

    HybridSigmaPressureFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("a:"))
          a = stoke.nextToken();
        else if (toke.equalsIgnoreCase("b:"))
          b = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ps:"))
          ps = stoke.nextToken();
        else if (toke.equalsIgnoreCase("p0:"))
          p0 = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds)  throws IOException {
      CoordinateTransform rs = new VerticalCT(varName, conventionName, VerticalCT.Type.HybridSigmaPressure);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = a(z)*p0 + b(z)*surfacePressure(x,y)"));
      rs.addParameter(new Parameter(HybridSigmaPressure.PS, ps));
      rs.addParameter(new Parameter(HybridSigmaPressure.A, a));
      rs.addParameter(new Parameter(HybridSigmaPressure.B, b));
      rs.addParameter(new Parameter(HybridSigmaPressure.P0, makeDataValues(ds, p0)));
      return rs;
    }

    public String toString() { return "HybridSigma "+a + b + ps + p0; }
  }

  // :formula_terms = "s: s_rho eta: zeta depth: h a: theta_s b: theta_b depth_c: hc";
  private class OceanSFormula {
    String s="", eta="", depth="", a="", b="", depth_c="";

    OceanSFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("s:"))
          s = stoke.nextToken();
        else if (toke.equalsIgnoreCase("eta:"))
          eta = stoke.nextToken();
        else if (toke.equalsIgnoreCase("depth:"))
          depth = stoke.nextToken();
        else if (toke.equalsIgnoreCase("a:"))
          a = stoke.nextToken();
        else if (toke.equalsIgnoreCase("b:"))
          b = stoke.nextToken();
        else if (toke.equalsIgnoreCase("depth_c:"))
          depth_c = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds)  throws IOException {

      CoordinateTransform rs = new VerticalCT("oceanS-"+varName, conventionName, VerticalCT.Type.OceanS);
      rs.addParameter((new Parameter("height formula", "height(x,y,z) = eta(x,y)*(1+s(z)) + depth_c*s(z) + (depth(x,y)-depth_c)*C(z)")));
      rs.addParameter((new Parameter("C formula", "C(z) = (1-b)*sinh(a*s(z))/sinh(a) + b*(tanh(a*(s(z)+0.5))/(2*tanh(0.5*a))-0.5)")));
      rs.addParameter((new Parameter(OceanS.ETA, eta)));
      rs.addParameter((new Parameter(OceanS.S, s)));
      rs.addParameter((new Parameter(OceanS.DEPTH, depth)));
      rs.addParameter((new Parameter(OceanS.DEPTH_C, makeDataValues(ds, depth_c))));
      rs.addParameter((new Parameter(OceanS.A, makeDataValues(ds, a))));
      rs.addParameter((new Parameter(OceanS.B, makeDataValues(ds, b))));

      return rs;
    }


    public String toString() { return "OceanS "+s + eta + depth + a + b + depth_c; }
  }


}

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

  ////////////////////////////////////////////
  /** subclasses must implement these
  public abstract void augmentDataset( NetcdfDataset ncDataset);
  public abstract boolean isXAxis( CoordinateAxis dc);
  public abstract boolean isYAxis( CoordinateAxis dc);
  public abstract boolean isZAxis( CoordinateAxis dc);
  public abstract boolean isTimeAxis( CoordinateAxis dc);
  public abstract boolean isLatAxis( CoordinateAxis dc);
  public abstract boolean isLonAxis( CoordinateAxis dc);
  public abstract boolean isHeightAxis( CoordinateAxis dc);
  public abstract boolean isPressureAxis( CoordinateAxis dc);
  public abstract String getZisPositive( CoordinateAxis dc); // up = coords get bigger as you go up in altitude, else down

  /** construct CoordinateTransforms in preparation of calling getCoordinateTransforms()
  protected void constructCoordinateTransforms(NetcdfDataset ncDataset) { }

  /** Get a list of CoordinateTransforms for the given Coordinate System
  protected abstract java.util.List getCoordinateTransforms(CoordinateSystem cs);

  // coordinate axes

  /**
   * Find all Coordinate axes, replace in dataset.
   * Default just finds coordinate variables, subclasses usually override.
   * @param ds: dataset
   *
  protected void constructCoordAxes(NetcdfDataset ds) {
    findCoordinateVariables(ds);
  }

  /**
   * Find the netCDF coordinate variables,
   * make them into CoordinateAxis, replace in dataset.
   *
  protected void findCoordinateVariables(NetcdfDataset ds) {
    Iterator vars = ds.getVariables().iterator(); // uses copy
    while (vars.hasNext()) {
      Variable ncvar = (Variable) vars.next();
      if (debug) System.out.println(" findCoordinateVariables "+ncvar.getName()+" = "+ncvar.getClass().getName());
      if (ncvar.isCoordinateVariable() && !(ncvar instanceof CoordinateAxis)) {
        ds.addCoordinateAxis( (VariableDS) ncvar);
        parseInfo.append("add Coordinate Variable= "+ncvar.getName()+"\n");
      }
    }
  }

  /**
   * Run through all coordinate axes and assign a AxisType based on calling
   *  the isXXXAxis() methods.
   *
  protected void assignAxisType(NetcdfDataset ds) {
    Iterator cas = ds.getCoordinateAxes().iterator();
    while (cas.hasNext()) {
      CoordinateAxis ca = (CoordinateAxis) cas.next();
      if (!(ca instanceof CoordinateAxis1D))
        parseInfo.append( "CoordinateAxis "+ca.getName()+ " not 1D\n");

      //if (ca.isAuxilary()) // skip aux
      //  continue;

      if (isLatAxis(ca))
        ca.setAxisType(AxisType.Lat);
      else if (isLonAxis(ca))
        ca.setAxisType(AxisType.Lon);
      else if (isXAxis(ca))
        ca.setAxisType(AxisType.GeoX);
      else if (isYAxis(ca))
        ca.setAxisType(AxisType.GeoY);
      else if (isTimeAxis(ca))
        ca.setAxisType(AxisType.Time);
      else if (isPressureAxis(ca)) {
        ca.setAxisType(AxisType.Pressure);
        ca.setPositive("down");
      }
      else if (isHeightAxis(ca)) {
        ca.setAxisType(AxisType.Height);
        ca.setPositive(getZisPositive(ca));
      }
      else if (isZAxis(ca)) {
        ca.setAxisType(AxisType.GeoZ);
        ca.setPositive(getZisPositive(ca));
      }
      else {
        parseInfo.append("** " + ca.getName() + " not georeferencing Axis\n");
      }

    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // coordinate systems

  /**
   * Default construction of Coordinate Systems.
   * This version constructs coordinate systems based on the list of coordinate axes that fit.
   * If multiple z axes, make coordinate system for each.
   *
  protected void constructCoordinateSystems(NetcdfDataset ds) {

    if (ds.getCoordinateAxes().size() == 0) {
      parseInfo.append("No coordinate axes were found\n");
      return;
    }
    parseInfo.append("Looking for coordinate systems:\n");

    // run through all top variables
    Iterator vars = ds.getVariables().iterator();
    while (vars.hasNext()) {
      VariableEnhanced v = (VariableEnhanced) vars.next();
      constructCoordinateSystems( ds, v);
    }
  }

  protected void constructCoordinateSystems(NetcdfDataset ds, VariableEnhanced v) {

      if (v instanceof StructureDS) {
        StructureDS s = (StructureDS) v;
        List members = s.getVariables();
        for (int i = 0; i < members.size(); i++) {
          VariableEnhanced nested =  (VariableEnhanced) members.get(i);
          constructCoordinateSystems( ds, nested);
        }
      } else {
        parseInfo.append("  check ");
        v.getNameAndDimensions(parseInfo, true, false);
        parseInfo.append(": ");

        if (wantCoordSystemsForVariable( ds, v))
          makeCoordSystemsForVariable(ds, v);
        parseInfo.append("\n");
      }

   }


  /**
   * Do we want a coordinate system for this variable?
   * only if rank > 1 and not a coordinate variable.
   *
  protected boolean wantCoordSystemsForVariable(NetcdfDataset ds, VariableEnhanced v) {
    int fullRank = v.getDimensionsAll().size();

    if (fullRank == 0) {
      parseInfo.append("is scalar\n");
      return false;
    }

    if (fullRank == 1) {  // why ??
      parseInfo.append("is rank 1\n");
      return false;
    }

    if (v.isCoordinateVariable()) {
      parseInfo.append("isCoordinateVariable\n");
      return false;
    }

    /* if ((v instanceof CoordinateAxis) && (v.getRank() == 1)) {
      parseInfo.append("isCoordinateAxis rank 1\n");
      return false;
    } *


    return true;
  }

  /**
   * Find the coordinate systems for this variable and add them to the variable.
   * If the coordinate system doesnt exist, make it.
   * Algorithm:
   *  1. Find all coordinate axes that fit the variable
   *  2. If both lat/lon and x/y axes, make seperate coordinate system for each.
   *  3. If multiple Z axes, make seperate coordinate system for each z axis.
   *
  protected void makeCoordSystemsForVariable(NetcdfDataset ds, VariableEnhanced v) {

    // find all axes that fit
    ArrayList axes = findAllCoordinateAxisForVariable(ds, v);
    if (axes.size() == 0) {
      parseInfo.append("none");
      return;
    }
    parseInfo.append("has coordSys ");

    // check for lat/lon and x/y, and seperate out the Z axis
    ArrayList axesXY = new ArrayList();
    ArrayList axesLL = new ArrayList();
    ArrayList axesZ = new ArrayList();
    ArrayList axesOther = new ArrayList();
    for (int i=0; i<axes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) axes.get(i);
      if ((axis.getAxisType() == AxisType.GeoZ) || (axis.getAxisType() == AxisType.Pressure) ||
          (axis.getAxisType() == AxisType.Height))
        axesZ.add( axis);
      else if ((axis.getAxisType() == AxisType.GeoX) || (axis.getAxisType() == AxisType.GeoY))
        axesXY.add( axis);
      else if ((axis.getAxisType() == AxisType.Lat) || (axis.getAxisType() == AxisType.Lon))
        axesLL.add( axis);
      else // hmm, could only be time, right ?
        axesOther.add(axis);
    }

    if (axesXY.size() > 0) {
      ArrayList axesO2 = new ArrayList( axesXY);
      axesO2.addAll( axesOther);
      seperateZaxes( ds, v, axesO2, axesZ);
    }

    if (axesLL.size() > 0) {
      ArrayList axesO2 = new ArrayList( axesLL);
      axesO2.addAll( axesOther);
      seperateZaxes( ds, v, axesO2, axesZ);
    }

    if ((axesXY.size() == 0) && (axesLL.size() == 0)) {
      seperateZaxes( ds, v, axesOther, axesZ);
    }

  }

  protected void seperateZaxes(NetcdfDataset ds, VariableEnhanced v, ArrayList axesOther, ArrayList axesZ) {

    // 0 or 1
    if (axesZ.size() < 2) {
      ArrayList axes = new ArrayList( axesOther);
      axes.addAll( axesZ);
      addCoordinateSystem( ds, v, axes);
      return;
    }

    // more than 1: make seperate coord sys for each
    for (int i=0; i<axesZ.size(); i++) {
      CoordinateAxis axisZ = (CoordinateAxis) axesZ.get(i);
      ArrayList axes = new ArrayList( axesOther);
      axes.add(axisZ);
      addCoordinateSystem( ds, v, axes);
    }

  }

  /**
   * Find all coordinate axes that fit the variable.
   *
  protected ArrayList findAllCoordinateAxisForVariable(NetcdfDataset ds, VariableEnhanced v) {
     // find all axes that fit
    ArrayList axes = new ArrayList();
    List coordAxes = ds.getCoordinateAxes();
    for (int i=0; i<coordAxes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) coordAxes.get(i);
      //if (axis.isAuxilary()) // skip aux
      //  continue;
      if ((axis != v) && isCoordinateAxisForVariable(axis, v))
        axes.add(axis);
    }
    return axes;
  }

  /**
   * Does this axis "fit" this variable?
   * True if all of the dimensions in the axis also appear in the variable
   *
  protected boolean isCoordinateAxisForVariable(Variable axis, VariableEnhanced v) {
    List varDims = v.getDimensionsAll();
    for (int i=0; i<axis.getRank(); i++) {
      Dimension axisDim = axis.getDimension(i);
      if (!varDims.contains( axisDim)) {
        if (showRejects) System.out.println(v.getName()+" missing dim "+axisDim.getName()+" in axis "+axis.getName());
        return false;
      }
    }
    return true;
  }

  /**
   * Get the coordinate system that consists of the given axes, and add it to the variable
   * If it doesnt exist, create it and add it to the dataset.
   * SIDE EFFECT: when you create it, call addCoordinateTransforms().
   *
  protected void addCoordinateSystem (NetcdfDataset ds, VariableEnhanced v, ArrayList axes) {
    CoordinateSystem cs = new CoordinateSystem(axes, null);
    if (!coordsys.containsKey( cs.getName())) {
      coordsys.put( cs.getName(), cs);
      ds.addCoordinateSystem( cs);
      cs.addCoordinateTransforms( getCoordinateTransforms( cs));
    }
    cs = (CoordinateSystem) coordsys.get( cs.getName());
    ds.addCoordinateSystem( v, cs);
    parseInfo.append(cs.getName()+" ");
  }

  //////////////////////////////////////////////////////////////////////////////
  // coordinate transforms

  protected void assignCoordinateTransforms(NetcdfDataset ds) {
    parseInfo.append("Looking for coordinate transforms:\n");

    // run through each coordinate system
    Iterator css = ds.getCoordinateSystems().iterator();
    while (css.hasNext()) {
      CoordinateSystem cs = (CoordinateSystem) css.next();
      List cts = getCoordinateTransforms( cs);
      if (cts.size() > 0) {
        cs.addCoordinateTransforms(cts);
        parseInfo.append("  "+cs.getName()+"  has transforms:");
        Iterator iter = cts.iterator();
        while (iter.hasNext()) {
          CoordinateTransform ct = (CoordinateTransform) iter.next();
          parseInfo.append(" "+ct.getName());
        }
        parseInfo.append("\n");
      }
    }
  }

  // not currently used
  private ProjectionImpl makeProjection(CoordinateTransform ct) {
    Parameter att = ct.findParameterIgnoreCase("Projection_Name");
    if (att != null) {
      String projName = att.getStringValue();
      if (projName.equalsIgnoreCase( "Lambert_Conformal_Conic"))
        return makeLambertProjection( ct);
    }
    return null;
  }

  private ProjectionImpl makeLambertProjection(CoordinateTransform ct) {

    double par1 = 0.0;
    Parameter att = ct.findParameterIgnoreCase("Standard_Parallel_1");
    if (att != null) par1 = att.getNumericValue();

    double par2 = 0.0;
    att = ct.findParameterIgnoreCase("Standard_Parallel_2");
    if (att != null) par2 = att.getNumericValue();

    double lon0 = 0.0;
    att = ct.findParameterIgnoreCase("Longitude_of_Central_Meridian");
    if (att != null) lon0 = att.getNumericValue();

    double lat0 = 0.0;
    att = ct.findParameterIgnoreCase("Latitude_of_Projection_Origin");
    if (att != null) lat0 = att.getNumericValue();

    return new LambertConformal(lat0, lon0, par1, par2);
  }


  /////////////////////////////////////
  // utilities

 /** Search for a coord axis with the given name in the collection of CoordAxis.
  * @param ds look in this dataset
  *  @param fullName name to search for.
  *  @return CoordAxisImpl with that name, or null if none.
  *
  protected CoordinateAxis findCoordAxis( NetcdfDataset ds, String fullName) {
    List coordAxes = ds.getCoordinateAxes();
    for (int i=0; i<coordAxes.size(); i++) {
      CoordinateAxis dc = (CoordinateAxis) coordAxes.get(i);
      if (fullName.equals( dc.getName()))
        return dc;
    }
    return null;
  }



}

  //////////////////////////////////////////////////////////////////////////
  // these are convenience routines for subclasses to use.

  /**
   * Add a CoordAxis for the named dimension from the variable ncvar.
   * Decide if its an edge or midpoint based on its size.
   * Add it to the coordAxes collection.
   *
   * @param Dimension dim: add CoordAxis for this dimension.
   * @param Variable ncvar: get values from this variable.
   *
  protected void addCoordAxisFromVariable( Dimension dim, Variable ncvar) {
    Array arr;
    try {
      arr = ncvar.read();
    } catch (java.io.IOException ioe) {
      System.out.println("DAMAGED NETCDF FILE: ParseStrategy failed to create CoordAxis for "+
        ncvar.getName()+" in file "+ netcdf.getPathName()+" "+ ioe);
      return;
     }

     Array mid = null, edge = null;
     if (arr.getSize() == dim.getLength())
       mid = arr;
     else
       edge = arr;

    DimCoordAxis dc = findDimCoordAxis( dim.getName());
    if (dc == null) {
      CoordAxisImpl ca =  new CoordAxisImpl(dim.getName(),
        netcdf.findAttValueIgnoreCase(ncvar, "long_name", ncvar.getName()),
        netcdf.findAttValueIgnoreCase(ncvar, "units", "none"),
        mid, edge);

      dc = new DimCoordAxis( dim, ca);
      coordAxes.add(dc);

    } else {
      // already got a CoordAxis, must supplement
      if (arr.getSize() == dim.getLength()) {
        dc.ca.setCoords( arr);
        dc.mid = ncvar;
      } else if (arr.getSize() == dim.getLength()+1) {
        dc.ca.setEdges( arr);
        dc.edge = ncvar;
      } else {
        System.out.println("Illegal cardinality on Variable "+ ncvar.getName()+
          " as coord axis for dimension "+ dim.getName());
       return;
      }
    }
  }

  /**
   * Add a CoordAxis for the named dimension from the variables top and bot,
   * interpreted as the top and bottom edge.
   * Add it to the coordAxes collection.
   *
   * @param Dimension dim: add CoordAxis for this dimension.
   * @param Variable top, bot: get values from these variables.
   *
  protected DimCoordAxis addCoordAxisFromTopBotVars( Dimension dim, Variable top, Variable bot) {
      //read data
    Array arrTop, arrBot;
    try {
      arrTop = top.read();
    } catch (java.io.IOException ioe) {
      System.out.println("DAMAGED NETCDF FILE: ParseStrategy failed to create CoordAxis for "+
        top.getName()+" in file "+ netcdf.getPathName()+" "+ ioe);
      return null;
     }
    try {
      arrBot = bot.read();
    } catch (java.io.IOException ioe) {
      System.out.println("DAMAGED NETCDF FILE: ParseStrategy failed to create CoordAxis for "+
        bot.getName()+" in file "+ netcdf.getPathName()+" "+ ioe);
      return null;
    }

      // check sizes
    if (arrTop.getSize() != arrBot.getSize()) {
      System.out.println("ParseStrategy addCoordAxisFromTopBotVars: illegal aarray sizes for dim "
        + dim.getName()+ " top ("+ top.getName() + ") = "+ arrTop.getSize()+
        " bot ("+ bot.getName() + ") = "+ arrBot.getSize());
      return null;
     }
    int size = (int) arrTop.getSize();
    ArrayDouble.D1 edge = new ArrayDouble.D1( size+1);

      // create edges
    Index imaTop = arrTop.getIndex();
    Index imaBot = arrBot.getIndex();
    double valTop = arrTop.getDouble(imaTop.set0(0));
    double valBot = arrBot.getDouble(imaBot.set0(0));
    IndexIterator iterHi, iterLo;
    if (valTop > valBot) {
      iterHi = arrTop.getIndexIterator();
      iterLo = arrBot.getIndexIterator();
    } else {
      iterHi = arrBot.getIndexIterator();
      iterLo = arrTop.getIndexIterator();
    }

    double lastHi = 0.0;
    int count = 0;
    while (iterHi.hasNext()) {
      double valHi = iterHi.getDoubleNext();
      double valLo = iterLo.getDoubleNext();
      edge.set(count, valLo);
      edge.set(count+1, valHi);
      if (count > 0) {
        if (valLo != lastHi) {
          System.out.println("ParseStrategy addCoordAxisFromTopBotVars: non continuous dim= "
            + dim.getName()+ "index = "+count+": top ("+ top.getName() + ") = "+ lastHi+
            " bot ("+ bot.getName() + ") = "+ valLo);
          return null;
        }
      }
      count++;
      lastHi = valHi;
    }

    if (debug) {
      System.out.print("addCoordAxisFromTopBotVars edges = ");
      for (int i=0; i<size-1; i++)
        System.out.print(" "+edge.get(i));
      System.out.println();
    }

    String long_name = netcdf.findAttValueIgnoreCase(top, "long_name", top.getName());
    String units = netcdf.findAttValueIgnoreCase(top, "units", top.getName());

    DimCoordAxis dc = findDimCoordAxis( dim.getName());
    if (dc == null) {
      CoordAxisImpl ca =  new CoordAxisImpl(dim.getName(), long_name, units, null, edge);
      dc = new DimCoordAxis( dim, ca);
      coordAxes.add(dc);
      return dc;
    } else {
      System.out.println("ERROR already have a coordinate axis for dimension "+ dim.getName()+
        " trying to add top = "+ top.getName() + " bot = "+ bot.getName());
    }
    return null;
  }

  /**
   * Make a dummy CoordAxis for the named dimension.
   * add it to the coordAxes collecion.
   * @param Dimension dim: add CoordAxis for this dimension.
   *
  protected void coordAxisDummy( Dimension dim) {
    double[] mid = new double[dim.getLength()];
    for (int i=0; i<dim.getLength(); i++)
      mid[i] = 1.0 * i;

    CoordAxisImpl ca =  new CoordAxisImpl(dim.getName(), "dummy", "none",
       mid, null);

    coordAxes.add(ca);
  } */

 /** Search for a coord axis with the given name in the collection of CoordAxis.
  *  @param String name: name to search for.
  *  @return CoordAxisImpl with that name, or null if none.
  *
  protected DimCoordAxis findDimCoordAxis( String name) {
    for (int i=0; i<coordAxes.size(); i++) {
      DimCoordAxis dc = (DimCoordAxis) coordAxes.get(i);
      if (name.equals( dc.dim.getName()))
        return dc;
    }
    return null;
  }

  /**
   * Make a GeoCoordSysImpl for the Variable v, by finding a coord axis with the same name as
   * each dimension, except for one dimension which can be skipped.
   * All other dimensions must have a coord axis, or return null.
   * @param String name: name of coord sys
   * @param String desc: description of the coords sys.
   * @param Variable: make coord sys for this variable.
   * @param int skipDim: skip this dimension
   * @return GeoCoordSysImpl for this variable, or null if failure.
   *
  protected GeoCoordSysImpl coordSysFromVar(String name, String desc, Variable v, int skipDim) {
    int rank = v.getRank()-1;
    int count = 0;
    CoordAxisImpl[] caxes = new CoordAxisImpl[ rank];
    for (int i=0; i< rank+1; i++) {
      if (i == skipDim)
        continue;
      caxes[count] = coordAxisFind( v.getDimension(i).getName());
      if (caxes[count] == null) {
        System.out.println("No coord var for dimension= <"+ v.getDimension(i).getName()+">");
        return null;
      }
      count++;
    }
    return coordSysFromAxes(name, desc, caxes);
  }

  /** Construct coord sys name from variable's dimension's names.
   *  @param Variable v
   *  @return String coordinate system name
   *
  private StringBuffer gb_hashkey = new StringBuffer(200);
  protected String coordSysNameForVariable( Variable v) {
    boolean first = true;
    gb_hashkey.setLength(0);
    for (int i=0; i<v.getRank(); i++) {
      if (!first)
        gb_hashkey.append(",");
      first = false;
      gb_hashkey.append( v.getDimension(i).getName());
    }
    return gb_hashkey.toString();
  }

  /** Construct coord sys name from variable's dimension's names, but
   *  skip the given dimension.
   *  @param Variable v
   *  @param in skipDim, the dimension to skip, as an index into the Variables
   *  @return String coordinate system name
   *
  private StringBuffer gb_hashkey = new StringBuffer(200);
  protected String coordSysNameForVariable( Variable v, int skipDim) {
    boolean first = true;
    gb_hashkey.setLength(0);
    for (int i=0; i<v.getRank(); i++) {
      if (i == skipDim)
        continue;
      if (!first)
        gb_hashkey.append(",");
      first = false;
      gb_hashkey.append( v.getDimension(i).getName());
    }
    return gb_hashkey.toString();
  } */

  /** Find a coord axis with the given name in the given list of CoordAxisimpl.
   *  @param List axes: list of axes to search through
   *  @param String name: name for coord axis of this name
   *  @return index in the list, or -1 if not found.
   *
  protected int findCoordAxis(List axes, String name) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      if (name.equals( cv.getName()))
        return i;
    }
    return -1;
  }

   /* look through the axes, whose coord var has the given attribute
  protected int findCoordAxisByAttribute( List axes, String att) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      DimCoordAxis dc = findDimCoordAxis( cv.getName()); // track down the original ncvars
      if (dc.mid != null) {
        if (null != netcdf.findAttValueIgnoreCase(dc.mid, att, null))
          return i;
      }
      if (dc.edge != null) {
        if (null != netcdf.findAttValueIgnoreCase(dc.edge, att, null))
          return i;
      }
    }
    return -1;
  }

   // look through the axes, whose coord var has the given attribute and value
  protected int findCoordAxisByAttributeValue( List axes, String att, String val) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      DimCoordAxis dc = findDimCoordAxis( cv.getName()); // track down the original ncvars
      if (dc.mid != null) {
        String attVal = netcdf.findAttValueIgnoreCase(dc.mid, att, null);
        if ((null != attVal) && attVal.equals( val))
          return i;
      }
      if (dc.edge != null) {
        String attVal = netcdf.findAttValueIgnoreCase(dc.edge, att, null);
        if ((null != attVal) && attVal.equals( val))
          return i;
      }
    }

    return -1;
  }

   /* look through the axes, which has units equal to "units"
  protected int findCoordAxisByUnit( List axes, String units) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      String axis_unit = cv.getUnitString();
      if (null == axis_unit) continue;
      if (units.equalsIgnoreCase(axis_unit))
        return i;
    }
    return -1;
  }

   // look through the axes, for one which can be convertible to "units"
  protected int findCoordAxisByUnitConvertible( List axes, String units) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      String axis_unit = cv.getUnitString();
      if (null == axis_unit) continue;
      if (units.equalsIgnoreCase("time")) {
        if (SimpleUnit.isDateUnit(axis_unit))
          return i;
      } else {
        if (SimpleUnit.isCompatible(units, axis_unit))
          return i;
      }
    }
    return -1;
  }

  /** Find a coord axis that starts with the given name in a given list of CoordAxisimpl.
   *  @param List axes: list of axes to search through
   *  @param String startingWith: look for coord axis whose name starts with this.
   *  @return index in the list, or -1 if not found.
   *
  protected int findCoordAxisStartsWith(List axes, String startingWith) {
    for (int i=0; i<axes.size(); i++) {
      CoordAxisImpl cv = (CoordAxisImpl) axes.get(i);
      if ((cv != null) && cv.getName().startsWith( startingWith))
        return i;
    }
    return -1;
  } */

  // can we pass in dimension, and combine next 2,3?


  /* See if this dimension has an aliased coordinate variable:
   *  A global attribute of that dimension name, whose value is the name of a coord var, eg:
   *  <pre>
   *    :dimName = alias;
   *    Variable alias(alias);
   *   </pre>
   *
   *  @param Dimension dim: look for this dimension name
   *  @return: aliased coordinate variable, or null if not exist.
   *
  protected Variable searchAliasedDimension( Dimension dim) {
    String dimName = dim.getName();
    String alias = netcdf.findAttValueIgnoreCase(null, dimName, null);
    if (alias == null)
      return null;

    Dimension dim2 = netcdf.findDimension( alias);
    if (dim2 == null)
      return null;
    return dim2.getCoordinateVariable();
  }

  /** Search for an aliased coord in the netcdf file
   * same as findCoordVarFromAlias, but doesnt have to be a coord var.
   * Has to be 1 dimensional, with dimension = dimName.
   * <pre>
   *    :dimName = alias;
   *    Variable alias(dimName);
   *   </pre>
   *
   *  @param Dimension dim: look for this dimension name
   *  @return: aliased variable, or null if not exist.
   *

  protected Variable searchAliasedDimension2( Dimension dim) {
    String dimName = dim.getName();
    String alias = netcdf.findAttValueIgnoreCase(null, dimName, null);
    if (alias == null)
      return null;

    Variable ncvar = netcdf.findVariable( alias);
    if (ncvar == null)
      return null;
    if (ncvar.getRank() != 1)
        return null;

    Iterator dimIter = ncvar.getDimensions().iterator();
    Dimension dim2 = (Dimension) dimIter.next();
    if (dimName.equals(dim2.getName()))
      return ncvar;

    return null;
  }





    // a mapping from dim -> CoordAxis, and the ncvars that define the CoordAxis
  protected class DimCoordAxis {
    Dimension dim;
    CoordAxisImpl ca;
    Variable mid = null, edge = null;
    boolean isReferential = false;
    boolean isGeoCoord = false;
    int type = 0; // GeoCoordSysImpl.?_DIM

    DimCoordAxis(Dimension dim, CoordAxisImpl ca) {
      this.dim = dim;
      this.ca = ca;
    }
  }

    private ProjectionImpl makeLambertProjection(CoordinateTransform ct) {

    double par1 = 0.0;
    Parameter att = ct.findParameterIgnoreCase("Standard_Parallel_1");
    if (att != null) par1 = att.getNumericValue();

    double par2 = 0.0;
    att = ct.findParameterIgnoreCase("Standard_Parallel_2");
    if (att != null) par2 = att.getNumericValue();

    double lon0 = 0.0;
    att = ct.findParameterIgnoreCase("Longitude_of_Central_Meridian");
    if (att != null) lon0 = att.getNumericValue();

    double lat0 = 0.0;
    att = ct.findParameterIgnoreCase("Latitude_of_Projection_Origin");
    if (att != null) lat0 = att.getNumericValue();

    return new LambertConformal(lat0, lon0, par1, par2);
  }
} */

/**
 * $Log: CoordSysBuilder.java,v $
 * Revision 1.7  2004/12/10 17:04:16  caron
 * *** empty log message ***
 *
 * Revision 1.6  2004/12/09 00:17:31  caron
 * *** empty log message ***
 *
 * Revision 1.5  2004/12/08 18:08:31  caron
 * implement _CoordinateAliasForDimension
 *
 * Revision 1.4  2004/12/07 02:43:21  caron
 * *** empty log message ***
 *
 * Revision 1.3  2004/12/07 01:29:29  caron
 * redo convention parsing, use _Coordinate encoding.
 *
 * Revision 1.2  2004/12/03 04:46:25  caron
 * no message
 *
 * Revision 1.1  2004/12/01 05:53:40  caron
 * ncml pass 2, new convention parsing
 *
 * Revision 1.12  2004/11/07 03:00:49  caron
 * *** empty log message ***
 *
 * Revision 1.11  2004/11/07 02:55:10  caron
 * no message
 *
 * Revision 1.10  2004/11/04 00:38:18  caron
 * no message
 *
 * Revision 1.9  2004/10/19 19:45:02  caron
 * misc
 *
 * Revision 1.8  2004/10/13 19:45:11  caron
 * add strict NCDump
 *
 * Revision 1.7  2004/10/06 19:03:40  caron
 * clean up javadoc
 * change useV3 -> useRecordsAsStructure
 * remove id, title, from NetcdfFile constructors
 * add "in memory" NetcdfFile
 *
 * Revision 1.6  2004/10/02 20:57:01  caron
 * coord vars and groups
 *
 * Revision 1.5  2004/09/28 21:30:48  caron
 * add GIEF
 *
 * Revision 1.4  2004/09/22 21:26:42  caron
 * ucar.unidata.util.Parameter
 *
 * Revision 1.3  2004/09/22 18:43:03  caron
 * move common to ucar.unidata; projections use Parameter, no tAttribute
 *
 * Revision 1.2  2004/09/09 22:47:40  caron
 * station updates
 *
 * Revision 1.1  2004/08/16 20:53:49  caron
 * 2.2 alpha (2)
 *
 * Revision 1.10  2003/10/28 23:56:57  caron
 * improve WRF
 *
 * Revision 1.9  2003/10/02 20:33:50  caron
 * move SimpleUnit to dataset; add <units> tag; add projections to CF
 *
 * Revision 1.8  2003/07/12 22:09:04  caron
 * add vertical transformations
 *
 * Revision 1.7  2003/06/26 22:35:43  caron
 * M3IO convention
 *
 * Revision 1.6  2003/06/24 21:57:27  caron
 * error message when no georeferencing Axis
 *
 * Revision 1.5  2003/06/03 20:06:10  caron
 * fix javadocs
 *
 * Revision 1.4  2003/05/15 23:11:32  caron
 * withdraw auxiliary axis for now
 *
 * Revision 1.3  2003/05/01 22:50:52  caron
 * look for comma delimited list
 *
 * Revision 1.2  2003/04/30 15:37:10  dmurray
 * add a hack for comma separated conventions - pick off the first one
 *
 * Revision 1.1  2003/04/08 15:06:26  caron
 * nc2 version 2.1
 *
 */
