All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.unidata.geoloc.projection.Sinusoidal Maven / Gradle / Ivy

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

package ucar.unidata.geoloc.projection;

import com.google.common.math.DoubleMath;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.util.Misc;
import ucar.unidata.geoloc.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import static ucar.unidata.geoloc.LatLonPointImmutable.INVALID;

/**
 * Sinusoidal projection, spherical earth.
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 243
 *
 * @author John Caron
 * @since Feb 24, 2013
 */

public class Sinusoidal extends ProjectionImpl {
  private final double earthRadius;
  private double centMeridian; // central Meridian in degrees
  private double falseEasting, falseNorthing;

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result = new Sinusoidal(getCentMeridian(), getFalseEasting(), getFalseNorthing(), getEarthRadius());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

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

  /**
   * Construct a Sinusoidal Projection.
   *
   * @param centMeridian central Meridian (degrees)
   * @param false_easting false_easting in km
   * @param false_northing false_northing in km
   * @param radius earth radius in km
   */
  public Sinusoidal(double centMeridian, double false_easting, double false_northing, double radius) {
    super(CF.SINUSOIDAL, false);

    this.centMeridian = centMeridian;
    this.falseEasting = false_easting;
    this.falseNorthing = false_northing;
    this.earthRadius = radius;

    addParameter(CF.GRID_MAPPING_NAME, CF.SINUSOIDAL);
    addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, centMeridian);
    addParameter(CF.EARTH_RADIUS, earthRadius * 1000);

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

  }

  /**
   * Get the central Meridian in degrees
   *
   * @return the central Meridian
   */
  public double getCentMeridian() {
    return centMeridian;
  }

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

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

  public double getEarthRadius() {
    return earthRadius;
  }

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

  /**
   * Set the central Meridian
   *
   * @param centMeridian central Meridian in degrees
   */
  @Deprecated
  public void setCentMeridian(double centMeridian) {
    this.centMeridian = centMeridian;
  }

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

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

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


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

    Sinusoidal that = (Sinusoidal) o;

    if (Double.compare(that.centMeridian, centMeridian) != 0) {
      return false;
    }
    if (Double.compare(that.earthRadius, earthRadius) != 0) {
      return false;
    }
    if (Double.compare(that.falseEasting, falseEasting) != 0) {
      return false;
    }
    return Double.compare(that.falseNorthing, falseNorthing) == 0;

  }

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

  @Override
  public String toString() {
    String sb = "Sinusoidal" + "{earthRadius=" + earthRadius + ", centMeridian=" + centMeridian + ", falseEasting="
        + falseEasting + ", falseNorthing=" + falseNorthing + '}';
    return sb;
  }

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

  /**
   * 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
   */
  @Override
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (LatLonPoints.isInfinite(pt1) || LatLonPoints.isInfinite(pt2)) {
      return true;
    }

    // opposite signed long lines
    double x1 = pt1.getX() - falseEasting;
    double x2 = pt2.getX() - falseEasting;
    return (x1 * x2 < 0) && (Math.abs(x1 - x2) > earthRadius);
  }

  /**
   * 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
   */
  @Override
  public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
    double deltaLon_d = LatLonPoints.range180(latLon.getLongitude() - centMeridian);
    double fromLat_r = Math.toRadians(latLon.getLatitude());

    double toX = earthRadius * Math.toRadians(deltaLon_d) * Math.cos(fromLat_r);
    double toY = earthRadius * fromLat_r; // p 247 Snyder

    result.setLocation(toX + falseEasting, toY + falseNorthing);
    return result;
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   *
   * @param world convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint the lat/lon coordinates
   */
  @Override
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double fromX = world.getX() - falseEasting;
    double fromY = world.getY() - falseNorthing;

    double toLat_r = fromY / earthRadius;
    double toLon_r;

    if (Misc.nearlyEquals(Math.abs(toLat_r), Math.PI / 2.0, 1e-10)) {
      toLat_r = toLat_r < 0 ? -Math.PI / 2.0 : Math.PI / 2.0;
      toLon_r = Math.toRadians(centMeridian); // if lat == +- pi/2, set lon = centMeridian (Snyder 248)
    } else if (Math.abs(toLat_r) < Math.PI / 2.0) {
      toLon_r = Math.toRadians(centMeridian) + fromX / (earthRadius * Math.cos(toLat_r));
    } else {
      return INVALID; // Projection point is off the map.
    }

    if (Misc.nearlyEquals(Math.abs(toLon_r), Math.PI, 1e-10)) {
      toLon_r = toLon_r < 0 ? -Math.PI : Math.PI;
    } else if (Math.abs(toLon_r) > Math.PI) {
      return INVALID; // Projection point is off the map.
    }

    result.setLatitude(Math.toDegrees(toLat_r));
    result.setLongitude(Math.toDegrees(toLon_r));
    return result;
  }

  @Override
  public LatLonRect projToLatLonBB(ProjectionRect projBB) {
    List pointsOfInterest = new LinkedList<>();

    ProjectionPoint northPole = latLonToProj(LatLonPoint.create(90, 0));
    if (projBB.contains(northPole)) {
      pointsOfInterest.add(northPole);
    }

    ProjectionPoint southPole = latLonToProj(LatLonPoint.create(-90, 0));
    if (projBB.contains(southPole)) {
      pointsOfInterest.add(southPole);
    }

    if (pointsOfInterest.size() == 2) { // projBB contains both north and south poles, and thus, the entire map.
      return new LatLonRect(LatLonPoint.create(-90, -180), LatLonPoint.create(90, 180));
    }

    List corners = Arrays.asList(projBB.getLowerLeftPoint(), projBB.getLowerRightPoint(),
        projBB.getUpperLeftPoint(), projBB.getUpperRightPoint());

    for (ProjectionPoint corner : corners) {
      if (projToLatLon(corner) != INVALID) {
        pointsOfInterest.add(corner);
      }
    }

    pointsOfInterest.addAll(getMapEdgeIntercepts(projBB));

    return makeLatLonRect(pointsOfInterest);
  }

  /**
   * Returns the points at which {@code projBB} intersects the map edge.
   *
   * @param projBB defines a bounding box that may intersect the map edge, in projection coordinates.
   * @return the points at which {@code projBB} intersects the map edge. May be empty.
   */
  public List getMapEdgeIntercepts(ProjectionRect projBB) {
    List intercepts = new LinkedList<>();

    for (ProjectionPoint topIntercept : getMapEdgeInterceptsAtY(projBB.getUpperRightPoint().getY())) {
      if (pointIsBetween(topIntercept, projBB.getUpperLeftPoint(), projBB.getUpperRightPoint())) {
        intercepts.add(topIntercept);
      }
    }

    for (ProjectionPoint rightIntercept : getMapEdgeInterceptsAtX(projBB.getUpperRightPoint().getX())) {
      if (pointIsBetween(rightIntercept, projBB.getUpperRightPoint(), projBB.getLowerRightPoint())) {
        intercepts.add(rightIntercept);
      }
    }

    for (ProjectionPoint bottomIntercept : getMapEdgeInterceptsAtY(projBB.getLowerLeftPoint().getY())) {
      if (pointIsBetween(bottomIntercept, projBB.getLowerLeftPoint(), projBB.getLowerRightPoint())) {
        intercepts.add(bottomIntercept);
      }
    }

    for (ProjectionPoint leftIntercept : getMapEdgeInterceptsAtX(projBB.getLowerLeftPoint().getX())) {
      if (pointIsBetween(leftIntercept, projBB.getLowerLeftPoint(), projBB.getUpperLeftPoint())) {
        intercepts.add(leftIntercept);
      }
    }

    return intercepts;
  }

  /**
   * Returns the points at which the line {@code x = x0} intersects the map edge.
   *
   * @param x0 defines a line that may intersect the map edge, in projection coordinates.
   * @return the points at which the line {@code x = x0} intersects the map edge. May be empty.
   */
  public List getMapEdgeInterceptsAtX(double x0) {
    List mapEdgeIntercepts = new LinkedList<>();
    if (projToLatLon(x0, falseNorthing) == INVALID) { // The line {@code x = x0} does not intersect the map.
      return mapEdgeIntercepts; // Empty list.
    }

    double x0natural = x0 - falseEasting;
    double limitLon_r = (x0natural < 0) ? -Math.PI : Math.PI;
    double deltaLon_r = limitLon_r - Math.toRadians(centMeridian);

    // This formula comes from solving 30-1 for phi, and then plugging it into 30-2. See Snyder, p 247.
    double minY = -earthRadius * Math.acos(x0natural / (earthRadius * deltaLon_r));
    double maxY = +earthRadius * Math.acos(x0natural / (earthRadius * deltaLon_r));

    mapEdgeIntercepts.add(ProjectionPoint.create(x0, minY + falseNorthing));
    mapEdgeIntercepts.add(ProjectionPoint.create(x0, maxY + falseNorthing));
    return mapEdgeIntercepts;
  }

  /**
   * Returns the points at which the line {@code y = y0} intersects the map edge.
   *
   * @param y0 defines a line that intersects the map edge, in projection coordinates.
   * @return the points at which the line {@code y = y0} intersects the map edge. May be empty.
   */
  public List getMapEdgeInterceptsAtY(double y0) {
    List mapEdgeIntercepts = new LinkedList<>();
    if (projToLatLon(falseEasting, y0) == INVALID) { // The line {@code y = y0} does not intersect the map.
      return mapEdgeIntercepts; // Empty list.
    }

    double minX = getXAt(y0, -Math.PI);
    double maxX = getXAt(y0, Math.PI);

    mapEdgeIntercepts.add(ProjectionPoint.create(minX, y0));
    mapEdgeIntercepts.add(ProjectionPoint.create(maxX, y0));
    return mapEdgeIntercepts;
  }

  private double getXAt(double y0, double lon_r) {
    double y0natural = y0 - falseNorthing;
    double deltaLon_r = lon_r - Math.toRadians(centMeridian);

    // This formula comes from plugging 30-6 into 30-1. See Snyder, p 247-248.
    double x = earthRadius * deltaLon_r * Math.cos(y0natural / earthRadius);

    return x + falseEasting;
  }

  private boolean pointIsBetween(ProjectionPoint point, ProjectionPoint linePoint1, ProjectionPoint linePoint2) {
    if (linePoint1.getX() == linePoint2.getX()) { // No fuzzy comparison necessary.
      assert point.getX() == linePoint1.getX() : "point should have the same X as the line.";

      double minY = Math.min(linePoint1.getY(), linePoint2.getY());
      double maxY = Math.max(linePoint1.getY(), linePoint2.getY());

      // Returns true if point.getY() is in the range [minY, maxY], with fuzzy math.
      return DoubleMath.fuzzyCompare(minY, point.getY(), TOLERANCE) <= 0
          && DoubleMath.fuzzyCompare(point.getY(), maxY, TOLERANCE) <= 0;
    } else if (linePoint1.getY() == linePoint2.getY()) { // No fuzzy comparison necessary.
      assert point.getY() == linePoint1.getY() : "point should have the same Y as the line.";

      double minX = Math.min(linePoint1.getX(), linePoint2.getX());
      double maxX = Math.max(linePoint1.getX(), linePoint2.getX());

      // Returns true if point.getX() is in the range [minX, maxX], with fuzzy math.
      return DoubleMath.fuzzyCompare(minX, point.getX(), TOLERANCE) <= 0
          && DoubleMath.fuzzyCompare(point.getX(), maxX, TOLERANCE) <= 0;
    } else {
      throw new AssertionError("CAN'T HAPPEN: linePoint1 and linePoint2 are corners on the same side of a "
          + "bounding box; they must have *identical* x or y values.");
    }
  }

  private LatLonRect makeLatLonRect(List projPoints) {
    if (projPoints.isEmpty()) {
      return LatLonRect.INVALID;
    }

    double minLat = +Double.MAX_VALUE;
    double minLon = +Double.MAX_VALUE;
    double maxLat = -Double.MAX_VALUE;
    double maxLon = -Double.MAX_VALUE;

    for (ProjectionPoint projPoint : projPoints) {
      LatLonPoint latLonPoint = projToLatLon(projPoint);
      assert latLonPoint != INVALID : "We should have filtered out bad points and added good ones. WTF?";

      minLat = Math.min(minLat, latLonPoint.getLatitude());
      minLon = Math.min(minLon, latLonPoint.getLongitude());
      maxLat = Math.max(maxLat, latLonPoint.getLatitude());
      maxLon = Math.max(maxLon, latLonPoint.getLongitude());
    }

    return new LatLonRect(LatLonPoint.create(minLat, minLon), LatLonPoint.create(maxLat, maxLon));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy