/** Get the scaling when it is described as a matrix */
  private boolean extractScaler2(double crpix1, double crpix2) throws TransformationException {

    // Look for the CD matrix...
    //
    double m11 = h.getDoubleValue("CD" + lonAxis + "_" + lonAxis, NaN);
    double m12 = h.getDoubleValue("CD" + lonAxis + "_" + latAxis, NaN);
    double m21 = h.getDoubleValue("CD" + latAxis + "_" + lonAxis, NaN);
    double m22 = h.getDoubleValue("CD" + latAxis + "_" + latAxis, NaN);

    boolean matrix = !isNaN(m11 + m12 + m21 + m22);
    if (!matrix) {
      return false;
    }
    wcsKeys.put("CD1_1", m11);
    wcsKeys.put("CD1_2", m12);
    wcsKeys.put("CD2_1", m21);
    wcsKeys.put("CD2_2", m22);

    m11 = toRadians(m11);
    m12 = toRadians(m12);
    m21 = toRadians(m21);
    m22 = toRadians(m22);

    // we have
    //   t = a11 (x-x0) + a12 (y-y0); u = a21(x-x0) + a22(y-y0)
    //       t = a11x + a12y - a11 x0 - a12 y0;
    //
    Scaler s =
        new Scaler(-m11 * crpix1 - m12 * crpix2, -m21 * crpix1 - m22 * crpix2, m11, m12, m21, m22);

    s = s.inverse();

    // Are longitude and latitude in unusual order?
    if (lonAxis > latAxis) {
      s.interchangeAxes();
    }
    this.scale = s;
    add(s);
    setWCSScale(s);
    return true;
  }
  /**
   * Write FITS WCS keywords given key values. Only relatively simple WCSs are handled here. We
   * assume we are dealing with axes 1 and 2.
   *
   * @param h The header to be updated.
   * @param s A Scaler giving the transformation between standard projection coordinates and
   *     pixel/device coordinates.
   * @param projString A three character string giving the projection used. Supported projections
   *     are: "Tan", "Sin", "Ait", "Car", "Zea".
   * @param coordString A string giving the coordinate system used. The first character gives the
   *     general frame. For most frames the remainder of the string gives the equinox of the
   *     coordinate system. E.g., J2000, B1950, Galactic, "E2000", "H2020.10375".
   */
  public void updateHeader(
      Header h, Scaler s, double[] crval, String projString, String coordString) throws Exception {

    if (proj.isFixedProjection()) {
      h.addValue("CRVAL1", toDegrees(proj.getReferencePoint()[0]), "Fixed reference center");
      h.addValue("CRVAL2", toDegrees(proj.getReferencePoint()[1]), "Fixed reference center");
    } else {
      h.addValue("CRVAL1", crval[0], "Reference longitude");
      h.addValue("CRVAL2", crval[1], "Reference latitude");
    }

    coordString = coordString.toUpperCase();
    String[] prefixes = new String[2];
    char c = coordString.charAt(0);
    if (c == 'J' || c == 'I') {
      h.addValue("RADESYS", "FK5", "Coordinate system");
      prefixes[0] = "RA--";
      prefixes[1] = "DEC-";
    } else if (c == 'B') {
      h.addValue("RADESYS", "FK4", "Coordinate system");
      prefixes[0] = "RA--";
      prefixes[1] = "DEC-";
    } else {
      prefixes[0] = c + "LON";
      prefixes[1] = c + "LAT";
    }

    if (c != 'G' && c != 'I') {
      try {
        double equinox = Double.parseDouble(coordString.substring(1));
        h.addValue("EQUINOX", equinox, "Epoch of the equinox");
      } catch (Exception e) {
        // Couldn't parse out the equinox
      }
    }
    if (c == 'I') {
      h.addValue("EQUINOX", 2000, "ICRS coordinates");
    }

    String upProj = projString.toUpperCase();

    h.addValue("CTYPE1", prefixes[0] + "-" + upProj, "Coordinates -- projection");
    h.addValue("CTYPE2", prefixes[1] + "-" + upProj, "Coordinates -- projection");

    // Note that the scaler transforms from the standard projection
    // coordinates to the pixel coordinates.
    //     P = P0 + M X  where X is the standard coordinates and P is the
    // pixel coordinates.  So the reference pixels are just the constants
    // in the scaler.
    // Remember that FITS pixels are offset by 0.5 from 0 offset pixels.

    h.addValue("CRPIX1", s.x0 + 0.5, "X reference pixel");
    h.addValue("CRPIX2", s.y0 + 0.5, "Y reference pixel");

    // Remember that the FITS values are of the form
    //    X = M(P-P0)
    // so we'll need to invert the scaler.
    //
    // Do we need a matrix?
    if (abs(s.a01) < 1.e-14 && abs(s.a10) < 1.e-14) {
      // No cross terms, so we'll just use CDELTs
      h.addValue("CDELT1", toDegrees(1 / s.a00), "X scale");
      h.addValue("CDELT2", toDegrees(1 / s.a11), "Y scale");
    } else {
      // We have cross terms.  It's simplest
      // just to use the CD matrix and not worry about
      // normalization.  First invert the matrix to get
      // the transformation in the direction that FITS uses.
      Scaler rev = s.inverse();
      h.addValue("CD1_1", toDegrees(rev.a00), "Matrix element");
      h.addValue("CD1_2", toDegrees(rev.a01), "Matrix element");
      h.addValue("CD2_1", toDegrees(rev.a10), "Matrix element");
      h.addValue("CD2_2", toDegrees(rev.a11), "Matrix element");
    }
  }
  /** Handle the NEAT special projection */
  private void doNeatWCS() throws TransformationException {

    // The NEAT transformation from the standard spherical system
    // includes:
    //   Transformation to J2000 spherical coordinates (a null operation)
    //   A tangent plane projection to the standard plane
    //   A scaler transformation to corrected pixel coordinates.
    //   A distorter to distorted pixel coordinates
    //   A scaler transformation of distorted coordinates to actual pixels

    CoordinateSystem csys = CoordinateSystem.factory("J2000");
    this.csys = csys;

    // The RA0/DEC0 pair are the actual center of the projection.

    double cv1 = toRadians(h.getDoubleValue("RA0"));
    double cv2 = toRadians(h.getDoubleValue("DEC0"));
    wcsKeys.put("CRVAL1", toDegrees(cv1));
    wcsKeys.put("CRVAL2", toDegrees(cv2));

    Projection proj = new Projection("Tan", new double[] {cv1, cv2});
    this.proj = proj;

    double cd1 = toRadians(h.getDoubleValue("CDELT1"));
    double cd2 = toRadians(h.getDoubleValue("CDELT2"));
    wcsKeys.put("CDELT1", toDegrees(cd1));
    wcsKeys.put("CDELT2", toDegrees(cd2));

    wcsScale = abs(cd1);

    double cp1 = h.getDoubleValue("CRPIX1");
    double cp2 = h.getDoubleValue("CRPIX2");

    wcsKeys.put("CPRIX1", cp1);
    wcsKeys.put("CRPIX2", cp2);

    wcsKeys.put("CTYPE1", "RA---XTN");
    wcsKeys.put("CTYPE2", "DEC--XTN");

    Scaler s1 = new Scaler(0., 0., -1 / cd1, 0, 0, -1 / cd2);

    // Note that the the A0,A1,A2, B0,B1,B2 rotation
    // is relative to the original pixel values, so
    // we need to put this in the secondary scaler.
    //
    double x0 = h.getDoubleValue("X0");
    double y0 = h.getDoubleValue("Y0");

    Distorter dis =
        new skyview.geometry.distorter.Neat(
            h.getDoubleValue("RADIAL"), h.getDoubleValue("XRADIAL"), h.getDoubleValue("YRADIAL"));

    double a0 = h.getDoubleValue("A0");
    double a1 = h.getDoubleValue("A1");
    double a2 = h.getDoubleValue("A2");
    double b0 = h.getDoubleValue("B0");
    double b1 = h.getDoubleValue("B1");
    double b2 = h.getDoubleValue("B2");

    // The reference pixel is to be computed in the distorted frame.
    double[] cpix = new double[] {cp1, cp2};
    double[] cout = new double[2];
    dis.transform(cpix, cout);
    Scaler s2 =
        new Scaler(
            cout[0] - a0 - a1 * x0 - a2 * y0,
            cout[1] - b0 - b2 * x0 - b1 * y0,
            -(1 + a1),
            -a2,
            -b2,
            -(1 + b1));

    wcsKeys.put("A0", a0);
    wcsKeys.put("A1", a1);
    wcsKeys.put("A2", a2);
    wcsKeys.put("B0", b0);
    wcsKeys.put("B1", b1);
    wcsKeys.put("B2", b2);

    this.distort = dis;
    add(csys.getSphereDistorter());
    add(csys.getRotater());
    add(proj.getRotater());
    add(proj.getProjecter());
    this.scale = s1;
    // Note that s1 is defined from the projection plane to the pixels coordinates,
    // so we don't need to invert it.
    //
    // But the second scaler, s2,  used in the NEAT correction
    // is defined in the direction from pixels to sphere, so we
    // need to take its inverse.
    this.scale = this.scale.add(s2.inverse());
    add(this.scale);
    add(dis);
  }
  /** Handle a DSS projection */
  private void doDSSWCS() throws TransformationException {

    double plateRA =
        h.getDoubleValue("PLTRAH")
            + h.getDoubleValue("PLTRAM") / 60
            + h.getDoubleValue("PLTRAS") / 3600;
    plateRA = toRadians(15 * plateRA);
    wcsKeys.put("PLTRAH", h.getDoubleValue("PLTRAH"));
    wcsKeys.put("PLTRAM", h.getDoubleValue("PLTRAM"));
    wcsKeys.put("PLTRAS", h.getDoubleValue("PLTRAS"));

    double plateDec =
        h.getDoubleValue("PLTDECD")
            + h.getDoubleValue("PLTDECM") / 60
            + h.getDoubleValue("PLTDECS") / 3600;
    plateDec = toRadians(plateDec);

    if (h.getStringValue("PLTDECSN").substring(0, 1).equals("-")) {
      plateDec = -plateDec;
    }
    wcsKeys.put("PLTDECD", h.getDoubleValue("PLTDECD"));
    wcsKeys.put("PLTDECM", h.getDoubleValue("PLTDECM"));
    wcsKeys.put("PLTDECS", h.getDoubleValue("PLTDECS"));
    wcsKeys.put("PLTDECSN", h.getStringValue("PLTDECSN"));

    double plateScale = h.getDoubleValue("PLTSCALE");
    double xPixelSize = h.getDoubleValue("XPIXELSZ");
    double yPixelSize = h.getDoubleValue("YPIXELSZ");
    wcsKeys.put("PLTSCALE", plateScale);
    wcsKeys.put("XPIXELSZ", xPixelSize);
    wcsKeys.put("YPIXELSZ", yPixelSize);

    double[] xCoeff = new double[20];
    double[] yCoeff = new double[20];

    for (int i = 1; i <= 20; i += 1) {
      xCoeff[i - 1] = h.getDoubleValue("AMDX" + i);
      yCoeff[i - 1] = h.getDoubleValue("AMDY" + i);
      wcsKeys.put("AMDX" + i, xCoeff[i - 1]);
      wcsKeys.put("AMDY" + i, yCoeff[i - 1]);
    }

    double[] ppo = new double[6];
    for (int i = 1; i <= 6; i += 1) {
      ppo[i - 1] = h.getDoubleValue("PPO" + i);
      wcsKeys.put("PPO" + i, ppo[i - 1]);
    }

    double plateCenterX = ppo[2];
    double plateCenterY = ppo[5];

    double cdelt1 = -plateScale / 1000 * xPixelSize / 3600;
    double cdelt2 = plateScale / 1000 * yPixelSize / 3600;

    wcsScale = abs(cdelt1);

    // This gives cdelts in degrees per pixel.

    // CNPIX pixels use a have the first pixel going from 1 - 2 so they are
    // off by 0.5 from FITS (which in turn is offset by 0.5 from the internal
    // scaling, but we handle that elsewhere).
    double crpix1 = plateCenterX / xPixelSize - h.getDoubleValue("CNPIX1", 0) - 0.5;
    double crpix2 = plateCenterY / yPixelSize - h.getDoubleValue("CNPIX2", 0) - 0.5;
    wcsKeys.put("CNPIX1", h.getDoubleValue("CNPIX1", 0));
    wcsKeys.put("CNPIX2", h.getDoubleValue("CNPIX2", 0));

    Projection proj = new Projection("Tan", new double[] {plateRA, plateDec});
    this.proj = proj;
    CoordinateSystem coords = CoordinateSystem.factory("J2000");
    this.csys = coords;

    cdelt1 = toRadians(cdelt1);
    cdelt2 = toRadians(cdelt2);

    Scaler s = new Scaler(-cdelt1 * crpix1, -cdelt2 * crpix2, cdelt1, 0, 0, cdelt2);

    // Got the transformers ready.  Add them in properly.
    add(coords.getSphereDistorter());
    add(coords.getRotater());
    add(proj.getRotater());
    add(proj.getProjecter());

    this.distort =
        new skyview.geometry.distorter.DSS(
            plateRA, plateDec, xPixelSize, yPixelSize, plateScale, ppo, xCoeff, yCoeff);
    add(this.distort);

    this.scale = s.inverse();

    add(this.scale);
  }
  /** Get the scaling when CDELT is specified */
  private boolean extractScaler1(double crpix1, double crpix2) throws TransformationException {

    boolean matrix = false;

    double cdelt1 = h.getDoubleValue("CDELT" + lonAxis, NaN);
    double cdelt2 = h.getDoubleValue("CDELT" + latAxis, NaN);
    wcsKeys.put("CDELT1", cdelt1);
    wcsKeys.put("CDELT2", cdelt2);
    if (isNaN(cdelt1) || isNaN(cdelt2)) {
      return false;
    }

    // We use 1 indexing to match better with the FITS files.
    double m11, m12, m21, m22;

    // We've got minimal information...  We might have more.
    double crota = h.getDoubleValue("CROTA" + latAxis, NaN);
    if (!isNaN(crota) && crota != 0) {
      wcsKeys.put("CROTA2", crota);
      crota = toRadians(crota);

      m11 = cos(crota);
      m12 = sin(crota);
      m21 = -sin(crota);
      m22 = cos(crota);
      matrix = true;

    } else {

      m11 = h.getDoubleValue("PC" + lonAxis + "_" + lonAxis, NaN);
      m12 = h.getDoubleValue("PC" + lonAxis + "_" + latAxis, NaN);
      m21 = h.getDoubleValue("PC" + latAxis + "_" + lonAxis, NaN);
      m22 = h.getDoubleValue("PC" + latAxis + "_" + latAxis, NaN);
      matrix = !isNaN(m11 + m12 + m21 + m22);
      if (matrix) {
        wcsKeys.put("PC1_1", m11);
        wcsKeys.put("PC1_2", m12);
        wcsKeys.put("PC2_1", m21);
        wcsKeys.put("PC2_2", m22);
      }
    }

    // Note that Scaler is defined with parameters t = x0 + a00 x + a01 y; u = y0 + a10 x + a11 y
    // which is different from what we have here...
    //    t = scalex (x-x0),  u = scaley (y-y0)
    //    t = scalex x - scalex x0; u = scaley y - scaley y0
    // or
    //    t = scalex [a11 (x-x0) + a12 (y-y0)], u = scaley [a21 (x-x0) + a22 (y-y0)] ->
    //       t = scalex a11 x - scalex a11 x0 + scalex a12 y + scalex a12 y0         ->
    //       t = - scalex (a11 x0 + a12 y0) + scalex a11 x + scalex a12 y (and similarly for u)

    Scaler s;
    cdelt1 = toRadians(cdelt1);
    cdelt2 = toRadians(cdelt2);
    if (!matrix) {
      s = new Scaler(-cdelt1 * crpix1, -cdelt2 * crpix2, cdelt1, 0, 0, cdelt2);
    } else {
      s =
          new Scaler(
              -cdelt1 * (m11 * crpix1 + m12 * crpix2),
              -cdelt2 * (m21 * crpix1 + m22 * crpix2),
              cdelt1 * m11,
              cdelt1 * m12,
              cdelt2 * m21,
              cdelt2 * m22);
    }

    // Note that this scaler transforms from pixel coordinates to standard projection
    // plane coordinates.  We want the inverse transformation as the scaler.
    s = s.inverse();

    // Are lon and lat in unusual order?
    if (lonAxis > latAxis) {
      s.interchangeAxes();
    }

    this.scale = s;
    add(s);
    setWCSScale(s);

    return true;
  }