/*
 * 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;

/**
 * Lambert Conformal Projection, one or two standard parallels, spherical earth.
 * Projection plane is a cone whose vertex lies on the line of the earth's axis,
 * and intersects the earth at two parellels (par1, par2), or is tangent to the earth at
 * one parellel par1 = par2. The cone is flattened by splitting along the longitude = lon0+180.
 * <p/>
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 104
 *
 * @author John Caron
 * @see Projection
 * @see ProjectionImpl
 */

public class LambertConformal extends ProjectionImpl {
  private final double earth_radius;
  private double lat0, lon0;  // lat/lon in radians
  private double par1, par2;  // standard parallel 1 and 2 degrees
  private double falseEasting, falseNorthing;

  private double n, F, rho; // constants from Snyder's equations
  private double earthRadiusTimesF;// Earth's radius time F
  private double lon0Degrees; // lon naught ??

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new LambertConformal(getOriginLat(), getOriginLon(), getParallelOne(), getParallelTwo(),
            getFalseEasting(), getFalseNorthing(), earth_radius);
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public LambertConformal() {
    this(40.0, -105.0, 20.0, 60.0, 0.0, 0.0, EARTH_RADIUS);
  }

  /**
   * Construct a LambertConformal 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 > +/-90 or if par1, par2 >= +/-90 deg
   */
  public LambertConformal(double lat0, double lon0, double par1, double par2) {
    this(lat0, lon0, par1, par2, 0.0, 0.0, EARTH_RADIUS);
  }

  /**
   * Construct a LambertConformal 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 false_easting  natural_x_coordinate + false_easting = x coordinate in km
   * @param false_northing natural_y_coordinate + false_northing = y coordinate  in km
   * @throws IllegalArgumentException if lat0 > +/-90 or if par1, par2 >= +/-90 deg
   */
  public LambertConformal(double lat0, double lon0, double par1,
                          double par2, double false_easting,
                          double false_northing) {
    this(lat0, lon0, par1, par2, false_easting, false_northing, EARTH_RADIUS);

  }

  /**
   * Construct a LambertConformal 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 false_easting  natural_x_coordinate + false_easting = x coordinate in km
   * @param false_northing natural_y_coordinate + false_northing = y coordinate  in km
   * @param earth_radius   radius of the earth in km
   * @throws IllegalArgumentException if lat0 > +/-90 or if par1, par2 >= +/-90 deg
   */
  public LambertConformal(double lat0, double lon0, double par1,
                          double par2, double false_easting,
                          double false_northing, double earth_radius) {

    super("LambertConformal", false);

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

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

    this.falseEasting = false_easting;
    this.falseNorthing = false_northing;
    this.earth_radius = earth_radius;

    precalculate();

    addParameter(CF.GRID_MAPPING_NAME, CF.LAMBERT_CONFORMAL_CONIC);
    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 ((false_easting != 0.0) || (false_northing != 0.0)) {
      addParameter(CF.FALSE_EASTING, false_easting);
      addParameter(CF.FALSE_NORTHING, false_northing);
      addParameter(CDM.UNITS, "km");
    }
    addParameter(CF.EARTH_RADIUS, earth_radius * 1000);
  }

  /**
   * Precalculate some stuff
   */
  private void precalculate() {
    if (Math.abs(lat0) > PI_OVER_2) {
      throw new IllegalArgumentException("LambertConformal lat0 outside range (-90,90)");
    }
    if (Math.abs(par1) >= 90.0) {
      throw new IllegalArgumentException("LambertConformal abs(par1) >= 90");
    }
    if (Math.abs(par2) >= 90.0) {
      throw new IllegalArgumentException("LambertConformal abs(par2) >= 90");
    }

    if (Math.abs(par1 - 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par1 = 90");
    }
    if (Math.abs(par1 + 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par1 = -90");
    }
    if (Math.abs(par2 - 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par2 = 90");
    }
    if (Math.abs(par2 + 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par2 = -90");
    }

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

    double t1 = Math.tan(Math.PI / 4 + par1r / 2);
    double t2 = Math.tan(Math.PI / 4 + par2r / 2);

    if (Math.abs(par2 - par1) < TOLERANCE) {  // single parallel
      n = Math.sin(par1r);
    } else {
      n = Math.log(Math.cos(par1r) / Math.cos(par2r))
              / Math.log(t2 / t1);
    }

    double t1n = Math.pow(t1, n);
    F = Math.cos(par1r) * t1n / n;
    earthRadiusTimesF = earth_radius * F;

    double t0n = Math.pow(Math.tan(Math.PI / 4 + lat0 / 2), n);
    rho = earthRadiusTimesF / t0n;

    lon0Degrees = Math.toDegrees(lon0);
    // need to know the pole value for crossSeam
    //Point2D pt = latLonToProj( 90.0, 0.0);
    //maxY = pt.getY();
    //System.out.println("LC = " +pt);
  }

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

    LambertConformal that = (LambertConformal) 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 = earth_radius != +0.0d ? Double.doubleToLongBits(earth_radius) : 0L;
    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));
    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));
    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 in degrees
   *
   * @return the origin longitude.
   */
  public double getOriginLon() {
    return Math.toDegrees(lon0);
  }

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

  /**
   * 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;
  }

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

  public void setOriginLat(double lat0) {
    this.lat0 = Math.toRadians(lat0);
    precalculate();
  }

  public void setOriginLon(double lon0) {
    this.lon0 = Math.toRadians(lon0);
    precalculate();
  }

  // sic
  public void setParellelOne(double par1) {
    this.par1 = par1;
    precalculate();
  }

  // sic
  public void setParellelTwo(double par2) {
    this.par2 = par2;
    precalculate();
  }

  public void setParallelOne(double par1) {
    this.par1 = par1;
    precalculate();
  }

  public void setParallelTwo(double par2) {
    this.par2 = par2;
    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 label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "Lambert conformal conic";
  }

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

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

  /**
   * Create a WKS string
   *
   * @return WKS string
   */
  public String toWKS() {
    StringBuilder sbuff = new StringBuilder();
    sbuff.append("PROJCS[\"").append(getName()).append("\",");
    if (true) {
      sbuff.append("GEOGCS[\"Normal Sphere (r=6371007)\",");
      sbuff.append("DATUM[\"unknown\",");
      sbuff.append("SPHEROID[\"sphere\",6371007,0]],");
    } else {
      sbuff.append("GEOGCS[\"WGS 84\",");
      sbuff.append("DATUM[\"WGS_1984\",");
      sbuff.append("SPHEROID[\"WGS 84\",6378137,298.257223563],");
      sbuff.append("TOWGS84[0,0,0,0,0,0,0]],");
    }
    sbuff.append("PRIMEM[\"Greenwich\",0],");
    sbuff.append("UNIT[\"degree\",0.0174532925199433]],");
    sbuff.append("PROJECTION[\"Lambert_Conformal_Conic_1SP\"],");
    sbuff.append("PARAMETER[\"latitude_of_origin\",").append(getOriginLat()).append("],");  // LOOK assumes getOriginLat = getParellel
    sbuff.append("PARAMETER[\"central_meridian\",").append(getOriginLon()).append("],");
    sbuff.append("PARAMETER[\"scale_factor\",1],");
    sbuff.append("PARAMETER[\"false_easting\",").append(falseEasting).append("],");
    sbuff.append("PARAMETER[\"false_northing\",").append(falseNorthing).append("],");

    return sbuff.toString();
  }

  /**
   * Get the scale at the given lat.
   *
   * @param lat lat to use
   * @return scale factor at this latitude
   */
  public double getScale(double lat) {
    lat = Math.toRadians(lat);
    double t = Math.tan(Math.PI / 4 + lat / 2);
    double tn = Math.pow(t, n);
    double r1 = n * F;
    double r2 = Math.cos(lat) * tn;
    return r1 / r2;
  }

