// Sort of bad to introduce a dependency on the Path class, but this works. // The option to have this logic in QPExtractor is worse imo (does not promote code reuse and is hard to test, // because QPExtractor would not expose the method). public static Orientation GetOrientationOfCycle <TVertex>(this IReadOnlyUndirectedGraph <TVertex> graph, IEnumerable <TVertex> closedPath) where TVertex : IEquatable <TVertex>, IVertexInPlane { if (graph is null) { throw new ArgumentNullException(nameof(graph)); } if (closedPath is null) { throw new ArgumentNullException(nameof(closedPath)); } if (!closedPath.First().Equals(closedPath.Last())) { throw new ArgumentException($"The path is not closed.", nameof(closedPath)); } if (closedPath.Count() == 1) { throw new ArgumentException($"The path is stationary."); } var pathAsCircularListOfVertices = new CircularList <TVertex>(closedPath.SkipLast(1)); // Sum the external angle at every vertex double externalAngleSum = 0; for (int i = 0; i < pathAsCircularListOfVertices.Count; i++) { var vertex1 = pathAsCircularListOfVertices[i - 1]; var vertex2 = pathAsCircularListOfVertices[i]; var vertex3 = pathAsCircularListOfVertices[i + 1]; var pos1 = vertex1.Position; var pos2 = vertex2.Position; var pos3 = vertex3.Position; externalAngleSum += PlaneUtility.GetExternalAngle(pos1, pos2, pos3); } const double Tolerance = 0.01; if (Math.Abs(externalAngleSum - 2 * Math.PI) < Tolerance) { return(Orientation.Counterclockwise); } else if (Math.Abs(externalAngleSum + 2 * Math.PI) < Tolerance) { return(Orientation.Clockwise); } else { throw new OrientationException($"Failed to determine the orientation of {closedPath}; external angle sum was {externalAngleSum}."); } }
/// <summary> /// Finds the faces of the specified graph. /// </summary> /// <typeparam name="TVertex">The type of the vertices.</typeparam> /// <param name="graph">The graph whose faces to find.</param> /// <param name="boundingCycles">Output parameter for the collection of faces, each /// represented by a bounding cycle oriented counterclockwise.</param> /// <returns><see langword="true"/> if the search was successful (or equivalently, the /// graph is plane). <see langword="false"/> if the search failed (or equivalently, the /// graph is not plane).</returns> public bool TryFindFaces <TVertex>(IReadOnlyUndirectedGraph <TVertex> graph, out IEnumerable <IEnumerable <TVertex> > boundingCycles) where TVertex : IEquatable <TVertex>, IComparable <TVertex>, IVertexInPlane { if (graph is null) { throw new ArgumentNullException(nameof(graph)); } if (!graph.IsPlane()) { boundingCycles = null; return(false); } boundingCycles = SearchForFaces(graph, Orientation.Counterclockwise); return(true); }
/// <summary> /// Returns a boolean value indicating whether the quiver is plane, i.e., whether the /// arrows (drawn as straight lines) are pairwise non-intersecting. /// </summary> /// <returns>A boolean value indicating whether the quiver is plane.</returns> public static bool IsPlane <TVertex>(this IReadOnlyUndirectedGraph <TVertex> graph) where TVertex : IEquatable <TVertex>, IVertexInPlane { // Remark: Deconstructing lambda arguments does not seem to be possible as of this writing. In other words, // the following would not work: // ... .Where( ((e1, e2)) => e1 != e2 ) foreach (var(edge1, edge2) in Utility.CartesianProduct(graph.Edges, graph.Edges).Where(p => p.Item1 != p.Item2)) { var lineSegment1 = new OrientedLineSegment(edge1.Vertex1.Position, edge1.Vertex2.Position); var lineSegment2 = new OrientedLineSegment(edge2.Vertex1.Position, edge2.Vertex2.Position); if (lineSegment1.IntersectsProperly(lineSegment2)) { return(false); } } return(true); }
private IEnumerable <IEnumerable <TVertex> > SearchForFaces <TVertex>(IReadOnlyUndirectedGraph <TVertex> graph, Orientation searchOrientation) where TVertex : IEquatable <TVertex>, IComparable <TVertex>, IVertexInPlane { // Algorithm (outline): // Keep track of traversed edges *and the direction in which the edge was traversed* // (i.e., keep track of traversed directed edges). // // For every directed edge (not already traversed), make a search: // Keep track of the path of edges traversed. // Start by traversing that edge. // Keep turning "maximally" in the direction given by the search orientation until a vertex is visited twice. // This gives a full path that decomposes into a path and a cycle. // The path always consists of only boundary directed edges (boundary w.r.t. the search orientation), // while the cycle bounds a (bounded) face precisely when its orientation agrees with the search orientation. // Remark: Sort of bad to use Arrow here (writing a DirectedEdge class with a common interface would be a proper solution), // but it allows so much code to be reused. var cycles = new HashSet <DetachedCycle <TVertex> >(); var directedEdges = graph.Edges.SelectMany(e => new Arrow <TVertex>[] { new Arrow <TVertex>(e.Vertex1, e.Vertex2), new Arrow <TVertex>(e.Vertex2, e.Vertex1) }); var remainingEdges = new HashSet <Arrow <TVertex> >(directedEdges); var remainingEdgesStack = new Stack <Arrow <TVertex> >(directedEdges); while (remainingEdges.Count > 0) { Arrow <TVertex> startEdge; while (!remainingEdges.Contains(startEdge = remainingEdgesStack.Pop())) { ; } var path = new Path <TVertex>(startEdge.Source, startEdge.Target); var prevVertex = startEdge.Source; var curVertex = startEdge.Target; // Begin the search for this start edge! while (true) { // Terminate the search if we have found a cycle if (path.TryExtractTrailingCycle(out var closedPath, out int startIndex)) { var cycleOrientation = graph.GetOrientationOfCycle(closedPath.Vertices); // If the orientations agree, then the cycle is a bounding cycle for a (bounded) face // Else, it bounds the unbounded face (which we do not care about) if (cycleOrientation == searchOrientation) { var detachedCycle = new DetachedCycle <TVertex>(closedPath); // When restarting with a boundary arrow (directed edge), we might get the same cycle again if (!cycles.Contains(detachedCycle)) { cycles.Add(new DetachedCycle <TVertex>(closedPath)); } } foreach (var edge in path.Arrows) { remainingEdges.Remove(edge); } break; } // Get next successor (next in the angle sense) // If no successor, terminate the search (all arrows are direction-boundary arrows) // Else, update path, prevVertex and curVertex and do next iteration var neighbors = graph.AdjacencyLists[curVertex]; // Note that this includes prevVertex if (neighbors.Count == 1) // The last edge was a dead end! { foreach (var arrow in path.Arrows) { remainingEdges.Remove(arrow); } break; } var baseVertex = curVertex; var basePos = baseVertex.Position; // Sort the vertices by angle so that the vertex following the predecessor vertex (prevVertex) is // the vertex corresponding to a "maximal" turn. var neighborsSortedByAngle = searchOrientation == Orientation.Clockwise ? neighbors.OrderBy(vertex => vertex.Position, new AngleBasedPointComparer(basePos)) : neighbors.OrderByDescending(vertex => vertex.Position, new AngleBasedPointComparer(basePos)); var neighborsSortedByAngleList = new CircularList <TVertex>(neighborsSortedByAngle); int predecessorIndex = neighborsSortedByAngleList.IndexOf(prevVertex); neighborsSortedByAngleList.RotateLeft(predecessorIndex); var nextVertex = neighborsSortedByAngleList.Skip(1).First(); path = path.AppendVertex(nextVertex); prevVertex = curVertex; curVertex = nextVertex; } } return(cycles.Select(cycle => cycle.CanonicalPath.Vertices)); }