// Returns the centroid of the shape multiplied by the measure of the shape, // which is defined as follows: // // - For dimension 0 shapes, the measure is shape.num_edges(). // - For dimension 1 shapes, the measure is GetLength(shape). // - For dimension 2 shapes, the measure is GetArea(shape). // // Note that the result is not unit length, so you may need to call // Normalize() before passing it to other S2 functions. // // The result is scaled by the measure defined above for two reasons: (1) it // is cheaper to compute this way, and (2) this makes it easier to compute the // centroid of a collection of shapes. (This requires simply summing the // centroids of all shapes in the collection whose dimension is maximal.) public static S2Point GetCentroid(S2Shape shape) { var centroid = S2Point.Empty; S2Point[] vertices; int dimension = shape.Dimension(); int num_chains = shape.NumChains(); for (int chain_id = 0; chain_id < num_chains; ++chain_id) { switch (dimension) { case 0: centroid += shape.GetEdge(chain_id).V0; break; case 1: GetChainVertices(shape, chain_id, out vertices); centroid += S2PolylineMeasures.GetCentroid(vertices); break; default: GetChainVertices(shape, chain_id, out vertices); centroid += S2.GetCentroid(vertices.ToList()); break; } } return(centroid); }
// Converts a 1-dimensional S2Shape into an S2Polyline. Converts the first // chain in the shape to a vector of S2Point vertices and uses that to construct // the S2Polyline. Note that the input 1-dimensional S2Shape must contain at // most 1 chain, and that this method does not accept an empty shape (i.e. a // shape with no vertices). public static S2Polyline ShapeToS2Polyline(S2Shape line) { System.Diagnostics.Debug.Assert(line.Dimension() == 1); System.Diagnostics.Debug.Assert(line.NumChains() == 1); S2Point[] vertices; S2.GetChainVertices(line, 0, out vertices); return new S2Polyline(vertices); }
// Converts a 0-dimensional S2Shape into a list of S2Points. // This method does allow an empty shape (i.e. a shape with no vertices). public static List<S2Point> ShapeToS2Points(S2Shape multipoint) { System.Diagnostics.Debug.Assert(multipoint.Dimension() == 0); List<S2Point> points = new(); points.Capacity = multipoint.NumEdges(); for (int i = 0; i < multipoint.NumEdges(); ++i) { points.Add(multipoint.GetEdge(i).V0); } return points; }
// For shapes of dimension 2, returns the area of the shape on the unit // sphere. The result is between 0 and 4*Pi steradians. Otherwise returns // zero. This method has good relative accuracy for both very large and very // small regions. // // All edges are modeled as spherical geodesics. The result can be converted // to an area on the Earth's surface (with a worst-case error of 0.900% near // the poles) using the functions in s2earth.h. public static double GetArea(S2Shape shape) { if (shape.Dimension() != 2) { return(0.0); } // Since S2Shape uses the convention that the interior of the shape is to // the left of all edges, in theory we could compute the area of the polygon // by simply adding up all the loop areas modulo 4*Pi. The problem with // this approach is that polygons holes typically have areas near 4*Pi, // which can create large cancellation errors when computing the area of // small polygons with holes. For example, a shell with an area of 4 square // meters (1e-13 steradians) surrounding a hole with an area of 3 square // meters (7.5e-14 sterians) would lose almost all of its accuracy if the // area of the hole was computed as 12.566370614359098. // // So instead we use S2.GetSignedArea() to ensure that all loops have areas // in the range [-2*Pi, 2*Pi]. // // TODO(ericv): Rarely, this function returns the area of the complementary // region (4*Pi - area). This can only happen when the true area is very // close to zero or 4*Pi and the polygon has multiple loops. To make this // function completely robust requires checking whether the signed area sum is // ambiguous, and if so, determining the loop nesting structure. This allows // the sum to be evaluated in a way that is guaranteed to have the correct // sign. double area = 0; double max_error = 0; int num_chains = shape.NumChains(); for (int chain_id = 0; chain_id < num_chains; ++chain_id) { GetChainVertices(shape, chain_id, out var vertices); area += S2.GetSignedArea(vertices.ToList()); #if DEBUG max_error += S2.GetCurvatureMaxError(new(vertices)); #endif } // Note that S2.GetSignedArea() guarantees that the full loop (containing // all points on the sphere) has a very small negative area. System.Diagnostics.Debug.Assert(Math.Abs(area) <= S2.M_4_PI + max_error); if (area < 0.0) { area += S2.M_4_PI; } return(area); }
// For shapes of dimension 2, returns the sum of all loop perimeters on the // unit sphere. Otherwise returns zero. (See GetLength for shapes of // dimension 1.) // // All edges are modeled as spherical geodesics. The result can be converted // to a distance on the Earth's surface (with a worst-case error of 0.562% // near the equator) using the functions in s2earth.h. public static S1Angle GetPerimeter(S2Shape shape) { if (shape.Dimension() != 2) { return(S1Angle.Zero); } var perimeter = S1Angle.Zero; int num_chains = shape.NumChains(); for (int chain_id = 0; chain_id < num_chains; ++chain_id) { GetChainVertices(shape, chain_id, out var vertices); perimeter += S2.GetPerimeter(vertices); } return(perimeter); }
// For shapes of dimension 1, returns the sum of all polyline lengths on the // unit sphere. Otherwise returns zero. (See GetPerimeter for shapes of // dimension 2.) // // All edges are modeled as spherical geodesics. The result can be converted // to a distance on the Earth's surface (with a worst-case error of 0.562% // near the equator) using the functions in s2earth.h. public static S1Angle GetLength(S2Shape shape) { if (shape.Dimension() != 1) { return(S1Angle.Zero); } var length = S1Angle.Zero; int num_chains = shape.NumChains(); for (int chain_id = 0; chain_id < num_chains; ++chain_id) { GetChainVertices(shape, chain_id, out var vertices); length += S2PolylineMeasures.GetLength(vertices); } return(length); }
// Converts a 2-dimensional S2Shape into an S2Polygon. Each chain is converted // to an S2Loop and the vector of loops is used to construct the S2Polygon. public static S2Polygon ShapeToS2Polygon(S2Shape poly) { if (poly.IsFull()) { return new S2Polygon(S2Loop.kFull); } System.Diagnostics.Debug.Assert(poly.Dimension() == 2); List<S2Loop> loops = new(); for (int i = 0; i < poly.NumChains(); ++i) { S2.GetChainVertices(poly, i, out var vertices); loops.Add(new S2Loop(vertices)); } var output_poly = new S2Polygon(loops, initOriented: true); return output_poly; }
// Overwrites "vertices" with the vertices of the given edge chain of "shape". // If dimension == 1, the chain will have (chain.length + 1) vertices, and // otherwise it will have (chain.length) vertices. // // This is a low-level helper method used in the implementations of some of // the methods above. public static void GetChainVertices(S2Shape shape, int chain_id, out S2Point[] vertices) { S2Shape.Chain chain = shape.GetChain(chain_id); int num_vertices = chain.Length + (shape.Dimension() == 1 ? 1 : 0); int e = 0; var verts = new List <S2Point>(); if ((num_vertices & 1) != 0) { verts.Add(shape.ChainEdge(chain_id, e++).V0); } for (; e < num_vertices; e += 2) { var edge = shape.ChainEdge(chain_id, e); verts.Add(edge.V0); verts.Add(edge.V1); } vertices = verts.ToArray(); }
/// <summary> /// Verifies that all methods of the two S2Shapes return identical results, /// except for id() and type_tag(). /// </summary> public static void ExpectEqual(S2Shape a, S2Shape b) { Assert.True(a.NumEdges() == b.NumEdges()); for (int i = 0; i < a.NumEdges(); ++i) { Assert.Equal(a.GetEdge(i), b.GetEdge(i)); Assert.True(a.GetChainPosition(i) == b.GetChainPosition(i)); } Assert.True(a.Dimension() == b.Dimension()); Assert.True(a.GetReferencePoint() == b.GetReferencePoint()); Assert.True(a.NumChains() == b.NumChains()); for (int i = 0; i < a.NumChains(); ++i) { Assert.True(a.GetChain(i) == b.GetChain(i)); int chain_length = a.GetChain(i).Length; for (int j = 0; j < chain_length; ++j) { Assert.True(a.ChainEdge(i, j) == b.ChainEdge(i, j)); } } }
// Like GetArea(), except that this method is faster and has more error. The // additional error is at most 2.22e-15 steradians per vertex, which works out // to about 0.09 square meters per vertex on the Earth's surface. For // example, a loop with 100 vertices has a maximum error of about 9 square // meters. (The actual error is typically much smaller than this.) public static double GetApproxArea(S2Shape shape) { if (shape.Dimension() != 2) { return(0.0); } double area = 0; int num_chains = shape.NumChains(); for (int chain_id = 0; chain_id < num_chains; ++chain_id) { GetChainVertices(shape, chain_id, out var vertices); area += S2.GetApproxArea(vertices.ToList()); //todo } // Special case to ensure that full polygons are handled correctly. if (area <= S2.M_4_PI) { return(area); } return(area % S2.M_4_PI); }
// Returns true if the given shape contains the given point. Most clients // should not use this method, since its running time is linear in the number // of shape edges. Instead clients should create an S2ShapeIndex and use // S2ContainsPointQuery, since this strategy is much more efficient when many // points need to be tested. // // Polygon boundaries are treated as being semi-open (see S2ContainsPointQuery // and S2VertexModel for other options). // // CAVEAT: Typically this method is only used internally. Its running time is // linear in the number of shape edges. public static bool ContainsBruteForce(this S2Shape shape, S2Point point) { if (shape.Dimension() < 2) { return(false); } var ref_point = shape.GetReferencePoint(); if (ref_point.Point == point) { return(ref_point.Contained); } S2CopyingEdgeCrosser crosser = new(ref_point.Point, point); bool inside = ref_point.Contained; for (int e = 0; e < shape.NumEdges(); ++e) { var edge = shape.GetEdge(e); inside ^= crosser.EdgeOrVertexCrossing(edge.V0, edge.V1); } return(inside); }
// This is a helper function for implementing S2Shape.GetReferencePoint(). // // Given a shape consisting of closed polygonal loops, the interior of the // shape is defined as the region to the left of all edges (which must be // oriented consistently). This function then chooses an arbitrary point and // returns true if that point is contained by the shape. // // Unlike S2Loop and S2Polygon, this method allows duplicate vertices and // edges, which requires some extra care with definitions. The rule that we // apply is that an edge and its reverse edge "cancel" each other: the result // is the same as if that edge pair were not present. Therefore shapes that // consist only of degenerate loop(s) are either empty or full; by convention, // the shape is considered full if and only if it contains an empty loop (see // S2LaxPolygonShape for details). // // Determining whether a loop on the sphere contains a point is harder than // the corresponding problem in 2D plane geometry. It cannot be implemented // just by counting edge crossings because there is no such thing as a "point // at infinity" that is guaranteed to be outside the loop. public static S2Shape.ReferencePoint GetReferencePoint(this S2Shape shape) { System.Diagnostics.Debug.Assert(shape.Dimension() == 2); if (shape.NumEdges() == 0) { // A shape with no edges is defined to be full if and only if it // contains at least one chain. return(S2Shape.ReferencePoint.FromContained(shape.NumChains() > 0)); } // Define a "matched" edge as one that can be paired with a corresponding // reversed edge. Define a vertex as "balanced" if all of its edges are // matched. In order to determine containment, we must find an unbalanced // vertex. Often every vertex is unbalanced, so we start by trying an // arbitrary vertex. var edge = shape.GetEdge(0); if (GetReferencePointAtVertex(shape, edge.V0, out var result)) { return(result); } // That didn't work, so now we do some extra work to find an unbalanced // vertex (if any). Essentially we gather a list of edges and a list of // reversed edges, and then sort them. The first edge that appears in one // list but not the other is guaranteed to be unmatched. int n = shape.NumEdges(); var edges = new S2Shape.Edge[n]; var rev_edges = new S2Shape.Edge[n]; for (int i = 0; i < n; ++i) { var edge2 = shape.GetEdge(i); edges[i] = edge2; rev_edges[i] = new S2Shape.Edge(edge2.V1, edge2.V0); } Array.Sort(edges); Array.Sort(rev_edges); for (int i = 0; i < n; ++i) { if (edges[i] < rev_edges[i]) { // edges[i] is unmatched System.Diagnostics.Debug.Assert(GetReferencePointAtVertex(shape, edges[i].V0, out result)); return(result); } if (rev_edges[i] < edges[i]) { // rev_edges[i] is unmatched System.Diagnostics.Debug.Assert(GetReferencePointAtVertex(shape, rev_edges[i].V0, out result)); return(result); } } // All vertices are balanced, so this polygon is either empty or full except // for degeneracies. By convention it is defined to be full if it contains // any chain with no edges. for (int i = 0; i < shape.NumChains(); ++i) { if (shape.GetChain(i).Length == 0) { return(S2Shape.ReferencePoint.FromContained(true)); } } return(S2Shape.ReferencePoint.FromContained(false)); }