  /**
   * 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 20,000 km
    return (pt1.getX() * pt2.getX() < 0)
            && (Math.abs(pt1.getX() - pt2.getX()) > 20000.0);
  }

  /*MACROBODY
    latLonToProj {} {
      fromLat = Math.toRadians(fromLat);
      double dlon = LatLonPointImpl.lonNormal(fromLon - lon0Degrees);
      double theta = n * Math.toRadians(dlon);
      double tn = Math.pow( Math.tan(PI_OVER_4 + fromLat/2), n);
      double r = earthRadiusTimesF / tn;
      toX = r * Math.sin(theta);
      toY = rho - r * Math.cos(theta);
    }
    projToLatLon {} {
      double rhop = rho;
      if (n < 0) {
          rhop *= -1.0;
          fromX *= -1.0;
          fromY *= -1.0;
      }

      double yd = (rhop - fromY);
      double theta = Math.atan2( fromX, yd);
      double r = Math.sqrt( fromX*fromX + yd*yd);
      if (n < 0.0)
          r *= -1.0;

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

      if (Math.abs(r) < TOLERANCE) {
          toLat = ((n < 0.0) ? -90.0 : 90.0);
      } else {
          double rn = Math.pow( EARTH_RADIUS * F / r, 1/n);
          toLat = Math.toDegrees(2.0 * Math.atan( rn) - Math.PI/2);
      }
          }

  MACROBODY*/

  /*BEGINGENERATED*/

  /*
  Note this section has been generated using the convert.tcl script.
  This script, run as:
  tcl convert.tcl LambertConformal.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);
    double dlon = LatLonPointImpl.lonNormal(fromLon - lon0Degrees);
    double theta = n * Math.toRadians(dlon);
    double tn = Math.pow(Math.tan(PI_OVER_4 + fromLat / 2), n);
    double r = earthRadiusTimesF / tn;
    toX = r * Math.sin(theta);
    toY = rho - r * Math.cos(theta);

    result.setLocation(toX + falseEasting, toY + falseNorthing);
    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 rhop = rho;

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

    double yd = (rhop - fromY);
    double theta = Math.atan2(fromX, yd);
    double r = Math.sqrt(fromX * fromX + yd * yd);
    if (n < 0.0) {
      r *= -1.0;
    }

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

    if (Math.abs(r) < TOLERANCE) {
      toLat = ((n < 0.0)
              ? -90.0
              : 90.0);
    } else {
      double rn = Math.pow(earth_radius * F / r, 1 / n);
      toLat = Math.toDegrees(2.0 * Math.atan(rn) - Math.PI / 2);
    }

    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[latIndex][i], from[lonIndex][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
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @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 dlon = LatLonPointImpl.lonNormal(fromLon - lon0Degrees);
      double theta = n * Math.toRadians(dlon);
      double tn = Math.pow(Math.tan(PI_OVER_4 + fromLat / 2), n);
      double r = earthRadiusTimesF / tn;
      toX = r * Math.sin(theta);
      toY = rho - r * Math.cos(theta);

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

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) 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] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;
      double rhop = rho;

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

      double yd = (rhop - fromY);
      double theta = Math.atan2(fromX, yd);
      double r = Math.sqrt(fromX * fromX + yd * yd);
      if (n < 0.0) {
        r *= -1.0;
      }

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

      if (Math.abs(r) < TOLERANCE) {
        toLat = ((n < 0.0)
                ? -90.0
                : 90.0);
      } else {
        double rn = Math.pow(earth_radius * F / r, 1 / n);
        toLat = Math.toDegrees(2.0 * Math.atan(rn) - Math.PI / 2);
      }

      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[latIndex][i], from[lonIndex][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
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @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 dlon = LatLonPointImpl.lonNormal(fromLon - lon0Degrees);
      double theta = n * Math.toRadians(dlon);
      double tn = Math.pow(Math.tan(PI_OVER_4 + fromLat / 2), n);
      double r = earthRadiusTimesF / tn;
      toX = r * Math.sin(theta);
      toY = rho - r * Math.cos(theta);

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

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) 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] - falseEasting;
      double fromY = fromYA[i] - falseNorthing;
      double rhop = rho;

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

      double yd = (rhop - fromY);
      double theta = Math.atan2(fromX, yd);
      double r = Math.sqrt(fromX * fromX + yd * yd);
      if (n < 0.0) {
        r *= -1.0;
      }

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

      if (Math.abs(r) < TOLERANCE) {
        toLat = ((n < 0.0)
                ? -90.0
                : 90.0);
      } else {
        double rn = Math.pow(earth_radius * F / r, 1 / n);
        toLat = Math.toDegrees(2.0 * Math.atan(rn) - Math.PI / 2);
      }

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

  /*ENDGENERATED*/

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information. */

package dap4.core.util;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

/**
 * Provide various methods for (un)escaping text
 */

public class Escape
{

    // define constants for consistency
    static final String ENTITY_AMP = "amp";
    static final String ENTITY_LT = "lt";
    static final String ENTITY_GT = "gt";
    static final String ENTITY_QUOT = "quot";
    static final String ENTITY_APOS = "apos";

    static public final String[][] DEFAULTTRANSTABLE = {
            {ENTITY_AMP, "&"},
            {ENTITY_LT, "<"},
            {ENTITY_GT, ">"},
            {ENTITY_QUOT, "\""},
            {ENTITY_APOS, "'"},
    };

    // For reference: set of all ascii printable non-alphanumeric characters
    static private final String nonAlphaNumeric = " !\"#$%&'()*+,-./:;<=>?@[]\\^_`|{}~";

    // define the printable backslash characters to escape (control chars not included)
    // Note that '\\' is always included
    static public final String BACKSLASHESCAPE = "/.";

    // define the default entity characters to escape
    static public final String ENTITYESCAPES = "\\<>&\"'";

    // Define the alphan characters

