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

package ucar.nc2.grib.grib1;

import ucar.nc2.grib.GdsHorizCoordSys;
import ucar.nc2.grib.GribNumbers;
import ucar.nc2.grib.QuasiRegular;
import ucar.nc2.util.Misc;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.geoloc.projection.Stereographic;

import javax.annotation.concurrent.Immutable;
import java.util.Arrays;
import java.util.Formatter;

/**
 * GRIB1 GDS - subclass for each type
 *

 *

 *
0 Latitude/longitude grid – equidistant cylindrical or Plate Carrée projection
 *
1 Mercator projection
 *
2 Gnomonic projection
 *
3 Lambert conformal, secant or tangent, conic or bi-polar, projection
 *
4 Gaussian latitude/longitude grid
 *
5 Polar stereographic projection
 *
6 Universal Transverse Mercator (UTM) projection
 *
7 Simple polyconic projection
 *
8 Albers equal-area, secant or tangent, conic or bi-polar, projection
 *
9 Miller’s cylindrical projection
 *
10 Rotated latitude/longitude grid
 *
13 Oblique Lambert conformal, secant or tangent, conic or bi-polar, projection
 *
14 Rotated Gaussian latitude/longitude grid
 *
20 Stretched latitude/longitude grid
 *
24 Stretched Gaussian latitude/longitude grid
 *
30 Stretched and rotated latitude/longitude grids
 *
34 Stretched and rotated Gaussian latitude/longitude grids
 *
50 Spherical harmonic coefficients
 *
60 Rotated spherical harmonic coefficients
 *
70 Stretched spherical harmonics
 *
80 Stretched and rotated spherical harmonic coefficients
 *
90 Space view, perspective or orthographic
 *

 *
 * @author John
 * @since 9/3/11
 */
