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

package ucar.unidata.geoloc.projection;

import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;

/**
 * FlatEarth Projection
 * This projection surface is tangent at some point (lat0, lon0) and
 * has a y axis rotated from true North by some angle.
 *

 * We call it "flat" because it should only be used where the spherical
 * geometry of the earth is not significant. In actuallity, we use the simple
 * "arclen" routine which computes dy along a meridian, and dx along a
 * latitude circle.  We rotate the coordinate system to/from a true north system.
 *

 * See John Snyder, Map Projections used by the USGS, Bulletin 1532,
 * 2nd edition (1983), p 145
 *
 * @author Unidata Development Team
 * @see Projection
 * @see ProjectionImpl
 */

public class FlatEarth extends ProjectionImpl {
  public static final String ROTATIONANGLE = "rotationAngle";

  /**
   * constants from Snyder's equations
   */
  private final double rotAngle, radius;
  private final double lat0, lon0; // center lat/lon in radians

  /**
   * some constants
   */
  private double cosRot, sinRot;

  /**
   * origin
   */
  //private LatLonPointImpl origin;  // why are we keeping this?

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result = new FlatEarth(getOriginLat(), getOriginLon(), getRotationAngle());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public FlatEarth() {
    this(0.0, 0.0, 0.0, EARTH_RADIUS);
  }

   public FlatEarth(double lat0, double lon0) {
     this(lat0, lon0, 0.0, EARTH_RADIUS);
   }

  public FlatEarth(double lat0, double lon0, double rotAngle) {
    this(lat0, lon0, rotAngle, EARTH_RADIUS);
  }

  /**
   * Construct a FlatEarth Projection, two standard parellels.
   * For the one standard parellel case, set them both to the same value.
   *
   * @param lat0     lat origin of the coord. system on the projection plane
   * @param lon0     lon origin of the coord. system on the projection plane
   * @param rotAngle angle of rotation, in degrees
   * @param radius  earth radius in km
   * @throws IllegalArgumentException if lat0, par1, par2 = +/-90 deg
   */
  public FlatEarth(double lat0, double lon0, double rotAngle, double radius) {
    super("FlatEarth", false);

    this.lat0 = Math.toRadians(lat0);
    this.lon0 = Math.toRadians(lon0);
    this.rotAngle = Math.toRadians(rotAngle);
    this.radius = radius;

    precalculate();

    addParameter(CF.GRID_MAPPING_NAME, "flat_earth");
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, lat0);
    addParameter(CF.LONGITUDE_OF_PROJECTION_ORIGIN, lon0);
    addParameter(ROTATIONANGLE, rotAngle);
    addParameter(CF.EARTH_RADIUS, radius * 1000);
  }

  /**
   * Precalculate some stuff
   */
  private void precalculate() {
    sinRot = Math.sin(rotAngle);
    cosRot = Math.cos(rotAngle);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    FlatEarth flatEarth = (FlatEarth) o;

    if (Double.compare(flatEarth.lat0, lat0) != 0) return false;
    if (Double.compare(flatEarth.lon0, lon0) != 0) return false;
    if (Double.compare(flatEarth.radius, radius) != 0) return false;
    if (Double.compare(flatEarth.rotAngle, rotAngle) != 0) return false;
    if ((defaultMapArea == null) != (flatEarth.defaultMapArea == null)) return false; // common case is that these are null
    if (defaultMapArea != null && !flatEarth.defaultMapArea.equals(defaultMapArea)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = rotAngle != +0.0d ? Double.doubleToLongBits(rotAngle) : 0L;
    result = (int) (temp ^ (temp >>> 32));
    temp = radius != +0.0d ? Double.doubleToLongBits(radius) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = lat0 != +0.0d ? Double.doubleToLongBits(lat0) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = lon0 != +0.0d ? Double.doubleToLongBits(lon0) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

// bean properties

  /**
   * Get the origin longitude.
   *
   * @return the origin longitude.
   */
  public double getOriginLon() {
    return Math.toDegrees(lon0);
  }

  /**
   * Get the origin latitude.
   *
   * @return the origin latitude.
   */
  public double getOriginLat() {
    return Math.toDegrees(lat0);
  }

  /**
   * Get the rotation angle.
   *
   * @return the origin latitude.
   */
  public double getRotationAngle() {
    return rotAngle;
  }

  /**
   * Get the label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "FlatEarth";
  }

  /**
   * Create a String of the parameters.
   *
   * @return a String of the parameters
   */
  public String paramsToString() {
    return toString();
  }

  @Override
  public String toString() {
    return "FlatEarth{" +
            "rotAngle=" + rotAngle +
            ", radius=" + radius +
            ", lat0=" + lat0 +
            ", lon0=" + lon0 +
            '}';
  }

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latLon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  public ProjectionPoint latLonToProj(LatLonPoint latLon,
                                      ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();
    double dx, dy;

    fromLat = Math.toRadians(fromLat);

    dy = radius * (fromLat - lat0);
    dx = radius * Math.cos(fromLat)
            * (Math.toRadians(fromLon) - lon0);

    toX = cosRot * dx - sinRot * dy;
    toY = sinRot * dx + cosRot * dy;

    result.setLocation(toX, toY);

    return result;
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param world  convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */

  public LatLonPoint projToLatLon(ProjectionPoint world,
                                  LatLonPointImpl result) {
    double toLat, toLon;
    double x = world.getX();
    double y = world.getY();
    double cosl;
    int TOLERENCE = 1;
    double xp, yp;

    xp = cosRot * x + sinRot * y;
    yp = -sinRot * x + cosRot * y;

    toLat = Math.toDegrees(lat0) + Math.toDegrees(yp / radius);
    //double lat2;
    //lat2 = lat0 + Math.toDegrees(yp/radius);
    cosl = Math.cos(Math.toRadians(toLat));
    if (Math.abs(cosl) < TOLERANCE) {
      toLon = Math.toDegrees(lon0);
    } else {
      toLon = Math.toDegrees(lon0)
              + Math.toDegrees(xp / cosl / radius);
    }

    toLon = LatLonPointImpl.lonNormal(toLon);

    result.setLatitude(toLat);
    result.setLongitude(toLon);
    return result;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n],
   *                 where from[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */

  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex,
                                int lonIndex) {
    int cnt = from[0].length;
    float[] fromLatA = from[latIndex];
    float[] fromLonA = from[lonIndex];
    float[] resultXA = to[INDEX_X];
    float[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      fromLat = Math.toRadians(fromLat);
      double dy = radius * (fromLat - lat0);
      double dx = radius * Math.cos(fromLat)
              * (Math.toRadians(fromLon) - lon0);

      toX = cosRot * dx - sinRot * dy;
      toY = sinRot * dx + cosRot * dy;

      resultXA[i] = (float) toX;
      resultYA[i] = (float) toY;
    }
    return to;
  }

  /**
   * This returns true when the line between pt1 and pt2 crosses the seam.
   * When the cone is flattened, the "seam" is lon0 +- 180.
   *
   * @param pt1 point 1
   * @param pt2 point 2
   * @return true when the line between pt1 and pt2 crosses the seam.
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {

    return (pt1.getX() * pt2.getX() < 0);
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */

  public float[][] projToLatLon(float[][] from, float[][] to) {
    int cnt = from[0].length;
    float[] fromXA = from[INDEX_X];
    float[] fromYA = from[INDEX_Y];
    float[] toLatA = to[INDEX_LAT];
    float[] toLonA = to[INDEX_LON];

    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i];
      double fromY = fromYA[i];

      double xp = cosRot * fromX + sinRot * fromY;
      double yp = -sinRot * fromX + cosRot * fromY;

      toLat = Math.toDegrees(lat0)
              + Math.toDegrees(yp / radius);
      double cosl = Math.cos(Math.toRadians(toLat));

      if (Math.abs(cosl) < TOLERANCE) {
        toLon = Math.toDegrees(lon0);
      } else {
        toLon = Math.toDegrees(lon0)
                + Math.toDegrees(xp / cosl / radius);
      }

      toLon = LatLonPointImpl.lonNormal(toLon);

      toLatA[i] = (float) toLat;
      toLonA[i] = (float) toLon;
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n],
   *                 where from[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */

  public double[][] latLonToProj(double[][] from, double[][] to,
                                 int latIndex, int lonIndex) {
    int cnt = from[0].length;
    double[] fromLatA = from[latIndex];
    double[] fromLonA = from[lonIndex];
    double[] resultXA = to[INDEX_X];
    double[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      fromLat = Math.toRadians(fromLat);
      double dy = radius * (fromLat - lat0);
      double dx = radius * Math.cos(fromLat)
              * (Math.toRadians(fromLon) - lon0);

      toX = cosRot * dx - sinRot * dy;
      toY = sinRot * dx + cosRot * dy;

      resultXA[i] = toX;
      resultYA[i] = toY;
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */

  public double[][] projToLatLon(double[][] from, double[][] to) {
    int cnt = from[0].length;
    double[] fromXA = from[INDEX_X];
    double[] fromYA = from[INDEX_Y];
    double[] toLatA = to[INDEX_LAT];
    double[] toLonA = to[INDEX_LON];

    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i];
      double fromY = fromYA[i];

      double xp = cosRot * fromX + sinRot * fromY;
      double yp = -sinRot * fromX + cosRot * fromY;

      //toLat =  lat0 + Math.toDegrees(yp);
      toLat = Math.toDegrees(lat0)
              + Math.toDegrees(yp / radius);
      double cosl = Math.cos(Math.toRadians(toLat));

      if (Math.abs(cosl) < TOLERANCE) {
        toLon = Math.toDegrees(lon0);
      } else {
        toLon = Math.toDegrees(lon0)
                + Math.toDegrees(xp / cosl / radius);
      }

      toLon = LatLonPointImpl.lonNormal(toLon);

      toLatA[i] = toLat;
      toLonA[i] = toLon;

    }
    return to;
  }

  /*ENDGENERATED*/

  /**
   * Test
   *
   * @param args not used
   */
  public static void main(String[] args) {
    FlatEarth a = new FlatEarth(90, -100, 0.0);
    ProjectionPoint p = a.latLonToProj(89, -101);
    System.out.println("proj point = " + p);
    LatLonPoint ll = a.projToLatLon(p);
    System.out.println("ll = " + ll);
  }

}


/*
 * Copyright (c) 1998 - 2009. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package thredds.catalog.util;

import thredds.catalog.*;

import java.util.List;
import java.util.ArrayList;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * _more_
 *
 * @author edavis
 * @since 4.0
 */
public class CatalogUtils
{
    private CatalogUtils() {}

    /**
     * Find all catalogRef elements in the dataset tree formed by the given dataset list.
     *
     * @param datasets         the list of datasets from which to find all the catalogRefs
     * @param log              StringBuilder into which any messages will be written
     * @param onlyRelativeUrls only include catalogRefs with relative HREF URLs if true, otherwise include all catalogRef datasets
     * @return the list of catalogRef datasets
     */
    public static List<InvCatalogRef> findAllCatRefsInDatasetTree( List<InvDataset> datasets, StringBuilder log, boolean onlyRelativeUrls )
    {
        List<InvCatalogRef> catRefList = new ArrayList<InvCatalogRef>();
        for ( InvDataset invds : datasets )
        {
            InvDatasetImpl curDs = (InvDatasetImpl) invds;

            if ( curDs instanceof InvDatasetScan)
                continue;

            if ( curDs instanceof InvCatalogRef )
            {
                InvCatalogRef catRef = (InvCatalogRef) curDs;
                String name = catRef.getName();
                String href = catRef.getXlinkHref();
                URI uri;
                try
                {
                    uri = new URI( href );
                }
                catch ( URISyntaxException e )
                {
                    log.append( log.length() > 0 ? "\n" : "" )
                            .append( "***WARN - CatalogRef [").append(name)
                            .append("] with bad HREF [" ).append( href ).append( "] " );
                    continue;
                }
                if ( onlyRelativeUrls && uri.isAbsolute() )
                    continue;

                catRefList.add( catRef );
                continue;
            }

            if ( curDs.hasNestedDatasets() )
                catRefList.addAll( findAllCatRefsInDatasetTree( curDs.getDatasets(), log, onlyRelativeUrls ) );
        }
        return catRefList;
    }

    // Escape the characters necessary for a path to be valid for a URL
    static public String escapePathForURL(String path) {
        try {
            return new URI(null, null, path, null).toString();
        } catch (URISyntaxException e) {
            return path;
        }
    }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.unidata.geoloc.vertical;

import ucar.ma2.Array;
import ucar.ma2.ArrayDouble;
import ucar.ma2.Index;
import ucar.ma2.IndexIterator;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.ArrayDouble.D1;

import ucar.nc2.Dimension;
import ucar.nc2.Variable;
import ucar.nc2.NetcdfFile;
import ucar.unidata.util.Parameter;

import java.io.IOException;
import java.util.List;

/**
 * Models the vertical coordinate for the Weather Research and Forecast
 * (WRF) model's vertical Eta coordinate
 *
 * @author IDV Development Team
 */
public class WRFEta extends VerticalTransformImpl {

  public static final String BasePressureVariable = "base_presure";
  public static final String PerturbationPressureVariable = "perturbation_presure";
  public static final String BaseGeopotentialVariable = "base_geopotential";
  public static final String PerturbationGeopotentialVariable = "perturbation_geopotential";
  public static final String IsStaggeredX = "staggered_x";
  public static final String IsStaggeredY = "staggered_y";
  public static final String IsStaggeredZ = "staggered_z";

  /**
   * perturbation variable
   */
  private Variable pertVar;

  /**
   * base variable
   */
  private Variable baseVar;

  /**
   * some boolean flags
   */
  private boolean isXStag, isYStag, isZStag;

  /**
   * Construct a vertical coordinate for the Weather Research and Forecast
   * (WRF) model's vertical Eta coordinate
   *
   * @param ds      netCDF dataset
   * @param timeDim time dimension
   * @param params  list of transformation Parameters
   */
  public WRFEta(NetcdfFile ds, Dimension timeDim, List<Parameter> params) {

    super(timeDim);

    isXStag = getParameterBooleanValue(params, IsStaggeredX);
    isYStag = getParameterBooleanValue(params, IsStaggeredY);
    isZStag = getParameterBooleanValue(params, IsStaggeredZ);

    String pertVarName;
    String baseVarName;
    if (isZStag) {
      //Geopotential is provided on the staggered z grid
      //so we'll transform like grids to height levels
      pertVarName = getParameterStringValue(params, PerturbationGeopotentialVariable);
      baseVarName = getParameterStringValue(params, BaseGeopotentialVariable);
      units = "m";  //PH and PHB are in m^2/s^2, so dividing by g=9.81 m/s^2 results in meters
    } else {
      //Pressure is provided on the non-staggered z grid
      //so we'll transform like grids to pressure levels
      pertVarName = getParameterStringValue(params, PerturbationPressureVariable);
      baseVarName = getParameterStringValue(params, BasePressureVariable);
      units = "Pa";  //P and PB are in Pascals  //ADD:safe assumption? grab unit attribute?
    }

    pertVar = ds.findVariable(pertVarName);
    baseVar = ds.findVariable(baseVarName);

    if (pertVar == null) {
      throw new RuntimeException(
          "Cant find perturbation pressure variable= " + pertVarName
              + " in WRF file");
    }
    if (baseVar == null) {
      throw new RuntimeException(
          "Cant find base state pressure variable=  " + baseVarName
              + " in WRF file");
    }
  }

  /**
   * Get the 3D vertical coordinate array for this time step.
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @return vertical coordinate array
   * @throws IOException problem reading data
   */
  public ArrayDouble.D3 getCoordinateArray(int timeIndex)
      throws IOException {
    ArrayDouble.D3 array;

    Array pertArray = getTimeSlice(pertVar, timeIndex);
    Array baseArray = getTimeSlice(baseVar, timeIndex);

    //ADD: use MAMath?
    //ADD: use IndexIterator from getIndexIteratorFast?
    int[] shape = pertArray.getShape();
    //ADD: assert that rank = 3
    //ADD: assert that both arrays are same shape
    int ni = shape[0];
    int nj = shape[1];
    int nk = shape[2];

    array = new ArrayDouble.D3(ni, nj, nk);
    Index index = array.getIndex();

    for (int i = 0; i < ni; i++) {
      for (int j = 0; j < nj; j++) {
        for (int k = 0; k < nk; k++) {
          index.set(i, j, k);
          double d = pertArray.getDouble(index) + baseArray.getDouble(index);
          if (isZStag) {
            d = d / 9.81;  //convert geopotential to height
          }
          array.setDouble(index, d);
        }
      }
    }

    if (isXStag) {
      array = addStagger(array, 2);  //assuming x dim index is 2
    }
    if (isYStag) {
      array = addStagger(array, 1);  //assuming y dim index is 1
    }

    return array;
  }

  /**
   * Get the 1D vertical coordinate array for this time step and point
   *
   * @param timeIndex the time index. Ignored if !isTimeDependent().
   * @param xIndex    the x index
   * @param yIndex    the y index
   * @return vertical coordinate array
   * @throws java.io.IOException problem reading data
   * @throws ucar.ma2.InvalidRangeException _more_
   */
  public D1 getCoordinateArray1D(int timeIndex, int xIndex, int yIndex)
  		throws IOException, InvalidRangeException {

	  ArrayDouble.D3 data = getCoordinateArray(timeIndex);

	  int[] origin = new int[3];
	  int[] shape = new int[3];

	  origin[0]=0;
	  origin[1]=yIndex;
	  origin[2]=xIndex;

	  shape[0] = data.getShape()[0];
	  shape[1] =1;
	  shape[2] =1;

	  Array tmp = data.section(origin, shape);
    return (ArrayDouble.D1)tmp.reduce();
  }

  /**
   * Add 1 to the size of the array for the given dimension.
   * Use linear average and interpolation to fill in the values.
   *
   * @param array    use this array
   * @param dimIndex use this dimension
   * @return new array with stagger
   */
  private ArrayDouble.D3 addStagger(ArrayDouble.D3 array, int dimIndex) {

    //ADD: assert 0<=dimIndex<=2

    int[] shape = array.getShape();
    int[] newShape = new int[3];
    System.arraycopy(shape, 0, newShape, 0, 3);

    newShape[dimIndex]++;
    int ni = newShape[0];
    int nj = newShape[1];
    int nk = newShape[2];
    ArrayDouble.D3 newArray = new ArrayDouble.D3(ni, nj, nk);
    //Index newIndex = newArray.getIndex();

    //extract 1d array to be extended
    int n = shape[dimIndex];  //length of extracted array
    double[] d = new double[n];  //tmp array to hold extracted values
    int[] eshape = new int[3];       //shape of extracted array
    int[] neweshape = new int[3];  //shape of new array slice to write into
    for (int i = 0; i < 3; i++) {
      eshape[i] = (i == dimIndex)
          ? n
          : 1;
      neweshape[i] = (i == dimIndex)
          ? n + 1
          : 1;
    }
    int[] origin = new int[3];

    try {

      //loop through the other 2 dimensions and "extrapinterpolate" the other
      for (int i = 0; i < ((dimIndex == 0)
          ? 1
          : ni); i++) {
        for (int j = 0; j < ((dimIndex == 1)
            ? 1
            : nj); j++) {
          for (int k = 0; k < ((dimIndex == 2)
              ? 1
              : nk); k++) {
            origin[0] = i;
            origin[1] = j;
            origin[2] = k;
            IndexIterator it = array.section(origin,
                eshape).getIndexIterator();
            for (int l = 0; l < n; l++) {
              d[l] = it.getDoubleNext();  //get the original values
            }
            double[] d2 = extrapinterpolate(d);  //compute new values
            //define slice of new array to write into
            IndexIterator newit =
                newArray.section(origin,
                    neweshape).getIndexIterator();
            for (int l = 0; l < n + 1; l++) {
              newit.setDoubleNext(d2[l]);
            }
          }
        }
      }
    } catch (InvalidRangeException e) {
      //ADD: report error?
      return null;
    }

    return newArray;
  }

  /**
   * Add one element to the array by linear interpolation
   * and extrapolation at the ends.
   *
   * @param array input array
   * @return extrapolated/interpolated array
   */
  private double[] extrapinterpolate(double[] array) {
    int n = array.length;
    double[] d = new double[n + 1];

    //end points from linear extrapolation
    //equations confirmed by Christopher Lindholm
    d[0] = 1.5 * array[0] - 0.5 * array[1];
    d[n] = 1.5 * array[n - 1] - 0.5 * array[n - 2];

    //inner points from simple average
    for (int i = 1; i < n; i++) {
      d[i] = 0.5 * (array[i - 1] + array[i]);
    }

    return d;
  }

  /**
   * Extract an Array (with rank reduced by one) from the Variable
   * for the given time index.
   *
   * @param v         variable to extract from
   * @param timeIndex time index
   * @return Array of data
   * @throws IOException problem getting Array
   */
  private Array getTimeSlice(Variable v, int timeIndex) throws IOException {
    //ADD: this would make a good utility method
    //ADD: use Array.slice?
    int[] shape = v.getShape();
    int[] origin = new int[v.getRank()];

    if (getTimeDimension() != null) {
      int dimIndex = v.findDimensionIndex(getTimeDimension().getShortName());
      if (dimIndex >= 0) {
        shape[dimIndex] = 1;
        origin[dimIndex] = timeIndex;
      }
    }

    try {
      return v.read(origin, shape).reduce();
    } catch (InvalidRangeException e) {
      throw new IOException(e);
    }
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.unidata.geoloc.projection;

import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;
import ucar.unidata.util.Parameter;

/**
 * Albers Equal Area Conic Projection, one or two standard parallels,
 * spherical earth.
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532,
 * 2nd edition (1983), p 98
 *
 * @author Unidata Development Team
 * @see Projection
 * @see ProjectionImpl
 */

public class AlbersEqualArea extends ProjectionImpl {

  private double lat0, lon0;  // radians
  private double par1, par2;  // degrees
  private double falseEasting, falseNorthing;
  private final double earth_radius; // radius in km

  /**
   * constants from Snyder's equations
   */
  private double n, C, rho0, lon0Degrees;

  /**
   * copy constructor - avoid clone !!
   */
  public ProjectionImpl constructCopy() {
    ProjectionImpl result = new AlbersEqualArea(getOriginLat(), getOriginLon(), getParallelOne(), getParallelTwo(),
            getFalseEasting(), getFalseNorthing(), getEarthRadius());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public AlbersEqualArea() {
    this(23, -96, 29.5, 45.5);
  }

  /**
   * Construct a AlbersEqualArea Projection, two standard parellels.
   * For the one standard parellel case, set them both to the same value.
   *
   * @param lat0 lat origin of the coord. system on the projection plane
   * @param lon0 lon origin of the coord. system on the projection plane
   * @param par1 standard parallel 1
   * @param par2 standard parallel 2
   * @throws IllegalArgumentException if lat0, par1, par2 = +/-90 deg
   */
  public AlbersEqualArea(double lat0, double lon0, double par1, double par2) {
    this(lat0, lon0, par1, par2, 0, 0, Earth.getRadius()*.001);
  }

  /**
   * Construct a AlbersEqualArea Projection, two standard parellels.
   * For the one standard parellel case, set them both to the same value.
   *
   * @param lat0          lat origin of the coord. system on the projection plane
   * @param lon0          lon origin of the coord. system on the projection plane
   * @param par1          standard parallel 1
   * @param par2          standard parallel 2
   * @param falseEasting  false easting in km
   * @param falseNorthing false easting in km
   * @throws IllegalArgumentException if lat0, par1, par2 = +/-90 deg
   */
  public AlbersEqualArea(double lat0, double lon0, double par1, double par2, double falseEasting, double falseNorthing) {
    this(lat0, lon0, par1, par2, falseEasting, falseNorthing, Earth.getRadius()*.001);
  }

  /**
   * Construct a AlbersEqualArea Projection, two standard parellels.
   * For the one standard parellel case, set them both to the same value.
   *
   * @param lat0          lat origin of the coord. system on the projection plane
   * @param lon0          lon origin of the coord. system on the projection plane
   * @param par1          standard parallel 1
   * @param par2          standard parallel 2
   * @param falseEasting  false easting in km
   * @param falseNorthing false easting in km
   * @param earth_radius  radius of the earth in km
   * @throws IllegalArgumentException if lat0, par1, par2 = +/-90 deg
   */
  public AlbersEqualArea(double lat0, double lon0, double par1, double par2, double falseEasting, double falseNorthing, double earth_radius) {
    super("AlbersEqualArea", false);

    this.lat0 = Math.toRadians(lat0);
    lon0Degrees = lon0;
    this.lon0 = Math.toRadians(lon0);

    this.par1 = par1;
    this.par2 = par2;

    this.falseEasting = falseEasting;
    this.falseNorthing = falseNorthing;
    this.earth_radius = earth_radius;

    precalculate();

    addParameter(CF.GRID_MAPPING_NAME, CF.ALBERS_CONICAL_EQUAL_AREA);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, lat0);
    addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, lon0);

    if (par2 == par1) {
      addParameter(CF.STANDARD_PARALLEL, par1);
    } else {
      double[] data = new double[2];
      data[0] = par1;
      data[1] = par2;
      addParameter(new Parameter(CF.STANDARD_PARALLEL, data));
    }

    if ((falseEasting != 0.0) || (falseNorthing != 0.0)) {
      addParameter(CF.FALSE_EASTING, falseEasting);
      addParameter(CF.FALSE_NORTHING, falseNorthing);
      addParameter(CDM.UNITS, "km");
    }

    addParameter(CF.EARTH_RADIUS, earth_radius * 1000); // must be in meters
  }

  /**
   * Precalculate some stuff
   */
  private void precalculate() {

    double par1r = Math.toRadians(this.par1);
    double par2r = Math.toRadians(this.par2);

    if (Math.abs(par2 - par1) < TOLERANCE) {  // single parallel
      n = Math.sin(par1r);
    } else {
      n = (Math.sin(par1r) + Math.sin(par2r)) / 2.0;
    }

    double c2 = Math.pow(Math.cos(par1r), 2);
    C = c2 + 2 * n * Math.sin(par1r);
    rho0 = computeRho(lat0);

  }

  /**
   * Compute the RHO parameter
   *
   * @param lat latitude
   * @return the rho parameter
   */
  private double computeRho(double lat) {
    return earth_radius * Math.sqrt(C - 2 * n * Math.sin(lat)) / n;
  }

  /**
   * Compute theta
   *
   * @param lon longitude
   * @return theta
   */
  private double computeTheta(double lon) {
    double dlon = LatLonPointImpl.lonNormal(Math.toDegrees(lon) - lon0Degrees);
    return n * Math.toRadians(dlon);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    AlbersEqualArea that = (AlbersEqualArea) o;

    if (Double.compare(that.earth_radius, earth_radius) != 0) return false;
    if (Double.compare(that.falseEasting, falseEasting) != 0) return false;
    if (Double.compare(that.falseNorthing, falseNorthing) != 0) return false;
    if (Double.compare(that.lat0, lat0) != 0) return false;
    if (Double.compare(that.lon0, lon0) != 0) return false;
    if (Double.compare(that.par1, par1) != 0) return false;
    if (Double.compare(that.par2, par2) != 0) return false;
    if ((defaultMapArea == null) != (that.defaultMapArea == null)) return false; // common case is that these are null
    if (defaultMapArea != null && !that.defaultMapArea.equals(defaultMapArea)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = lat0 != +0.0d ? Double.doubleToLongBits(lat0) : 0L;
    result = (int) (temp ^ (temp >>> 32));
    temp = lon0 != +0.0d ? Double.doubleToLongBits(lon0) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = par1 != +0.0d ? Double.doubleToLongBits(par1) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = par2 != +0.0d ? Double.doubleToLongBits(par2) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = falseEasting != +0.0d ? Double.doubleToLongBits(falseEasting) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = falseNorthing != +0.0d ? Double.doubleToLongBits(falseNorthing) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = earth_radius != +0.0d ? Double.doubleToLongBits(earth_radius) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

  // bean properties

  /**
   * Get the second standard parallel
   *
   * @return the second standard parallel
   */
  public double getParallelTwo() {
    return par2;
  }

    /**
   * Get the first standard parallel
   *
   * @return the first standard parallel
   */
  public double getParallelOne() {
    return par1;
  }

  /**
   * Get the origin longitude.
   *
   * @return the origin longitude.
   */
  public double getOriginLon() {
    return Math.toDegrees(lon0);
  }

  /**
   * Get the origin latitude.
   *
   * @return the origin latitude.
   */
  public double getOriginLat() {
    return Math.toDegrees(lat0);
  }

  //////////////////////////////////////////////
  // setters for IDV serialization - do not use except for object creating

  /**
     * Set the second standard parallel
     *
     * @param par   the second standard parallel
     */
    public void setParallelTwo(double par) {
        par2 = par;
        precalculate();
    }

  /**
   * Set the first standard parallel
   *
   * @param par   the first standard parallel
   */
  public void setParallelOne(double par) {
      par1 = par;
      precalculate();
  }

  /**
   * Set the origin longitude.
   * @param lon   the origin longitude.
   */
  public void setOriginLon(double lon) {
      lon0 = Math.toRadians(lon);
      precalculate();
  }

  /**
   * Set the origin latitude.
   *
   * @param lat   the origin latitude.
   */
  public void setOriginLat(double lat) {
      lat0 = Math.toRadians(lat);
      precalculate();
  }

  /**
   * Set the false_easting, in km.
   * natural_x_coordinate + false_easting = x coordinate
   *
   * @param falseEasting x offset
   */
  public void setFalseEasting(double falseEasting) {
    this.falseEasting = falseEasting;
  }

  /**
   * Set the false northing, in km.
   * natural_y_coordinate + false_northing = y coordinate
   *
   * @param falseNorthing y offset
   */
  public void setFalseNorthing(double falseNorthing) {
    this.falseNorthing = falseNorthing;
  }

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

  /**
   * Get the false easting, in km.
   *
   * @return the false easting.
   */
  public double getFalseEasting() {
    return falseEasting;
  }

  /**
   * Get the false northing, in km.
   *
   * @return the false northing.
   */
  public double getFalseNorthing() {
    return falseNorthing;
  }

  /**
   * Earth radius in km
   * @return Earth radius in km
   */
  public double getEarthRadius() {
    return this.earth_radius;
  }

  /**
   * Get the label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "Albers Equal Area";
  }

  /**
   * Create a String of the parameters.
   *
   * @return a String of the parameters
   */
  public String paramsToString() {
    return toString();
  }

  @Override
  public String toString() {
    return "AlbersEqualArea{" +
            "lat0=" + lat0 +
            ", lon0=" + lon0 +
            ", par1=" + par1 +
            ", par2=" + par2 +
            ", falseEasting=" + falseEasting +
            ", falseNorthing=" + falseNorthing +
            ", earth_radius=" + earth_radius +
            '}';
  }

  /**
   * Get the scale at the given lat.
   *
   * @param lat lat to use
   * @return scale factor at that latitude
   */
  public double getScale(double lat) {
    lat = Math.toRadians(lat);
    double n = Math.cos(lat);
    double d = Math.sqrt(C - 2 * n * Math.sin(lat));
    return n / d;
  }

  /**
   * This returns true when the line between pt1 and pt2 crosses the seam.
   * When the cone is flattened, the "seam" is lon0 +- 180.
   *
   * @param pt1 point 1
   * @param pt2 point 2
   * @return true when the line between pt1 and pt2 crosses the seam.
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (ProjectionPointImpl.isInfinite(pt1)
            || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }
    // opposite signed X values, larger then 5000 km
    return (pt1.getX() * pt2.getX() < 0)
            && (Math.abs(pt1.getX() - pt2.getX()) > 5000.0);
  }

  /*MACROBODY
    latLonToProj {} {
      fromLat = Math.toRadians(fromLat);
      fromLon = Math.toRadians(fromLon);
      double rho = computeRho(fromLat);
      double theta = computeTheta(fromLon);

      toX = rho * Math.sin(theta);
      toY = rho0 - rho*Math.cos(theta);
    }
    projToLatLon {double rrho0 = rho0;} {
      if (n < 0) {
          rrho0 *= -1.0;
          fromX *= -1.0;
          fromY *= -1.0;
      }

      double yd = rrho0-fromY;
      double rho = Math.sqrt(fromX * fromX + yd*yd);
      double theta = Math.atan2( fromX, yd);
      if (n < 0) rho *= -1.0;

      toLat = Math.toDegrees(Math.asin((C-Math.pow((rho*n/EARTH_RADIUS),2))/(2*n)));

      toLon = Math.toDegrees(theta/n + lon0);

           }
  MACROBODY*/

  /*BEGINGENERATED*/

  /*
  Note this section has been generated using the convert.tcl script.
  This script, run as:
  tcl convert.tcl AlbersEqualArea.java
  takes the actual projection conversion code defined in the MACROBODY
  section above and generates the following 6 methods
  */

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latLon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();

    fromLat = Math.toRadians(fromLat);
    fromLon = Math.toRadians(fromLon);
    double rho = computeRho(fromLat);
    double theta = computeTheta(fromLon);

    toX = rho * Math.sin(theta) + falseEasting;
    toY = rho0 - rho * Math.cos(theta) + falseNorthing;

    result.setLocation(toX, toY);
    return result;
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param world  convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double toLat, toLon;
    double fromX = world.getX() - falseEasting;
    double fromY = world.getY() - falseNorthing;
    double rrho0 = rho0;

    if (n < 0) {
      rrho0 *= -1.0;
      fromX *= -1.0;
      fromY *= -1.0;
    }

    double yd = rrho0 - fromY;
    double rho = Math.sqrt(fromX * fromX + yd * yd);
    double theta = Math.atan2(fromX, yd);
    if (n < 0) {
      rho *= -1.0;
    }
    toLat = Math.toDegrees(Math.asin((C - Math.pow((rho * n / earth_radius), 2)) / (2 * n)));

    toLon = Math.toDegrees(theta / n + lon0);

    result.setLatitude(toLat);
    result.setLongitude(toLon);
    return result;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n],
   *                 where from[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */
  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex,
                                int lonIndex) {
    int cnt = from[0].length;
    float[] fromLatA = from[latIndex];
    float[] fromLonA = from[lonIndex];
    float[] resultXA = to[INDEX_X];
    float[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      fromLat = Math.toRadians(fromLat);
      fromLon = Math.toRadians(fromLon);
      double rho = computeRho(fromLat);
      double theta = computeTheta(fromLon);

      toX = rho * Math.sin(theta);
      toY = rho0 - rho * Math.cos(theta);

      resultXA[i] = (float) (toX + falseEasting);
      resultYA[i] = (float) (toY + falseNorthing);
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public float[][] projToLatLon(float[][] from, float[][] to) {
    int cnt = from[0].length;
    float[] fromXA = from[INDEX_X];
    float[] fromYA = from[INDEX_Y];
    float[] toLatA = to[INDEX_LAT];
    float[] toLonA = to[INDEX_LON];
    double rrho0 = rho0;
    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;

      if (n < 0) {
        rrho0 *= -1.0;
        fromX *= -1.0;
        fromY *= -1.0;
      }

      double yd = rrho0 - fromY;
      double rho = Math.sqrt(fromX * fromX + yd * yd);
      double theta = Math.atan2(fromX, yd);
      if (n < 0) {
        rho *= -1.0;
      }

      toLat = Math.toDegrees(Math.asin((C
              - Math.pow((rho * n / earth_radius), 2)) / (2 * n)));

      toLon = Math.toDegrees(theta / n + lon0);

      toLatA[i] = (float) toLat;
      toLonA[i] = (float) toLon;
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n],
   *                 where from[0][i], from[1][i] is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates,
   *                 where to[0][i], to[1][i] is the (x,y) coordinate
   *                 of the ith point
   * @param latIndex index of latitude in "from"
   * @param lonIndex index of longitude in "from"
   * @return the "to" array.
   */
  public double[][] latLonToProj(double[][] from, double[][] to,
                                 int latIndex, int lonIndex) {
    int cnt = from[0].length;
    double[] fromLatA = from[latIndex];
    double[] fromLonA = from[lonIndex];
    double[] resultXA = to[INDEX_X];
    double[] resultYA = to[INDEX_Y];
    double toX, toY;

    for (int i = 0; i < cnt; i++) {
      double fromLat = fromLatA[i];
      double fromLon = fromLonA[i];

      fromLat = Math.toRadians(fromLat);
      fromLon = Math.toRadians(fromLon);
      double rho = computeRho(fromLat);
      double theta = computeTheta(fromLon);

      toX = rho * Math.sin(theta);
      toY = rho0 - rho * Math.cos(theta);

      resultXA[i] = toX + falseEasting;
      resultYA[i] = toY + falseNorthing;
    }
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from array of lat/lon coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (lat,lon) coordinate
   *             of the ith point
   * @param to   resulting array of projection coordinates: to[2][n]
   *             where (to[0][i], to[1][i]) is the (x,y) coordinate
   *             of the ith point
   * @return the "to" array
   */
  public double[][] projToLatLon(double[][] from, double[][] to) {
    int cnt = from[0].length;
    double[] fromXA = from[INDEX_X];
    double[] fromYA = from[INDEX_Y];
    double[] toLatA = to[INDEX_LAT];
    double[] toLonA = to[INDEX_LON];
    double rrho0 = rho0;
    double toLat, toLon;
    for (int i = 0; i < cnt; i++) {
      double fromX = fromXA[i] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;

      if (n < 0) {
        rrho0 *= -1.0;
        fromX *= -1.0;
        fromY *= -1.0;
      }

      double yd = rrho0 - fromY;
      double rho = Math.sqrt(fromX * fromX + yd * yd);
      double theta = Math.atan2(fromX, yd);
      if (n < 0) {
        rho *= -1.0;
      }

      toLat = Math.toDegrees(Math.asin((C
              - Math.pow((rho * n / earth_radius), 2)) / (2 * n)));

      toLon = Math.toDegrees(theta / n + lon0);

      toLatA[i] = toLat;
      toLonA[i] = toLon;
    }
    return to;
  }

  /*ENDGENERATED*/

  /**
   * Test
   *
   * @param args not used
   */
  public static void main(String[] args) {
    AlbersEqualArea a = new AlbersEqualArea(23, -96, 29.5, 45.5);
    System.out.printf("name=%s%n", a.getName());
    System.out.println("ll = 35N 75W");
    ProjectionPoint p = a.latLonToProj(35, -75);
    System.out.println("proj point = " + p);
    LatLonPoint ll = a.projToLatLon(p);
    System.out.println("ll = " + ll);
  }

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.servlet.ascii;

import java.io.*;

import opendap.dap.*;

/**

 */
public class asciiString extends DString implements toASCII {

    private static boolean _Debug = false;

    /**
     * Constructs a new asciiString.
     */
    public asciiString() {
        this(null);
    }

    /**
     * Constructs a new asciiString with name n.
     *
     * @param n the name of the variable.
     */
    public asciiString(String n) {
        super(n);
    }

    /**
     * Returns a string representation of the variables value. This
     * is really foreshadowing functionality for Server types, but
     * as it may come in useful for clients it is added here. Simple
     * types (example: DFloat32) will return a single value. DConstuctor
     * and DVector types will be flattened. DStrings and DURL's will
     * have double quotes around them.
     *
     * @param addName is a flag indicating if the variable name should
     *                appear at the begining of the returned string.
     */
    public void toASCII(PrintWriter pw,
                        boolean addName,
                        String rootName,
                        boolean newLine) {

        if (addName)
            pw.print(", ");

        String s = getValue();

        // Get rid of null terminations on strings
        if ((s.length() > 0) && s.charAt(s.length() - 1) == ((char) 0)) { // jc mod

            if (_Debug) System.out.println("Removing null termination from string \"" + getEncodedName() + "\".");

            char cArray[] = s.toCharArray();
            s = new String(cArray, 0, cArray.length - 1);
        }

        pw.print("\"" + s + "\"");

        if (newLine)
            pw.print("\n");

    }

    public String toASCIIAddRootName(PrintWriter pw, boolean addName, String rootName) {

        if (addName) {
            rootName = toASCIIFlatName(rootName);
            pw.print(rootName);
        }
        return (rootName);

    }

    public String toASCIIFlatName(String rootName) {
        String s;
        if (rootName != null) {
            s = rootName + "." + getEncodedName();
        } else {
            s = getEncodedName();
        }
        return (s);
    }

}


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

// $Id: DCWriter.java 48 2006-07-12 16:15:40Z caron $

package thredds.catalog.dl;

import thredds.catalog.*;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.units.DateType;

import org.jdom2.*;
import org.jdom2.output.*;

import java.io.*;
import java.util.*;

public class DCWriter {
  private static final Namespace defNS = Namespace.getNamespace("http://purl.org/dc/elements/1.1/");
  private static final String schemaLocation = defNS.getURI() + " http://www.unidata.ucar.edu/schemas/other/dc/dc.xsd";
  private static final String threddsServerURL = "http://localhost:8080/thredds/subset.html";

  private InvCatalog cat;
  public DCWriter( InvCatalog cat) {
    this.cat = cat;
  }

  public void writeItems( String fileDir) {
    File dir = new File(fileDir);
    if (!dir.exists()) {
      boolean ret = dir.mkdirs();
      assert ret;
    }

    for (InvDataset dataset :  cat.getDatasets()) {
      doDataset(dataset, fileDir);
    }

  }

  private void doDataset( InvDataset ds, String fileDir) {
    if (ds.isHarvest() && (ds.getID() != null)) {
      String fileOutName = fileDir+"/"+ds.getID()+".dc.xml";
      try ( OutputStream out = new BufferedOutputStream(new FileOutputStream(fileOutName))) {
        writeOneItem(ds, System.out);
        writeOneItem(ds, out);
        return;
      } catch (IOException ioe) {
        ioe.printStackTrace();
      }
    }

    for (InvDataset nested :  ds.getDatasets()) {
      doDataset( nested, fileDir);
    }
  }

  private void writeOneItem( InvDataset ds, OutputStream out) throws IOException {
    Element rootElem = new Element("dc", defNS);
    Document doc = new Document(rootElem);
    writeDataset( ds, rootElem);
    rootElem.addNamespaceDeclaration(XMLEntityResolver.xsiNS);
    // rootElem.setAttribute("schemaLocation", schemaLocation, XMLEntityResolver.xsiNS);
    rootElem.setAttribute("schemaLocation", defNS.getURI()+" "+schemaLocation, XMLEntityResolver.xsiNS);

    // Output the document, use standard formatter
    //XMLOutputter fmt = new XMLOutputter("  ", true);
    XMLOutputter fmt = new XMLOutputter( Format.getPrettyFormat());
    fmt.output( doc, out);
  }

  /*
        <xs:element ref="dc:title"/>
        <xs:element ref="dc:creator"/>
        <xs:element ref="dc:subject"/>
        <xs:element ref="dc:description"/>
        <xs:element ref="dc:publisher"/>
        <xs:element ref="dc:contributor"/>
        <xs:element ref="dc:date"/>
        <xs:element ref="dc:type"/>
        <xs:element ref="dc:format"/>
        <xs:element ref="dc:identifier"/>
        <xs:element ref="dc:source"/>
        <xs:element ref="dc:language"/>
        <xs:element ref="dc:relation"/>
        <xs:element ref="dc:coverage"/>
        <xs:element ref="dc:rights"/>
        <!-- controlled vocabulary from dcmitype -->
        <xs:element ref="dcmitype:dcmitype"/>
  */
  public void writeDataset(InvDataset ds, Element rootElem) {

    rootElem.addContent( new Element("title", defNS).addContent(ds.getName()));

    rootElem.addContent( new Element("Entry_ID", defNS).addContent(ds.getUniqueID()));

    // keywords
    List<ThreddsMetadata.Vocab> list = ds.getKeywords();
    if (list.size() > 0) {
      for (ThreddsMetadata.Vocab k : list) {
        rootElem.addContent( new Element("Keyword", defNS).addContent( k.getText()));
      }
    }

    //temporal
    CalendarDateRange tm = ds.getCalendarDateCoverage();
    Element tmElem = new Element("Temporal_Coverage", defNS);
    rootElem.addContent( tmElem);

    tmElem.addContent( new Element("Start_Date", defNS).addContent( tm.getStart().toString()));
    tmElem.addContent( new Element("End_Date", defNS).addContent( tm.getEnd().toString()));

    //geospatial
    ThreddsMetadata.GeospatialCoverage geo = ds.getGeospatialCoverage();
    Element geoElem = new Element("Spatial_Coverage", defNS);
    rootElem.addContent( geoElem);

    geoElem.addContent( new Element("Southernmost_Latitude", defNS).addContent( Double.toString(geo.getLatSouth())));
    geoElem.addContent( new Element("Northernmost_Latitude", defNS).addContent(Double.toString(geo.getLatNorth())));
    geoElem.addContent( new Element("Westernmost_Latitude", defNS).addContent(Double.toString(geo.getLonWest())));
    geoElem.addContent( new Element("Easternmost_Latitude", defNS).addContent(Double.toString(geo.getLonEast())));

    rootElem.addContent( new Element("Use_Constraints", defNS).addContent(ds.getDocumentation("rights")));

    // data center
    List<ThreddsMetadata.Source> slist = ds.getPublishers();
    if (list.size() > 0) {
      for ( ThreddsMetadata.Source p : slist) {
        Element dataCenter = new Element("Data_Center", defNS);
        rootElem.addContent( dataCenter);
        writePublisher(p, dataCenter);
      }
    }

    rootElem.addContent( new Element("Summary", defNS).addContent(ds.getDocumentation("summary")));

    Element primaryURLelem = new Element("Related_URL", defNS);
    rootElem.addContent( primaryURLelem);

    String primaryURL = threddsServerURL +
        "?catalog="+((InvCatalogImpl)ds.getParentCatalog()).getBaseURI().toString() +
        "&dataset="+ds.getID();
    primaryURLelem.addContent( new Element("URL_Content_Type", defNS).addContent("THREDDS access page"));
    primaryURLelem.addContent( new Element("URL", defNS).addContent(primaryURL));

    DateType today = new DateType(false, new Date());
    rootElem.addContent(new Element("DIF_Creation_Date", defNS).addContent(today.toDateTimeStringISO()));

  }

  protected void writePublisher(ThreddsMetadata.Source p, Element dataCenter) {
    Element name = new Element("Data_Center_Name", defNS);
    dataCenter.addContent( name);
    name.addContent( new Element("Short_Name", defNS).addContent(p.getName()));
    //name.addContent( new Element("Long_Name", defNS).addContent(p.getLongName()));

    if ((p.getUrl() != null) && p.getUrl().length() > 0)
      dataCenter.addContent( new Element("Data_Center_URL", defNS).addContent(p.getUrl()));

    Element person = new Element("Personnel", defNS);
    dataCenter.addContent( person);
    person.addContent( new Element("Role", defNS).addContent("DATA CENTER CONTACT"));
    person.addContent( new Element("Email", defNS).addContent(p.getEmail()));
  }

  private void writeVariable( Element param, ThreddsMetadata.Variable v) {
    String vname = v.getVocabularyName();
    StringTokenizer stoker = new StringTokenizer(vname,">");
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Category", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Topic", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Term", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Variable", defNS).addContent(stoker.nextToken().trim()));
    if (stoker.hasMoreTokens())
      param.addContent( new Element("Detailed_Variable", defNS).addContent(stoker.nextToken().trim()));
  }

  // test
  private static void doOne( InvCatalogFactory fac, String url) {
    System.out.println("***read "+url);
    try {
        InvCatalogImpl cat = fac.readXML(url);
        StringBuilder buff = new StringBuilder();
        boolean isValid = cat.check( buff, false);
        System.out.println("catalog <" + cat.getName()+ "> "+ (isValid ? "is" : "is not") + " valid");
        System.out.println(" validation output=\n" + buff);
        // System.out.println(" catalog=\n" + fac.writeXML(cat));

        DCWriter w = new DCWriter( cat);
        w.writeItems( "C:/temp/dif");
      } catch (Exception e) {
        e.printStackTrace();
      }

  }

   /** testing */
  public static void main (String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(true);

    doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/TestHarvest.xml");
  }

}


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

package ucar.nc2.iosp.gempak;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.iosp.AbstractIOServiceProvider;
import ucar.nc2.util.CancelTask;
import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;

/**
 * An IOSP for Gempak Station (SF,SN) data.
 *
 * @author dmurray
 */
public abstract class GempakStationFileIOSP extends AbstractIOServiceProvider {
  private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GempakStationFileIOSP.class);

  /**
   * Gempak file reader
   */
  protected AbstractGempakStationFileReader gemreader;

  /**
   * place to store debug stuff
   */
  protected StringBuilder parseInfo = new StringBuilder();

  /**
   * Float missing attribute
   */
  protected final static Number RMISS = GempakConstants.RMISSD;

  /**
   * Integer missing attribute
   */
  protected final static Number IMISS = GempakConstants.IMISSD;

  /**
   * static for shared dimension of length 4
   */
  protected final static Dimension DIM_LEN8 = new Dimension("len8", 8, true);

  /**
   * static for shared dimension of length 4
   */
  protected final static Dimension DIM_LEN4 = new Dimension("len4", 4, true);

  /**
   * static for shared dimension of length 2
   */
  protected final static Dimension DIM_LEN2 = new Dimension("len2", 2, true);

  /**
   * name for the time variable
   */
  protected final static String TIME_VAR = "time";

  /**
   * name for the time variable
   */
  protected final static String MISSING_VAR = "_isMissing";

  /**
   * station variable names
   */
  private static String[] stnVarNames = {
          GempakStation.STID, GempakStation.STNM, GempakStation.SLAT,
          GempakStation.SLON, GempakStation.SELV, GempakStation.STAT,
          GempakStation.COUN, GempakStation.STD2, GempakStation.SPRI,
          GempakStation.SWFO, GempakStation.WFO2
  };

  /**
   * lengths of station variable
   */
  private static int[] stnVarSizes = {
          8, 4, 4, 4, 4, 2, 2, 4, 4, 4, 4
  };

  /**
   * Is this a valid file?
   *
   * @param raf RandomAccessFile to check
   * @return true if a valid Gempak grid file
   * @throws IOException problem reading file
   */
  @Override
  public boolean isValidFile(RandomAccessFile raf) throws IOException {
    try {
      gemreader = makeStationReader();
      return gemreader.init(raf, false);
    } catch (Exception ioe) {
      return false;
    }
  }

  /**
   * Make the appropriate station file reader, subclasses need to implement
   * this
   *
   * @return the appropriate reader for that subclass
   */
  protected abstract AbstractGempakStationFileReader makeStationReader();

  /**
   * Open the service provider for reading.
   *
   * @param raf        file to read from
   * @param ncfile     netCDF file we are writing to (memory)
   * @param cancelTask task for cancelling
   * @throws IOException problem reading file
   */
  @Override
  public void open(RandomAccessFile raf, NetcdfFile ncfile, CancelTask cancelTask) throws IOException {

    //System.out.printf("GempakSurfaceIOSP open %s (%s) %n", raf.getLocation(), Calendar.getInstance().getTime());
    super.open(raf, ncfile, cancelTask);
    if (gemreader == null) {
      gemreader = makeStationReader();
    }
    initTables();
    gemreader.init(raf, true);
    buildNCFile();
  }

  /**
   * Initialize the parameter tables.
   */
  private void initTables() throws IOException {
    GempakParameters.addParameters("resources/nj22/tables/gempak/params.tbl");
  }

  /**
   * Get the detail information
   *
   * @return the detail info
   */
  public String getDetailInfo() {
    Formatter ff = new Formatter();
    ff.format("%s", super.getDetailInfo());
    ff.format("%s", parseInfo);
    return ff.toString();
  }

  /*
   * Sync the file
   *
   * @return true if needed to sync
   * @throws IOException problem synching the file
   *
  public boolean sync() throws IOException {
    //printStack("***************************** sync ************************", 100);
    //System.out.printf("check sync on %s (%s) %n", raf.getLocation(), Calendar.getInstance().getTime());

    if (gemreader.getInitFileSize() < raf.length()) {
      long start = System.currentTimeMillis();
      log.debug("GEMPAKStationIOSP.sync: file " + raf.getLocation()
              + " is bigger: " + raf.length() + " > "
              + gemreader.getInitFileSize());
      gemreader.init(raf, true);
      // reconstruct the ncfile objects
      buildNCFile();
      //System.out.printf("sync on %s took %d msecs%n", raf.getLocation(), (System.currentTimeMillis()-start));
      return true;
    }
    return false;
  } */

  /**
   * Build the netCDF file
   *
   * @throws IOException problem reading the file
   */
  protected void buildNCFile() throws IOException {
    ncfile.empty();
    fillNCFile();
    addGlobalAttributes();
    ncfile.finish();
    //System.out.println(ncfile);
  }

  /**
   * Fill the contents of the netCDF file.  Assumes that the file has been
   * cleared.
   *
   * @throws IOException problem reading the file
   */
  protected abstract void fillNCFile() throws IOException;

  /**
   * Make a structure for the part
   *
   * @param partName       partname
   * @param dimensions     dimensions for the structure
   * @param includeMissing true to include the missing variable
   * @return a Structure
   */
  protected Structure makeStructure(String partName, List<Dimension> dimensions, boolean includeMissing) {
    List<GempakParameter> params = gemreader.getParameters(partName);
    if (params == null) {
      return null;
    }
    Structure sVar = new Structure(ncfile, null, null, partName);
    sVar.setDimensions(dimensions);
    for (GempakParameter param : params) {
      sVar.addMemberVariable(makeParamVariable(param, null));
    }
    if (includeMissing) {
      sVar.addMemberVariable(makeMissingVariable());
    }
    return sVar;
  }

  /**
   * Make the missing variable
   *
   * @return the missing variable
   */
  protected Variable makeMissingVariable() {
    Variable var = new Variable(ncfile, null, null, MISSING_VAR);
    var.setDataType(DataType.BYTE);
    var.setDimensions((List<Dimension>) null);
    var.addAttribute(new Attribute("description", "missing flag - 1 means all params are missing"));
    var.addAttribute(new Attribute(CDM.MISSING_VALUE, (byte) 1));
    return var;
  }

  /**
   * Make a variable from a GempakParmaeter
   *
   * @param param GempakParameter
   * @param dims  Variable dimensions
   * @return the Variable
   */
  protected Variable makeParamVariable(GempakParameter param, List<Dimension> dims) {
    Variable var = new Variable(ncfile, null, null, param.getName());
    var.setDataType(DataType.FLOAT);
    var.setDimensions(dims);
    var.addAttribute(new Attribute(CDM.LONG_NAME, param.getDescription()));
    String units = param.getUnit();
    if ((units != null) && !units.equals("")) {
      var.addAttribute(new Attribute(CDM.UNITS, units));
    }
    var.addAttribute(new Attribute(CDM.MISSING_VALUE, RMISS));
    return var;
  }

  /**
   * Add on global attributes for all types
   */
  protected void addGlobalAttributes() {
    // global stuff
    ncfile.addAttribute(null, new Attribute(CDM.CONVENTIONS, getConventions()));
    String fileType = "GEMPAK " + gemreader.getFileType();
    ncfile.addAttribute(null, new Attribute("file_format", fileType));
    ncfile.addAttribute(null, new Attribute("history", "Direct read of " + fileType + " into NetCDF-Java API"));
    ncfile.addAttribute(null, new Attribute(CF.FEATURE_TYPE, getCFFeatureType()));
  }

  /**
   * Get the netCDF conventions identifier.
   *
   * @return the convention name
   */
  public String getConventions() {
    return "GEMPAK/CDM";
  }

  /**
   * Get the CF feature type, subclasses should override
   *
   * @return the feature type
   */
  public String getCFFeatureType() {
    return CF.FeatureType.point.toString();
  }

  /**
   * Get the size of a particular station variable
   *
   * @param name name of the variable (key)
   * @return size or -1
   */
  protected int getStnVarSize(String name) {
    int size = -1;
    for (int i = 0; i < stnVarNames.length; i++) {
      if (name.equals(stnVarNames[i])) {
        size = stnVarSizes[i];
        break;
      }
    }
    return size;
  }

  /**
   * Make the station variables from a representative station
   *
   * @param stations list of stations
   * @param dim      station dimension
   * @return the list of variables
   */
  protected List<Variable> makeStationVars(List<GempakStation> stations, Dimension dim) {
    int numStations = stations.size();
    List<Variable> vars = new ArrayList<>();
    List<String> stnKeyNames = gemreader.getStationKeyNames();
    for (String varName : stnKeyNames) {
      Variable v = makeStationVariable(varName, dim);
      Attribute stIDAttr = new Attribute(CF.STANDARD_NAME, "station_id");
      if (varName.equals(GempakStation.STID)) {  // Use STID as the station_id for the dataset.
        v.addAttribute(stIDAttr);
      }
      vars.add(v);
    }
    // see if we fill these in completely now
    if ((dim != null) && (numStations > 0)) {
      for (Variable v : vars) {
        Array varArray;
        if (v.getDataType().equals(DataType.CHAR)) {
          int[] shape = v.getShape();
          varArray = new ArrayChar.D2(shape[0], shape[1]);
        } else {
          varArray = get1DArray(v.getDataType(), numStations);
        }
        assert varArray != null;

        int index = 0;
        String varname = v.getFullName();
        for (GempakStation stn : stations) {
          String test = "";
          switch (varname) {
            case GempakStation.STID:
              test = stn.getName();
              break;
            case GempakStation.STNM:
              ((ArrayInt.D1) varArray).set(index, stn.getSTNM());
              break;
            case GempakStation.SLAT:
              ((ArrayFloat.D1) varArray).set(index, (float) stn.getLatitude());
              break;
            case GempakStation.SLON:
              ((ArrayFloat.D1) varArray).set(index, (float) stn.getLongitude());
              break;
            case GempakStation.SELV:
              ((ArrayFloat.D1) varArray).set(index, (float) stn.getAltitude());
              break;
            case GempakStation.STAT:
              test = stn.getSTAT();
              break;
            case GempakStation.COUN:
              test = stn.getCOUN();
              break;
            case GempakStation.STD2:
              test = stn.getSTD2();
              break;
            case GempakStation.SPRI:
              ((ArrayInt.D1) varArray).set(index, stn.getSPRI());
              break;
            case GempakStation.SWFO:
              test = stn.getSWFO();
              break;
            case GempakStation.WFO2:
              test = stn.getWFO2();
              break;
          }
          if (!test.equals("")) {
            ((ArrayChar.D2) varArray).setString(index, test);
          }
          index++;
        }
        v.setCachedData(varArray, false);
      }
    }
    return vars;
  }

  /**
   * Get a 1DArray for the type and length
   *
   * @param type DataType
   * @param len  length
   * @return the array
   */
  private Array get1DArray(DataType type, int len) {
    Array varArray = null;
    if (type.equals(DataType.FLOAT)) {
      varArray = new ArrayFloat.D1(len);
    } else if (type.equals(DataType.DOUBLE)) {
      varArray = new ArrayDouble.D1(len);
    } else if (type.equals(DataType.INT)) {
      varArray = new ArrayInt.D1(len, false);
    }
    return varArray;
  }

  /**
   * Make a station variable
   *
   * @param varname  variable name
   * @param firstDim station dimension
   * @return corresponding variable
   */
  protected Variable makeStationVariable(String varname, Dimension firstDim) {
    String longName = varname;
    String unit = null;
    DataType type = DataType.CHAR;
    List<Dimension> dims = new ArrayList<>();
    List<Attribute> attrs = new ArrayList<>();
    if (firstDim != null) {
      dims.add(firstDim);
    }

    switch (varname) {
      case GempakStation.STID:
        longName = "Station identifier";
        dims.add(DIM_LEN8);
        break;
      case GempakStation.STNM:
        longName = "WMO station id";
        type = DataType.INT;
        break;
      case GempakStation.SLAT:
        longName = "latitude";
        unit = CDM.LAT_UNITS;
        type = DataType.FLOAT;
        attrs.add(new Attribute(CF.STANDARD_NAME, "latitude"));
        break;
      case GempakStation.SLON:
        longName = "longitude";
        unit = CDM.LON_UNITS;
        type = DataType.FLOAT;
        attrs.add(new Attribute(CF.STANDARD_NAME, "longitude"));
        break;
      case GempakStation.SELV:
        longName = "altitude";
        unit = "meter";
        type = DataType.FLOAT;
        attrs.add(new Attribute(CF.POSITIVE, CF.POSITIVE_UP));
        attrs.add(new Attribute(CF.STANDARD_NAME, CF.STATION_ALTITUDE));
        break;
      case GempakStation.STAT:
        longName = "state or province";
        dims.add(DIM_LEN2);
        break;
      case GempakStation.COUN:
        longName = "country code";
        dims.add(DIM_LEN2);
        break;
      case GempakStation.STD2:
        longName = "Extended station id";
        dims.add(DIM_LEN4);
        break;
      case GempakStation.SPRI:
        longName = "Station priority";
        type = DataType.INT;
        break;
      case GempakStation.SWFO:
        longName = "WFO code";
        dims.add(DIM_LEN4);
        break;
      case GempakStation.WFO2:
        longName = "Second WFO code";
        dims.add(DIM_LEN4);
        break;
    }
    Variable v = new Variable(ncfile, null, null, varname);
    v.setDataType(type);
    v.addAttribute(new Attribute(CDM.LONG_NAME, longName));
    if (unit != null) {
      v.addAttribute(new Attribute(CDM.UNITS, unit));
    }
    if (type.equals(DataType.FLOAT)) {
      v.addAttribute(new Attribute(CDM.MISSING_VALUE, RMISS));
    } else if (type.equals(DataType.INT)) {
      v.addAttribute(new Attribute(CDM.MISSING_VALUE, IMISS));
    }
    if (!attrs.isEmpty()) {
      for (Attribute attr : attrs) {
        v.addAttribute(attr);
      }
    }
    if (!dims.isEmpty()) {
      v.setDimensions(dims);
    } else {
      v.setDimensions((String) null);
    }
    return v;
  }

}