    /* Defind loose set of characters that can appear in an xml entity name */
    static public boolean entitychar(char c)
    {
        return ("#_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(c) >= 0);
    }

    /**
     * Escape selected characters in a string using XML entities
     *
     * @param s   string to escape
     * @param wrt which chars to escape
     * @return escaped string
     */
    static public String
    entityEscape(String s, String wrt)
    {
        if(wrt == null)
            wrt = ENTITYESCAPES;
        StringBuilder escaped = new StringBuilder();
        for(int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            int index = wrt.indexOf(c);
            if(index < 0)
                escaped.append(c);
            else switch (c) {
            case '&':
                escaped.append('&' + ENTITY_AMP + ';');
                break;
            case '<':
                escaped.append('&' + ENTITY_LT + ';');
                break;
            case '>':
                escaped.append('&' + ENTITY_GT + ';');
                break;
            case '"':
                escaped.append('&' + ENTITY_QUOT + ';');
                break;
            case '\'':
                escaped.append('&' + ENTITY_APOS + ';');
                break;
            case '\r':
            case '\t':
            case '\n':
                escaped.append(c);  // These are the only legal control chars
                break;
            case '\0':
                // What to do about nul? currrently we suppress it
                break;
            default:
                if(c >= ' ')
                    escaped.append(c);
                break;
            }
        }
        return escaped.toString();
    }

    static public String
    entityUnescape(String s)
    {
        return entityUnescape(s, null);
    }

    static public String
    entityUnescape(String s, String[][] translations)
    {
        int count, len;
        boolean found;
        StringBuilder u; // returned string with entities unescaped
        int p; // insertion point into u
        int q; // next char from s
        int stop;
        StringBuilder entity;

        if(translations == null)
            translations = DEFAULTTRANSTABLE;

        if(s == null) len = 0;
        else len = s.length();
        u = new StringBuilder();
        p = 0;
        q = 0;
        stop = (len);
        entity = new StringBuilder();

        while(q < stop) {
            char c = s.charAt(q++);
            switch (c) {
            case '&': // see if this is a legitimate entity
                entity.setLength(0);
                // move forward looking for a semicolon;
                for(found = true, count = 0; ; count++) {
                    if(q + count >= len) break;
                    c = s.charAt(q + count);
                    if(c == ';')
                        break;
                    if(!entitychar(c)) {
                        found = false; // not a legitimate entity
                        break;
                    }
                    entity.append(c);
                }
                if(q + count >= len || count == 0 || !found) {
                    // was not in correct form for entity
                    u.append('&');
                } else { // looks legitimate
                    String test = entity.toString();
                    String replacement = null;
                    for(String[] trans : translations) {
                        if(trans[0].equals(test)) {
                            replacement = trans[1];
                            break;
                        }
                    }
                    if(replacement == null) { // no translation, ignore
                        u.append('&');
                    } else { // found it
                        q += (count + 1); // skip input entity, including trailing semicolon
                        u.append(replacement);
                    }
                }
                break;
            default:
                u.append(c);
                break;
            }
        }
        return u.toString();
    }

    /**
     * Escape control chars plus
     * selected other characters in a string using backslash
     * The definitive list is in netcdf-c/ncgen/ncgen.l.
     *
     * @param s   the string to escape
     * @param wrt what printable characters to escape;
     *            control characters and '\\' are always escaped.
     * @return
     */
    static public String
    backslashEscape(String s, String wrt)
    {
        if(wrt == null)
            wrt = BACKSLASHESCAPE;
        StringBuilder escaped = new StringBuilder();
        for(int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if(c < ' ' || c == 127) {
                escaped.append('\\');
                switch (c) {
                case '\r':
                    c = 'r';
                    break;
                case '\n':
                    c = 'n';
                    break;
                case '\t':
                    c = 't';
                    break;
                case '\f':
                    c = 'f';
                    break;
                default:
                    escaped.append('x');
                    escaped.append(Escape.toHex((int) c));
                    continue; /* since this is a string */
                }
            } else if(c == '\\' || wrt.indexOf(c) >= 0)
                escaped.append('\\');
            escaped.append(c);
        }
        return escaped.toString();
    }

    /**
     * Remove backslashed characters in a string
     */
    static public String
    backslashUnescape(String s)
    {
        StringBuilder clear = new StringBuilder();
        for(int i = 0; i < s.length(); ) {
            char c = s.charAt(i++);
            if(c == '\\') {
                c = s.charAt(i++);
                switch (c) {
                case 'r':
                    c = '\r';
                    break;
                case 'n':
                    c = '\n';
                    break;
                case 't':
                    c = '\t';
                    break;
                case 'f':
                    c = '\f';
                    break;
                default:
                    break;
                }
                clear.append(c);
            } else
                clear.append(c);
        }
        return clear.toString();
    }

    /**
     * Split a string with respect to a separator
     * character and taking backslashes into consideration.
     *
     * @param s   The string to split
     * @param sep The character on which to split
     * @return a List of strings (all with escaping still intact)
     * representing s split at unescaped instances of sep.
     */
    static public List<String>
    backslashsplit(String s, char sep)
    {
        List<String> path = new ArrayList<String>();
        int len = s.length();
        StringBuilder piece = new StringBuilder();
        int i = 0;
        for(; i <= len - 1; i++) {
            char c = s.charAt(i);
            if(c == '\\' && i < (len - 1)) {
                piece.append(c); // keep escapes in place
                piece.append(s.charAt(++i));
            } else if(c == sep) {
                path.add(piece.toString());
                piece.setLength(0);
            } else
                piece.append(c);
        }
        path.add(piece.toString());
        return path;
    }

/*    static public String backslashEscape(String s, String wrt)
    {
        StringBuilder buf = new StringBuilder();
        for(int i = 0;i < s.length();i++) {
            char c = s.charAt(i);
            if(wrt.indexOf(c) >= 0)
                buf.append('\\');
            buf.append(c);
        }
        return buf.toString();
    }
    */

    static final public String hexchars = "0123456789abcdef";
    static final String allhexchars = "0123456789abcdefABCDEF";

    static public String toHex(int i)
    {
        char digit1 = hexchars.charAt((i >>> 4) & 0xf);
        char digit2 = hexchars.charAt((i & 0xf));
        return Character.toString(digit1) + digit2;
    }

    static public int fromHex(char c)
    {
        c = Character.toLowerCase(c);
        int index = hexchars.indexOf(c);
        if(index < 0)
            return -1;
        return index;
    }

    static public boolean isHexDigit(char c)
    {
        return allhexchars.indexOf(c) >= 0;
    }

    static public String
    bytes2hex(byte[] bytes)
    {
        int len = bytes.length;
        StringBuilder buf = new StringBuilder(2 + (len * 2));
        buf.append("0x");
        for(int i = 0; i < len; i++) {
            byte b = bytes[i];
            buf.append(hexchars.charAt((b >>> 4) & 0xF));
            buf.append(hexchars.charAt((i & 0xF)));
        }
        return buf.toString();
    }

    static public String urlDecode(String s)
    {
        try {
            s = URLDecoder.decode(s, "UTF8");
        } catch (UnsupportedEncodingException uee) {
        }
        return s;
    }

    // (Minimal?) Set of non-alphanum characters that need to be escaped in a query
    // before sending it to server
    static final String URLESCAPECHARS = " %";

    static public String urlEncodeQuery(String s)
    {
        if(s == null || s.length() == 0) return s;
        if(false) try {// Note that URLEncoder over encodes. For practical purposes,
            // only a limited set of characters needs to be encoded
            s = URLEncoder.encode(s, "UTF8");
        } catch (UnsupportedEncodingException uee) {
        }
        else {
            StringBuilder buf = new StringBuilder();
            for(int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                if(URLESCAPECHARS.indexOf(c) >= 0) {
                    buf.append("%");
                    String encode = Integer.toHexString((int) c);
                    if(encode.length() == 1) buf.append("0");
                    buf.append(encode);
                } else
                    buf.append(c);
            }
        }
        return s;
    }

    /**
     * Clean up a string: currently means:
     * 1. strip off everything after the first nul character
     *
     * @param s string to clean
     * @return cleaned string
     */
    static public String
    cleanString(String s)
    {
        int index = s.indexOf((char) 0);
        if(index >= 0)
            s = s.substring(0, index);
        return s;
    }

}//Escape


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

package thredds.server.cdmrfeature;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import thredds.core.AllowedServices;
import thredds.core.StandardService;
import thredds.core.TdsRequestedDataset;
import thredds.server.config.TdsContext;
import thredds.server.exception.ServiceNotAllowed;
import thredds.server.ncss.params.NcssGridParamsBean;
import thredds.servlet.ServletUtil;
import thredds.util.ContentType;
import thredds.util.TdsPathUtils;
import ucar.ma2.Array;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Range;
import ucar.ma2.Section;
import ucar.nc2.ft2.coverage.*;
import ucar.nc2.ft2.coverage.remote.CdmrFeatureProto;
import ucar.nc2.ft2.coverage.remote.CdmrfWriter;
import ucar.nc2.iosp.IospHelper;
import ucar.nc2.stream.NcStream;
import ucar.nc2.stream.NcStreamDataCol;
import ucar.nc2.stream.NcStreamProto;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.DeflaterOutputStream;

/**
 * Controller for CdmrFeature service.
 *
 * @author caron
 */
@Controller
@RequestMapping("/cdmrfeature/grid")
public class CdmrGridController {
  // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CdmrGridController.class);
  private static final boolean showReq = false;
  private static final boolean showRes = false;

  @Autowired
  TdsContext tdsContext;

  @Autowired
  private AllowedServices allowedServices;

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

  @RequestMapping(value = "/**",  method = RequestMethod.GET, params = "req=featureType")
  public ResponseEntity<String> handleFeatureTypeRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
    if (!allowedServices.isAllowed(StandardService.cdmrFeatureGrid))
      throw new ServiceNotAllowed(StandardService.cdmrFeatureGrid.toString());

    String datasetPath = TdsPathUtils.extractPath(request, StandardService.cdmrFeatureGrid.getBase());

