com.google.common.geometry.S2LatLngRect.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.geometry.S2LatLngRect.java

Source

/*
 * 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() + "]";
    }
}