@Immutable
public abstract class Grib1Gds {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Grib1Gds.class);
  static final double maxReletiveErrorPos = .01; // reletive error in position - GRIB numbers sometime miscoded

  /*
  Code table 6 – Data representation type
  Code figure Meaning
 *  0 Latitude/longitude grid – equidistant cylindrical or Plate Carrée projection
 *  1 Mercator projection
    2 Gnomonic projection
 *  3 Lambert conformal, secant or tangent, conic or bi-polar, projection
 *  4 Gaussian latitude/longitude grid
 *  5 Polar stereographic projection
    6 Universal Transverse Mercator (UTM) projection
    7 Simple polyconic projection
    8 Albers equal-area, secant or tangent, conic or bi-polar, projection
    9 Miller’s cylindrical projection
 *  10 Rotated latitude/longitude grid
    11–12 Reserved
    13 Oblique Lambert conformal, secant or tangent, conic or bi-polar, projection
    14 Rotated Gaussian latitude/longitude grid
    15–19 Reserved
    20 Stretched latitude/longitude grid
    21–23 Reserved
    24 Stretched Gaussian latitude/longitude grid
    25–29 Reserved
    30 Stretched and rotated latitude/longitude grids
    31–33 Reserved
    34 Stretched and rotated Gaussian latitude/longitude grids
    35–49 Reserved
 ^  50 Spherical harmonic coefficients
    51–59 Reserved
    60 Rotated spherical harmonic coefficients
    61–69 Reserved
    70 Stretched spherical harmonics
    71–79 Reserved
    80 Stretched and rotated spherical harmonic coefficients
    81–89 Reserved
    90 Space view, perspective or orthographic
    91–191 Reserved
    192–254 Reserved for local use
   */
  public static Grib1Gds factory(int template, byte[] data) {
    switch (template) {
      case 0:
        return new LatLon(data, 0);
      case 1:
        return new Mercator(data, 1);
      case 3:
        return new LambertConformal(data, 3);
      case 4:
        return new GaussianLatLon(data, 4);
      case 5:
        return new PolarStereographic(data, 5);
      case 10:
        return new RotatedLatLon(data, 10);
      case 50:
        return new SphericalHarmonicCoefficients(data, 50);
      default:
        return new UnknownGds(data, template);
    }
  }

  ///////////////////////////////////////////////////
  private static final float scale3 = (float) 1.0e-3;
  // private static final float scale6 = (float) 1.0e-6;

  protected final byte[] data;
  protected int[] nptsInLine; // thin grids, else null

  public final int template;
  protected int nx, ny;
  public int scanMode, resolution;
  protected int lastOctet;

  protected Grib1Gds(int template) {
    this.template = template;
    this.data = null;
  }

  public Grib1Gds(byte[] data, int template) {
    this.data = data;
    this.template = template;

    nx = getOctet2(7);
    ny = getOctet2(9);
  }

  public byte[] getRawBytes() {
    return data;
  }

  public int getNpts() {

    if (nptsInLine != null) {
      int npts = 0;
      for (int pts : nptsInLine) {
        npts += pts;
      }
      return npts;
    } else {
      return nx * ny;
    }

  }

  public int[] getNptsInLine() {
    return nptsInLine;
  }

  void setNptsInLine(int[] nptsInLine) {
    this.nptsInLine = nptsInLine;
  }

  protected int getOctet(int index) {
    if (index > data.length) {
      return GribNumbers.UNDEFINED;
    }
    return data[index - 1] & 0xff;
  }

  // signed
  protected int getOctet2(int start) {
    return GribNumbers.int2(getOctet(start), getOctet(start + 1));
  }

  // signed
  protected int getOctet3(int start) {
    return GribNumbers.int3(getOctet(start), getOctet(start + 1), getOctet(start + 2));
  }

  // signed
  protected int getOctet4(int start) {
    return GribNumbers
        .int4(getOctet(start), getOctet(start + 1), getOctet(start + 2), getOctet(start + 3));
  }

  /*
  Code table 7 – Resolution and component flags
  Bit Value Meaning
    1 0     Direction increments not given
      1     Direction increments given
    2 0     Earth assumed spherical with radius 6367.47 km
      1     Earth assumed oblate spheroidal with size as determined by IAU in 1965 (6378.160 km, 6356.775 km, f = 1/297.0)
    3–4     Reserved
    5 0     Resolved u- and v-components of vector quantities relative to easterly and northerly directions
      1     Resolved u- and v-components of vector quantities relative to the defined grid in the direction of increasing x and y (or i and j) coordinates respectively
   6–8 0    Reserved – set to zero */

  private static boolean getDirectionIncrementsGiven(int resolution) {
    return GribNumbers.testGribBitIsSet(resolution,1);
  }

  private static boolean getEarthShapeIsSpherical(int resolution) {
    return !GribNumbers.testGribBitIsSet(resolution,2);
  }

  private static boolean getUVisReletiveToEastNorth(int resolution) {
    return !GribNumbers.testGribBitIsSet(resolution,5);
  }

  protected Earth getEarth() {
    if (getEarthShapeIsSpherical(resolution)) {
      return new Earth(6367470.0);
    } else {
      return EarthEllipsoid.IAU;
    }
  }

  public int getEarthShape() {
    return getEarthShapeIsSpherical(resolution) ? 0 : 1;
  }

  public boolean getUVisReletiveToEastNorth() {
    return getUVisReletiveToEastNorth(resolution);
  }

  public int getResolution() {
    return resolution;
  }

  public boolean isLatLon() {
    return false;
  }

  /*
  Flag/Code table 8 – Scanning mode
 Bit Value Meaning
  1   0   Points scan in +i direction
      1   Points scan in –i direction
  2   0   Points scan in –j direction
      1   Points scan in +j direction
  3   0   Adjacent points in i direction are consecutive
      1   Adjacent points in j direction are consecutive
   */
  public int getScanMode() {
    return scanMode;
  }

  public int getNxRaw() {
    return nx;
  }

  public int getNyRaw() {
    return ny;
  }

  ///////// thin grid crapola
  /*
    For data on a quasi-regular grid, in which all the rows or columns do not necessarily have the same number of grid
    points, either Ni (octets 7–8) or Nj (octets 9–10) and the corresponding Di (octets 24–25) or Dj (octets 26–27) shall be
    coded with all bits set to 1 (missing); the actual number of points along each parallel or meridian shall be coded.
  */

  // number of points along nx, adjusted for thin grid
  public int getNx() {
    if (nptsInLine == null || nx > 0) {
      return nx;
    }
    return QuasiRegular.getMax(nptsInLine);
  }

  // number of points along ny, adjusted for thin grid
  public int getNy() {
    if (nptsInLine == null || ny > 0) {
      return ny;
    }
    return QuasiRegular.getMax(nptsInLine);
  }

  // delta in x direction
  public float getDx() {
    return getDxRaw();
  }

  // delta in y direction
  public float getDy() {
    return getDyRaw();
  }

  public abstract float getDxRaw();

  public abstract float getDyRaw();

  public abstract GdsHorizCoordSys makeHorizCoordSys();

  public abstract void testHorizCoordSys(Formatter f);

  public String getNameShort() {
    String className = getClass().getName();
    int pos = className.lastIndexOf("$");
    return className.substring(pos + 1);
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder("Grib1Gds{");
    sb.append(" template=").append(template);
    sb.append(", nx=").append(nx);
    sb.append(", ny=").append(ny);
    sb.append(", scanMode=").append(scanMode);
    sb.append(", resolution=").append(resolution);
    sb.append(", lastOctet=").append(lastOctet);
    if (nptsInLine == null) {
      sb.append(", nptsInLine=null");
    } else {
      sb.append(", nptsInLine (").append(nptsInLine.length);
      sb.append(")=").append(Arrays.toString(nptsInLine));
    }
    sb.append('}');
    return sb.toString();
  }

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

    Grib1Gds Grib1Gds = (Grib1Gds) o;
    if (nx != Grib1Gds.nx) {
      return false;
    }
    if (ny != Grib1Gds.ny) {
      return false;
    }
    if (template != Grib1Gds.template) {
      return false;
    }

    return true;
  }

  @Override
  public int hashCode() {
    int result = template;
    result = 31 * result + nx;
    result = 31 * result + ny;
    return result;
  }

  protected int hashCode = 0;

  /*
    Grid definition –   latitude/longitude grid (or equidistant cylindrical, or Plate Carrée)
    Octet No. Contents
    7–8 Ni – number of points along a parallel
    9–10 Nj – number of points along a meridian
    11–13 La1 – latitude of first grid point
    14–16 Lo1 – longitude of first grid point
    17 Resolution and component flags (see Code table 7)
    18–20 La2 – latitude of last grid point
    21–23 Lo2 – longitude of last grid point
    24–25 Di – i direction increment
    26–27 Dj – j direction increment
    28 Scanning mode (flags – see Flag/Code table 8)
    29–32 Set to zero (reserved)
    33–35 Latitude of the southern pole in millidegrees (integer) / Latitude of pole of stretching in millidegrees (integer)
    36–38 Longitude of the southern pole in millidegrees (integer) / Longitude of pole of stretching in millidegrees (integer)
    39–42 Angle of rotation (represented in the same way as the reference value) / Stretching factor (representation as for the reference value)
    43–45 Latitude of pole of stretching in millidegrees (integer)
    46–48 Longitude of pole of stretching in millidegrees (integer)
    49–52 Stretching factor (representation as for the reference value)
*/
  public static class LatLon extends Grib1Gds {

    protected float la1, lo1, la2, lo2, deltaLon, deltaLat;

    protected LatLon(int template) {
      super(template);
    }

    LatLon(byte[] data, int template) {
      super(data, template);

      la1 = getOctet3(11) * scale3;
      lo1 = getOctet3(14) * scale3;
      resolution = getOctet(17);    // Resolution and component flags (see Code table 7)
      la2 = getOctet3(18) * scale3;
      lo2 = getOctet3(21) * scale3;

      if (lo2 < lo1) {
        lo2 += 360.0F;
      }

      deltaLon = getOctet2(24);
      float calcDelta = (lo2 - lo1) / (nx - 1); // more accurate - deltaLon may have roundoff
      if (deltaLon != GribNumbers.UNDEFINED) {
        deltaLon *= scale3; // undefined for thin grids
      } else {
        deltaLon = calcDelta;
      }
      if (!Misc.nearlyEquals(deltaLon, calcDelta)) {
        log.debug("deltaLon != calcDeltaLon");
        deltaLon = calcDelta;
      }

      deltaLat = getOctet2(26);
      calcDelta = (la2 - la1) / (ny - 1); // more accurate - deltaLon may have roundoff
      if (deltaLat != GribNumbers.UNDEFINED) {
        deltaLat *= scale3; // undefined for thin grids
        if (la2 < la1) {
          deltaLat *= -1.0;
        }
      } else {
        deltaLat = calcDelta;
      }

      /*  thanks to johann.sorel@geomatys.com 11/1/2012. withdrawn for now 11/2/2012
      if (deltaLat != GribNumbers.UNDEFINED) {
        deltaLat *= scale3; // undefined for thin grids
        if (la2 < la1) {
          //flip declaration order
          float latemp = la1;
          la1 = la2;
          la2 = latemp;
          calcDelta *= -1.0;

          //we must also consider the cell corner, since we flipped the order
          //we should specify that the true value is at the BOTTOM-LEFT corner
          //but we can't show this information so we make a one cell displacement
          //to move the value on a TOP-LEFT corner.
          la1 -= calcDelta;
          la2 -= calcDelta;
        }
      } else {
        deltaLat = calcDelta;
      }  */

      if (!Misc.nearlyEquals(deltaLat, calcDelta)) {
        log.debug("deltaLat != calcDeltaLat");
        deltaLat = calcDelta;
      }

      scanMode = (byte) getOctet(28);

      lastOctet = 28;
    }

    @Override
    public void setNptsInLine(int[] nptsInLine) {
      super.setNptsInLine(nptsInLine);
      // now that we know this, we may have to recalc some stuff
      int n = QuasiRegular.getMax(nptsInLine);
      if (nx < 0) {
        deltaLon = (lo2 - lo1) / (n - 1);
      }
      if (ny < 0) {
        deltaLat = (la2 - la1) / (n - 1);
      }
    }

    @Override
    public boolean isLatLon() {
      return true;
    }

    @Override
    public float getDx() {
      if (nptsInLine == null || deltaLon != GribNumbers.UNDEFINED) {
        return deltaLon;
      }
      return (lo2 - lo1) / (getNx() - 1);
    }

    @Override
    public float getDy() {
      if (nptsInLine == null || deltaLat != GribNumbers.UNDEFINED) {
        return deltaLat;
      }
      return (la2 - la1) / (getNy() - 1);
    }

    @Override
    public float getDxRaw() {
      return deltaLon;
    }

    @Override
    public float getDyRaw() {
      return deltaLat;
    }

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

      LatLon other = (LatLon) o;
      if (!Misc.nearlyEqualsAbs(la1, other.la1, maxReletiveErrorPos * deltaLat)) {
        return false;   // allow some slop, reletive to grid size
      }
      if (!Misc.nearlyEqualsAbs(lo1, other.lo1, maxReletiveErrorPos * deltaLon)) {
        return false;
      }
      if (!Misc.nearlyEquals(deltaLat, other.deltaLat)) {
        return false;
      }
      if (!Misc.nearlyEquals(deltaLon, other.deltaLon)) {
        return false;
      }
      return true;
    }

    @Override
    public int hashCode() {
      if (hashCode == 0) {
        int useLat = (int) (la1 / (maxReletiveErrorPos
            * deltaLat));  //  Two equal objects must have the same hashCode() value
        int useLon = (int) (lo1 / (maxReletiveErrorPos * deltaLon));
        int useDeltaLon = (int) (deltaLon / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLat = (int) (deltaLat / Misc.defaultMaxRelativeDiffFloat);

        int result = super.hashCode();
        result = 31 * result + useLat;
        result = 31 * result + useLon;
        result = 31 * result + useDeltaLon;
        result = 31 * result + useDeltaLat;
        hashCode = result;
      }
      return hashCode;
    }

    @Override
    public String toString() {
      return "LatLon{" +
              "la1=" + la1 +
              ", lo1=" + lo1 +
              ", la2=" + la2 +
              ", lo2=" + lo2 +
              ", deltaLon=" + deltaLon +
              ", deltaLat=" + deltaLat +
              "} " + super.toString();
    }

    @Override
    public GdsHorizCoordSys makeHorizCoordSys() {
      LatLonProjection proj = new LatLonProjection(getEarth());
      //ProjectionPoint startP = proj.latLonToProj(new LatLonPointImpl(la1, lo1));
      double startx = lo1; // startP.getX();
      double starty = la1; // startP.getY();
      return new GdsHorizCoordSys(getNameShort(), template, 0, scanMode, proj, startx, getDx(),
          starty, getDy(), getNx(), getNy(), null);
    }

    public void testHorizCoordSys(Formatter f) {
      GdsHorizCoordSys cs = makeHorizCoordSys();
      double Lo2 = lo2;
      if (Lo2 < lo1) {
        Lo2 += 360;
      }
      LatLonPointImpl startLL = new LatLonPointImpl(la1, lo1);
      LatLonPointImpl endLL = new LatLonPointImpl(la2, Lo2);

      f.format("%s testProjection%n", getClass().getName());
      f.format("  start at latlon= %s%n", startLL);
      f.format("    end at latlon= %s%n", endLL);

      ProjectionPointImpl endPP = (ProjectionPointImpl) cs.proj.latLonToProj(endLL, new ProjectionPointImpl());
      f.format("   start at proj coord= %s%n", new ProjectionPointImpl(cs.startx, cs.starty));
      f.format("     end at proj coord= %s%n", endPP);

      double endx = cs.startx + (getNx() - 1) * cs.dx;
      double endy = cs.starty + (getNy() - 1) * cs.dy;
      f.format("   should end at x= (%f,%f)%n", endx, endy);
    }
  }

  /* Template 4
    Grid definition – Gaussian latitude/longitude grid (including rotated, stretched or stretched and rotated)

    Octet No. Contents
    7–8     Ni – number of points along a parallel
    9–10   Nj – number of points along a meridian
    11–13   La1 – latitude of first grid point
    14–16   Lo1 – longitude of first grid point
    17     Resolution and component flags (see Code table 7)
    18–20   La2 – latitude of last grid point
    21–23   Lo2 – longitude of last grid point
    24–25   Di – i direction increment
    26–27   N – number of parallels between a pole and the equator
    28     Scanning mode (flags – see Flag/Code table 8)
    29–32   Set to zero (reserved)
    33–35   Latitude of the southern pole in millidegrees (integer)
          Latitude of pole of stretching in millidegrees (integer)
    36–38   Longitude of the southern pole in millidegrees (integer)
          Longitude of pole of stretching in millidegrees (integer)
    39–42   Angle of rotation (represented in the same way as the reference value)
          Stretching factor (representation as for the reference value)
    43–45   Latitude of pole of stretching in millidegrees (integer)
    46–48   Longitude of pole of stretching in millidegrees (integer)
    49–52   Stretching factor (representation as for the reference value)

    Notes:
    (1) Latitude, longitude and increments are in millidegrees.

    (2) Latitude values are limited to the range 0–90 000; bit 1 is set to 1 to indicate south latitude.

    (3) Longitude values are limited to the range 0–360 000; bit 1 is set to 1 to indicate west longitude.

    (4) The number of parallels between a pole and the equator is used to establish the variable (Gaussian) spacing of the
    parallels; this value must always be given.

    (5) The latitude and longitude of the last grid point and the first grid point should always be given for regular grids.

    (6) Where items are not given, the appropriate octet(s) should have all bits set to 1.

    (7) See Notes 6 to 11 under Grid definition – latitude/longitude grid (or equidistant cylindrical, or Plate Carrée) –
    page I.2 – Bi — 8.

    (6) Three parameters define a general latitude/longitude coordinate system, formed by a general rotation of the sphere. One
    choice for these parameters is:
      (a) The geographic latitude in degrees of the southern pole of the coordinate system, θp for example;
      (b) The geographic longitude in degrees of the southern pole of the coordinate system, λp for example;
      (c) The angle of rotation in degrees about the new polar axis (measured clockwise when looking from the southern to
          the northern pole) of the coordinate system, assuming the new axis to have been obtained by first rotating the
          sphere through λp degrees about the geographic polar axis, and then rotating through (90 + θp) degrees so that
          the southern pole moved along the (previously rotated) Greenwich meridian.
    (7) For rotated grids, the vertical coordinate parameters start at octet 43 instead of 33.
    (8) The stretching is defined by three parameters:
      (a) The latitude in degrees (measured in the model coordinate system) of the pole of stretching;
      (b) The longitude in degrees (measured in the model coordinate system) of the pole of stretching;
      (c) The stretching factor C.
      The stretching is defined by representing data uniformly in a coordinate system with longitude λ and latitude θ1, where:
        (1 – C2) + (1 + C2) sin θ
        θ1 = sin–1
        (1 + C2) + (1 – C2) sin θ
      and λ and θ are longitude and latitude in a coordinate system in which the pole of stretching is the northern pole. C = 1
      gives uniform resolution, while C > 1 gives enhanced resolution around the pole of stretching.
    (9) For stretched grids, the vertical coordinate parameters start at octet 43 instead of 33.
    (10) For stretched and rotated latitude/longitude grids, the vertical coordinate parameters start at octet 53.
    (11) The first and last grid points may not necessarily correspond to the first and last data points, respectively, if the bit-map
      section is used.

    (8) Quasi-regular Gaussian latitude/longitude grids are defined only for subsets of global grids containing full latitude rows (360°).

    (9) For data on a quasi-regular grid, in which all the rows do not necessarily have the same number of grid points, Ni
    (octets 7–8) and the corresponding Di (octets 24–25) shall be coded with all bits set to 1 (missing); the actual number
    of points along each parallel shall be coded.

    (10) A quasi-regular Gaussian latitude/longitude grid is only defined for the grid scanning mode with consecutive points on
    parallels (bit 3 set to zero in Code table 8). The first point in each row shall be positioned at the meridian indicated by
    octets 14–16 and the last shall be positioned at the meridian indicated by octets 21–23. The grid points along each
    parallel shall be evenly spaced in longitude.

    // see E:/ecmwf/ICMGGECE3_000000.grb  (1/14/2015)
   */
  public static class GaussianLatLon extends LatLon {

    final int nparellels;
    public float latSouthPole, lonSouthPole, rotAngle, latPole, lonPole, stretchFactor;

    GaussianLatLon(byte[] data, int template) {
      super(data, template);
      nparellels = getOctet2(26);

      if (data.length > 32) {
        latSouthPole = getOctet3(33) * scale3;
        lonSouthPole = getOctet3(36) * scale3;
        rotAngle = getOctet4(39) * scale3;
        latPole = getOctet3(43) * scale3;
        lonPole = getOctet3(46) * scale3;
        stretchFactor = getOctet4(49) * scale3;
      }

      lastOctet = 52;
    }

    @Override
    public GdsHorizCoordSys makeHorizCoordSys() {
      GdsHorizCoordSys hc = super.makeHorizCoordSys();
      hc.setGaussianLats(nparellels, la1, la2);
      return hc;
    }

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

      GaussianLatLon that = (GaussianLatLon) o;
      if (nparellels != that.nparellels) {
        return false;
      }
      return true;
    }

    @Override
    public int hashCode() {
      int result = super.hashCode();
      result = 31 * result + nparellels;
      return result;
    }

    @Override
    public String toString() {
      return "GaussianLatLon{" +
              "nparellels=" + nparellels +
              ", latSouthPole=" + latSouthPole +
              ", lonSouthPole=" + lonSouthPole +
              ", rotAngle=" + rotAngle +
              ", latPole=" + latPole +
              ", lonPole=" + lonPole +
              ", stretchFactor=" + stretchFactor +
              "} " + super.toString();
    }
  }

  /*
  Grid definition –   polar stereographic
   Octet No. Contents
   7–8    Nx – number of points along x-axis
   9–10   Ny – number of points along y-axis
   11–13  La1 – latitude of first grid point
   14–16  Lo1 – longitude of first grid point
   17     Resolution and component flags (see Code table 7)
   18–20  LoV – orientation of the grid; i.e. the longitude value of the meridian which is parallel to the y-axis (or columns
                of the grid) along which latitude increases as the Y-coordinate increases (the orientation longitude may or may not appear on a particular grid)
   21–23  Dx – X-direction grid length (see Note 2)
   24–26  Dy – Y-direction grid length (see Note 2)
   27     Projection centre flag (see Note 5)
   28     Scanning mode (flags – see Flag/Code table 8)
   29–32  Set to zero (reserved)
    */
  public static class PolarStereographic extends Grib1Gds {

    protected float la1, lo1, lov, dX, dY;
    protected int projCenterFlag;
    private final float lad = (float) 60.0; // LOOK

    protected PolarStereographic(int template) {
      super(template);
    }

    PolarStereographic(byte[] data, int template) {
      super(data, template);

      la1 = getOctet3(11) * scale3;
      lo1 = getOctet3(14) * scale3;
      resolution = getOctet(17); // Resolution and component flags (see Code table 7)
      lov = getOctet3(18) * scale3;

      dX = getOctet3(21) * scale3;
      dY = getOctet3(24) * scale3;

      projCenterFlag = getOctet(27);
      scanMode = getOctet(28);

      lastOctet = 28;
    }

    @Override
    public String toString() {
      return "PolarStereographic{" +
              "la1=" + la1 +
              ", lo1=" + lo1 +
              ", lov=" + lov +
              ", dX=" + dX +
              ", dY=" + dY +
              ", projCenterFlag=" + projCenterFlag +
              ", lad=" + lad +
              "} " + super.toString();
    }

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

      PolarStereographic that = (PolarStereographic) o;

      if (!Misc.nearlyEqualsAbs(la1, that.la1, maxReletiveErrorPos * dY)) {
        return false;   // allow some slop, reletive to grid size
      }
      if (!Misc.nearlyEqualsAbs(lo1, that.lo1, maxReletiveErrorPos * dX)) {
        return false;
      }
      if (!Misc.nearlyEquals(lov, that.lov)) {
        return false;
      }
      if (!Misc.nearlyEquals(dY, that.dY)) {
        return false;
      }
      if (!Misc.nearlyEquals(dX, that.dX)) {
        return false;
      }

      if (projCenterFlag != that.projCenterFlag) {
        return false;
      }

      return true;
    }

    @Override
    public int hashCode() {
      if (hashCode == 0) {
        int useLat = (int) (la1 / (maxReletiveErrorPos
            * dY));  //  Two equal objects must have the same hashCode() value
        int useLon = (int) (lo1 / (maxReletiveErrorPos * dX));
        int useLov = (int) (lov / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLon = (int) (dX / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLat = (int) (dY / Misc.defaultMaxRelativeDiffFloat);

        int result = super.hashCode();
        result = 31 * result + useLat;
        result = 31 * result + useLon;
        result = 31 * result + useLov;
        result = 31 * result + useDeltaLon;
        result = 31 * result + useDeltaLat;
        result = 31 * result + projCenterFlag;
        hashCode = result;
      }
      return hashCode;
    }

    @Override
    public float getDxRaw() {
      return dX;
    }

    @Override
    public float getDyRaw() {
      return dY;
    }

    public GdsHorizCoordSys makeHorizCoordSys() {
      boolean northPole = (projCenterFlag & 128) == 0;
      double latOrigin = northPole ? 90.0 : -90.0;

      // Why the scale factor?. according to GRIB docs:
      // "Grid lengths are in units of meters, at the 60 degree latitude circle nearest to the pole"
      // since the scale factor at 60 degrees = k = 2*k0/(1+sin(60))  [Snyder,Working Manual p157]
      // then to make scale = 1 at 60 degrees, k0 = (1+sin(60))/2 = .933
      double scale;
      if (Double.isNaN(lad)) { // LOOK ??
        scale = 0.9330127018922193;
      } else {
        scale = (1.0 + Math.sin(Math.toRadians(Math.abs(lad)))) / 2;
      }

      ProjectionImpl proj;

      Earth earth = getEarth();
      if (earth.isSpherical()) {
        proj = new Stereographic(latOrigin, lov, scale);
      } else {
        proj = new ucar.unidata.geoloc.projection.proj4.StereographicAzimuthalProjection(latOrigin,
            lov, scale, lad, 0.0, 0.0, earth);
      }

      ProjectionPointImpl start = (ProjectionPointImpl) proj
          .latLonToProj(new LatLonPointImpl(la1, lo1));
      return new GdsHorizCoordSys(getNameShort(), template, 0, scanMode, proj, start.getX(),
          getDx(), start.getY(), getDy(), getNx(), getNy(), null);
    }

    public void testHorizCoordSys(Formatter f) {
      GdsHorizCoordSys cs = makeHorizCoordSys();
      f.format("%s testProjection %s%n", getClass().getName(), cs.proj.getClass().getName());

      double endx = cs.startx + (getNx() - 1) * cs.dx;
      double endy = cs.starty + (getNy() - 1) * cs.dy;
      ProjectionPointImpl endPP = new ProjectionPointImpl(endx, endy);
      f.format("   start at proj coord= %s%n", new ProjectionPointImpl(cs.startx, cs.starty));
      f.format("     end at proj coord= %s%n", endPP);

      LatLonPointImpl startLL = new LatLonPointImpl(la1, lo1);
      LatLonPoint endLL = cs.proj.projToLatLon(endPP, new LatLonPointImpl());

      f.format("  start at latlon= %s%n", startLL);
      f.format("    end at latlon= %s%n", endLL);
    }

  }

  /*
  Grid definition –   Lambert conformal, secant or tangent, conic or bi-polar (normal or oblique), or
  Albers  equal-area,  secant  or  tangent,  conic  or  bi-polar  (normal  or  oblique), projection
  Octet No. Contents
  7–8   Nx – number of points along x-axis
  9–10  Ny – number of points along y-axis
  11–13 La1 – latitude of first grid point
  14–16 Lo1– longitude of first grid point
  17    Resolution and component flags (see Code table 7)
  18–20 LoV –   orientation of the grid; i.e. the east longitude value of the meridian which is parallel to the y-axis
    (or columns of the grid) along which latitude increases as the y-coordinate increases (the orientation longitude may
    or may not appear on a particular grid)
  21–23 Dx – x-direction grid length (see Note 2)
  24–26 Dy – y-direction grid length (see Note 2)
  27    Projection centre flag (see Note 5)
  28    Scanning mode (flags – see Flag/Code table 8)
  29–31 Latin 1 – first latitude from the pole at which the secant cone cuts the sphere
  32–34 Latin 2 – second latitude from the pole at which the secant cone cuts the sphere
  35–37 Latitude of the southern pole in millidegrees (integer)
  38–40 Longitude of the southern pole in millidegrees (integer)
  41–42 Set to zero (reserved)
   */
  public static class LambertConformal extends Grib1Gds {

    protected final float la1;
    protected final float lo1;
    protected final float lov;
    protected float lad;
    protected final float dX;
    protected final float dY;
    protected final float latin1;
    protected final float latin2;
    protected final float latSouthPole;
    protected final float lonSouthPole;
    protected final int projCenterFlag;

    // private int hla1, hlo1, hlov, hlad, hdX, hdY, hlatin1, hlatin2; // hasheesh

    LambertConformal(byte[] data, int template) {
      super(data, template);

      la1 = getOctet3(11) * scale3;
      lo1 = getOctet3(14) * scale3;
      resolution = getOctet(17); // Resolution and component flags (see Code table 7)
      lov = getOctet3(18) * scale3;

      dX = getOctet3(21) * scale3;
      dY = getOctet3(24) * scale3;

      projCenterFlag = getOctet(27);
      scanMode = getOctet(28);

      latin1 = getOctet3(29) * scale3;
      latin2 = getOctet3(32) * scale3;
      latSouthPole = getOctet3(35) * scale3;
      lonSouthPole = getOctet3(38) * scale3;

      lastOctet = 42;
    }

    @Override
    public float getDxRaw() {
      return dX;
    }

    @Override
    public float getDyRaw() {
      return dY;
    }

    @Override
    public String toString() {
      return "LambertConformal{" +
              "la1=" + la1 +
              ", lo1=" + lo1 +
              ", lov=" + lov +
              ", lad=" + lad +
              ", dX=" + dX +
              ", dY=" + dY +
              ", latin1=" + latin1 +
              ", latin2=" + latin2 +
              ", latSouthPole=" + latSouthPole +
              ", lonSouthPole=" + lonSouthPole +
              ", projCenterFlag=" + projCenterFlag +
              "} " + super.toString();
    }

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

      LambertConformal that = (LambertConformal) o;

      if (!Misc.nearlyEqualsAbs(la1, that.la1, maxReletiveErrorPos * dY)) {
        return false;   // allow some slop, relative to grid size
      }
      if (!Misc.nearlyEqualsAbs(lo1, that.lo1, maxReletiveErrorPos * dX)) {
        return false;
      }
      if (!Misc.nearlyEquals(lad, that.lad)) {
        return false;
      }
      if (!Misc.nearlyEquals(lov, that.lov)) {
        return false;
      }
      if (!Misc.nearlyEquals(dY, that.dY)) {
        return false;
      }
      if (!Misc.nearlyEquals(dX, that.dX)) {
        return false;
      }
      if (!Misc.nearlyEquals(latin1, that.latin1)) {
        return false;
      }
      if (!Misc.nearlyEquals(latin2, that.latin2)) {
        return false;
      }

      return true;
    }

    @Override
    public int hashCode() {
      if (hashCode == 0) {
        int useLat = (int) (la1 / (maxReletiveErrorPos
            * dY));  //  Two equal objects must have the same hashCode() value
        int useLon = (int) (lo1 / (maxReletiveErrorPos * dX));
        int useLad = (int) (lad / Misc.defaultMaxRelativeDiffFloat);
        int useLov = (int) (lov / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLon = (int) (dX / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLat = (int) (dY / Misc.defaultMaxRelativeDiffFloat);
        int useLatin1 = (int) (latin1 / Misc.defaultMaxRelativeDiffFloat);
        int useLatin2 = (int) (latin2 / Misc.defaultMaxRelativeDiffFloat);

        int result = super.hashCode();
        result = 31 * result + useLat;
        result = 31 * result + useLon;
        result = 31 * result + useLad;
        result = 31 * result + useLov;
        result = 31 * result + useDeltaLon;
        result = 31 * result + useDeltaLat;
        result = 31 * result + useLatin1;
        result = 31 * result + useLatin2;
        result = 31 * result + projCenterFlag;

        hashCode = result;
      }
      return hashCode;
    }

    public GdsHorizCoordSys makeHorizCoordSys() {
      ProjectionImpl proj;

      Earth earth = getEarth();
      if (earth.isSpherical()) {
        proj = new ucar.unidata.geoloc.projection.LambertConformal(latin1, lov, latin1, latin2, 0.0,
            0.0, earth.getEquatorRadius() * .001);
      } else {
        proj = new ucar.unidata.geoloc.projection.proj4.LambertConformalConicEllipse(latin1, lov,
            latin1, latin2, 0.0, 0.0, earth);
      }

      LatLonPointImpl startLL = new LatLonPointImpl(la1, lo1);
      ProjectionPointImpl start = (ProjectionPointImpl) proj.latLonToProj(startLL);
      return new GdsHorizCoordSys(getNameShort(), template, 0, scanMode, proj, start.getX(),
          getDx(), start.getY(), getDy(), getNx(), getNy(), null);
    }

    public void testHorizCoordSys(Formatter f) {
      GdsHorizCoordSys cs = makeHorizCoordSys();
      f.format("%s testProjection %s%n", getClass().getName(), cs.proj.getClass().getName());

      double endx = cs.startx + (getNx() - 1) * cs.dx;
      double endy = cs.starty + (getNy() - 1) * cs.dy;
      ProjectionPointImpl endPP = new ProjectionPointImpl(endx, endy);
      f.format("   start at proj coord= %s%n", new ProjectionPointImpl(cs.startx, cs.starty));
      f.format("     end at proj coord= %s%n", endPP);

      LatLonPointImpl startLL = new LatLonPointImpl(la1, lo1);
      LatLonPoint endLL = cs.proj.projToLatLon(endPP, new LatLonPointImpl());

      f.format("  start at latlon= %s%n", startLL);
      f.format("    end at latlon= %s%n", endLL);
    }

  }

  /*
  Grid definition – Mercator
  Octet  Contents
    7–8   Ni – number of points along a parallel
    9–10  Nj – number of points along a meridian
    11–13 La1 – latitude of first grid point
    14–16 Lo1 – longitude of first grid point
    17    Resolution and component flags (see Code table 7)
    18–20 La2 – latitude of last grid point
    21–23 Lo2 – longitude of last grid point
    24–26 Latin – latitude(s) at which the Mercator projection cylinder intersects the Earth
    27    Set to zero (reserved)
    28    Scanning mode (flags – see Flag/Code table 8)
    29–31 Di – longitudinal direction grid length (see Note 2)
    32–34 Dj – latitudinal direction grid length (see Note 2)
    35–42 Set to zero (reserved)
   */

  public static class Mercator extends Grib1Gds {

    protected final float la1;
    protected final float lo1;
    protected final float la2;
    protected final float lo2;
    protected final float latin;
    protected final float dX;
    protected final float dY;

    Mercator(byte[] data, int template) {
      super(data, template);

      la1 = getOctet3(11) * scale3;
      lo1 = getOctet3(14) * scale3;
      resolution = getOctet(17); // Resolution and component flags (see Code table 7)
      la2 = getOctet3(18) * scale3;
      lo2 = getOctet3(21) * scale3;
      latin = getOctet3(24) * scale3;

      scanMode = getOctet(28);

      dX = getOctet3(29) * scale3;  // km
      dY = getOctet3(32) * scale3;  // km

      lastOctet = 42;
    }

    @Override
    public float getDxRaw() {
      return dX;
    }

    @Override
    public float getDyRaw() {
      return dY;
    }

    @Override
    public String toString() {
      return "Mercator{" +
              "la1=" + la1 +
              ", lo1=" + lo1 +
              ", la2=" + la2 +
              ", lo2=" + lo2 +
              ", latin=" + latin +
              ", dX=" + dX +
              ", dY=" + dY +
              "} " + super.toString();
    }

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

      Mercator that = (Mercator) o;

      if (!Misc.nearlyEqualsAbs(la1, that.la1, maxReletiveErrorPos * dY)) {
        return false;   // allow some slop, reletive to grid size
      }
      if (!Misc.nearlyEqualsAbs(lo1, that.lo1, maxReletiveErrorPos * dX)) {
        return false;
      }
      if (!Misc.nearlyEquals(latin, that.latin)) {
        return false;
      }
      if (!Misc.nearlyEquals(dY, that.dY)) {
        return false;
      }
      if (!Misc.nearlyEquals(dX, that.dX)) {
        return false;
      }

      return true;
    }

    @Override
    public int hashCode() {
      if (hashCode == 0) {
        int useLat = (int) (la1 / (maxReletiveErrorPos
            * dY));  //  Two equal objects must have the same hashCode() value
        int useLon = (int) (lo1 / (maxReletiveErrorPos * dX));
        int useLad = (int) (latin / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLon = (int) (dX / Misc.defaultMaxRelativeDiffFloat);
        int useDeltaLat = (int) (dY / Misc.defaultMaxRelativeDiffFloat);

        int result = super.hashCode();
        result = 31 * result + useLat;
        result = 31 * result + useLon;
        result = 31 * result + useLad;
        result = 31 * result + useDeltaLon;
        result = 31 * result + useDeltaLat;
        hashCode = result;
      }
      return hashCode;
    }

    public GdsHorizCoordSys makeHorizCoordSys() {
      // put longitude origin at first point - doesnt actually matter
      // param par standard parallel (degrees). cylinder cuts earth at this latitude.
      // LOOK dont have an elipsoidal Mercator projection
      Earth earth = getEarth();
      ucar.unidata.geoloc.projection.Mercator proj = new ucar.unidata.geoloc.projection.Mercator(
          lo1, latin, 0, 0, earth.getEquatorRadius() * .001);

      // find out where things start
      ProjectionPoint startP = proj.latLonToProj(new LatLonPointImpl(la1, lo1));
      double startx = startP.getX();
      double starty = startP.getY();

      return new GdsHorizCoordSys(getNameShort(), template, 0, scanMode, proj, startx, getDx(),
          starty, getDy(), getNx(), getNy(), null);
    }

    public void testHorizCoordSys(Formatter f) {
      GdsHorizCoordSys cs = makeHorizCoordSys();
      double Lo2 = lo2;
      if (Lo2 < lo1) {
        Lo2 += 360;
      }
      LatLonPointImpl startLL = new LatLonPointImpl(la1, lo1);
      LatLonPointImpl endLL = new LatLonPointImpl(la2, Lo2);

      f.format("%s testProjection%n", getClass().getName());
      f.format("  start at latlon= %s%n", startLL);
      f.format("    end at latlon= %s%n", endLL);

      ProjectionPointImpl endPP = (ProjectionPointImpl) cs.proj
          .latLonToProj(endLL, new ProjectionPointImpl());
      f.format("   start at proj coord= %s%n", new ProjectionPointImpl(cs.startx, cs.starty));
      f.format("     end at proj coord= %s%n", endPP);

      double endx = cs.startx + (getNx() - 1) * cs.dx;
      double endy = cs.starty + (getNy() - 1) * cs.dy;
      f.format("   should end at x= (%f,%f)%n", endx, endy);
    }

  }

  public static class RotatedLatLon extends LatLon {

    protected final float angleRotation; // Angle of rotation (represented in the same way as the reference value)
    protected final float latSouthPole; // Latitude of pole of stretching in millidegrees (integer)
    protected final float lonSouthPole;  // Longitude of pole of stretching in millidegrees (integer)

    RotatedLatLon(byte[] data, int template) {
      super(data, template);

      latSouthPole = getOctet3(33) * scale3;
      lonSouthPole = getOctet3(36) * scale3;
      angleRotation = getOctet4(39) * scale3;

      lastOctet = 43;
    }

    public String getName() {
      return "Rotated latitude/longitude";
    }

    @Override
    public String toString() {
      return "RotatedLatLon{" +
              "angleRotation=" + angleRotation +
              ", latSouthPole=" + latSouthPole +
              ", lonSouthPole=" + lonSouthPole +
              "} " + super.toString();
    }

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

      RotatedLatLon other = (RotatedLatLon) o;
      return Misc.nearlyEquals(angleRotation, other.angleRotation);
    }

    @Override
    public int hashCode() {
      if (hashCode == 0) {
        int result = super.hashCode();
        result = 31 * result + (angleRotation != +0.0f ? Float.floatToIntBits(angleRotation) : 0);
        hashCode = result;
      }
      return hashCode;
    }

    public GdsHorizCoordSys makeHorizCoordSys() {
      ucar.unidata.geoloc.projection.RotatedLatLon proj =
          new ucar.unidata.geoloc.projection.RotatedLatLon(latSouthPole, lonSouthPole,
              angleRotation);
      // LOOK dont transform - works for grib1 Q:/cdmUnitTest/transforms/HIRLAMhybrid.grib
      // LatLonPoint startLL = proj.projToLatLon(new ProjectionPointImpl(lo1, la1));
      //double startx = startLL.getLongitude();
      //double starty = startLL.getLatitude();
      return new GdsHorizCoordSys(getNameShort(), template, 0, scanMode, proj, lo1, deltaLon, la1,
          deltaLat, nx, ny, null);
    }

    public void testHorizCoordSys(Formatter f) {
      GdsHorizCoordSys cs = makeHorizCoordSys();
      LatLonPoint startLL = cs.proj.projToLatLon(new ProjectionPointImpl(lo1, la1));
      LatLonPoint endLL = cs.proj.projToLatLon(new ProjectionPointImpl(lo2, la2));

      f.format("%s testProjection%n", getClass().getName());
      f.format("  start at latlon= %s%n", startLL);
      f.format("    end at latlon= %s%n", endLL);

      ProjectionPointImpl endPP = (ProjectionPointImpl) cs.proj
          .latLonToProj(endLL, new ProjectionPointImpl());
      f.format("   start at proj coord= %s%n", new ProjectionPointImpl(cs.startx, cs.starty));
      f.format("     end at proj coord= %s%n", endPP);

      double endx = cs.startx + (nx - 1) * cs.dx;
      double endy = cs.starty + (ny - 1) * cs.dy;
      f.format("   should end at x= (%f,%f)%n", endx, endy);
    }

  }

  /*  LOOK not done yet
  Grid definition – spherical harmonic coefficients (including rotated, stretched or stretched and rotated)
  Octet No. Contents
  7–8   J – pentagonal resolution parameter
  9–10  K – pentagonal resolution parameter
  11–12 M – pentagonal resolution parameter
  13    Representation type (see Code table 9)
  14    Representation mode (see Code table 10)
  15–32 Set to zero (reserved)
  33–35 Latitude of the southern pole in millidegrees (integer)
        Latitude of pole of stretching in millidegrees (integer)
  36–38 Longitude of the southern pole in millidegrees (integer)
        Longitude of pole of stretching in millidegrees (integer)
  39–42 Angle of rotation (represented in the same way as the reference value)
        Stretching factor (representation as for the reference value)
  43–45 Latitude of pole of stretching in millidegrees (integer)
  46–48 Longitude of pole of stretching in millidegrees (integer)
  49–52 Stretching factor (representation as for the reference value
   */
  public static class SphericalHarmonicCoefficients extends Grib1Gds {

    final int j;
    final int k;
    final int m;
    final int type;
    final int mode;

    SphericalHarmonicCoefficients(byte[] data, int template) {
      super(data, template);

      j = getOctet2(7);
      k = getOctet2(9);
      m = getOctet2(11);
      type = getOctet(13);  // code table 9
      mode = getOctet(14);  // code table 10
    }

    @Override
    public float getDxRaw() {
      return 0;
    }

    @Override
    public float getDyRaw() {
      return 0;
    }

    @Override
    public GdsHorizCoordSys makeHorizCoordSys() {
      return null; // LOOK not done yet
    }

    @Override
    public void testHorizCoordSys(Formatter f) {
    }

    @Override
    public String toString() {
      return "SphericalHarmonicCoefficients{" +
              "j=" + j +
              ", k=" + k +
              ", m=" + m +
              ", type=" + type +
              ", mode=" + mode +
              "} " + super.toString();
    }
  }

  public static class UnknownGds extends Grib1Gds {

    UnknownGds(byte[] data, int template) {
      super(data, template);
    }

    @Override
    public float getDxRaw() {
      return 0;
    }

    @Override
    public float getDyRaw() {
      return 0;
    }

    @Override
    public GdsHorizCoordSys makeHorizCoordSys() {
      return null; // LOOK
    }

    @Override
    public void testHorizCoordSys(Formatter f) {

    }

    @Override
    public String toString() {
      return "UnknownGds{} " + super.toString();
    }
  }

}


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

