/*
* Copyright 2006 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.Objects;
import com.google.common.base.Preconditions;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
/**
* An S2Polyline represents a sequence of zero or more vertices connected by
* straight edges (geodesics). Edges of length 0 and 180 degrees are not
* allowed, i.e. adjacent vertices should not be identical or antipodal.
*
*
Note: Polylines do not have a Contains(S2Point) method, because
* "containment" is not numerically well-defined except at the polyline
* vertices.
*
*/
public final strictfp class S2Polyline implements S2Region {
private static final Logger log = Logger.getLogger(S2Polyline.class.getCanonicalName());
private final int numVertices;
private final S2Point[] vertices;
/**
* Create a polyline that connects the given vertices. Empty polylines are
* allowed. Adjacent vertices should not be identical or antipodal. All
* vertices should be unit length.
*/
public S2Polyline(List vertices) {
// assert isValid(vertices);
this.numVertices = vertices.size();
this.vertices = vertices.toArray(new S2Point[numVertices]);
}
/**
* Copy constructor.
*
* TODO(dbeaumont): Now that S2Polyline is immutable, remove this.
*/
public S2Polyline(S2Polyline src) {
this.numVertices = src.numVertices();
this.vertices = src.vertices.clone();
}
/**
* Return true if the given vertices form a valid polyline.
*/
public boolean isValid(List vertices) {
// All vertices must be unit length.
int n = vertices.size();
for (int i = 0; i < n; ++i) {
if (!S2.isUnitLength(vertices.get(i))) {
log.info("Vertex " + i + " is not unit length");
return false;
}
}
// Adjacent vertices must not be identical or antipodal.
for (int i = 1; i < n; ++i) {
if (vertices.get(i - 1).equals(vertices.get(i))
|| vertices.get(i - 1).equals(S2Point.neg(vertices.get(i)))) {
log.info("Vertices " + (i - 1) + " and " + i + " are identical or antipodal");
return false;
}
}
return true;
}
public int numVertices() {
return numVertices;
}
public S2Point vertex(int k) {
// assert (k >= 0 && k < numVertices);
return vertices[k];
}
/**
* Return the angle corresponding to the total arclength of the polyline on a
* unit sphere.
*/
public S1Angle getArclengthAngle() {
double lengthSum = 0;
for (int i = 1; i < numVertices(); ++i) {
lengthSum += vertex(i - 1).angle(vertex(i));
}
return S1Angle.radians(lengthSum);
}
/**
* Return the point whose distance from vertex 0 along the polyline is the
* given fraction of the polyline's total length. Fractions less than zero or
* greater than one are clamped. The return value is unit length. This cost of
* this function is currently linear in the number of vertices.
*/
public S2Point interpolate(double fraction) {
// We intentionally let the (fraction >= 1) case fall through, since
// we need to handle it in the loop below in any case because of
// possible roundoff errors.
if (fraction <= 0) {
return vertex(0);
}
double lengthSum = 0;
for (int i = 1; i < numVertices(); ++i) {
lengthSum += vertex(i - 1).angle(vertex(i));
}
double target = fraction * lengthSum;
for (int i = 1; i < numVertices(); ++i) {
double length = vertex(i - 1).angle(vertex(i));
if (target < length) {
// This code interpolates with respect to arc length rather than
// straight-line distance, and produces a unit-length result.
double f = Math.sin(target) / Math.sin(length);
return S2Point.add(S2Point.mul(vertex(i - 1), (Math.cos(target) - f * Math.cos(length))),
S2Point.mul(vertex(i), f));
}
target -= length;
}
return vertex(numVertices() - 1);
}
// S2Region interface (see {@code S2Region} for details):
/** Return a bounding spherical cap. */
@Override
public S2Cap getCapBound() {
return getRectBound().getCapBound();
}
/** Return a bounding latitude-longitude rectangle. */
@Override
public S2LatLngRect getRectBound() {
S2EdgeUtil.RectBounder bounder = new S2EdgeUtil.RectBounder();
for (int i = 0; i < numVertices(); ++i) {
bounder.addPoint(vertex(i));
}
return bounder.getBound();
}
/**
* If this method returns true, the region completely contains the given cell.
* Otherwise, either the region does not contain the cell or the containment
* relationship could not be determined.
*/
@Override
public boolean contains(S2Cell cell) {
throw new UnsupportedOperationException(
"'containment' is not numerically well-defined " + "except at the polyline vertices");
}
/**
* If this method returns false, the region does not intersect the given cell.
* Otherwise, either region intersects the cell, or the intersection
* relationship could not be determined.
*/
@Override
public boolean mayIntersect(S2Cell cell) {
if (numVertices() == 0) {
return false;
}
// We only need to check whether the cell contains vertex 0 for correctness,
// but these tests are cheap compared to edge crossings so we might as well
// check all the vertices.
for (int i = 0; i < numVertices(); ++i) {
if (cell.contains(vertex(i))) {
return true;
}
}
S2Point[] cellVertices = new S2Point[4];
for (int i = 0; i < 4; ++i) {
cellVertices[i] = cell.getVertex(i);
}
for (int j = 0; j < 4; ++j) {
S2EdgeUtil.EdgeCrosser crosser =
new S2EdgeUtil.EdgeCrosser(cellVertices[j], cellVertices[(j + 1) & 3], vertex(0));
for (int i = 1; i < numVertices(); ++i) {
if (crosser.robustCrossing(vertex(i)) >= 0) {
// There is a proper crossing, or two vertices were the same.
return true;
}
}
}
return false;
}
/**
* Given a point, returns the index of the start point of the (first) edge on
* the polyline that is closest to the given point. The polyline must have at
* least one vertex. Throws IllegalStateException if this is not the case.
*/
public int getNearestEdgeIndex(S2Point point) {
Preconditions.checkState(numVertices() > 0, "Empty polyline");
if (numVertices() == 1) {
// If there is only one vertex, the "edge" is trivial, and it's the only one
return 0;
}
// Initial value larger than any possible distance on the unit sphere.
S1Angle minDistance = S1Angle.radians(10);
int minIndex = -1;
// Find the line segment in the polyline that is closest to the point given.
for (int i = 0; i < numVertices() - 1; ++i) {
S1Angle distanceToSegment = S2EdgeUtil.getDistance(point, vertex(i), vertex(i + 1));
if (distanceToSegment.lessThan(minDistance)) {
minDistance = distanceToSegment;
minIndex = i;
}
}
return minIndex;
}
/**
* Given a point p and the index of the start point of an edge of this polyline,
* returns the point on that edge that is closest to p.
*/
public S2Point projectToEdge(S2Point point, int index) {
Preconditions.checkState(numVertices() > 0, "Empty polyline");
Preconditions.checkState(numVertices() == 1 || index < numVertices() - 1, "Invalid edge index");
if (numVertices() == 1) {
// If there is only one vertex, it is always closest to any given point.
return vertex(0);
}
return S2EdgeUtil.getClosestPoint(point, vertex(index), vertex(index + 1));
}
@Override
public boolean equals(Object that) {
if (!(that instanceof S2Polyline)) {
return false;
}
S2Polyline thatPolygon = (S2Polyline) that;
if (numVertices != thatPolygon.numVertices) {
return false;
}
for (int i = 0; i < vertices.length; i++) {
if (!vertices[i].equals(thatPolygon.vertices[i])) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return Objects.hashCode(numVertices, Arrays.deepHashCode(vertices));
}
}