// $Id: CoordinateSystem.java,v 1.6 2004/12/07 01:29:30 caron Exp $
/*
 * Copyright 2002-2004 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.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;
import java.util.*;

/**
 * A CoordinateSystem specifies the coordinates of a Variable's values.
 *
 *  Mathmatically it is a vector function F from index space to Sn:
 * <pre>
 *  F(i,j,k,...) -> (S1, S2, ...Sn)
 *  where i,j,k are integers, and S is the set of reals (R) or Strings.
 * </pre>
 *  The components of F are just its coordinate axes:
 *  <pre>
 *  F = (A1, A2, ...An)
 *    A1(i,j,k,...) -> S1
 *    A2(i,j,k,...) -> S1
 *    An(i,j,k,...) -> Sn
 * </pre>
 *
 *  Concretely, a CoordinateSystem is a set of coordinate axes, and an optional set
 *   of coordinate transforms.
 *  The domain rank of F is the number of dimensions it is a function of. The range rank is the number
 *   of coordinate axes.
 *
 * <p>
 * An important class of CoordinateSystems are <i>georeferencing</i> Coordinate Systems, that locate a
 *  Variable's values in space and time. A CoordinateSystem that has a Lat and Lon axis, or a GeoX and GeoY
 *  axis and a Projection CoordinateTransform will have <i>isGeoReferencing()</i> true.
 *  A CoordinateSystem that has a Height, Pressure, or GeoZ axis will have <i>hasVerticalAxis()</i> true.
 * <p>
 * Further CoordinateSystems specialization is done by "data type specific" clasess such as
 * ucar.nc2.dataset.grid.GridCoordSys.
 *
 * @see ucar.nc2.dataset.grid.GridCoordSys
 * @author caron
 * @version $Revision: 1.6 $ $Date: 2004/12/07 01:29:30 $
 */
public class CoordinateSystem {

