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
*/
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
*/
public void setFalseEasting(double falseEasting) {
this.falseEasting = falseEasting;
}
/**
* Set the false northing, in km.
* natural_y_coordinate + false_northing = y coordinate
*
* @param falseNorthing y offset
*/
public void setFalseNorthing(double falseNorthing) {
this.falseNorthing = falseNorthing;
}
/////////////////////////////////////////////////////
@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;
}
if (Double.compare(that.falseNorthing, falseNorthing) != 0) {
return false;
}
return true;
}
@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() {
final StringBuilder sb = new StringBuilder();
sb.append("Sinusoidal");
sb.append("{earthRadius=").append(earthRadius);
sb.append(", centMeridian=").append(centMeridian);
sb.append(", falseEasting=").append(falseEasting);
sb.append(", falseNorthing=").append(falseNorthing);
sb.append('}');
return sb.toString();
}
/**
* 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 (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.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 = LatLonPointImpl.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), PI_OVER_2, 1e-10)) {
toLat_r = toLat_r < 0 ? -PI_OVER_2 : +PI_OVER_2;
toLon_r = Math.toRadians(centMeridian); // if lat == +- pi/2, set lon = centMeridian (Snyder 248)
} else if (Math.abs(toLat_r) < PI_OVER_2) {
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), PI, 1e-10)) {
toLon_r = toLon_r < 0 ? -PI : +PI;
} else if (Math.abs(toLon_r) > 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(new LatLonPointImpl(90, 0));
if (projBB.contains(northPole)) {
pointsOfInterest.add(northPole);
}
ProjectionPoint southPole = latLonToProj(new LatLonPointImpl(-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(new LatLonPointImpl(-90, -180), new LatLonPointImpl(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) ? -PI : +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(new ProjectionPointImpl(x0, minY + falseNorthing));
mapEdgeIntercepts.add(new ProjectionPointImpl(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, -PI);
double maxX = getXAt(y0, +PI);
mapEdgeIntercepts.add(new ProjectionPointImpl(minX, y0));
mapEdgeIntercepts.add(new ProjectionPointImpl(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(new LatLonPointImpl(minLat, minLon), new LatLonPointImpl(maxLat, maxLon));
}
}