1 /*
2  * Copyright 2006 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.geometry;
18 
19 import com.google.common.base.Objects;
20 import com.google.common.base.Preconditions;
21 
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.logging.Logger;
25 
26 /**
27  * An S2Polyline represents a sequence of zero or more vertices connected by
28  * straight edges (geodesics). Edges of length 0 and 180 degrees are not
29  * allowed, i.e. adjacent vertices should not be identical or antipodal.
30  *
31  * <p>Note: Polylines do not have a Contains(S2Point) method, because
32  * "containment" is not numerically well-defined except at the polyline
33  * vertices.
34  *
35  */
36 public final strictfp class S2Polyline implements S2Region {
37   private static final Logger log = Logger.getLogger(S2Polyline.class.getCanonicalName());
38 
39   private final int numVertices;
40   private final S2Point[] vertices;
41 
42   /**
43    * Create a polyline that connects the given vertices. Empty polylines are
44    * allowed. Adjacent vertices should not be identical or antipodal. All
45    * vertices should be unit length.
46    */
S2Polyline(List<S2Point> vertices)47   public S2Polyline(List<S2Point> vertices) {
48     // assert isValid(vertices);
49     this.numVertices = vertices.size();
50     this.vertices = vertices.toArray(new S2Point[numVertices]);
51   }
52 
53   /**
54    * Copy constructor.
55    *
56    * TODO(dbeaumont): Now that S2Polyline is immutable, remove this.
57    */
S2Polyline(S2Polyline src)58   public S2Polyline(S2Polyline src) {
59     this.numVertices = src.numVertices();
60     this.vertices = src.vertices.clone();
61   }
62 
63   /**
64    * Return true if the given vertices form a valid polyline.
65    */
isValid(List<S2Point> vertices)66   public boolean isValid(List<S2Point> vertices) {
67     // All vertices must be unit length.
68     int n = vertices.size();
69     for (int i = 0; i < n; ++i) {
70       if (!S2.isUnitLength(vertices.get(i))) {
71         log.info("Vertex " + i + " is not unit length");
72         return false;
73       }
74     }
75 
76     // Adjacent vertices must not be identical or antipodal.
77     for (int i = 1; i < n; ++i) {
78       if (vertices.get(i - 1).equals(vertices.get(i))
79           || vertices.get(i - 1).equals(S2Point.neg(vertices.get(i)))) {
80         log.info("Vertices " + (i - 1) + " and " + i + " are identical or antipodal");
81         return false;
82       }
83     }
84 
85     return true;
86   }
87 
numVertices()88   public int numVertices() {
89     return numVertices;
90   }
91 
vertex(int k)92   public S2Point vertex(int k) {
93     // assert (k >= 0 && k < numVertices);
94     return vertices[k];
95   }
96 
97   /**
98    * Return the angle corresponding to the total arclength of the polyline on a
99    * unit sphere.
100    */
getArclengthAngle()101   public S1Angle getArclengthAngle() {
102     double lengthSum = 0;
103     for (int i = 1; i < numVertices(); ++i) {
104       lengthSum += vertex(i - 1).angle(vertex(i));
105     }
106     return S1Angle.radians(lengthSum);
107   }
108 
109   /**
110    * Return the point whose distance from vertex 0 along the polyline is the
111    * given fraction of the polyline's total length. Fractions less than zero or
112    * greater than one are clamped. The return value is unit length. This cost of
113    * this function is currently linear in the number of vertices.
114    */
interpolate(double fraction)115   public S2Point interpolate(double fraction) {
116     // We intentionally let the (fraction >= 1) case fall through, since
117     // we need to handle it in the loop below in any case because of
118     // possible roundoff errors.
119     if (fraction <= 0) {
120       return vertex(0);
121     }
122 
123     double lengthSum = 0;
124     for (int i = 1; i < numVertices(); ++i) {
125       lengthSum += vertex(i - 1).angle(vertex(i));
126     }
127     double target = fraction * lengthSum;
128     for (int i = 1; i < numVertices(); ++i) {
129       double length = vertex(i - 1).angle(vertex(i));
130       if (target < length) {
131         // This code interpolates with respect to arc length rather than
132         // straight-line distance, and produces a unit-length result.
133         double f = Math.sin(target) / Math.sin(length);
134         return S2Point.add(S2Point.mul(vertex(i - 1), (Math.cos(target) - f * Math.cos(length))),
135             S2Point.mul(vertex(i), f));
136       }
137       target -= length;
138     }
139     return vertex(numVertices() - 1);
140   }
141 
142   // S2Region interface (see {@code S2Region} for details):
143 
144   /** Return a bounding spherical cap. */
145   @Override
getCapBound()146   public S2Cap getCapBound() {
147     return getRectBound().getCapBound();
148   }
149 
150 
151   /** Return a bounding latitude-longitude rectangle. */
152   @Override
getRectBound()153   public S2LatLngRect getRectBound() {
154     S2EdgeUtil.RectBounder bounder = new S2EdgeUtil.RectBounder();
155     for (int i = 0; i < numVertices(); ++i) {
156       bounder.addPoint(vertex(i));
157     }
158     return bounder.getBound();
159   }
160 
161   /**
162    * If this method returns true, the region completely contains the given cell.
163    * Otherwise, either the region does not contain the cell or the containment
164    * relationship could not be determined.
165    */
166   @Override
contains(S2Cell cell)167   public boolean contains(S2Cell cell) {
168     throw new UnsupportedOperationException(
169         "'containment' is not numerically well-defined " + "except at the polyline vertices");
170   }
171 
172   /**
173    * If this method returns false, the region does not intersect the given cell.
174    * Otherwise, either region intersects the cell, or the intersection
175    * relationship could not be determined.
176    */
177   @Override
mayIntersect(S2Cell cell)178   public boolean mayIntersect(S2Cell cell) {
179     if (numVertices() == 0) {
180       return false;
181     }
182 
183     // We only need to check whether the cell contains vertex 0 for correctness,
184     // but these tests are cheap compared to edge crossings so we might as well
185     // check all the vertices.
186     for (int i = 0; i < numVertices(); ++i) {
187       if (cell.contains(vertex(i))) {
188         return true;
189       }
190     }
191     S2Point[] cellVertices = new S2Point[4];
192     for (int i = 0; i < 4; ++i) {
193       cellVertices[i] = cell.getVertex(i);
194     }
195     for (int j = 0; j < 4; ++j) {
196       S2EdgeUtil.EdgeCrosser crosser =
197           new S2EdgeUtil.EdgeCrosser(cellVertices[j], cellVertices[(j + 1) & 3], vertex(0));
198       for (int i = 1; i < numVertices(); ++i) {
199         if (crosser.robustCrossing(vertex(i)) >= 0) {
200           // There is a proper crossing, or two vertices were the same.
201           return true;
202         }
203       }
204     }
205     return false;
206   }
207 
208   /**
209    * Given a point, returns the index of the start point of the (first) edge on
210    * the polyline that is closest to the given point. The polyline must have at
211    * least one vertex. Throws IllegalStateException if this is not the case.
212    */
getNearestEdgeIndex(S2Point point)213   public int getNearestEdgeIndex(S2Point point) {
214     Preconditions.checkState(numVertices() > 0, "Empty polyline");
215 
216     if (numVertices() == 1) {
217       // If there is only one vertex, the "edge" is trivial, and it's the only one
218       return 0;
219     }
220 
221     // Initial value larger than any possible distance on the unit sphere.
222     S1Angle minDistance = S1Angle.radians(10);
223     int minIndex = -1;
224 
225     // Find the line segment in the polyline that is closest to the point given.
226     for (int i = 0; i < numVertices() - 1; ++i) {
227       S1Angle distanceToSegment = S2EdgeUtil.getDistance(point, vertex(i), vertex(i + 1));
228       if (distanceToSegment.lessThan(minDistance)) {
229         minDistance = distanceToSegment;
230         minIndex = i;
231       }
232     }
233     return minIndex;
234   }
235 
236   /**
237    * Given a point p and the index of the start point of an edge of this polyline,
238    * returns the point on that edge that is closest to p.
239    */
projectToEdge(S2Point point, int index)240   public S2Point projectToEdge(S2Point point, int index) {
241     Preconditions.checkState(numVertices() > 0, "Empty polyline");
242     Preconditions.checkState(numVertices() == 1 || index < numVertices() - 1, "Invalid edge index");
243     if (numVertices() == 1) {
244       // If there is only one vertex, it is always closest to any given point.
245       return vertex(0);
246     }
247     return S2EdgeUtil.getClosestPoint(point, vertex(index), vertex(index + 1));
248   }
249 
250   @Override
251   public boolean equals(Object that) {
252     if (!(that instanceof S2Polyline)) {
253       return false;
254     }
255 
256     S2Polyline thatPolygon = (S2Polyline) that;
257     if (numVertices != thatPolygon.numVertices) {
258       return false;
259     }
260 
261     for (int i = 0; i < vertices.length; i++) {
262       if (!vertices[i].equals(thatPolygon.vertices[i])) {
263         return false;
264       }
265     }
266     return true;
267   }
268 
269   @Override
270   public int hashCode() {
271     return Objects.hashCode(numVertices, Arrays.deepHashCode(vertices));
272   }
273 }
274