/* * Copyright 2005 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.geometry; /** * This class represents a point on the unit sphere as a pair of * latitude-longitude coordinates. Like the rest of the "geometry" package, the * intent is to represent spherical geometry as a mathematical abstraction, so * functions that are specifically related to the Earth's geometry (e.g. * easting/northing conversions) should be put elsewhere. * */ public strictfp class S2LatLng { /** * Approximate "effective" radius of the Earth in meters. */ public static final double EARTH_RADIUS_METERS = 6367000.0; /** The center point the lat/lng coordinate system. */ public static final S2LatLng CENTER = new S2LatLng(0.0, 0.0); private final double latRadians; private final double lngRadians; public static S2LatLng fromRadians(double latRadians, double lngRadians) { return new S2LatLng(latRadians, lngRadians); } public static S2LatLng fromDegrees(double latDegrees, double lngDegrees) { return new S2LatLng(S1Angle.degrees(latDegrees), S1Angle.degrees(lngDegrees)); } public static S2LatLng fromE5(long latE5, long lngE5) { return new S2LatLng(S1Angle.e5(latE5), S1Angle.e5(lngE5)); } public static S2LatLng fromE6(long latE6, long lngE6) { return new S2LatLng(S1Angle.e6(latE6), S1Angle.e6(lngE6)); } public static S2LatLng fromE7(long latE7, long lngE7) { return new S2LatLng(S1Angle.e7(latE7), S1Angle.e7(lngE7)); } public static S1Angle latitude(S2Point p) { // We use atan2 rather than asin because the input vector is not necessarily // unit length, and atan2 is much more accurate than asin near the poles. return S1Angle.radians( Math.atan2(p.get(2), Math.sqrt(p.get(0) * p.get(0) + p.get(1) * p.get(1)))); } public static S1Angle longitude(S2Point p) { // Note that atan2(0, 0) is defined to be zero. return S1Angle.radians(Math.atan2(p.get(1), p.get(0))); } /** This is internal to avoid ambiguity about which units are expected. */ private S2LatLng(double latRadians, double lngRadians) { this.latRadians = latRadians; this.lngRadians = lngRadians; } /** * Basic constructor. The latitude and longitude must be within the ranges * allowed by is_valid() below. * * TODO(dbeaumont): Make this a static factory method (fromLatLng() ?). */ public S2LatLng(S1Angle lat, S1Angle lng) { this(lat.radians(), lng.radians()); } /** * Default constructor for convenience when declaring arrays, etc. * * TODO(dbeaumont): Remove the default constructor (just use CENTER). */ public S2LatLng() { this(0, 0); } /** * Convert a point (not necessarily normalized) to an S2LatLng. * * TODO(dbeaumont): Make this a static factory method (fromPoint() ?). */ public S2LatLng(S2Point p) { this(Math.atan2(p.z, Math.sqrt(p.x * p.x + p.y * p.y)), Math.atan2(p.y, p.x)); // The latitude and longitude are already normalized. We use atan2 to // compute the latitude because the input vector is not necessarily unit // length, and atan2 is much more accurate than asin near the poles. // Note that atan2(0, 0) is defined to be zero. } /** Returns the latitude of this point as a new S1Angle. */ public S1Angle lat() { return S1Angle.radians(latRadians); } /** Returns the latitude of this point as radians. */ public double latRadians() { return latRadians; } /** Returns the latitude of this point as degrees. */ public double latDegrees() { return 180.0 / Math.PI * latRadians; } /** Returns the longitude of this point as a new S1Angle. */ public S1Angle lng() { return S1Angle.radians(lngRadians); } /** Returns the longitude of this point as radians. */ public double lngRadians() { return lngRadians; } /** Returns the longitude of this point as degrees. */ public double lngDegrees() { return 180.0 / Math.PI * lngRadians; } /** * Return true if the latitude is between -90 and 90 degrees inclusive and the * longitude is between -180 and 180 degrees inclusive. */ public boolean isValid() { return Math.abs(lat().radians()) <= S2.M_PI_2 && Math.abs(lng().radians()) <= S2.M_PI; } /** * Returns a new S2LatLng based on this instance for which {@link #isValid()} * will be {@code true}. *
If the current point is valid then the returned point will have the same * coordinates. */ public S2LatLng normalized() { // drem(x, 2 * S2.M_PI) reduces its argument to the range // [-S2.M_PI, S2.M_PI] inclusive, which is what we want here. return new S2LatLng(Math.max(-S2.M_PI_2, Math.min(S2.M_PI_2, lat().radians())), Math.IEEEremainder(lng().radians(), 2 * S2.M_PI)); } // Clamps the latitude to the range [-90, 90] degrees, and adds or subtracts // a multiple of 360 degrees to the longitude if necessary to reduce it to // the range [-180, 180]. /** Convert an S2LatLng to the equivalent unit-length vector (S2Point). */ public S2Point toPoint() { double phi = lat().radians(); double theta = lng().radians(); double cosphi = Math.cos(phi); return new S2Point(Math.cos(theta) * cosphi, Math.sin(theta) * cosphi, Math.sin(phi)); } /** * Return the distance (measured along the surface of the sphere) to the given * point. */ public S1Angle getDistance(final S2LatLng o) { // This implements the Haversine formula, which is numerically stable for // small distances but only gets about 8 digits of precision for very large // distances (e.g. antipodal points). Note that 8 digits is still accurate // to within about 10cm for a sphere the size of the Earth. // // This could be fixed with another sin() and cos() below, but at that point // you might as well just convert both arguments to S2Points and compute the // distance that way (which gives about 15 digits of accuracy for all // distances). double lat1 = lat().radians(); double lat2 = o.lat().radians(); double lng1 = lng().radians(); double lng2 = o.lng().radians(); double dlat = Math.sin(0.5 * (lat2 - lat1)); double dlng = Math.sin(0.5 * (lng2 - lng1)); double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2); return S1Angle.radians(2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0, 1.0 - x)))); // Return the distance (measured along the surface of the sphere) to the // given S2LatLng. This is mathematically equivalent to: // // S1Angle::FromRadians(ToPoint().Angle(o.ToPoint()) // // but this implementation is slightly more efficient. } /** * Returns the surface distance to the given point assuming a constant radius. */ public double getDistance(final S2LatLng o, double radius) { // TODO(dbeaumont): Maybe check that radius >= 0 ? return getDistance(o).radians() * radius; } /** * Returns the surface distance to the given point assuming the default Earth * radius of {@link #EARTH_RADIUS_METERS}. */ public double getEarthDistance(final S2LatLng o) { return getDistance(o, EARTH_RADIUS_METERS); } /** * Adds the given point to this point. * Note that there is no guarantee that the new point will be valid. */ public S2LatLng add(final S2LatLng o) { return new S2LatLng(latRadians + o.latRadians, lngRadians + o.lngRadians); } /** * Subtracts the given point from this point. * Note that there is no guarantee that the new point will be valid. */ public S2LatLng sub(final S2LatLng o) { return new S2LatLng(latRadians - o.latRadians, lngRadians - o.lngRadians); } /** * Scales this point by the given scaling factor. * Note that there is no guarantee that the new point will be valid. */ public S2LatLng mul(final double m) { // TODO(dbeaumont): Maybe check that m >= 0 ? return new S2LatLng(latRadians * m, lngRadians * m); } @Override public boolean equals(Object that) { if (that instanceof S2LatLng) { S2LatLng o = (S2LatLng) that; return (latRadians == o.latRadians) && (lngRadians == o.lngRadians); } return false; } @Override public int hashCode() { long value = 17; value += 37 * value + Double.doubleToLongBits(latRadians); value += 37 * value + Double.doubleToLongBits(lngRadians); return (int) (value ^ (value >>> 32)); } /** * Returns true if both the latitude and longitude of the given point are * within {@code maxError} radians of this point. */ public boolean approxEquals(S2LatLng o, double maxError) { return (Math.abs(latRadians - o.latRadians) < maxError) && (Math.abs(lngRadians - o.lngRadians) < maxError); } /** * Returns true if the given point is within {@code 1e-9} radians of this * point. This corresponds to a distance of less than {@code 1cm} at the * surface of the Earth. */ public boolean approxEquals(S2LatLng o) { return approxEquals(o, 1e-9); } @Override public String toString() { return "(" + latRadians + ", " + lngRadians + ")"; } public String toStringDegrees() { return "(" + latDegrees() + ", " + lngDegrees() + ")"; } }