  /** Create standard name from list of axes. sort the axes first */
  static public String makeName( List axes) {
    ArrayList axesSorted = new ArrayList( axes);
    Collections.sort( axesSorted, new CoordinateAxis.AxisComparator());
    StringBuffer buff = new StringBuffer();
    for (int i=0; i<axesSorted.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) axesSorted.get(i);
      if (i>0) buff.append("-");
      buff.append( axis.getName());
    }
    return buff.toString();
  }

  protected ArrayList coordAxes = new ArrayList();
  protected ArrayList coordTrans = new ArrayList();
  protected ArrayList domain = new ArrayList(); // set of dimension
  protected String name;
  protected CoordinateAxis xAxis, yAxis, zAxis, tAxis, latAxis, lonAxis, hAxis, pAxis;
  protected boolean isImplicit;

  // subclasses
  protected CoordinateSystem() {}

  /**
   * Constructor.
   * @param axes Collection of type CoordinateAxis, must be at least one.
   * @param coordTrans Collection of type CoordinateTransform, may be empty or null.
   */
  public CoordinateSystem(Collection axes, Collection coordTrans) {
    this.coordAxes = new ArrayList( axes);
    this.name = makeName( coordAxes);

    if (coordTrans != null)
      this.coordTrans = new ArrayList( coordTrans);

    for (int i=0; i<coordAxes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) coordAxes.get(i);
      // look for AxisType
      AxisType axisType = axis.getAxisType();
      if (axisType != null) {
        if (axisType == AxisType.GeoX) xAxis = lesserRank( xAxis, axis);
        if (axisType == AxisType.GeoY) yAxis = lesserRank( yAxis, axis);
        if (axisType == AxisType.GeoZ) zAxis = lesserRank( zAxis, axis);
        if (axisType == AxisType.Time) tAxis = lesserRank( tAxis, axis);
        if (axisType == AxisType.Lat) latAxis = lesserRank( latAxis, axis);
        if (axisType == AxisType.Lon) lonAxis = lesserRank( lonAxis, axis);
        if (axisType == AxisType.Height) hAxis = lesserRank( hAxis, axis);
        if (axisType == AxisType.Pressure) pAxis = lesserRank( pAxis, axis);
      }

      // collect dimensions
      List dims = axis.getDimensions();
      for (int j=0; j<dims.size(); j++) {
        Dimension dim = (Dimension) dims.get(j);
        if (!domain.contains(dim))
          domain.add(dim);
      }
    }
  }

  // prefer smaller ranks, in case more than one
  private CoordinateAxis lesserRank( CoordinateAxis a1, CoordinateAxis a2) {
    if (a1 == null) return a2;
    return (a1.getRank() <= a2.getRank()) ? a1 : a2;
  }

  /** add a CoordinateTransform */
  public void addCoordinateTransform(CoordinateTransform ct) { coordTrans.add( ct); }

  /** add a Collection of CoordinateTransform */
  public void addCoordinateTransforms(Collection ct) {
    if (ct != null)
      coordTrans.addAll( ct);
  }

  /** get the List of CoordinateAxis objects */
  public ArrayList getCoordinateAxes() { return coordAxes; }
  /** get the List of CoordinateTransform objects */
  public ArrayList getCoordinateTransforms() { return coordTrans; }
  /** get the name of the Coordinate System */
  public String getName() { return name; }

  /** List of Dimensions that constitute the domain. */
  public ArrayList getDomain() { return domain; }

  /** Get the domain rank of the coordinate system = number of dimensions it is a function of. */
  public int getRankDomain() { return domain.size(); }

  /** Get the range rank of the coordinate system = number of coordinate axes. */
  public int getRankRange() { return coordAxes.size(); }

  ///////////////////////////////////////////////////////////////////////////
  // Convenience routines for finding georeferencing axes

  /** get the CoordinateAxis with AxisType.GeoX, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getXaxis() { return xAxis; }
  /** get the CoordinateAxis with AxisType.GeoY, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getYaxis() { return yAxis; }
  /** get the CoordinateAxis with AxisType.GeoZ, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getZaxis() { return zAxis; }
  /** get the CoordinateAxis with AxisType.Time, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getTaxis() { return tAxis; }
  /** get the CoordinateAxis with AxisType.Lat, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getLatAxis() { return latAxis; }
  /** get the CoordinateAxis with AxisType.Lon, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getLonAxis() { return lonAxis; }
  /** get the CoordinateAxis with AxisType.Height, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getHeightAxis() { return hAxis; }
  /** get the CoordinateAxis with AxisType.Pressure, or null if none.
   *  if more than one, choose one with smallest rank */
  public CoordinateAxis getPressureAxis() { return pAxis; }

  /** Get the Projection for this coordinate system.
   *  If isLatLon(), then returns a LatLonProjection. Otherwise, extracts the
   *  projection from any ProjectionCT CoordinateTransform.
   *  @return ProjectionImpl or null if none.
   */
  public ProjectionImpl getProjection() {
    if (projection == null) {
      if (isLatLon()) projection = new LatLonProjection();
      for (int i=0; i<coordTrans.size(); i++) {
        CoordinateTransform ct = (CoordinateTransform) coordTrans.get(i);
        if (ct instanceof ProjectionCT) {
          projection = ((ProjectionCT) ct).getProjection();
          break;
        }
      }
    }
    return projection;
  }
  private ProjectionImpl projection = null;

  ////////////////////////////////////////////////////////////////////////////
  // classification
  /** true if it has X and Y CoordinateAxis, and a CoordTransform Projection */
  public boolean isGeoXY() {
    if ((xAxis == null) || (yAxis == null)) return false;
    if (null == getProjection()) return false;
    return true;
  }

  /** true if it has Lat and Lon CoordinateAxis */
  public boolean isLatLon() { return (latAxis != null) && (lonAxis != null); }

  /** true if isGeoXY or isLatLon */
  public boolean isGeoReferencing() { return isGeoXY() || isLatLon(); }

  /** true if all axes are CoordinateAxis1D */
  public boolean isProductSet() {
    for (int i=0; i<coordAxes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) coordAxes.get(i);
      if (!(axis instanceof CoordinateAxis1D)) return false;
    }
    return true;
  }

  /** true if all dimensions in V (including parents) are in the domain of this coordinate system. */
  public boolean isComplete(VariableIF v) {
    List dims = v.getDimensionsAll();
    for (int i=0; i<dims.size(); i++) {
      Dimension d = (Dimension) dims.get(i);
      if (!(domain.contains(d))) return false;
    }

    return true;
  }

  public boolean isImplicit() { return isImplicit; }
  public void setImplicit(boolean isImplicit) { this.isImplicit = isImplicit; }

  /** true if has Height, Pressure, or GeoZ axis */
  public boolean hasVerticalAxis() { return (hAxis != null) || (pAxis != null) || (zAxis != null); }

  /** true if has Time axis */
  public boolean hasTimeAxis() { return (tAxis != null); }

  /**
    * Do we have all the axes in the list?
    * @param wantAxes List of CoordinateAxis
    * @return true if all in our list.
    */
   protected boolean containsAxes(List wantAxes) {
     for (int i=0; i<wantAxes.size(); i++) {
       CoordinateAxis ca = (CoordinateAxis) wantAxes.get(i);
       if (!coordAxes.contains(ca))
         return false;
     }
     return true;
   }

  /**
    * Do we have all the axes types in the list?
    * @param wantAxes List of AxisType
    * @return true if all in our list.
    */
   protected boolean containsAxisTypes(List wantAxes) {
     for (int i=0; i<wantAxes.size(); i++) {
       AxisType wantAxisType = (AxisType) wantAxes.get(i);
       boolean gotit = false;
       for (int j = 0; j < coordAxes.size(); j++) {
         CoordinateAxis axis = (CoordinateAxis) coordAxes.get(j);
         if (axis.getAxisType() == wantAxisType)  gotit = true;
       }
       if (!gotit) return false;
     }
     return true;
   }


  ////////////////////////////////////////////////////////////////////////////
  /**
   * Instances which have same name are equal.
   */
  public boolean equals(Object oo) {
    if (this == oo) return true;
    if ( !(oo instanceof CoordinateSystem))
      return false;

    CoordinateSystem d = (CoordinateSystem) oo;
    return getName().equals(d.getName());
  }

  /** Override Object.hashCode() to implement equals. */
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37*result + getName().hashCode();
      result = 37*result + getCoordinateAxes().hashCode();
      result = 37*result + getCoordinateTransforms().hashCode();
      hashCode = result;
    }
    return hashCode;
  }
  private volatile int hashCode = 0;

}

/* Change History:
   $Log: CoordinateSystem.java,v $
   Revision 1.6  2004/12/07 01:29:30  caron
   redo convention parsing, use _Coordinate encoding.

   Revision 1.5  2004/12/03 04:46:26  caron
   no message

   Revision 1.4  2004/12/01 05:53:41  caron
   ncml pass 2, new convention parsing

   Revision 1.3  2004/11/07 03:00:49  caron
   *** empty log message ***

   Revision 1.2  2004/10/06 19:03:41  caron
   clean up javadoc
   change useV3 -> useRecordsAsStructure
   remove id, title, from NetcdfFile constructors
   add "in memory" NetcdfFile

   Revision 1.1  2004/08/16 20:53:47  caron
   2.2 alpha (2)

   Revision 1.9  2003/07/14 22:56:15  caron
   fix no projection found bug

   Revision 1.8  2003/07/12 23:08:52  caron
   add cvs headers, trailers

*/
