// 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); }
// This is a helper function for GetReferencePoint() below. // // If the given vertex "vtest" is unbalanced (see definition below), sets // "result" to a ReferencePoint indicating whther "vtest" is contained and // returns true. Otherwise returns false. private static bool GetReferencePointAtVertex(S2Shape shape, S2Point vtest, out S2Shape.ReferencePoint result) { // Let P be an unbalanced vertex. Vertex P is defined to be inside the // region if the region contains a particular direction vector starting from // P, namely the direction S2::RefDir(P). This can be calculated using // S2ContainsVertexQuery. var contains_query = new S2ContainsVertexQuery(vtest); int n = shape.NumEdges(); for (int e = 0; e < n; ++e) { var edge = shape.GetEdge(e); if (edge.V0 == vtest) { contains_query.AddEdge(edge.V1, 1); } if (edge.V1 == vtest) { contains_query.AddEdge(edge.V0, -1); } } int contains_sign = contains_query.ContainsSign(); if (contains_sign == 0) { result = new S2Shape.ReferencePoint(S2Point.Empty, false); return(false); // There are no unmatched edges incident to this vertex. } result = new S2Shape.ReferencePoint(vtest, contains_sign > 0); return(true); }
// 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; }
// A ShapeEncoder that encode all standard S2Shape types, preferring // compact (but slower) encodings. For example, points that have been snapped // to S2CellId centers will be encoded using at most 8 bytes. // // REQUIRES: "encoder" uses the default constructor, so that its buffer // can be enlarged as necessary by calling Ensure(int). public static bool CompactEncodeShape(S2Shape shape, Encoder encoder) { var tag = shape.GetTypeTag(); if (tag == S2Shape.TypeTag.kNoTypeTag) { System.Diagnostics.Debug.WriteLine("(DFATAL) Unsupported S2Shape type: " + tag); return(false); } // Update the following constant when adding new S2Shape encodings. System.Diagnostics.Debug.Assert(tag < S2Shape.TypeTag.kNextAvailableTypeTag); shape.Encode(encoder, CodingHint.COMPACT); return(true); }
private static void CompareS2LoopToShape(S2Loop loop, S2Shape shape) { MutableS2ShapeIndex index = new(); index.Add(shape); S2Cap cap = loop.GetCapBound(); var query = index.MakeS2ContainsPointQuery(); for (int iter = 0; iter < 100; ++iter) { S2Point point = S2Testing.SamplePoint(cap); Assert.Equal(loop.Contains(point), query.ShapeContains(index.Shape(0), point)); } }
// 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 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); }
// 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); }
// 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; }
private static void AddShapeWithLabels(S2Shape shape, EdgeType edge_type, S2Builder builder, EdgeLabelMap edge_label_map) { const int kLabelBegin = 1234; // Arbitrary. for (int e = 0; e < shape.NumEdges(); ++e) { Int32 label = kLabelBegin + e; builder.SetLabel(label); // For undirected edges, reverse the direction of every other input edge. S2Shape.Edge edge = shape.GetEdge(e); if (edge_type == EdgeType.UNDIRECTED && ((e & 1) != 0)) { edge = new S2Shape.Edge(edge.V1, edge.V0); } builder.AddEdge(edge.V0, edge.V1); edge_label_map[GetKey(edge, edge_type)].Add(label); } }
// 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); }
// Returns a pointer to the clipped shape corresponding to the given shape, // or null if the shape does not intersect this cell. public S2ClippedShape FindClipped(S2Shape shape) { return(FindClipped(shape.Id)); }
// 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)); }
// The purpose of this function is to construct polygons consisting of // multiple loops. It takes as input a collection of loops whose boundaries // do not cross, and groups them into polygons whose interiors do not // intersect (where the boundary of each polygon may consist of multiple // loops). // // some of those islands have lakes, then the input to this function would // islands, and their lakes. Each loop would actually be present twice, once // in each direction (see below). The output would consist of one polygon // representing each lake, one polygon representing each island not including // islands or their lakes, and one polygon representing the rest of the world // // This method is intended for internal use; external clients should use // S2Builder, which has more convenient interface. // // The input consists of a set of connected components, where each component // consists of one or more loops. The components must satisfy the following // properties: // // - The loops in each component must form a subdivision of the sphere (i.e., // they must cover the entire sphere without overlap), except that a // component may consist of a single loop if and only if that loop is // degenerate (i.e., its interior is empty). // // - The boundaries of different components must be disjoint (i.e. no // crossing edges or shared vertices). // // - No component should be empty, and no loop should have zero edges. // // The output consists of a set of polygons, where each polygon is defined by // the collection of loops that form its boundary. This function does not // actually construct any S2Shapes; it simply identifies the loops that belong // to each polygon. public static void BuildPolygonBoundaries(List <List <S2Shape> > components, List <List <S2Shape> > polygons) { polygons.Clear(); if (!components.Any()) { return; } // Since the loop boundaries do not cross, a loop nesting hierarchy can be // defined by choosing any point on the sphere as the "point at infinity". // Loop A then contains loop B if (1) A contains the boundary of B and (2) // loop A does not contain the point at infinity. // // We choose S2.Origin for this purpose. The loop nesting hierarchy then // determines the face structure. Here are the details: // // 1. Build an S2ShapeIndex of all loops that do not contain S2.Origin. // This leaves at most one unindexed loop per connected component // (the "outer loop"). // // 2. For each component, choose a representative vertex and determine // which indexed loops contain it. The "depth" of this component is // defined as the number of such loops. // // 3. Assign the outer loop of each component to the containing loop whose // depth is one less. This generates a set of multi-loop polygons. // // 4. The outer loops of all components at depth 0 become a single face. var index = new MutableS2ShapeIndex(); // A map from shape.id() to the corresponding component number. var component_ids = new List <int>(); var outer_loops = new List <S2Shape>(); for (int i = 0; i < components.Count; ++i) { var component = components[i]; foreach (var loop in component) { if (component.Count > 1 && !loop.ContainsBruteForce(S2.Origin)) { // Ownership is transferred back at the end of this function. index.Add(loop); component_ids.Add(i); } else { outer_loops.Add(loop); } } // Check that there is exactly one outer loop in each component. System.Diagnostics.Debug.Assert(i + 1 == outer_loops.Count); // Component is not a subdivision } // Find the loops containing each component. var ancestors = new List <List <S2Shape> >(components.Count); var contains_query = index.MakeS2ContainsPointQuery(); for (int i = 0; i < outer_loops.Count; ++i) { var loop = outer_loops[i]; System.Diagnostics.Debug.Assert(loop.NumEdges() > 0); ancestors[i] = contains_query.GetContainingShapes(loop.GetEdge(0).V0); } // Assign each outer loop to the component whose depth is one less. // Components at depth 0 become a single face. Dictionary <S2Shape, List <S2Shape> > children = new(); // btree_map for (int i = 0; i < outer_loops.Count; ++i) { S2Shape?ancestor = null; int depth = ancestors[i].Count; if (depth > 0) { foreach (var candidate in ancestors[i]) { if (ancestors[component_ids[candidate.Id]].Count == depth - 1) { System.Diagnostics.Debug.Assert(ancestor is null); ancestor = candidate; } } System.Diagnostics.Debug.Assert(ancestor is not null); } S2Shape notNullAncestor = ancestor !; if (!children.ContainsKey(notNullAncestor)) { children.Add(notNullAncestor, new List <S2Shape>()); } children[notNullAncestor].Add(outer_loops[i]); } // There is one face per loop that is not an outer loop, plus one for the // outer loops of components at depth 0. polygons.Resize(index.NumShapeIds() + 1, () => new List <S2Shape>()); for (int i = 0; i < index.NumShapeIds(); ++i) { var polygon = polygons[i]; var loop = index.Shape(i); var itr = children.ContainsKey(loop) ? children[loop] : null; if (itr != null) { polygon = itr; } polygon.Add(loop); } polygons[^ 1] = children[null];
public ShapeEdge(S2Shape shape, Int32 edge_id) : this(shape.Id, edge_id, shape.GetEdge(edge_id)) { }
public S2WrappedShape(S2Shape shape) => shape_ = shape;