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