Java tutorial
/* * 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; import com.google.common.base.Preconditions; /** * An S2LatLngRect represents a latitude-longitude rectangle. It is capable of * representing the empty and full rectangles as well as single points. * */ public strictfp class S2LatLngRect implements S2Region { private final R1Interval lat; private final S1Interval lng; /** * Construct a rectangle from minimum and maximum latitudes and longitudes. If * lo.lng() > hi.lng(), the rectangle spans the 180 degree longitude line. */ public S2LatLngRect(final S2LatLng lo, final S2LatLng hi) { lat = new R1Interval(lo.lat().radians(), hi.lat().radians()); lng = new S1Interval(lo.lng().radians(), hi.lng().radians()); // assert (isValid()); } /** Construct a rectangle from latitude and longitude intervals. */ public S2LatLngRect(R1Interval lat, S1Interval lng) { this.lat = lat; this.lng = lng; // assert (isValid()); } /** The canonical empty rectangle */ public static S2LatLngRect empty() { return new S2LatLngRect(R1Interval.empty(), S1Interval.empty()); } /** The canonical full rectangle. */ public static S2LatLngRect full() { return new S2LatLngRect(fullLat(), fullLng()); } /** The full allowable range of latitudes. */ public static R1Interval fullLat() { return new R1Interval(-S2.M_PI_2, S2.M_PI_2); } /** * The full allowable range of longitudes. */ public static S1Interval fullLng() { return S1Interval.full(); } /** * Construct a rectangle from a center point (in lat-lng space) and size in * each dimension. If size.lng() is greater than 360 degrees it is clamped, * and latitudes greater than +/- 90 degrees are also clamped. So for example, * FromCenterSize((80,170),(20,20)) -> (lo=(60,150),hi=(90,-170)). */ public static S2LatLngRect fromCenterSize(S2LatLng center, S2LatLng size) { return fromPoint(center).expanded(size.mul(0.5)); } /** Convenience method to construct a rectangle containing a single point. */ public static S2LatLngRect fromPoint(S2LatLng p) { // assert (p.isValid()); return new S2LatLngRect(p, p); } /** * Convenience method to construct the minimal bounding rectangle containing * the two given points. This is equivalent to starting with an empty * rectangle and calling AddPoint() twice. Note that it is different than the * S2LatLngRect(lo, hi) constructor, where the first point is always used as * the lower-left corner of the resulting rectangle. */ public static S2LatLngRect fromPointPair(S2LatLng p1, S2LatLng p2) { // assert (p1.isValid() && p2.isValid()); return new S2LatLngRect(R1Interval.fromPointPair(p1.lat().radians(), p2.lat().radians()), S1Interval.fromPointPair(p1.lng().radians(), p2.lng().radians())); } /** * Return a latitude-longitude rectangle that contains the edge from "a" to * "b". Both points must be unit-length. Note that the bounding rectangle of * an edge can be larger than the bounding rectangle of its endpoints. */ public static S2LatLngRect fromEdge(S2Point a, S2Point b) { // assert (S2.isUnitLength(a) && S2.isUnitLength(b)); S2LatLngRect r = fromPointPair(new S2LatLng(a), new S2LatLng(b)); // Check whether the min/max latitude occurs in the edge interior. // We find the normal to the plane containing AB, and then a vector "dir" in // this plane that also passes through the equator. We use RobustCrossProd // to ensure that the edge normal is accurate even when the two points are // very close together. S2Point ab = S2.robustCrossProd(a, b); S2Point dir = S2Point.crossProd(ab, new S2Point(0, 0, 1)); double da = dir.dotProd(a); double db = dir.dotProd(b); if (da * db >= 0) { // Minimum and maximum latitude are attained at the vertices. return r; } // Minimum/maximum latitude occurs in the edge interior. This affects the // latitude bounds but not the longitude bounds. double absLat = Math.acos(Math.abs(ab.z / ab.norm())); if (da < 0) { return new S2LatLngRect(new R1Interval(r.lat().lo(), absLat), r.lng()); } else { return new S2LatLngRect(new R1Interval(-absLat, r.lat().hi()), r.lng()); } } /** * Return true if the rectangle is valid, which essentially just means that * the latitude bounds do not exceed Pi/2 in absolute value and the longitude * bounds do not exceed Pi in absolute value. * */ public boolean isValid() { // The lat/lng ranges must either be both empty or both non-empty. return (Math.abs(lat.lo()) <= S2.M_PI_2 && Math.abs(lat.hi()) <= S2.M_PI_2 && lng.isValid() && lat.isEmpty() == lng.isEmpty()); } // Accessor methods. public S1Angle latLo() { return S1Angle.radians(lat.lo()); } public S1Angle latHi() { return S1Angle.radians(lat.hi()); } public S1Angle lngLo() { return S1Angle.radians(lng.lo()); } public S1Angle lngHi() { return S1Angle.radians(lng.hi()); } public R1Interval lat() { return lat; } public S1Interval lng() { return lng; } public S2LatLng lo() { return new S2LatLng(latLo(), lngLo()); } public S2LatLng hi() { return new S2LatLng(latHi(), lngHi()); } /** * Return true if the rectangle is empty, i.e. it contains no points at all. */ public boolean isEmpty() { return lat.isEmpty(); } // Return true if the rectangle is full, i.e. it contains all points. public boolean isFull() { return lat.equals(fullLat()) && lng.isFull(); } /** * Return true if lng_.lo() > lng_.hi(), i.e. the rectangle crosses the 180 * degree latitude line. */ public boolean isInverted() { return lng.isInverted(); } /** Return the k-th vertex of the rectangle (k = 0,1,2,3) in CCW order. */ public S2LatLng getVertex(int k) { // Return the points in CCW order (SW, SE, NE, NW). switch (k) { case 0: return S2LatLng.fromRadians(lat.lo(), lng.lo()); case 1: return S2LatLng.fromRadians(lat.lo(), lng.hi()); case 2: return S2LatLng.fromRadians(lat.hi(), lng.hi()); case 3: return S2LatLng.fromRadians(lat.hi(), lng.lo()); default: throw new IllegalArgumentException("Invalid vertex index."); } } /** * Return the center of the rectangle in latitude-longitude space (in general * this is not the center of the region on the sphere). */ public S2LatLng getCenter() { return S2LatLng.fromRadians(lat.getCenter(), lng.getCenter()); } /** * Return the minimum distance (measured along the surface of the sphere) * from a given point to the rectangle (both its boundary and its interior). * The latLng must be valid. */ public S1Angle getDistance(S2LatLng p) { // The algorithm here is the same as in getDistance(S2LagLngRect), only // with simplified calculations. S2LatLngRect a = this; Preconditions.checkState(!a.isEmpty()); Preconditions.checkArgument(p.isValid()); if (a.lng().contains(p.lng().radians())) { return S1Angle.radians( Math.max(0.0, Math.max(p.lat().radians() - a.lat().hi(), a.lat().lo() - p.lat().radians()))); } S1Interval interval = new S1Interval(a.lng().hi(), a.lng().complement().getCenter()); double aLng = a.lng().lo(); if (interval.contains(p.lng().radians())) { aLng = a.lng().hi(); } S2Point lo = S2LatLng.fromRadians(a.lat().lo(), aLng).toPoint(); S2Point hi = S2LatLng.fromRadians(a.lat().hi(), aLng).toPoint(); S2Point loCrossHi = S2LatLng.fromRadians(0, aLng - S2.M_PI_2).normalized().toPoint(); return S2EdgeUtil.getDistance(p.toPoint(), lo, hi, loCrossHi); } /** * Return the minimum distance (measured along the surface of the sphere) to * the given S2LatLngRect. Both S2LatLngRects must be non-empty. */ public S1Angle getDistance(S2LatLngRect other) { S2LatLngRect a = this; S2LatLngRect b = other; Preconditions.checkState(!a.isEmpty()); Preconditions.checkArgument(!b.isEmpty()); // First, handle the trivial cases where the longitude intervals overlap. if (a.lng().intersects(b.lng())) { if (a.lat().intersects(b.lat())) { return S1Angle.radians(0); // Intersection between a and b. } // We found an overlap in the longitude interval, but not in the latitude // interval. This means the shortest path travels along some line of // longitude connecting the high-latitude of the lower rect with the // low-latitude of the higher rect. S1Angle lo, hi; if (a.lat().lo() > b.lat().hi()) { lo = b.latHi(); hi = a.latLo(); } else { lo = a.latHi(); hi = b.latLo(); } return S1Angle.radians(hi.radians() - lo.radians()); } // The longitude intervals don't overlap. In this case, the closest points // occur somewhere on the pair of longitudinal edges which are nearest in // longitude-space. S1Angle aLng, bLng; S1Interval loHi = S1Interval.fromPointPair(a.lng().lo(), b.lng().hi()); S1Interval hiLo = S1Interval.fromPointPair(a.lng().hi(), b.lng().lo()); if (loHi.getLength() < hiLo.getLength()) { aLng = a.lngLo(); bLng = b.lngHi(); } else { aLng = a.lngHi(); bLng = b.lngLo(); } // The shortest distance between the two longitudinal segments will include // at least one segment endpoint. We could probably narrow this down further // to a single point-edge distance by comparing the relative latitudes of the // endpoints, but for the sake of clarity, we'll do all four point-edge // distance tests. S2Point aLo = new S2LatLng(a.latLo(), aLng).toPoint(); S2Point aHi = new S2LatLng(a.latHi(), aLng).toPoint(); S2Point aLoCrossHi = S2LatLng.fromRadians(0, aLng.radians() - S2.M_PI_2).normalized().toPoint(); S2Point bLo = new S2LatLng(b.latLo(), bLng).toPoint(); S2Point bHi = new S2LatLng(b.latHi(), bLng).toPoint(); S2Point bLoCrossHi = S2LatLng.fromRadians(0, bLng.radians() - S2.M_PI_2).normalized().toPoint(); return S1Angle.min(S2EdgeUtil.getDistance(aLo, bLo, bHi, bLoCrossHi), S1Angle.min(S2EdgeUtil.getDistance(aHi, bLo, bHi, bLoCrossHi), S1Angle.min(S2EdgeUtil.getDistance(bLo, aLo, aHi, aLoCrossHi), S2EdgeUtil.getDistance(bHi, aLo, aHi, aLoCrossHi)))); } /** * Return the width and height of this rectangle in latitude-longitude space. * Empty rectangles have a negative width and height. */ public S2LatLng getSize() { return S2LatLng.fromRadians(lat.getLength(), lng.getLength()); } /** * More efficient version of Contains() that accepts a S2LatLng rather than an * S2Point. */ public boolean contains(S2LatLng ll) { // assert (ll.isValid()); return (lat.contains(ll.lat().radians()) && lng.contains(ll.lng().radians())); } /** * Return true if and only if the given point is contained in the interior of * the region (i.e. the region excluding its boundary). The point 'p' does not * need to be normalized. */ public boolean interiorContains(S2Point p) { return interiorContains(new S2LatLng(p)); } /** * More efficient version of InteriorContains() that accepts a S2LatLng rather * than an S2Point. */ public boolean interiorContains(S2LatLng ll) { // assert (ll.isValid()); return (lat.interiorContains(ll.lat().radians()) && lng.interiorContains(ll.lng().radians())); } /** * Return true if and only if the rectangle contains the given other * rectangle. */ public boolean contains(S2LatLngRect other) { return lat.contains(other.lat) && lng.contains(other.lng); } /** * Return true if and only if the interior of this rectangle contains all * points of the given other rectangle (including its boundary). */ public boolean interiorContains(S2LatLngRect other) { return (lat.interiorContains(other.lat) && lng.interiorContains(other.lng)); } /** Return true if this rectangle and the given other rectangle have any points in common. */ public boolean intersects(S2LatLngRect other) { return lat.intersects(other.lat) && lng.intersects(other.lng); } /** * Returns true if this rectangle intersects the given cell. (This is an exact * test and may be fairly expensive, see also MayIntersect below.) */ public boolean intersects(S2Cell cell) { // First we eliminate the cases where one region completely contains the // other. Once these are disposed of, then the regions will intersect // if and only if their boundaries intersect. if (isEmpty()) { return false; } if (contains(cell.getCenter())) { return true; } if (cell.contains(getCenter().toPoint())) { return true; } // Quick rejection test (not required for correctness). if (!intersects(cell.getRectBound())) { return false; } // Now check whether the boundaries intersect. Unfortunately, a // latitude-longitude rectangle does not have straight edges -- two edges // are curved, and at least one of them is concave. // Precompute the cell vertices as points and latitude-longitudes. S2Point[] cellV = new S2Point[4]; S2LatLng[] cellLl = new S2LatLng[4]; for (int i = 0; i < 4; ++i) { cellV[i] = cell.getVertex(i); // Must be normalized. cellLl[i] = new S2LatLng(cellV[i]); if (contains(cellLl[i])) { return true; // Quick acceptance test. } } for (int i = 0; i < 4; ++i) { S1Interval edgeLng = S1Interval.fromPointPair(cellLl[i].lng().radians(), cellLl[(i + 1) & 3].lng().radians()); if (!lng.intersects(edgeLng)) { continue; } final S2Point a = cellV[i]; final S2Point b = cellV[(i + 1) & 3]; if (edgeLng.contains(lng.lo())) { if (intersectsLngEdge(a, b, lat, lng.lo())) { return true; } } if (edgeLng.contains(lng.hi())) { if (intersectsLngEdge(a, b, lat, lng.hi())) { return true; } } if (intersectsLatEdge(a, b, lat.lo(), lng)) { return true; } if (intersectsLatEdge(a, b, lat.hi(), lng)) { return true; } } return false; } /** * Return true if and only if the interior of this rectangle intersects any * point (including the boundary) of the given other rectangle. */ public boolean interiorIntersects(S2LatLngRect other) { return (lat.interiorIntersects(other.lat) && lng.interiorIntersects(other.lng)); } public S2LatLngRect addPoint(S2Point p) { return addPoint(new S2LatLng(p)); } // Increase the size of the bounding rectangle to include the given point. // The rectangle is expanded by the minimum amount possible. public S2LatLngRect addPoint(S2LatLng ll) { // assert (ll.isValid()); R1Interval newLat = lat.addPoint(ll.lat().radians()); S1Interval newLng = lng.addPoint(ll.lng().radians()); return new S2LatLngRect(newLat, newLng); } /** * Return a rectangle that contains all points whose latitude distance from * this rectangle is at most margin.lat(), and whose longitude distance from * this rectangle is at most margin.lng(). In particular, latitudes are * clamped while longitudes are wrapped. Note that any expansion of an empty * interval remains empty, and both components of the given margin must be * non-negative. * * NOTE: If you are trying to grow a rectangle by a certain *distance* on the * sphere (e.g. 5km), use the ConvolveWithCap() method instead. */ public S2LatLngRect expanded(S2LatLng margin) { // assert (margin.lat().radians() >= 0 && margin.lng().radians() >= 0); if (isEmpty()) { return this; } return new S2LatLngRect(lat.expanded(margin.lat().radians()).intersection(fullLat()), lng.expanded(margin.lng().radians())); } /** * Return the smallest rectangle containing the union of this rectangle and * the given rectangle. */ public S2LatLngRect union(S2LatLngRect other) { return new S2LatLngRect(lat.union(other.lat), lng.union(other.lng)); } /** * Return the smallest rectangle containing the intersection of this rectangle * and the given rectangle. Note that the region of intersection may consist * of two disjoint rectangles, in which case a single rectangle spanning both * of them is returned. */ public S2LatLngRect intersection(S2LatLngRect other) { R1Interval intersectLat = lat.intersection(other.lat); S1Interval intersectLng = lng.intersection(other.lng); if (intersectLat.isEmpty() || intersectLng.isEmpty()) { // The lat/lng ranges must either be both empty or both non-empty. return empty(); } return new S2LatLngRect(intersectLat, intersectLng); } /** * Return a rectangle that contains the convolution of this rectangle with a * cap of the given angle. This expands the rectangle by a fixed distance (as * opposed to growing the rectangle in latitude-longitude space). The returned * rectangle includes all points whose minimum distance to the original * rectangle is at most the given angle. */ public S2LatLngRect convolveWithCap(S1Angle angle) { // The most straightforward approach is to build a cap centered on each // vertex and take the union of all the bounding rectangles (including the // original rectangle; this is necessary for very large rectangles). // Optimization: convert the angle to a height exactly once. S2Cap cap = S2Cap.fromAxisAngle(new S2Point(1, 0, 0), angle); S2LatLngRect r = this; for (int k = 0; k < 4; ++k) { S2Cap vertexCap = S2Cap.fromAxisHeight(getVertex(k).toPoint(), cap.height()); r = r.union(vertexCap.getRectBound()); } return r; } /** Return the surface area of this rectangle on the unit sphere. */ public double area() { if (isEmpty()) { return 0; } // This is the size difference of the two spherical caps, multiplied by // the longitude ratio. return lng().getLength() * Math.abs(Math.sin(latHi().radians()) - Math.sin(latLo().radians())); } /** Return true if two rectangles contains the same set of points. */ @Override public boolean equals(Object that) { if (!(that instanceof S2LatLngRect)) { return false; } S2LatLngRect otherRect = (S2LatLngRect) that; return lat().equals(otherRect.lat()) && lng().equals(otherRect.lng()); } /** * Return true if the latitude and longitude intervals of the two rectangles * are the same up to the given tolerance (see r1interval.h and s1interval.h * for details). */ public boolean approxEquals(S2LatLngRect other, double maxError) { return (lat.approxEquals(other.lat, maxError) && lng.approxEquals(other.lng, maxError)); } public boolean approxEquals(S2LatLngRect other) { return approxEquals(other, 1e-15); } @Override public int hashCode() { int value = 17; value = 37 * value + lat.hashCode(); return (37 * value + lng.hashCode()); } // ////////////////////////////////////////////////////////////////////// // S2Region interface (see {@code S2Region} for details): @Override public S2Region clone() { return new S2LatLngRect(this.lo(), this.hi()); } @Override public S2Cap getCapBound() { // We consider two possible bounding caps, one whose axis passes // through the center of the lat-long rectangle and one whose axis // is the north or south pole. We return the smaller of the two caps. if (isEmpty()) { return S2Cap.empty(); } double poleZ, poleAngle; if (lat.lo() + lat.hi() < 0) { // South pole axis yields smaller cap. poleZ = -1; poleAngle = S2.M_PI_2 + lat.hi(); } else { poleZ = 1; poleAngle = S2.M_PI_2 - lat.lo(); } S2Cap poleCap = S2Cap.fromAxisAngle(new S2Point(0, 0, poleZ), S1Angle.radians(poleAngle)); // For bounding rectangles that span 180 degrees or less in longitude, the // maximum cap size is achieved at one of the rectangle vertices. For // rectangles that are larger than 180 degrees, we punt and always return a // bounding cap centered at one of the two poles. double lngSpan = lng.hi() - lng.lo(); if (Math.IEEEremainder(lngSpan, 2 * S2.M_PI) >= 0) { if (lngSpan < 2 * S2.M_PI) { S2Cap midCap = S2Cap.fromAxisAngle(getCenter().toPoint(), S1Angle.radians(0)); for (int k = 0; k < 4; ++k) { midCap = midCap.addPoint(getVertex(k).toPoint()); } if (midCap.height() < poleCap.height()) { return midCap; } } } return poleCap; } @Override public S2LatLngRect getRectBound() { return this; } @Override public boolean contains(S2Cell cell) { // A latitude-longitude rectangle contains a cell if and only if it contains // the cell's bounding rectangle. (This is an exact test.) return contains(cell.getRectBound()); } /** * This test is cheap but is NOT exact. Use Intersects() if you want a more * accurate and more expensive test. Note that when this method is used by an * S2RegionCoverer, the accuracy isn't all that important since if a cell may * intersect the region then it is subdivided, and the accuracy of this method * goes up as the cells get smaller. */ @Override public boolean mayIntersect(S2Cell cell) { // This test is cheap but is NOT exact (see s2latlngrect.h). return intersects(cell.getRectBound()); } /** The point 'p' does not need to be normalized. */ public boolean contains(S2Point p) { return contains(new S2LatLng(p)); } /** * Return true if the edge AB intersects the given edge of constant longitude. */ private static boolean intersectsLngEdge(S2Point a, S2Point b, R1Interval lat, double lng) { // Return true if the segment AB intersects the given edge of constant // longitude. The nice thing about edges of constant longitude is that // they are straight lines on the sphere (geodesics). return S2.simpleCrossing(a, b, S2LatLng.fromRadians(lat.lo(), lng).toPoint(), S2LatLng.fromRadians(lat.hi(), lng).toPoint()); } /** * Return true if the edge AB intersects the given edge of constant latitude. */ private static boolean intersectsLatEdge(S2Point a, S2Point b, double lat, S1Interval lng) { // Return true if the segment AB intersects the given edge of constant // latitude. Unfortunately, lines of constant latitude are curves on // the sphere. They can intersect a straight edge in 0, 1, or 2 points. // assert (S2.isUnitLength(a) && S2.isUnitLength(b)); // First, compute the normal to the plane AB that points vaguely north. S2Point z = S2Point.normalize(S2.robustCrossProd(a, b)); if (z.z < 0) { z = S2Point.neg(z); } // Extend this to an orthonormal frame (x,y,z) where x is the direction // where the great circle through AB achieves its maximium latitude. S2Point y = S2Point.normalize(S2.robustCrossProd(z, new S2Point(0, 0, 1))); S2Point x = S2Point.crossProd(y, z); // assert (S2.isUnitLength(x) && x.z >= 0); // Compute the angle "theta" from the x-axis (in the x-y plane defined // above) where the great circle intersects the given line of latitude. double sinLat = Math.sin(lat); if (Math.abs(sinLat) >= x.z) { return false; // The great circle does not reach the given latitude. } // assert (x.z > 0); double cosTheta = sinLat / x.z; double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); double theta = Math.atan2(sinTheta, cosTheta); // The candidate intersection points are located +/- theta in the x-y // plane. For an intersection to be valid, we need to check that the // intersection point is contained in the interior of the edge AB and // also that it is contained within the given longitude interval "lng". // Compute the range of theta values spanned by the edge AB. S1Interval abTheta = S1Interval.fromPointPair(Math.atan2(a.dotProd(y), a.dotProd(x)), Math.atan2(b.dotProd(y), b.dotProd(x))); if (abTheta.contains(theta)) { // Check if the intersection point is also in the given "lng" interval. S2Point isect = S2Point.add(S2Point.mul(x, cosTheta), S2Point.mul(y, sinTheta)); if (lng.contains(Math.atan2(isect.y, isect.x))) { return true; } } if (abTheta.contains(-theta)) { // Check if the intersection point is also in the given "lng" interval. S2Point intersection = S2Point.sub(S2Point.mul(x, cosTheta), S2Point.mul(y, sinTheta)); if (lng.contains(Math.atan2(intersection.y, intersection.x))) { return true; } } return false; } @Override public String toString() { return "[Lo=" + lo() + ", Hi=" + hi() + "]"; } }