import opendap.dap.Util;
import opendap.dap.parsers.ParseException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

/**
 * Default handler for OPeNDAP directory requests. This class is used
 * by AbstractServlet. This code exists as a seperate class in order to alleviate
 * code bloat in the AbstractServlet class. As such, it contains virtually no
 * state, just behaviors.
 *
 * @author Nathan David Potter
 */

public class GetDirHandler {

    private static final boolean _Debug = false;
    private String separator = "/";

    /**
     * ************************************************************************
     * Default handler for OPeNDAP directory requests. Returns an html document
     * with a list of all datasets on this server with links to their
     * DDS, DAS, Information, and HTML responses.
     *
     * @param rs       The request state object for this client request.
     * @see ReqState
     */
    public void sendDIR(ReqState rs)
            throws opendap.dap.DAP2Exception, ParseException {

        if (_Debug) System.out.println("sendDIR request = " + rs.getRequest());

        try {

            PrintWriter pw = new PrintWriter(new OutputStreamWriter(rs.getResponse().getOutputStream(), Util.UTF8));

            // ignore String ddxCacheDir = rs.getDDXCache(rs.getRootPath());
            String ddsCacheDir = rs.getDDSCache(rs.getRootPath());

            String thisServer = rs.getRequest().getRequestURL().toString();
            pw.println("");
            pw.println("");
            pw.println("<title>OPeNDAP Directory</title>");
            pw.println("<meta http-equiv=\"Content-Type\" content=\"text/html\">");
            pw.println("");

            pw.println("");

            pw.println("
OPeNDAP Directory for:
");
            pw.println("
" + thisServer + "
");

            // ignore printDIR(pw, ddxCacheDir, "DDX", thisServer);

            printDIR(pw, ddsCacheDir, "DDS", thisServer);
            pw.println("
");
            pw.println("");
            pw.flush();

        } catch (FileNotFoundException fnfe) {
            System.out.println("OUCH! FileNotFoundException: " + fnfe.getMessage());
            fnfe.printStackTrace(System.out);
        } catch (IOException ioe) {
            System.out.println("OUCH! IOException: " + ioe.getMessage());
            ioe.printStackTrace(System.out);
        }

    }

    private void printDIR(PrintWriter pw, String dirName, String dirType, String thisServer) {

        pw.println("
");
        pw.println("
" + dirType + "
");

        File dir = new File(dirName);

        if (dir.exists()) {

            if (dir.isDirectory()) {

                if (_Debug) System.out.println("lastIndexOf(" + separator + "): " + thisServer.lastIndexOf(separator));
                if (_Debug) System.out.println("length: " + thisServer.length());

                if (thisServer.lastIndexOf(separator) != (thisServer.length() - 1)) {
                    if (_Debug) System.out.println("Server URL does not end with: " + separator);
                    thisServer += separator;
                } else {
                    if (_Debug) System.out.println("Server URL ends with: " + separator);
                }

                File fList[] = dir.listFiles();

                pw.println("
");

                for (int i = 0; i < fList.length; i++) {
                    if (fList[i].isFile()) {

                        pw.println("
");

                        pw.print("    ");
                        pw.print("
");
                        pw.print("" +
                                fList[i].getName() +
                                ": ");
                        pw.print("
");
                        pw.println("	");

                        /* ignore
                        pw.print("    ");
                        pw.print("
");
                        pw.print(" DDX ");
                        pw.print("
");
                        pw.println("	"); */

                        pw.print("    ");
                        pw.print("
");
                        pw.print(" DDS ");
                        pw.print("
");
                        pw.println("	");

                        pw.print("    ");
                        pw.print("
");
                        pw.print(" DAS ");
                        pw.print("
");
                        pw.println("	");

                        pw.print("    ");
                        pw.print("
");
                        pw.print(" Information ");
                        pw.print("
");
                        pw.println("	");

                        pw.print("    ");
                        pw.print("
");
                        pw.print(" HTML Data Request Form ");
                        pw.print("
");
                        pw.println("	");

                        pw.println("
");
                    }
                }
                pw.println("
");

            } else {
                pw.println("
");
                pw.println("Specified " + dirType + " cache:
");
                pw.println("" + dirName + "
");
                pw.println("is not a directory!");
                pw.println("
");
            }
        } else {
            pw.println("
");
            pw.println("Cannot Find " + dirType + " Directory:
");
            pw.println("" + dirName + "
");
            pw.println("
");
        }

    }

}


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

import ucar.unidata.geoloc.*;

/**
 * Adapts a Projection interface into a subclass of
 * ProjectionImpl, so we can assume a Projection is a ProjectionImpl
 * without loss of generality.
 *
 * @author John Caron
 * @see Projection
 * @see ProjectionImpl
 */

public class ProjectionAdapter extends ProjectionImpl {

  /**
   * projection to adapt
   */
  private Projection proj;

  /**
   * Create a ProjectionImpl from the projection
   *
   * @param proj projection
   * @return a ProjectionImpl representing the projection
   */
  static public ProjectionImpl factory(Projection proj) {
    if (proj instanceof ProjectionImpl) {
      return (ProjectionImpl) proj;
    }
    return new ProjectionAdapter(proj);
  }

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new ProjectionAdapter(proj);
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Create a new ProjectionImpl from a Projection
   *
   * @param proj projection to use
   */
  public ProjectionAdapter(Projection proj) {
    super(proj.getName(), proj instanceof LatLonProjection);
    this.proj = proj;
  }

  /**
   * Get the class name
   *
   * @return the class name
   */
  public String getClassName() {
    return proj.getClassName();
  }

  /**
   * Get the parameters as a String
   *
   * @return the parameters as a String
   */
  public String paramsToString() {
    return proj.paramsToString();
  }

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

  @Override
  public int hashCode() {
    return proj.hashCode();
  }

  /**
   * 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) {
    return proj.latLonToProj(latlon, 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) {
    return proj.projToLatLon(world, result);
  }

  /**
   * Does the line between these two points cross the projection "seam".
   *
   * @param pt1 the line goes between these two points
   * @param pt2 the line goes between these two points
   * @return false if there is no seam
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    return proj.crossSeam(pt1, pt2);
  }

  /**
   * Get a reasonable bounding box for this projection.
   *
   * @return reasonable bounding box
   */
  public ProjectionRect getDefaultMapArea() {
    return proj.getDefaultMapArea();
  }

}

/*
 *  Change History:
 *  $Log: ProjectionAdapter.java,v $
 *  Revision 1.17  2006/11/18 19:03:23  dmurray
 *  jindent
 *
 *  Revision 1.16  2005/05/13 18:29:18  jeffmc
 *  Clean up the odd copyright symbols
 *
 *  Revision 1.15  2005/05/13 11:14:10  jeffmc
 *  Snapshot
 *
 *  Revision 1.14  2004/09/22 21:19:52  caron
 *  use Parameter, not Attribute; remove nc2 dependencies
 *
 *  Revision 1.13  2004/07/30 17:22:20  dmurray
 *  Jindent and doclint
 *
 *  Revision 1.12  2004/02/27 21:21:40  jeffmc
 *  Lots of javadoc warning fixes
 *
 *  Revision 1.11  2004/01/29 17:35:00  jeffmc
 *  A big sweeping checkin after a big sweeping reformatting
 *  using the new jindent.
 *
 *  jindent adds in javadoc templates and reformats existing javadocs. In the new javadoc
 *  templates there is a '_more_' to remind us to fill these in.
 *
 *  Revision 1.10  2003/07/12 23:08:59  caron
 *  add cvs headers, trailers
 *
 */


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.ft.point;

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

import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft.PointFeatureCC;
import ucar.nc2.ft.PointFeatureCCIterator;
import ucar.nc2.ft.StationProfileFeature;
import ucar.nc2.ft.StationProfileFeatureCollection;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.util.IOIterator;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.Station;

/**
 * Abstract superclass for StationProfileFeatureCollection
 * Subclasses must implement getNestedPointFeatureCollection
 * @author caron
 * @since Mar 20, 2008
 */
public abstract class StationProfileCollectionImpl extends PointFeatureCCCImpl implements StationProfileFeatureCollection {
  private volatile StationHelper stationHelper;

  public StationProfileCollectionImpl(String name, CalendarDateUnit timeUnit, String altUnits) {
    super( name, timeUnit, altUnits, FeatureType.STATION_PROFILE);
  }

  // Double-check idiom for lazy initialization of instance fields. See Effective Java 2nd Ed, p. 283.
  protected StationHelper getStationHelper() {
    if (stationHelper == null) {
      synchronized (this) {
        if (stationHelper == null) {
          try {
            stationHelper = createStationHelper();
          } catch (IOException e) {
            // The methods that will call getStationHelper() aren't declared to throw IOException, so we must
            // wrap it in an unchecked exception.
            throw new RuntimeException(e);
          }
        }
      }
    }

    assert stationHelper != null : "We screwed this up.";
    return stationHelper;
  }

  // Allow StationHelper to be lazily initialized.
  protected abstract StationHelper createStationHelper() throws IOException;

  @Override
  public LatLonRect getBoundingBox() {
    return getStationHelper().getBoundingBox();
  }

  @Override
  public List<StationFeature> getStationFeatures() throws IOException {
    return getStationHelper().getStationFeatures();
  }

  @Override
  public List<StationFeature> getStationFeatures(List<String> stnNames) {
    return getStationHelper().getStationFeaturesFromNames(stnNames);
  }

  @Override
  public List<StationFeature> getStationFeatures(ucar.unidata.geoloc.LatLonRect boundingBox) throws IOException {
    return getStationHelper().getStationFeatures(boundingBox);
  }

  @Override
  public StationFeature findStationFeature(String name) {
    return getStationHelper().getStation(name);
  }

  @Override
  public StationProfileFeature getStationProfileFeature(StationFeature s) throws IOException {
    return (StationProfileFeature) s; // LOOK
  }

  @Override
  public StationProfileCollectionImpl subset(List<StationFeature> stations) throws IOException {
    if (stations == null) return this;
    return new StationProfileFeatureCollectionSubset(this, stations);
  }

  @Override
  public StationProfileCollectionImpl subset(ucar.unidata.geoloc.LatLonRect boundingBox) throws IOException {
    return subset(getStationFeatures(boundingBox));
  }

  @Override
  public StationProfileFeatureCollection subset(LatLonRect boundingBox, CalendarDateRange dateRange) throws IOException {
    return subset(getStationFeatures(boundingBox), dateRange);
  }

  @Override
  public StationProfileCollectionImpl subset(List<StationFeature> stnsWanted, CalendarDateRange dateRange) throws IOException {
    if (dateRange == null)
      return subset(stnsWanted);

    List<StationFeature> subsetStations = new ArrayList<>();
    for (StationFeature sf : stnsWanted) {
      StationProfileFeature stsf = (StationProfileFeature) sf; // LOOK
      StationProfileFeature subset = stsf.subset(dateRange);
      subsetStations.add(subset);
    }

    return new StationProfileFeatureCollectionSubset(this, subsetStations);
  }

  public int compareTo(Station so) {
    return name.compareTo( so.getName());
  }

  // LOOK subset by filtering on the stations, but it would be easier if we could get the StationFeature from the Station
  private static class StationProfileFeatureCollectionSubset extends StationProfileCollectionImpl {
    private final StationProfileCollectionImpl from;
    private final List<StationFeature> stations;

    StationProfileFeatureCollectionSubset(StationProfileCollectionImpl from, List<StationFeature> stations) throws IOException {
      super( from.getName(), from.getTimeUnit(), from.getAltUnits());
      this.from = from;
      this.stations = stations;
    }

    @Override
    protected StationHelper createStationHelper() throws IOException {
      return from.getStationHelper().subset(stations);
    }

    @Override
    public PointFeatureCCIterator getNestedPointFeatureCollectionIterator() throws IOException {
      return new PointFeatureCCIteratorFiltered( from.getNestedPointFeatureCollectionIterator(), new Filter());
    }

    @Override
    public IOIterator<PointFeatureCC> getCollectionIterator() throws IOException {
      return new NestedCollectionIOIteratorAdapter<>(getNestedPointFeatureCollectionIterator());
    }

    private class Filter implements PointFeatureCCIterator.Filter {
      @Override
      public boolean filter(PointFeatureCC pointFeatureCollection) {
        StationProfileFeature stationFeature = (StationProfileFeature) pointFeatureCollection;
        return getStationHelper().getStation(stationFeature.getName()) != null;
      }
    }
  }

  // LOOK make into top-level; how come section didnt need this?
  public class NestedCollectionIOIteratorAdapter<T> implements IOIterator<T> {
    PointFeatureCCIterator pfIterator;

    public NestedCollectionIOIteratorAdapter(PointFeatureCCIterator pfIterator) {
      this.pfIterator = pfIterator;
    }

    @Override
    public boolean hasNext() {
      try {
        return pfIterator.hasNext();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public T next() {
      try {
        return (T) pfIterator.next();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

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

  @Override
  public Iterator<StationProfileFeature> iterator() {
    try {
      PointFeatureCCIterator pfIterator = getNestedPointFeatureCollectionIterator();
      return new NestedCollectionIteratorAdapter<>(pfIterator);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /////////////////////////////////////////////////////////////////////////////////////
  // deprecated
  protected PointFeatureCCIterator localIterator;

  @Override
  public boolean hasNext() throws IOException {
    if (localIterator == null) resetIteration();
    return localIterator.hasNext();
  }

  @Override
  public StationProfileFeature next() throws IOException {
    return (StationProfileFeature) localIterator.next();
  }

  @Override
  public void resetIteration() throws IOException {
    localIterator = getNestedPointFeatureCollectionIterator();
  }

}


/* Copyright Unidata */

package ucar.nc2.util;

import ucar.nc2.constants.CDM;

import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
  We want to refactor the escaping.
  1. EscapeStrings (this class)
  2. StringUtil2.escape() DONE
  3. URLnaming DONE
  4. Eliminate URLencode/decode in favor of guava.

  https://texnoblog.wordpress.com/2014/06/11/urlencode-just-one-is-not-enough/
  "Starting from version 15.0, Google's excellent Guava libraries have UrlEscapers class to do just that!
  The documentation is pretty clear, use
    1) urlPathSegmentEscaper to encode URL path segments (things that go between slashes),
    2) urlFragmentEscaper if you already have a path/with/slashes
    3) urlPathSegmentEscaper for the names and values of request parameters (things after the '?').

  from Guava:

  1) HtmlEscapers
  HTML escaping is particularly tricky: For example, some elements' text contents must not be HTML escaped.
  As a result, it is impossible to escape an HTML document correctly without domain-specific knowledge beyond what HtmlEscapers provides.
  We strongly encourage the use of HTML templating systems.

  2) XMLEscaper
  Escaper instances suitable for strings to be included in XML attribute values and elements' text contents. When possible, avoid manual escaping
  by using templating systems and high-level APIs that provide autoescaping. For example, consider XOM or JDOM.

  3) PercentEscaper
  PercentEscaper(String safeChars, boolean plusForSpace)
  Constructs a percent escaper with the specified safe characters and optional handling of the space character.

 */

public class EscapeStrings {
  static protected final String alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  static protected final String numeric = "0123456789";
  static protected final String alphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  static protected final String _allowableInUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&'()*+,-./:;=?@_~";
  static protected final String _allowableInUrlQuery = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()*+,-./:;=?@_~";
  static protected final String _allowableInDAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!~*'-\"./";
  static protected final String _allowableInOGC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
  static protected final char _URIEscape = '%';
  static protected final char _JavaEscape = '\\';
  static final byte blank = ((byte) ' ');
  static final byte plus = ((byte) '+');

  /**
   *
   * @param in   String to escape
   * @param allowable  allowedcharacters
   * @param esc   escape char prefix
   * @param spaceplus  true =>convert ' ' to '+'
     * @return
     */
    private static String xescapeString(String in, String allowable, char esc, boolean spaceplus) {
    try {
      StringBuffer out = new StringBuffer();
      if (in == null) return null;
      byte[] utf8 = in.getBytes(CDM.utf8Charset);
      byte[] allow8 = allowable.getBytes(CDM.utf8Charset);
      for (byte b : utf8) {
        if (b == blank && spaceplus) {
          out.append('+');
        } else {
          // search allow8
          boolean found = false;
          for (byte a : allow8) {
            if (a == b) {
              found = true;
              break;
            }
          }
          if (found) {
            out.append((char) b);
          } else {
            String c = Integer.toHexString(b);
            out.append(esc);
            if (c.length() < 2) out.append('0');
            out.append(c);
          }
        }
      }

      return out.toString();

    } catch (Exception e) {
      return in;
    }
  }

  private static String escapeString(String in, String allowable) {
    return xescapeString(in, allowable, _URIEscape, false);
  }

  /**
   * Given a string that contains WWW escape sequences, translate those escape
   * sequences back into ASCII characters. Return the modified string.
   *
   * @param in        The string to modify.
   * @param escape    The character used to signal the begining of an escape sequence.
   *                  param except If there is some escape code that should not be removed by
   *                  this call (e.g., you might not want to remove spaces, %20) use this
   *                  parameter to specify that code. The function will then transform all
   *                  escapes except that one.
   * @param spaceplus True if spaces should be replaced by '+'.
   * @return The modified string.
   */

  private static String xunescapeString(String in, char escape, boolean spaceplus) {
    try {
      if (in == null) return null;

      byte[] utf8 = in.getBytes(CDM.utf8Charset);
      byte escape8 = (byte) escape;
      byte[] out = new byte[utf8.length]; // Should be max we need

      int index8 = 0;
      for (int i = 0; i < utf8.length; ) {
        byte b = utf8[i++];
        if (b == plus && spaceplus) {
          out[index8++] = blank;
        } else if (b == escape8) {
          // check to see if there are enough characters left
          if (i + 2 <= utf8.length) {
            b = (byte) (fromHex(utf8[i]) << 4 | fromHex(utf8[i + 1]));
            i += 2;
          }
        }
        out[index8++] = b;
      }
      return new String(out, 0, index8, CDM.utf8Charset);
    } catch (Exception e) {
      return in;
    }

  }

  private static String unescapeString(String in) {
    return xunescapeString(in, _URIEscape, false);
  }

  static final byte hexa = (byte) 'a';
  static final byte hexf = (byte) 'f';
  static final byte hexA = (byte) 'A';
  static final byte hexF = (byte) 'F';
  static final byte hex0 = (byte) '0';
  static final byte hex9 = (byte) '9';
  static final byte ten = (byte) 10;

  private static byte fromHex(byte b) throws NumberFormatException {
    if (b >= hex0 && b <= hex9) return (byte) (b - hex0);
    if (b >= hexa && b <= hexf) return (byte) (ten + (b - hexa));
    if (b >= hexA && b <= hexF) return (byte) (ten + (b - hexA));
    throw new NumberFormatException("Illegal hex character: " + b);
  }

  /**
   * Define the DEFINITIVE opendap identifier unescape function.
   *
   * @param id The identifier to unescape.
   * @return The unescaped identifier.
   */
  public static String unescapeDAPIdentifier(String id) {
    String s;
    try {
      s = unescapeString(id);
    } catch (Exception e) {
      s = null;
    }
    return s;
  }

  static private final Pattern p = Pattern.compile("([\\w]+)://([.\\w]+(:[\\d]+)?)([/][^?#])?([?][^#]*)?([#].*)?");

  public static String escapeURL(String url) {
    String protocol;
    String authority;
    String path;
    String query;
    String fragment;
    if (false) {
      // We split the url ourselves to minimize character dependencies
      Matcher m = p.matcher(url);
      boolean match = m.matches();
      if (!match) return null;
      protocol = m.group(1);
      authority = m.group(2);
      path = m.group(3);
      query = m.group(4);
      fragment = m.group(5);
    } else {// faster, but may not work quite right
      URL u;
      try {
        u = new URL(url);
      } catch (MalformedURLException e) {
        return null;
      }
      protocol = u.getProtocol();
      authority = u.getAuthority();
      path = u.getPath();
      query = u.getQuery();
      fragment = u.getRef();
    }
    // Reassemble
    StringBuilder ret = new StringBuilder(protocol);
    ret.append("://");
    ret.append(authority);
    if (path != null && path.length() > 0) {
      // Encode pieces between '/'
      String pieces[] = path.split("[/]", -1);
      for (int i = 0; i < pieces.length; i++) {
        String p = pieces[i];
        if (p == null) p = "";
        if (i > 0) ret.append("/");
        ret.append(urlEncode(p));
      }
    }
    if (query != null && query.length() > 0) {
      ret.append("?");
      ret.append(escapeURLQuery(query));
    }
    if (fragment != null && fragment.length() > 0) {
      ret.append("#");
      ret.append(urlEncode(fragment));
    }
    return ret.toString();
  }

  /**
   * Define the DEFINITIVE URL constraint expression escape function.
   *
   * @param ce The expression to modify.
   * @return The escaped expression.
   */
  public static String escapeURLQuery(String ce) {
    try {
      ce = escapeString(ce, _allowableInUrlQuery);
    } catch (Exception e) {
      ce = null;
    }
    return ce;
  }

  /**
   * Define the DEFINITIVE URL constraint expression unescape function.
   *
   * @param ce The expression to unescape.
   * @return The unescaped expression.
   */
  public static String unescapeURLQuery(String ce) {
    try {
      ce = unescapeString(ce);
    } catch (Exception e) {
      ce = null;
    }
    return ce;
  }

  /**
   * Define the DEFINITIVE URL escape function.
   * Note that the whole string is escaped, so
   * be careful what you pass into this procedure.
   *
   * @param s The string to modify.
   * @return The escaped expression.
   */
  private static String urlEncode(String s) {
    //try {s = URLEncoder.encode(s,"UTF-8");} catch(Exception e) {s = null;}
    s = escapeString(s, _allowableInUrl);
    return s;
  }

  /**
   * Define the DEFINITIVE URL unescape function.
   *
   * @param s The string to unescape.
   * @return The unescaped expression.
   */
  public static String urlDecode(String s) {
    try {
      //s = unescapeString(s, _URIEscape, "", false);
      s = URLDecoder.decode(s, "UTF-8");
    } catch (Exception e) {
      s = null;
    }
    return s;
  }

  /**
   * Decode all of the parts of the url including query and fragment
   *
   * @param url the url to encode
   */
  public static String unescapeURL(String url) {
    String newurl;
    newurl = urlDecode(url);
    return newurl;
  }

  /**
   * Define the DAP escape identifier function.
   *
   * @param s The string to encode.
   * @return The escaped string.
   */
  public static String escapeDAPIdentifier(String s) {
    return escapeString(s, _allowableInDAP);
  }

  /**
   * Define the OGC Web Services escape function.
   *
   * @param s The string to encode.
   * @return The escaped string.
   */
  public static String escapeOGC(String s) {
    return escapeString(s, _allowableInOGC);
  }

  static public void testOGC() {
    for (char c : (alphaNumeric + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~").toCharArray()) {
      String encoded = EscapeStrings.escapeOGC("" + c);
      System.err.printf("|%c|=|%s|%n", c, encoded);
    }
  }

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

  /**
   * backslash escape a string
   *
   * @param x             escape this; may be null
   * @param reservedChars these chars get a backslash in front of them
   * @return escaped string
   */
  static public String backslashEscape(String x, String reservedChars) {
    if (x == null) {
      return null;
    } else if (reservedChars == null) {
      return x;
    }

    boolean ok = true;
    for (int pos = 0; pos < x.length(); pos++) {
      char c = x.charAt(pos);
      if (reservedChars.indexOf(c) >= 0) {
        ok = false;
        break;
      }
    }
    if (ok) return x;

    // gotta do it
    StringBuilder sb = new StringBuilder(x);
    for (int pos = 0; pos < sb.length(); pos++) {
      char c = sb.charAt(pos);
      if (reservedChars.indexOf(c) < 0) {
        continue;
      }

      sb.setCharAt(pos, '\\');
      pos++;
      sb.insert(pos, c);
      pos++;
    }

    return sb.toString();
  }

  /**
   * backslash unescape a string
   *
   * @param x unescape this
   * @return string with \c -> c
   */
  static public String backslashUnescape(String x) {
    if (!x.contains("\\")) return x;

    // gotta do it
    StringBuilder sb = new StringBuilder(x.length());
    for (int pos = 0; pos < x.length(); pos++) {
      char c = x.charAt(pos);
      if (c == '\\') {
        c = x.charAt(++pos); // skip backslash, get next cha
      }
      sb.append(c);
    }

    return sb.toString();
  }

  /**
   * Tokenize an escaped name using "." as delimiter, skipping "\."
   *
   * @param escapedName an escaped name
   * @return list of tokens
   */
  public static List<String> tokenizeEscapedName(String escapedName) {
    List<String> result = new ArrayList<>();
    int pos = 0;
    int start = 0;
    while (true) {
      pos = escapedName.indexOf(sep, pos + 1);
      if (pos <= 0) break;
      if ((pos > 0) && escapedName.charAt(pos - 1) != '\\') {
        result.add(escapedName.substring(start, pos));
        start = pos + 1;
      }
    }
    result.add(escapedName.substring(start, escapedName.length())); // remaining
    return result;
  }

  static private final int sep = '.';

  /**
   * Find first occurence of char c in escapedName, excluding escaped c.
   *
   * @param escapedName search in this string
   * @param c           for this char but not \\cha
   * @return pos in string, or -1
   */
  public static int indexOf(String escapedName, char c) {
    int pos = 0;
    while (true) {
      pos = escapedName.indexOf(c, pos + 1);
      if (pos <= 0) return pos;
      if ((pos > 0) && escapedName.charAt(pos - 1) != '\\') return pos;
    }
  }

  // Some handy definitons.
  static final String xmlGT = ">";
  static final String xmlLT = "<";
  static final String xmlAmp = "&";
  static final String xmlApos = "'";
  static final String xmlQuote = """;

  public static String normalizeToXML(String s) {
    StringBuffer sb = new StringBuffer(s);
    for (int offset = 0; offset < sb.length(); offset++) {
      char c = sb.charAt(offset);
      switch (c) {
        case '>': // GreaterThan
          sb.replace(offset, offset + 1, xmlGT);
          break;
        case '<': // Less Than
          sb.replace(offset, offset + 1, xmlLT);
          break;
        case '&': // Ampersand
          sb.replace(offset, offset + 1, xmlAmp);
          break;
        case '\'': // Single Quote
          sb.replace(offset, offset + 1, xmlApos);
          break;
        case '\"': // Double Quote
          sb.replace(offset, offset + 1, xmlQuote);
          break;
        default:
          break;
      }
    }
    return (sb.toString());

  }

  /**
   * Given a backslash escaped name,
   * convert to a DAP escaped name
   *
   * @param bs the string to DAP encode; may have backslash escapes
   * @return escaped string
   */

  public static String backslashToDAP(String bs) {
    StringBuilder buf = new StringBuilder();
    int len = bs.length();
    for (int i = 0; i < len; i++) {
      char c = bs.charAt(i);
      if (i < (len - 1) && c == '\\') {
        c = bs.charAt(++i);
      }
      if (_allowableInDAP.indexOf(c) < 0) {
        buf.append(_URIEscape);
        // convert the char to hex
        String ashex = Integer.toHexString((int) c);
        if (ashex.length() < 2) buf.append('0');
        buf.append(ashex);
      } else
        buf.append(c);
    }
    return buf.toString();
  }

  /**
   * Given a DAP (attribute) string, insert backslashes
   * before '"' and '/' characters. This code also escapes
   * control characters, although the spec does not call for it;
   * make that code conditional.
   */
  static public String backslashEscapeDapString(String s) {
    StringBuilder buf = new StringBuilder();
    for(int i=0;i<s.length();i++) {
	int c = s.charAt(i);
        if(true) {
            if(c < ' ') {
                switch (c) {
                case '\n': case '\r': case '\t': case '\f':
                    buf.append((char)c);
                    break;
                default:
                    buf.append(String.format("\\x%02x",(c&0xff)));
                    break;
                }
                continue;
            }
        }
        if(c == '"') {
            buf.append("\\\"");
        } else if(c == '\\') {
            buf.append("\\\\");
        } else
            buf.append((char)c);
    }
    return buf.toString();
  }

  /**
     * Given a CDM string, insert backslashes
     * before <toescape> characters.
     */
    static public String backslashEscapeCDMString(String s, String toescape)
    {
      if(toescape == null || toescape.length() == 0) return s;
      if(s == null || s.length() == 0) return s;
      StringBuilder buf = new StringBuilder();
      for(int i=0;i<s.length();i++) {
        int c = s.charAt(i);
        if(toescape.indexOf(c) >= 0) {
          buf.append('\\');
        }
        buf.append((char)c);
      }
      return buf.toString();
    }

}