    try (CoverageCollection cc = TdsRequestedDataset.getCoverageCollection(request, response, datasetPath)) {
      if (cc == null) return null;
        // return new ResponseEntity<>("", HttpStatus.NOT_FOUND);

      HttpHeaders responseHeaders = new HttpHeaders();
      responseHeaders.set(ContentType.HEADER, ContentType.text.getContentHeader());
      return new ResponseEntity<>(cc.getCoverageType().toString(), responseHeaders, HttpStatus.OK);

    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=header")
  public void handleHeaderRequest(HttpServletRequest request, HttpServletResponse response, OutputStream out) throws IOException {
    if (showReq)
      System.out.printf("CdmrGridController '%s?%s'%n", request.getRequestURI(), request.getQueryString());

    if (!allowedServices.isAllowed(StandardService.cdmrFeatureGrid))
      throw new ServiceNotAllowed(StandardService.cdmrFeatureGrid.toString());

    String datasetPath = TdsPathUtils.extractPath(request, StandardService.cdmrFeatureGrid.getBase());

    try (CoverageCollection gridCoverageDataset = TdsRequestedDataset.getCoverageCollection(request, response, datasetPath)) {
      if (gridCoverageDataset == null) return;

      response.setContentType(ContentType.binary.getContentHeader());
      response.setHeader("Content-Description", "ncstream");
      response.addDateHeader("Last-Modified", TdsRequestedDataset.getLastModified(datasetPath));

      CdmrfWriter writer = new CdmrfWriter();
      long size = writer.sendHeader(out, gridCoverageDataset, ServletUtil.getRequestBase(request));
      out.flush();

      if (showRes)
        System.out.printf(" CdmrGridController.getHeader sent, message size=%s%n", size);
    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=form")
  public ResponseEntity<String> handleFormRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
    if (showReq)
      System.out.printf("CdmrGridController '%s?%s'%n", request.getRequestURI(), request.getQueryString());

    if (!allowedServices.isAllowed(StandardService.cdmrFeatureGrid))
      throw new ServiceNotAllowed(StandardService.cdmrFeatureGrid.toString());

    String datasetPath = TdsPathUtils.extractPath(request, StandardService.cdmrFeatureGrid.getBase());
    HttpHeaders responseHeaders;

    try (CoverageCollection gridCoverageDataset = TdsRequestedDataset.getCoverageCollection(request, response, datasetPath)) {
      if (gridCoverageDataset == null) return null;

      String text = gridCoverageDataset.toString();

      if (showRes)
        System.out.printf(" CdmrGridController.getHeader sent, message size=%s%n", text.length());

      responseHeaders = new HttpHeaders();
      responseHeaders.set(ContentType.HEADER, ContentType.text.getContentHeader());
      responseHeaders.setDate("Last-Modified", TdsRequestedDataset.getLastModified(datasetPath));
      return new ResponseEntity<>(text, responseHeaders, HttpStatus.OK);

    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=coord")
  public void handleCoordRequest(HttpServletRequest request, HttpServletResponse response,
                                 @RequestParam String var,
                                 OutputStream out) throws IOException, BindException, InvalidRangeException {

    if (!allowedServices.isAllowed(StandardService.cdmrFeatureGrid))
      throw new ServiceNotAllowed(StandardService.cdmrFeatureGrid.toString());

    String datasetPath = TdsPathUtils.extractPath(request, StandardService.cdmrFeatureGrid.getBase());

    try (CoverageCollection gridCoverageDataset = TdsRequestedDataset.getCoverageCollection(request, response, datasetPath)) {
      if (gridCoverageDataset == null) return;

      response.setContentType(ContentType.binary.getContentHeader());
      response.setHeader("Content-Description", "ncstream");

      String[] coordNames = var.split(",");
      for (String coordName : coordNames) {
        CoverageCoordAxis coord = gridCoverageDataset.findCoordAxis(coordName);
        double[] values;
        if (!coord.isRegular())
          values = coord.getValues();
        else {
          values = new double[coord.getNcoords()];
          for (int i=0; i<values.length; i++)
            values[i] = coord.getStartValue() + i*coord.getResolution();
        }
        sendCoordData(coord.getName(), new Section(new Range(values.length)), Array.makeFromJavaArray(values), out);
      }
      out.flush();

    } catch (Throwable t) {
      throw new RuntimeException("CdmrGridController on dataset "+datasetPath, t);
    }
  }

  private long sendCoordData(String name, Section section, Array data, OutputStream out) throws IOException, InvalidRangeException {
    NcStreamDataCol encoder = new NcStreamDataCol();
    NcStreamProto.DataCol dataProto = encoder.encodeData2(name, false, section, data);

    long size = 0;
    size += writeBytes(out, NcStream.MAGIC_DATA2); // data version 3

    byte[] datab = dataProto.toByteArray();
    size += NcStream.writeVInt(out, datab.length); // dataProto len
    size += writeBytes(out, datab); // dataProto
    return size;
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=data")
  public void handleDataRequest(HttpServletRequest request, HttpServletResponse response,
                                @Valid NcssGridParamsBean qb, BindingResult validationResult, OutputStream out) throws IOException, BindException, InvalidRangeException {

    if (showReq)
      System.out.printf("CdmrGridController '%s?%s'%n", request.getRequestURI(), request.getQueryString());
    long start = System.currentTimeMillis();

    if (!allowedServices.isAllowed(StandardService.cdmrFeatureGrid))
      throw new ServiceNotAllowed(StandardService.cdmrFeatureGrid.toString());

    if (validationResult.hasErrors())
      throw new BindException(validationResult);

    String datasetPath = TdsPathUtils.extractPath(request, StandardService.cdmrFeatureGrid.getBase());

    try (CoverageCollection gridCoverageDataset = TdsRequestedDataset.getCoverageCollection(request, response, datasetPath)) {
      if (gridCoverageDataset == null) return;

      response.setContentType(ContentType.binary.getContentHeader());
      response.setHeader("Content-Description", "ncstream");

      // construct the subsetted dataset
      SubsetParams params = qb.makeSubset(gridCoverageDataset);
      List<GeoReferencedArray> arrays = new ArrayList<>();
      for (String gridWanted : qb.getVar()) {
        Coverage grid = gridCoverageDataset.findCoverage(gridWanted);
        GeoReferencedArray array = grid.readData(params);
        arrays.add(array);
      }
      sendDataResponse(arrays, out, false); // LOOK deflate ??
      out.flush();

    } catch (Throwable t) {
      throw new RuntimeException("CdmrGridController on dataset "+datasetPath, t);
    }

    if (showReq)
       System.out.printf(" that took %d msecs%n", System.currentTimeMillis() - start);
  }

  private long sendDataResponse(List<GeoReferencedArray> arrays, OutputStream out, boolean deflate) throws IOException, InvalidRangeException {

    // turns List into a Set
    Set<CoverageCoordSys> sysSet = arrays.stream().map(GeoReferencedArray::getCoordSysForData).collect(Collectors.toSet());

    Set<CoverageTransform> transformSet = new HashSet<>();
    Set<CoverageCoordAxis> axisSet = new HashSet<>();
    for (CoverageCoordSys sys : sysSet) {
      axisSet.addAll(sys.getAxes().stream().collect(Collectors.toList()));
      transformSet.addAll(sys.getTransforms().stream().collect(Collectors.toList()));
    }

    CdmrfWriter cdmrfWriter = new CdmrfWriter();
    long size = 0;
    size += writeBytes(out, NcStream.MAGIC_DATACOV);
    CdmrFeatureProto.CoverageDataResponse dataProto = cdmrfWriter.encodeDataResponse(axisSet, sysSet, transformSet, arrays, deflate);
    byte[] datab = dataProto.toByteArray();
    size += NcStream.writeVInt(out, datab.length); // dataProto len
    size += writeBytes(out, datab); // dataProto

    /* float ratio = ((float) uncompressedLength) / deflatedSize;
    if (showRes)
      System.out.printf("  org/compress= %d/%d = %f%n", uncompressedLength, deflatedSize, ratio); */

    /*
    size += NcStream.writeVInt(out, arrays.size()); // lenn

    for (GeoReferencedArray array : arrays) {
      long dataMessageLen = sendData(array.getData(), out, deflate);
      if (showRes) System.out.printf(" CdmrGridController.sendData grid='%s' data message len=%d%n", array.getCoverageName(), dataMessageLen);
      size += dataMessageLen;
    } */

    return size;
  }

  private long sendData(Array data, OutputStream out, boolean deflate) throws IOException, InvalidRangeException {

    // length of data uncompressed
    long uncompressedLength = data.getSizeBytes();
    long size = 0;

    if (deflate) {
      // write to an internal buffer, so we can find out the size
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      DeflaterOutputStream dout = new DeflaterOutputStream(bout);
      IospHelper.copyToOutputStream(data, dout);

      // write internal buffer to output stream
      dout.close();
      int deflatedSize = bout.size();
      size += NcStream.writeVInt(out, deflatedSize);
      bout.writeTo(out);
      size += deflatedSize;
      float ratio = ((float) uncompressedLength) / deflatedSize;
      if (showRes)
        System.out.printf("  org/compress= %d/%d = %f%n", uncompressedLength, deflatedSize, ratio);

    } else {
      size += NcStream.writeVInt(out, (int) uncompressedLength);
      size += IospHelper.copyToOutputStream(data, out);
    }

    return size;
  }

  private int writeBytes(OutputStream out, byte[] b) throws IOException {
    out.write(b);
    return b.length;
  }

}


/*
 * Copyright (c) 1998-2017 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE.txt for license information.
 */

package ucar.nc2.ft.fmrc;

import org.jdom2.Element;
import org.jdom2.Namespace;

import thredds.featurecollection.FeatureCollectionConfig;
import thredds.inventory.*;
import ucar.nc2.Attribute;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.grid.GridDataset;
import ucar.nc2.ncml.NcMLWriter;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;

import javax.annotation.concurrent.ThreadSafe;
import java.io.Closeable;
import java.io.IOException;
import java.util.*;

/**
 * Forecast Model Run Collection, manages dynamic collections of GridDatasets.
 * Fmrc represents a virtual dataset.
 * To instantiate, you obtain an FmrcInv "snapshot" from which you can call getDatatset().
 *

 * Assumes that we dont have multiple runtimes in the same file.
 * Can handle different time steps in different files.
 * Can handle different grids in different files. However this creates problems for the "typical dataset".
 * Cannot handle different ensembles in different files.  (LOOK fix)
 * Cannot handle different levels in different files. ok
 *
 * @author caron
 * @since Jan 11, 2010
 */
@ThreadSafe
public class Fmrc implements Closeable {
  static private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Fmrc.class);
  static private final Namespace ncNSHttps = thredds.client.catalog.Catalog.ncmlNSHttps;
  static private NcMLWriter ncmlWriter = new NcMLWriter();

  /**
   * Factory method
   *
   * @param collection describes the collection. May be one of:
   *

   *
collection specification string
   *
catalog:catalogURL
   *
filename.ncml
   *

   *

   *  collectionSpec date extraction is used to get rundates
   * @param errlog     place error messages here
   * @return Fmrc or null on error
   * @throws IOException on read error
   * @see "http://www.unidata.ucar.edu/software/netcdf-java/reference/collections/CollectionSpecification.html"
   */
  public static Fmrc open(String collection, Formatter errlog) throws IOException {
    if (collection.startsWith(MFileCollectionManager.CATALOG)) {
      CollectionManagerCatalog manager = new CollectionManagerCatalog(collection, collection, null, errlog);
      return new Fmrc(manager, new FeatureCollectionConfig());

    } else if (collection.endsWith(".ncml")) {
      NcmlCollectionReader ncmlCollection = NcmlCollectionReader.open(collection, errlog);
      if (ncmlCollection == null) return null;
      Fmrc fmrc = new Fmrc(ncmlCollection.getCollectionManager(), new FeatureCollectionConfig());
      fmrc.setNcml(ncmlCollection.getNcmlOuter(), ncmlCollection.getNcmlInner());
      return fmrc;
    }

    return new Fmrc(collection, errlog);
  }

  public static Fmrc readNcML(String ncmlString, Formatter errlog) throws IOException {
      NcmlCollectionReader ncmlCollection = NcmlCollectionReader.readNcML(ncmlString, errlog);
      if (ncmlCollection == null) return null;
      Fmrc fmrc = new Fmrc(ncmlCollection.getCollectionManager(), new FeatureCollectionConfig());
      fmrc.setNcml(ncmlCollection.getNcmlOuter(), ncmlCollection.getNcmlInner());
      return fmrc;
  }

  public static Fmrc open(FeatureCollectionConfig config, Formatter errlog) throws IOException {
    if (config.spec.startsWith(MFileCollectionManager.CATALOG)) {
      String name = config.collectionName != null ? config.collectionName : config.spec;
      CollectionManagerCatalog manager = new CollectionManagerCatalog(name, config.spec, null, errlog);
      return new Fmrc(manager, config);
    }

    return new Fmrc(config, errlog);
  }

  ////////////////////////////////////////////////////////////////////////
  private final MCollection manager;
  private final FeatureCollectionConfig config;

  // should be final
  // private Element ncmlOuter, ncmlInner;

  // the current state - changing must be thread safe
  private final Object lock = new Object();
  private FmrcDataset fmrcDataset;
  private volatile boolean forceProto = false;
  private volatile long lastInvChanged;
  private volatile long lastProtoChanged;

  private Fmrc(String collectionSpec, Formatter errlog) throws IOException {
    this.manager = MFileCollectionManager.open(collectionSpec, collectionSpec, null, errlog);  // LOOK no name
    this.config = new FeatureCollectionConfig();
    this.config.spec = collectionSpec;
  }

  private Fmrc(FeatureCollectionConfig config, Formatter errlog) {
    this.manager = new MFileCollectionManager(config, errlog, null);
    this.config = config;
  }

  // from AggregationFmrc
  public Fmrc(MCollection manager, FeatureCollectionConfig config) {
    this.manager = manager;
    this.config = config;
  }

  public void setNcml(Element outerNcml, Element innerNcml) {
    config.protoConfig.outerNcml = outerNcml;
    config.innerNcml = innerNcml;
  }

  public void close() {
    if (manager != null)
      manager.close();
  }

  // exposed for debugging

  public MCollection getManager() {
    return manager;
  }

  public FmrcInv getFmrcInv(Formatter debug) throws IOException {
    return makeFmrcInv( debug);
  }

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

  public CalendarDateRange getDateRangeForRun(CalendarDate run) {
    return fmrcDataset.getDateRangeForRun( run);
  }

  public CalendarDateRange getDateRangeForOffset(double offset) {
    return fmrcDataset.getDateRangeForOffset( offset);
  }

  public List<CalendarDate> getRunDates() throws IOException {
    checkNeeded( false); // ??
    return fmrcDataset.getRunDates();
  }

  public List<CalendarDate> getForecastDates() throws IOException {
    checkNeeded( false); // ??
    return fmrcDataset.getForecastDates();
  }

  // for making offset datasets
  public double[] getForecastOffsets() throws IOException {
    checkNeeded( false); // ??
    return fmrcDataset.getForecastOffsets();
  }

  public GridDataset getDataset2D(NetcdfDataset result) throws IOException {
    checkNeeded( false);
    return fmrcDataset.getNetcdfDataset2D(result);
  }

  public GridDataset getDatasetBest() throws IOException {
    checkNeeded( false);
    return fmrcDataset.getBest();
  }

  public GridDataset getDatasetBest(FeatureCollectionConfig.BestDataset bd) throws IOException {
    checkNeeded( false);
    return fmrcDataset.getBest(bd);
  }

  public GridDataset getRunTimeDataset(CalendarDate run) throws IOException {
    checkNeeded( false);
    return fmrcDataset.getRunTimeDataset(run);
  }

  public GridDataset getConstantForecastDataset(CalendarDate time) throws IOException {
    checkNeeded( false);
    return fmrcDataset.getConstantForecastDataset(time);
  }

  public GridDataset getConstantOffsetDataset(double hour) throws IOException {
    checkNeeded( false);
    return fmrcDataset.getConstantOffsetDataset(hour);
  }

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

  public void updateProto() {
    forceProto = true;
  }

  public void update() {
     synchronized (lock) {
      boolean forceProtoLocal = forceProto;

      if (fmrcDataset == null) {
        try {
          fmrcDataset = new FmrcDataset(config);
        } catch (Throwable t) {
          logger.error(config.name+": initial fmrcDataset creation failed", t);
          //throw new RuntimeException(t);
        }
      }

      try {
        FmrcInv fmrcInv = makeFmrcInv(null);
        fmrcDataset.setInventory(fmrcInv, forceProtoLocal);
        logger.debug("{}: make new Dataset, new proto = {}", config.name, forceProtoLocal);
        if (forceProtoLocal) forceProto = false;
        this.lastInvChanged = System.currentTimeMillis();
        if (forceProtoLocal) this.lastProtoChanged = this.lastInvChanged;

      } catch (Throwable t) {
        logger.error(config.name+": makeFmrcInv failed", t);
        //throw new RuntimeException(t);
      }
    }

  }

  // true if things have changed since given time
  public boolean checkInvState(long lastInvChange) throws IOException {
    return this.lastInvChanged > lastInvChange;
  }
  // true if things have changed since given time
  public boolean checkProtoState(long lastProtoChanged) throws IOException {
    return this.lastProtoChanged > lastProtoChanged;
  }

  private void checkNeeded(boolean force) {
    if (fmrcDataset == null) {
      try {
        update();
      } catch (Throwable t) {
        logger.error(config.name+": rescan failed");
        throw new RuntimeException(t);
      }
    }
  }

  // scan has been done, create FmrcInv
  private FmrcInv makeFmrcInv(Formatter debug) throws IOException {
    try {
      Map<CalendarDate, FmrInv> fmrMap = new HashMap<>(); // all files are grouped by run date in an FmrInv
      List<FmrInv> fmrList = new ArrayList<>(); // an fmrc is a collection of fmr

      // get the inventory, sorted by path
      for (MFile f : manager.getFilesSorted()) {
        Map<String, String> filesRunDateMap = ((MFileCollectionManager) manager).getFilesRunDateMap();
        CalendarDate runDate;

        if (!filesRunDateMap.isEmpty()) {
          // run time has been defined in NcML FMRC agg by the coord attribute,
          // so explicitly set it in the dataset using the _Coordinate.ModelBaseDate
          // global attribute, otherwise the run time offsets might be incorrectly
          // computed if the incorrect run date is found in GridDatasetInv.java (line
          // 177 with comment // Look: not really right )
          runDate = CalendarDate.parseISOformat(null, filesRunDateMap.get(f.getPath()));
          Element element = new Element("netcdf", ncNSHttps);
          Element runDateAttr = ncmlWriter.makeAttributeElement(new Attribute(_Coordinate.ModelRunDate, runDate.toString()));
          config.innerNcml = element.addContent(runDateAttr);
        }

        GridDatasetInv inv;
        try {
          inv = GridDatasetInv.open(manager, f, config.innerNcml); // inventory is discovered for each GDS
        } catch (IOException ioe) {
          logger.warn("Error opening " + f.getPath() + "(skipped)", ioe);
          continue; // skip
        }

        runDate = inv.getRunDate();
        if (debug != null) debug.format("  opened %s rundate = %s%n", f.getPath(), inv.getRunDateString());

        // add to fmr for that rundate
        FmrInv fmr = fmrMap.get(runDate);
        if (fmr == null) {
          fmr = new FmrInv(runDate);
          fmrMap.put(runDate, fmr);
          fmrList.add(fmr);
        }
        fmr.addDataset(inv, debug);
      }
      if (debug != null) debug.format("%n");

      // finish the FmrInv
      Collections.sort(fmrList);
      for (FmrInv fmr : fmrList) {
        fmr.finish();
        if (logger.isDebugEnabled())
          logger.debug("Fmrc:"+config.name+": made fmr with rundate="+fmr.getRunDate()+" nfiles= "+fmr.getFiles().size());
      }

      return new FmrcInv("fmrc:"+manager.getCollectionName(), fmrList, config.fmrcConfig.regularize);

    } catch (Throwable t) {
      logger.error("makeFmrcInv", t);
      throw new RuntimeException(t);
    }
  }

  public void showDetails(Formatter out) throws IOException {
    checkNeeded(false);
    fmrcDataset.showDetails(out);
  }
}


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

package thredds.logs;

import ucar.unidata.util.StringUtil2;

/**
 * Class Description.
 *
 * @author caron
 * @since Mar 23, 2009
 */
public class LogCategorizer {
  static boolean showRoots = false;

  /* default roots
  static private String roots =
          "terminal/level3/IDD,/data/ldm/pub/native/radar/level3/\n" +
                  "terminal/level3,/data/ldm/pub/native/radar/level3/\n" +
                  "station/soundings,/data/ldm/pub/native/profiler/bufr/profiler3/\n" +
                  "station/profiler/wind/1hr,/data/ldm/pub/native/profiler/wind/01hr/\n" +
                  "station/profiler/wind/06min,/data/ldm/pub/native/profiler/wind/06min/\n" +
                  "station/profiler/RASS/1hr,/data/ldm/pub/native/profiler/RASS/01hr/\n" +
                  "station/profiler/RASS/06min,/data/ldm/pub/native/profiler/RASS/06min/\n" +
                  "station/metar,/data/ldm/pub/decoded/netcdf/surface/metar/\n" +
                  "satellite/WV,/data/ldm/pub/native/satellite/WV/\n" +
                  "satellite/VIS,/data/ldm/pub/native/satellite/VIS/\n" +
                  "satellite/SOUND-VIS,/data/ldm/pub/native/satellite/SOUND-VIS/\n" +
                  "satellite/SOUND-7.43,/data/ldm/pub/native/satellite/SOUND-7.43/\n" +
                  "satellite/SOUND-7.02,/data/ldm/pub/native/satellite/SOUND-7.02/\n" +
                  "satellite/SOUND-6.51,/data/ldm/pub/native/satellite/SOUND-6.51/\n" +
                  "satellite/SOUND-4.45,/data/ldm/pub/native/satellite/SOUND-4.45/\n" +
                  "satellite/SOUND-3.98,/data/ldm/pub/native/satellite/SOUND-3.98/\n" +
                  "satellite/SOUND-14.06,/data/ldm/pub/native/satellite/SOUND-14.06/\n" +
                  "satellite/SOUND-11.03,/data/ldm/pub/native/satellite/SOUND-11.03/\n" +
                  "satellite/SFC-T/SUPER-NATIONAL_1km,/data/ldm/pub/native/satellite/SFC-T/SUPER-NATIONAL_1km/\n" +
                  "satellite/PW,/data/ldm/pub/native/satellite/PW/\n" +
                  "satellite/LI,/data/ldm/pub/native/satellite/LI/\n" +
                  "satellite/IR,/data/ldm/pub/native/satellite/IR/\n" +
                  "satellite/CTP,/data/ldm/pub/native/satellite/CTP/\n" +
                  "satellite/3.9/WEST-CONUS_4km,/data/ldm/pub/native/satellite/3.9/WEST-CONUS_4km/\n" +
                  "satellite/3.9/PR-REGIONAL_4km,/data/ldm/pub/native/satellite/3.9/PR-REGIONAL_4km/\n" +
                  "satellite/3.9/HI-REGIONAL_4km,/data/ldm/pub/native/satellite/3.9/HI-REGIONAL_4km/\n" +
                  "satellite/3.9/EAST-CONUS_4km,/data/ldm/pub/native/satellite/3.9/EAST-CONUS_4km/\n" +
                  "satellite/3.9/AK-REGIONAL_8km,/data/ldm/pub/native/satellite/3.9/AK-REGIONAL_8km/\n" +
                  "satellite/13.3,/data/ldm/pub/native/satellite/13.3/\n" +
                  "satellite/12.0,/data/ldm/pub/native/satellite/12.0/\n" +
                  "restrict,/opt/tds-test/content/thredds/public/\n" +
                  "nexrad/level3/IDD,/data/ldm/pub/native/radar/level3/\n" +
                  "nexrad/level3/CCS039,/data/ldm/pub/casestudies/ccs039/images/radar/nids\n" +
                  "nexrad/level3,/data/ldm/pub/native/radar/level3/\n" +
                  "nexrad/level2/IDD,/data/ldm/pub/native/radar/level2\n" +
                  "nexrad/level2/CCS039,/data/ldm/pub/casestudies/ccs039/images/radar/level2\n" +
                  "nexrad/level2,/data/ldm/pub/native/radar/level2/\n" +
                  "nexrad/composite/nws,/data/ldm/pub/native/radar/10km_mosaic/\n" +
                  "nexrad/composite/gini,/data/ldm/pub/native/radar/composite/gini/\n" +
                  "nexrad/composite/1km/files,/data/ldm/pub/native/radar/composite/grib2/\n" +
                  "modelsNc/NCEP/SST/Global_5x2p5deg,/data/ldm/pub/decoded/netcdf/grid/NCEP/SST/Global_5x2p5deg/\n" +
                  "modelsNc/NCEP/SST/Global_2x2deg,/data/ldm/pub/decoded/netcdf/grid/NCEP/SST/Global_2x2deg/\n" +
                  "modelsNc/NCEP/RUC2/CONUS_40km,/data/ldm/pub/decoded/netcdf/grid/NCEP/RUC2/CONUS_40km/\n" +
                  "modelsNc/NCEP/RUC/CONUS_80km,/data/ldm/pub/decoded/netcdf/grid/NCEP/RUC/CONUS_80km/\n" +
                  "modelsNc/NCEP/OCEAN/Global_5x2p5deg,/data/ldm/pub/decoded/netcdf/grid/NCEP/OCEAN/Global_5x2p5deg/\n" +
                  "modelsNc/NCEP/NAM/CONUS_80km,/data/ldm/pub/decoded/netcdf/grid/NCEP/NAM/CONUS_80km/\n" +
                  "modelsNc/NCEP/GFS/Global_5x2p5deg,/data/ldm/pub/decoded/netcdf/grid/NCEP/GFS/Global_5x2p5deg/\n" +
                  "modelsNc/NCEP/GFS/Extended_Global_5p0deg,/data/ldm/pub/decoded/netcdf/grid/NCEP/GFS/Extended_Global_5p0deg/\n" +
                  "modelsNc/NCEP/GFS/CONUS_80km,/data/ldm/pub/decoded/netcdf/grid/NCEP/GFS/CONUS_80km/\n" +
                  "model/NCEP/RUC2/CONUS_20km/surface,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/surface/\n" +
                  "model/NCEP/RUC2/CONUS_20km/pressure,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/pressure/\n" +
                  "model/NCEP/RUC2/CONUS_20km/hybrid,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/hybrid/\n" +
                  "model/NCEP/RUC/CONUS_80km,/data/ldm/pub/native/grid/NCEP/RUC/CONUS_80km/\n" +
                  "model/NCEP/NDFD/CONUS_5km,/data/ldm/pub/native/grid/NCEP/NDFD/CONUS_5km/\n" +
                  "model/NCEP/NAM/Polar_90km,/data/ldm/pub/native/grid/NCEP/NAM/Polar_90km/\n" +
                  "model/NCEP/NAM/CONUS_80km,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_80km/\n" +
                  "model/NCEP/NAM/CONUS_40km/conduit,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_40km/conduit/\n" +
                  "model/NCEP/NAM/CONUS_20km/surface,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/surface/\n" +
                  "model/NCEP/NAM/CONUS_20km/selectsurface,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/selectsurface/\n" +
                  "model/NCEP/NAM/CONUS_20km/noaaport,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/noaaport/\n" +
                  "model/NCEP/NAM/CONUS_12km,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_12km/\n" +
                  "model/NCEP/NAM/Alaska_95km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_95km/\n" +
                  "model/NCEP/NAM/Alaska_45km/noaaport,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_45km/noaaport/\n" +
                  "model/NCEP/NAM/Alaska_45km/conduit,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_45km/conduit/\n" +
                  "model/NCEP/NAM/Alaska_22km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_22km/\n" +
                  "model/NCEP/NAM/Alaska_11km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_11km/\n" +
                  "model/NCEP/GFS/Puerto_Rico_191km,/data/ldm/pub/native/grid/NCEP/GFS/Puerto_Rico_191km/\n" +
                  "model/NCEP/GFS/N_Hemisphere_381km,/data/ldm/pub/native/grid/NCEP/GFS/N_Hemisphere_381km/\n" +
                  "model/NCEP/GFS/Hawaii_160km,/data/ldm/pub/native/grid/NCEP/GFS/Hawaii_160km/\n" +
                  "model/NCEP/GFS/Global_onedeg,/data/ldm/pub/native/grid/NCEP/GFS/Global_onedeg/\n" +
                  "model/NCEP/GFS/Global_2p5deg,/data/ldm/pub/native/grid/NCEP/GFS/Global_2p5deg/\n" +
                  "model/NCEP/GFS/Global_0p5deg,/data/ldm/pub/native/grid/NCEP/GFS/Global_0p5deg/\n" +
                  "model/NCEP/GFS/CONUS_95km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_95km/\n" +
                  "model/NCEP/GFS/CONUS_80km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_80km/\n" +
                  "model/NCEP/GFS/CONUS_191km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_191km/\n" +
                  "model/NCEP/GFS/Alaska_191km,/data/ldm/pub/native/grid/NCEP/GFS/Alaska_191km/\n" +
                  "model/NCEP/DGEX/CONUS_12km,/data/ldm/pub/native/grid/NCEP/DGEX/CONUS_12km/\n" +
                  "model/NCEP/DGEX/Alaska_12km,/data/ldm/pub/native/grid/NCEP/DGEX/Alaska_12km/\n" +
                  "gis/test,/data/thredds/gis/\n" +
                  "galeon/testdata,/data/thredds/galeon/\n" +
                  "galeon/ndfd/testdata,/data/ldm/pub/native/grid/NCEP/NDFD/CONUS_5km/\n" +
                  "galeon/global/testdata,/data/ldm/pub/native/grid/NCEP/GFS/Global_0p5deg/\n" +
                  "fmrc/Unidata/rtmodel/nmm/files,/data/ldm/pub/rtmodel/\n" +
                  "fmrc/Unidata/rtmodel/nmm-alt/files,/data/ldm/pub/rtmodel/\n" +
                  "fmrc/NCEP/SREF/CONUS_40km/pgrb_biasc,/data/ldm/pub/native/grid/NCEP/SREF/CONUS_40km/pgrb_biasc/\n" +
                  "fmrc/NCEP/SREF/CONUS_40km/ensprod_biasc,/data/ldm/pub/native/grid/NCEP/SREF/CONUS_40km/ensprod_biasc/\n" +
                  "fmrc/NCEP/RUC2/CONUS_40km,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_40km/\n" +
                  "fmrc/NCEP/RUC2/CONUS_20km/surface,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/surface/\n" +
                  "fmrc/NCEP/RUC2/CONUS_20km/pressure,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/pressure/\n" +
                  "fmrc/NCEP/RUC2/CONUS_20km/hybrid,/data/ldm/pub/native/grid/NCEP/RUC2/CONUS_20km/hybrid/\n" +
                  "fmrc/NCEP/RUC/CONUS_80km,/data/ldm/pub/native/grid/NCEP/RUC/CONUS_80km/\n" +
                  "fmrc/NCEP/NEWGBXNDFD/CONUS_5km,/data/ldm/pub/native/grid/NCEP/NEWGBXNDFD/CONUS_5km/\n" +
                  "fmrc/NCEP/NDFD/CONUS_5km,/data/ldm/pub/native/grid/NCEP/NDFD/CONUS_5km/\n" +
                  "fmrc/NCEP/NAM/Polar_90km,/data/ldm/pub/native/grid/NCEP/NAM/Polar_90km/\n" +
                  "fmrc/NCEP/NAM/CONUS_80km,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_80km/\n" +
                  "fmrc/NCEP/NAM/CONUS_40km/conduit,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_40km/conduit/\n" +
                  "fmrc/NCEP/NAM/CONUS_20km/surface,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/surface/\n" +
                  "fmrc/NCEP/NAM/CONUS_20km/selectsurface,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/selectsurface/\n" +
                  "fmrc/NCEP/NAM/CONUS_20km/noaaport,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_20km/noaaport/\n" +
                  "fmrc/NCEP/NAM/CONUS_12km/conduit,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_12km_conduit/\n" +
                  "fmrc/NCEP/NAM/CONUS_12km,/data/ldm/pub/native/grid/NCEP/NAM/CONUS_12km/\n" +
                  "fmrc/NCEP/NAM/Alaska_95km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_95km/\n" +
                  "fmrc/NCEP/NAM/Alaska_45km/noaaport,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_45km/noaaport/\n" +
                  "fmrc/NCEP/NAM/Alaska_45km/conduit,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_45km/conduit/\n" +
                  "fmrc/NCEP/NAM/Alaska_22km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_22km/\n" +
                  "fmrc/NCEP/NAM/Alaska_11km,/data/ldm/pub/native/grid/NCEP/NAM/Alaska_11km/\n" +
                  "fmrc/NCEP/GFS/Puerto_Rico_191km,/data/ldm/pub/native/grid/NCEP/GFS/Puerto_Rico_191km/\n" +
                  "fmrc/NCEP/GFS/N_Hemisphere_381km,/data/ldm/pub/native/grid/NCEP/GFS/N_Hemisphere_381km/\n" +
                  "fmrc/NCEP/GFS/Hawaii_160km,/data/ldm/pub/native/grid/NCEP/GFS/Hawaii_160km/\n" +
                  "fmrc/NCEP/GFS/Global_onedeg,/data/ldm/pub/native/grid/NCEP/GFS/Global_onedeg/\n" +
                  "fmrc/NCEP/GFS/Global_2p5deg,/data/ldm/pub/native/grid/NCEP/GFS/Global_2p5deg/\n" +
                  "fmrc/NCEP/GFS/Global_1p0deg_Ensemble,/data/ldm/pub/native/grid/NCEP/GFS/Global_1p0deg_Ensemble/\n" +
                  "fmrc/NCEP/GFS/Global_0p5deg,/data/ldm/pub/native/grid/NCEP/GFS/Global_0p5deg/\n" +
                  "fmrc/NCEP/GFS/CONUS_95km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_95km/\n" +
                  "fmrc/NCEP/GFS/CONUS_80km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_80km/\n" +
                  "fmrc/NCEP/GFS/CONUS_191km,/data/ldm/pub/native/grid/NCEP/GFS/CONUS_191km/\n" +
                  "fmrc/NCEP/GFS/Alaska_191km,/data/ldm/pub/native/grid/NCEP/GFS/Alaska_191km/\n" +
                  "fmrc/NCEP/DGEX/CONUS_12km,/data/ldm/pub/native/grid/NCEP/DGEX/CONUS_12km/\n" +
                  "fmrc/NCEP/DGEX/Alaska_12km,/data/ldm/pub/native/grid/NCEP/DGEX/Alaska_12km/\n" +
                  "casestudies/vgee_demo,/data/ldm/pub/casestudies/vgee_demo\n" +
                  "casestudies/july18_2002/grids,/data/ldm/pub/casestudies/july18_2002/grids\n" +
                  "casestudies/idvtest/grids,/data/ldm/pub/casestudies/idvtest/grids\n" +
                  "casestudies/ccs039/grids,/data/ldm/pub/casestudies/ccs039/grids\n" +
                  "casestudies/ccs034/netcdf,/data/ldm/pub/casestudies/ccs034/netcdf\n" +
                  "casestudies/ccs034/grib,/data/ldm/pub/casestudies/ccs034/grib\n" +
                  "casestudies/ccs034/acars,/data/ldm/pub/casestudies/ccs034/acars\n" +
                  "GEMPAK/model,/data/ldm/pub/decoded/gempak/model";

  public static String[] getRoots() {
    String rootString = StringUtil2.replace(roots, "\n", ",");
    String[] root2 = rootString.split(",");
    String[] result = new String[root2.length/2];
    for (int i = 0; i < root2.length; i += 2) {
      result[i/2] = root2[i];
    }
    return result;
  } */

  private static String roots;

  public static void setRoots(String raw) {
    if (null != raw)
      roots = raw;
  }

  public static PathMatcher readRoots() {
    PathMatcher pathMatcher = new PathMatcher();
    String rootString = StringUtil2.replace(roots, "\n", ",");
    String[] roots = rootString.split(",");
    for (int i = 0; i < roots.length; i += 2) {
      if (showRoots) System.out.printf("  %-40s %-40s%n", roots[i], roots[i + 1]);
      pathMatcher.put(roots[i]);
    }

    return pathMatcher;
  }

  static private PathMatcher pathMatcher = null;

  static public String getDataroot(String path, int status) {
    if (pathMatcher == null)
      pathMatcher = readRoots();

    String ss =  getServiceSpecial( path);
    if (ss != null) return  "service-"+ss;

    if (path.startsWith("//thredds/"))
      path = path.substring(1);

    if (!path.startsWith("/thredds/"))
      return "zervice-root";

    String dataRoot = null;
    String spath = path.substring(9); // remove /thredds/
    String service = findService(spath);
    if (service != null) {
      //if (service.equals("radarServer")) dataRoot = "radarServer";
        //else if (service.equals("casestudies")) dataRoot = "casestudies";
     // else if (service.equals("dqc")) dataRoot = "dqc";
      if (spath.length() > service.length()) {
        spath = spath.substring(service.length() + 1);
        PathMatcher.Match match = pathMatcher.match(spath);
        if (match != null) dataRoot = match.root;
      }
    }

    if (dataRoot == null) {
      if (status >= 400)
        dataRoot = "zBad";
    }

    if (dataRoot == null) {
      service = getService(path);
      dataRoot = (service != null) ? "zervice-"+service : "unknown";
    }
    return dataRoot;
  }

  // the ones that dont start with thredds
  static public String getServiceSpecial(String path) {
    String ss = null;
    if (path.startsWith("/dqcServlet"))
      ss = "dqcServlet";
    else if (path.startsWith("/cdmvalidator"))
      ss = "cdmvalidator";
    return ss;
  }

  public static String getService(String path) {
    String service =  getServiceSpecial( path);
    if (service != null) return service;

    if (path.startsWith("/dts"))
      return "dts";

    if (path.startsWith("/dqcServlet"))
      return "dqcServlet";

    if (path.startsWith("/thredds/")) {
      String spath = path.substring(9);
      service = findService(spath);
      if ((service == null) && (spath.startsWith("ncml") || spath.startsWith("uddc") || spath.startsWith("iso")))
        service = "ncIso";
      if ((service == null) && (spath.endsWith("xml") || spath.endsWith("html")))
        service = "catalog";

      if (service == null) {
        int pos = spath.indexOf('?');
        if (pos > 0) {
          String req = spath.substring(0, pos);
          if (req.endsWith("xml") || req.endsWith("html"))
            service = "catalog";
        }
      }
    }

    if (service == null)
      service = "other";
    return service;
  }

  public static String[] services = new String[] {
          "admin", "cataloggen", "catalog", "cdmremote", "cdmrfeature", "dodsC", "dqc", "fileServer", "godiva2",
          "ncss", "ncstream", "radarServer", "remoteCatalogService", "view", "wcs", "wms"
  };

  public static String findService(String path) {
    for (String service : services) {
      if (path.startsWith(service)) return service;
    }
    return null;
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package thredds.util.filesource;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import org.springframework.util.StringUtils;

import java.io.File;

/**
 * _more_
 *
 * @author edavis
 * @since 4.0
 */
public class BasicDescendantFileSource implements DescendantFileSource {
  //private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BasicDescendantFileSource.class);

  private final String rootDirectoryPath;
  private final File rootDirectory;

  public BasicDescendantFileSource(File rootDirectory) {
    if (rootDirectory == null)
      throw new IllegalArgumentException("Root directory must not be null.");
    if (!rootDirectory.exists())
      throw new IllegalArgumentException("Root directory must exist: " + rootDirectory.getAbsolutePath() + ".");
    if (!rootDirectory.isDirectory())
      throw new IllegalArgumentException("Root directory must be a directory: " + rootDirectory.getAbsolutePath() + ".");
    this.rootDirectoryPath = StringUtils.cleanPath(rootDirectory.getAbsolutePath());
    this.rootDirectory = new File(this.rootDirectoryPath);
  }

  public BasicDescendantFileSource(String rootDirectoryPath) {
    if (rootDirectoryPath == null)
      throw new IllegalArgumentException("Root directory must not be null.");
    File file = new File(rootDirectoryPath);
    if (!file.exists())
      throw new IllegalArgumentException("Root directory must exist: " + rootDirectoryPath + ".");
    if (!file.isDirectory())
      throw new IllegalArgumentException("Root directory must be a directory: " + rootDirectoryPath + ".");

    this.rootDirectoryPath = StringUtils.cleanPath(file.getAbsolutePath());
    this.rootDirectory = new File(this.rootDirectoryPath);
  }

  /**
   * This implementation requires the path to be relative to the root
   * directory and a descendant of the root directory. It also requires the
   * relative path at each path segment to be a descendant of the root
   * directory, i.e., it cannot start with "../" or contain "../" path
   * segments such that once "normalized" it would start with "../"
   * (e.g., "dir1/../../dir2" once normalized would be "../dir2").
   *
   * @param path the relative path to the descendant File.
   * @return the descendant File represented by the given relative path or null if the path is null, not relative to the root, not a descendant, or the File doesn't exist.
   */
  public File getFile(String path) {
    if (path == null)
      return null;
    String workPath = StringUtils.cleanPath(path);
    if (workPath.startsWith("../"))
      return null;
    if (new File(workPath).isAbsolute())
      return null;

    File file = new File(this.rootDirectory, workPath);
    if (file.exists())
      return file;
    else
      return null;
  }

  public DescendantFileSource getDescendant(String relativePath) {
    File descendantFile = getFile(relativePath);
    if (descendantFile == null || !descendantFile.isDirectory())
      return null;
    return new BasicDescendantFileSource(descendantFile);
  }

  public File getRootDirectory() {
    return this.rootDirectory;
  }

  public String getRootDirectoryPath() {
    return this.rootDirectoryPath;
  }

  public boolean isDescendant(File file) {
    if (file == null)
      return false;
    String relPath = getRelativePath(file);
    return !(relPath == null || relPath.equals(""));
  }

  public boolean isDescendant(String filePath) {
    return filePath != null && isDescendant(new File(filePath));
  }

  public String getRelativePath(File file) {
    if (file == null)
      return null;
    String cleanPath = StringUtils.cleanPath(file.getAbsolutePath()).trim();
    if (cleanPath.startsWith(this.rootDirectoryPath + "/")
            && cleanPath.length() > this.rootDirectoryPath.length() + 1) {
      String relativePath = cleanPath.substring(this.rootDirectoryPath.length() + 1);
      return relativePath.trim();
    }
    return null;
  }

  public String getRelativePath(String filePath) {
    if (filePath == null)
      return null;
    return getRelativePath(new File(filePath));
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
            .add("rootDirectoryPath", rootDirectoryPath)
            .toString();
  }
}