/// <summary> /// Checks if the other group's outer perimeter is inside this group's perimeter. Inclusive of the two groups /// sharing vertices. /// </summary> /// <param name="otherGroup"></param> /// <returns>True if the other group is inside this group, false otherwise.</returns> public bool IsOtherGroupInThisGroup(ConnectedNodeGroup otherGroup) { HashSet <Vector2> sharedVertices = this.GetSharedVertices(otherGroup); if (sharedVertices.Count == 0) { return(GeometryFuncs.IsPolyInPoly(otherGroup.outerPerimSimplified, this.outerPerimSimplified)); } else { bool isInside = true; foreach (PolygonSplittingGraphNode node in otherGroup.nodes.Values) { var nodeCoord = new Vector2(node.x, node.y); if (sharedVertices.Contains(nodeCoord)) { continue; } if (!GeometryFuncs.IsPointInPoly(nodeCoord, this.outerPerimSimplified)) { isInside = false; break; } } return(isInside); } }
/// <summary> /// Checks if another ConnectedNodeGroup shares a vertex (or vertices) with this group, IFF the shared vertex nodes /// have different IDs. /// </summary> /// <param name="otherGroup"></param> /// <returns>A HashSet of Vector2s of the vertices that appear in both groups but with different IDs.</returns> public HashSet <Vector2> GetSharedVertices(ConnectedNodeGroup otherGroup) { if (otherGroup is null) { throw new ArgumentNullException(nameof(otherGroup)); } var sharedVertices = new HashSet <Vector2>(); foreach (PolygonSplittingGraphNode thisNode in this.outerPerimNodes) { foreach (PolygonSplittingGraphNode otherNode in otherGroup.outerPerimNodes) { if (thisNode.x == otherNode.x && thisNode.y == otherNode.y) { if (thisNode.id == otherNode.id) { continue; } sharedVertices.Add(new Vector2(thisNode.x, thisNode.y)); } } } return(sharedVertices); }
/// <summary> /// Given the input <param>connectedGroup</param>, gets all faces by running the following algorithm for EACH /// CONNECTION for EVERY NODE: /// 1. Ensure that all nodes have their connections ordered counter-clockwise (already done in ConnectedNodeGroup /// constructor) /// 2. Given some starting node S, travel any of its connections, which we will define as C /// 3. From C, pick its connection that is next in order from the previous node (in this case, S) /// 4. Repeat until we get back to S, and now we have a face. /// This method gets all faces, many of which will be duplicates. /// A better explanation is from here: https://math.stackexchange.com/questions/8140/find-all-cycles-faces-in-a-graph /// </summary> /// <param name="connectedGroup"></param> /// <returns></returns> private static List <List <PolygonSplittingGraphNode> > _GetAllFaces(ConnectedNodeGroup connectedGroup) { var allFaces = new List <List <PolygonSplittingGraphNode> >(); foreach (PolygonSplittingGraphNode node in connectedGroup.nodes.Values) { foreach (int connID in node.connectedNodeIDs) { if (!connectedGroup.nodes.ContainsKey(connID)) { continue; //thanks to bridges a node may have a connection to a node that is not present in its node group (bridges split node groups) } var face = new List <PolygonSplittingGraphNode> { node }; int prevID = node.id; int currentID = connID; do { face.Add(connectedGroup.nodes[currentID]); int nextID; do { //in case of bridge, keep getting nextID if it does not exist in connectedGroup nextID = connectedGroup.GetCCWNextNodeID(prevID, currentID); } while (!connectedGroup.nodes.ContainsKey(nextID)); prevID = currentID; currentID = nextID; } while (prevID != node.id); allFaces.Add(face); } } return(allFaces); }
/// <summary> /// Final step in partitioning a ConnectedNodeGroup, by converting it into a RectangularPolygon and checking if /// the partition contains any holes (AKA the outerPerim of any ConnectedNodeGroups its parent ConnectedNodeGroup /// contains). /// </summary> /// <param name="cycle">Cycle discovered from BFS, AKA partition of ConnectedNodeGroup.></param> /// <param name="connectedGroup">The ConnectedNodeGroup <param>cycle</param> was derived from.</param> /// <param name="connectedGroups">A list of all ConnectedNodeGroups.</param> /// <param name="idsContainedInGroup">The IDs of the ConnectedNodeGroups contained within <param>connectedGroup</param></param> /// <param name="nodes"></param> /// <param name="innerHoles">Holes extracted from the same ConnectedNodeGroup.</param> /// <returns></returns> private static ChordlessPolygon _FinalisePartition(List <PolygonSplittingGraphNode> cycle, ConnectedNodeGroup connectedGroup, SortedList <int, ConnectedNodeGroup> connectedGroups, SortedDictionary <int, HashSet <int> > idsContainedInGroup, SortedList <int, PolygonSplittingGraphNode> nodes, List <ChordlessPolygon> innerHoles) { var cycleAsVectorArray = new Vector2[cycle.Count]; for (int i = 0; i < cycle.Count; i++) { PolygonSplittingGraphNode node = cycle[i]; cycleAsVectorArray[i] = new Vector2(node.x, node.y); } var potentialHoles = new List <List <Vector2> >(); potentialHoles.AddRange(innerHoles.Select(innerHole => innerHole.outerPerim.ToList())); var bridges = new Dictionary <Vector2, HashSet <Vector2> >(); foreach (int connectedGroupID in idsContainedInGroup[connectedGroup.id]) { ConnectedNodeGroup groupInside = connectedGroups[connectedGroupID]; potentialHoles.Add(groupInside.outerPerimSimplified.ToList()); foreach (int nodeID in groupInside.bridges.Keys) { if (!cycle.Exists(x => x.id == nodeID)) { continue; } var nodeCoord = new Vector2(nodes[nodeID].x, nodes[nodeID].y); foreach (int neighbourID in groupInside.bridges[nodeID]) { if (!cycle.Exists(x => x.id == neighbourID)) { continue; } var neighbourCoord = new Vector2(nodes[neighbourID].x, nodes[neighbourID].y); if (!bridges.ContainsKey(nodeCoord)) { bridges[nodeCoord] = new HashSet <Vector2>(); } bridges[nodeCoord].Add(neighbourCoord); } } } return(new ChordlessPolygon(cycleAsVectorArray, potentialHoles.ToArray(), bridges, false)); }
/// <summary> /// Creates a dictionary that stores IDs of ConnectedNodeGroups contained within each ConnectedNodeGroup. /// </summary> /// <param name="connectedGroups">List of ConnectedNodeGroups, sorted in order of area size, descending.</param> /// <returns></returns> public static SortedDictionary <int, HashSet <int> > StoreNestedGroups(SortedList <int, ConnectedNodeGroup> connectedGroups) { if (connectedGroups is null) { throw new ArgumentNullException(nameof(connectedGroups)); } var idsContainedInGroup = new SortedDictionary <int, HashSet <int> >(); for (int i = 0; i < connectedGroups.Count; i++) { ConnectedNodeGroup thisGroup = connectedGroups[i]; idsContainedInGroup.Add(thisGroup.id, new HashSet <int>()); for (int j = 0; j < connectedGroups.Count; j++) { ConnectedNodeGroup otherGroup = connectedGroups[j]; if (i != j && thisGroup.IsOtherGroupInThisGroup(otherGroup)) { idsContainedInGroup[thisGroup.id].Add(otherGroup.id); } } } return(_SimplifyNestedGroups(idsContainedInGroup)); }
private bool _Equals(ConnectedNodeGroup other) { return(this.id == other.id && this.nodes.Equals(other.nodes) && this.outerPerimNodes.Equals(other.outerPerimNodes)); }
/// <summary> /// Partitions a connected node group by: /// 1. Repeatedly running BFS on its nodes to find cycles and removing edges for the next BFS until no more /// cycles can be found. /// 1.1 Note that the BFS only runs on nodes with two or less VALID EDGES. Edges are only removed IFF either /// of its vertices has two or less valid edges. This ensures we do not remove an edge that could be used /// twice, for example two adjacent cycles that share an edge. /// 2. The cycle is added to a list of perimeters (AKA a list of nodes) IFF it is NOT a hole. /// </summary> /// <param name="connectedGroup">ConnectedNodeGroup that is being partitioned.</param> /// <param name="idsContainedInGroup">The IDs of the ConnectedNodeGroups contained within <param>connectedGroup</param></param> /// <param name="connectedGroups">List of all ConnectedNodeGroups.</param> /// <param name="nodes">List of all nodes.</param> /// <param name="holes">Holes of the polygon that was made into a PolygonSplittingGraph.</param> /// <returns></returns> public static List <ChordlessPolygon> PartitionConnectedNodeGroup(ConnectedNodeGroup connectedGroup, SortedDictionary <int, HashSet <int> > idsContainedInGroup, SortedList <int, ConnectedNodeGroup> connectedGroups, SortedList <int, PolygonSplittingGraphNode> nodes, List <Vector2>[] holes) { if (connectedGroup is null) { throw new ArgumentNullException(nameof(connectedGroup)); } if (idsContainedInGroup is null) { throw new ArgumentNullException(nameof(idsContainedInGroup)); } if (connectedGroups is null) { throw new ArgumentNullException(nameof(connectedGroups)); } if (nodes is null) { throw new ArgumentNullException(nameof(nodes)); } if (holes is null) { throw new ArgumentNullException(nameof(holes)); } var outerPerimCycle = new List <PolygonSplittingGraphNode>(); var polyCycles = new List <List <PolygonSplittingGraphNode> >(); var holeCycles = new List <List <PolygonSplittingGraphNode> >(); List <List <PolygonSplittingGraphNode> > allFaces = _GetAllFaces(connectedGroup); var uniqueFaces = new List <List <Vector2> >(); foreach (List <PolygonSplittingGraphNode> newFace in allFaces) { //construct Vector2[] or List<Vector2> describing face perim in Vector2s var newFacePerim = new Vector2[newFace.Count]; for (int i = 0; i < newFace.Count; i++) { PolygonSplittingGraphNode node = newFace[i]; newFacePerim[i] = new Vector2(node.x, node.y); } bool newFaceUnique = true; foreach (List <Vector2> uniqueFace in uniqueFaces) { if (GeometryFuncs.ArePolysIdentical(uniqueFace.ToArray(), newFacePerim)) { newFaceUnique = false; break; } } if (newFaceUnique) { uniqueFaces.Add(newFacePerim.ToList()); if (IsCycleHole(newFacePerim, holes)) { holeCycles.Add(newFace); } else if (IsCycleOuterPerim(newFacePerim, connectedGroup.outerPerimNodes)) { outerPerimCycle.AddRange(newFace); } else if (IsCycleComplex(newFacePerim)) { continue; } else { polyCycles.Add(newFace); } } } var innerHoles = holeCycles.Select(_FinaliseInnerHole).ToList(); var partitions = new List <ChordlessPolygon>(); foreach (List <PolygonSplittingGraphNode> polyCycle in polyCycles) { partitions.Add(_FinalisePartition(polyCycle, connectedGroup, connectedGroups, idsContainedInGroup, nodes, innerHoles)); } if (partitions.Count == 0 && outerPerimCycle.Count > 0) //planar face that represents the outer perim is only relevant IFF there are no other (non-hole) faces { partitions.Add(_FinalisePartition(outerPerimCycle, connectedGroup, connectedGroups, idsContainedInGroup, nodes, innerHoles)); } return(partitions); }