Beispiel #1
0
        /// <summary>
        /// Find element containing (x,y) coordinate. Returns null if no element found.
        /// <para>
        /// If (x,y) is exacly on the boundary between two elements, one of them will be returned.
        /// If (x,y) is matching exacly a node coordinate, one of the elements including the node will be returned.
        /// </para>
        /// </summary>
        public MeshElement FindElement(double x, double y)
        {
            // Find potential elements for (x,y) point
            Envelope targetEnvelope = new Envelope(x, x, y, y);

#if NTS173
            IList potentialSourceElmts = _elementSearchTree.Query(targetEnvelope);
#else
            IList <MeshElement> potentialSourceElmts = _elementSearchTree.Query(targetEnvelope);
#endif

            // Loop over all potential elements
            for (int i = 0; i < potentialSourceElmts.Count; i++)
            {
#if NTS173
                MeshElement element = (MeshElement)potentialSourceElmts[i];
#else
                MeshElement element = potentialSourceElmts[i];
#endif

                // Check if element includes the (x,y) point
                if (element.Includes(x, y))
                {
                    return(element);
                }
            }

            return(null);
        }
Beispiel #2
0
        protected override Cell GetCell_internal(Vector2 cellPosition)
        {
            var cellPos = Utils.ConvertVector2(cellPosition);
            var result  = NTSSTRTreeCellTree.Query(new Envelope(cellPos[0].MinVal, cellPos[0].MaxVal,
                                                                cellPos[1].MinVal, cellPos[1].MaxVal));

            return(result.First());
        }
Beispiel #3
0
        internal PointCollection KeepWithin(List <VertexPositionColor> coastTriangles)
        {
            var rtree = new STRtree <Polygon>();

            for (int i = 0; i < coastTriangles.Count / 3; i++)
            {
                Coordinate[] coords = new Coordinate[] { new Coordinate(coastTriangles[i * 3].Position.X, coastTriangles[i * 3].Position.Y),
                                                         new Coordinate(coastTriangles[i * 3 + 1].Position.X, coastTriangles[i * 3 + 1].Position.Y),
                                                         new Coordinate(coastTriangles[i * 3 + 2].Position.X, coastTriangles[i * 3 + 2].Position.Y),
                                                         new Coordinate(coastTriangles[i * 3].Position.X, coastTriangles[i * 3].Position.Y) };
                var polygon = new Polygon(new LinearRing(coords));
                rtree.Insert(polygon.EnvelopeInternal, polygon);
            }
            rtree.Build();
            List <Vector2d> newpoints = new List <Vector2d>();

            foreach (var x in points)
            {
                bool contains = false;
                foreach (var p in rtree.Query(new Envelope(x.X, x.X, x.Y, x.Y)))
                {
                    if (p.Covers(new NetTopologySuite.Geometries.Point(x.X, x.Y)))
                    {
                        contains = true;
                        break;
                    }
                }
                if (contains)
                {
                    newpoints.Add(x);
                }
            }
            points = newpoints;
            return(this);
        }
Beispiel #4
0
        public List <SlimAccommodationData> GetNearest(Contracts.MultilingualAccommodation accommodation,
                                                       STRtree <SlimAccommodationData> tree)
        {
            var accommodationEnvelope = new Envelope(accommodation.Location.Coordinates.Longitude - 0.01,
                                                     accommodation.Location.Coordinates.Longitude + 0.01,
                                                     accommodation.Location.Coordinates.Latitude - 0.01, accommodation.Location.Coordinates.Latitude + 0.01);

            return(tree.Query(accommodationEnvelope).ToList());
        }
        public int FindSurroundingSector(Vector2 position)
        {
            var envelope = new Envelope(new Coordinate(position.X, position.Y));

            foreach (var sectorId in _sectorIdLookup.Query(envelope))
            {
                if (IsInsideSector(sectorId, ref position))
                {
                    return(sectorId);
                }
            }

            return(-1);
        }
        private void DisplayShapeByRTree(Coordinate p1, Coordinate p2)
        {
            if (areaSTRtree.IsEmpty)
            {
                return;
            }
            var areaQuery = new Envelope(p1, p2);
            var areaItems = areaSTRtree.Query(areaQuery);

            foreach (IGeometry item in areaItems)
            {
                DrawShape(item);
                areaSTRtree.Remove(item.EnvelopeInternal, item);
            }
        }
Beispiel #7
0
        public void RunQueries()
        {
            var visitor = new CountItemVisitor <object>();

            int size = _index.Count;
            int side = (int)Math.Sqrt(size);

            for (int i = 0; i < side; i++)
            {
                for (int j = 0; j < side; j++)
                {
                    var env = new Envelope(i, i + QUERY_ENV_SIZE, j, j + QUERY_ENV_SIZE);
                    _index.Query(env, visitor);
                }
            }
            Console.WriteLine($"Total query result items = {visitor.Count}");
        }
Beispiel #8
0
        public void TestStrIndex()
        {
            STRtree ndx = new STRtree();

            foreach (var v in CreateTestGeometries(1000, 0.0, 0.0, 3000.0, 3000.0))
            {
                ndx.Insert(v.EnvelopeInternal, v);
            }

            ndx.Build();
            IEnvelope queryExtents = new Envelope(100.0, 120.0, 100.0, 120.0);
            IList     matches      = ndx.Query(queryExtents);

            foreach (IGeometry list in matches)
            {
                Assert.IsTrue(list.EnvelopeInternal.Intersects(queryExtents), "a result from the index does not intersect the query bounds");
            }
        }
Beispiel #9
0
        private HashSet <string> GetCodesFromGeo(string bbox)
        {
            var envelope = GeoUtils.ToEnvelope(bbox);

            var codesFromPoints    = _kdTree.Query(envelope);
            var codesFromEnvelopes = _stRtree.Query(envelope);

            HashSet <string> codesFromGeo = new HashSet <string>();

            foreach (var code in codesFromPoints)
            {
                codesFromGeo.Add(code.Data);
            }

            foreach (var code in codesFromEnvelopes)
            {
                codesFromGeo.Add(code);
            }

            return(codesFromGeo);
        }
Beispiel #10
0
        internal PointCollection RemoveNear(LineGraph roads, double minDis)
        {
            if (roads.nodes.Count == 0)
            {
                return(this);
            }
            var rtree = new STRtree <LineString>();

            foreach (var node in roads.nodes)
            {
                foreach (var next in node.nextConnections)
                {
                    Coordinate[] coords = new Coordinate[] { new Coordinate(node.pos.X, node.pos.Y), new Coordinate(next.pos.X, next.pos.Y) };
                    LineString   ls     = new LineString(coords);
                    rtree.Insert(ls.EnvelopeInternal, ls);
                }
            }
            rtree.Build();
            List <Vector2d> newpoints = new List <Vector2d>();

            foreach (var x in points)
            {
                bool isNear = false;
                foreach (var ls in rtree.Query(new Envelope(x.X - minDis, x.X + minDis, x.Y - minDis, x.Y + minDis)))
                {
                    if (ls.IsWithinDistance(new NetTopologySuite.Geometries.Point(x.X, x.Y), minDis))
                    {
                        isNear = true;
                        break;
                    }
                }
                if (!isNear)
                {
                    newpoints.Add(x);
                }
            }
            points = newpoints;
            return(this);
        }
Beispiel #11
0
        private void CheckPiles(List <Pile> piles)
        {
            STRtree <Pile> rtree = new STRtree <Pile> ();

            foreach (var p in piles)
            {
                var r = getPileEnvelope(p);
                rtree.Insert(r, p);
            }

            var minLenAbs = Options.PileSide * PileOptions.PileRatioLmin;
            var minLen    = minLenAbs - 0.1;

            List <Pile> pilesErrMinLen = new List <Pile>();

            foreach (var p in piles)
            {
                var envelope    = new Envelope(p.Position.X - minLen, p.Position.X + minLen, p.Position.Y - minLen, p.Position.Y + minLen);
                var pilesMinLen = rtree.Query(envelope);
                foreach (var item in pilesMinLen)
                {
                    if (p == item)
                    {
                        continue;
                    }
                    if (p.Position.DistanceTo(item.Position) < minLen)
                    {
                        pilesErrMinLen.Add(p);
                    }
                }
            }
            foreach (var item in pilesErrMinLen)
            {
                Inspector.AddError($"Нарушено минимальное расстояние между сваями - Сторона сваи * {PileOptions.PileRatioLmin} = {minLenAbs}. Точка вставки сваи {item.Position}",
                                   item.IdBlRef, System.Drawing.SystemIcons.Warning);
            }
        }
        //Dictionary<Coordinate, bool> isExsitedOnView = new Dictionary<Coordinate, bool>();
        private void DisplayGPSByRTree(Coordinate p1, Coordinate p2)
        {
            if (gpsSTRtree.IsEmpty)
            {
                return;
            }
            var gpsQuery = new Envelope(p1, p2);
            var gpsItems = gpsSTRtree.Query(gpsQuery);

            //MainMap.Markers.Clear();
            //button10_Click(null, null);
            foreach (Coordinate gps in gpsItems)
            {
                //if (!isExsitedOnView.ContainsKey(gps))
                //{
                AddMakerToGmap(gps);
                //isExsitedOnMap.Add(gps, true);
                //}
                //else
                //{
                //   isExsitedOnView[gps] = true;
                //}
                gpsSTRtree.Remove(new Envelope(gps), gps);
            }

            //foreach (var item in isExsitedOnView)
            //{
            //   if (item.Value == false)
            //   {
            //      GMapMarker gps = new GMapMarker(new PointLatLng(item.Key.Y, item.Key.X));
            //      MainMap.Markers.Remove(gps);
            //   }
            //}
            //isExsitedOnView.Clear();
            //isExsitedOnView = gpsItems.GroupBy(x => x).Select(x => x.First()).ToDictionary(key => key, value => true);
        }
 /// <summary>
 /// Query the geospatial model for all entities that fall within a given viewport
 /// </summary>
 /// <param name="viewPort"></param>
 /// <returns></returns>
 public IList <ISpatiallyIndexable> Query(Envelope viewPort)
 {
     return(_geospatialElements.Query(viewPort));
 }
 internal static void DoIntersections(BlobCollection blobs)
 {
     Dictionary<string, long> uids = new Dictionary<string, long>(); // finally decided I needed something to guarantee uniqueness like this TODO: can probably eliminate this
     long uidCounter = -1000;
     List<Way> ways = TempGetWays(blobs);
     ways.Add(blobs.borderWay);
     List<WayRef> wayRefs = new List<WayRef>();
     STRtree<WayRef> rtree = new STRtree<WayRef>();
     foreach (var way in ways)
     {
         for (int i = 1; i < way.refs.Count; i++)
         {
             WayRef wayRef = new WayRef() { wayRef = way, nodePos = i - 1 };
             wayRefs.Add(wayRef);
             Vector2d pos1 = blobs.nodes[way.refs[i - 1]];
             Vector2d pos2 = blobs.nodes[way.refs[i]];
             var env = new Envelope(Math.Min(pos1.X, pos2.X), Math.Max(pos1.X, pos2.X), Math.Min(pos1.Y, pos2.Y), Math.Max(pos1.Y, pos2.Y));
             rtree.Insert(env, wayRef);
         }
     }
     rtree.Build();
     Dictionary<Way, List<NewIntersection>> intersections = new Dictionary<Way, List<NewIntersection>>();
     foreach (var n1 in wayRefs)
     {
         Vector2d pos1 = blobs.nodes[n1.wayRef.refs[n1.nodePos]];
         Vector2d pos2 = blobs.nodes[n1.wayRef.refs[n1.nodePos + 1]];
         var env = new Envelope(Math.Min(pos1.X, pos2.X), Math.Max(pos1.X, pos2.X), Math.Min(pos1.Y, pos2.Y), Math.Max(pos1.Y, pos2.Y));
         foreach (var n2 in rtree.Query(env))
         {
             if (n1.GetHashCode() <= n2.GetHashCode()) continue; // as a way of preventing the same query twice
             long Aid = n1.wayRef.refs[n1.nodePos];
             long Bid = n1.wayRef.refs[n1.nodePos + 1];
             long Cid = n2.wayRef.refs[n2.nodePos];
             long Did = n2.wayRef.refs[n2.nodePos + 1];
             Vector2d A = blobs.nodes[Aid];
             Vector2d B = blobs.nodes[Bid];
             Vector2d C = blobs.nodes[Cid];
             Vector2d D = blobs.nodes[Did];
             if (Math.Min(A.X, B.X) > Math.Max(C.X, D.X)) continue;
             if (Math.Max(A.X, B.X) < Math.Min(C.X, D.X)) continue;
             if (Math.Min(A.Y, B.Y) > Math.Max(C.Y, D.Y)) continue;
             if (Math.Max(A.Y, B.Y) < Math.Min(C.Y, D.Y)) continue;
             string intersectionKey = Aid + "," + Cid;
             // TODO: we're going to treat -1 as always matching for now
             bool ACSame = Aid == Cid;
             bool ADSame = Aid == Did;
             bool BCSame = Bid == Cid;
             bool BDSame = Bid == Did;
             // a subset of possible tiny angles that can cause rounding errors
             // only thing that changes between these is the condition, line direction, and the newpoint id
             // TODO: is this really what fixed the nonsense at 240202043? the angleDiff was only 0.009, which seems too big to cause an issue
             bool someCollinear = false;
             bool isBorderIntersection = Aid < 0 || Bid < 0 || Cid < 0 || Did < 0;
             someCollinear |= CheckCollinear(Aid, n2.nodePos, n2.nodePos + 1, n2, intersections, blobs, true, isBorderIntersection);
             someCollinear |= CheckCollinear(Bid, n2.nodePos, n2.nodePos + 1, n2, intersections, blobs, true, isBorderIntersection);
             someCollinear |= CheckCollinear(Cid, n1.nodePos, n1.nodePos + 1, n1, intersections, blobs, true, isBorderIntersection);
             someCollinear |= CheckCollinear(Did, n1.nodePos, n1.nodePos + 1, n1, intersections, blobs, true, isBorderIntersection);
             if (!ACSame && !ADSame && !BCSame && !BDSame) // proper intersection
             {
                 if (someCollinear)
                 {
                     if (n1.wayRef.id == n2.wayRef.id)
                     {
                         n1.wayRef.selfIntersects = true; // mark for destruction, probably
                     }
                 }
                 else
                 {
                     // let's sort these lines to guarantee duplicates by value
                     long[] ns = new long[] { Aid, Bid, Cid, Did };
                     if (ns[0] >= ns[2]) ns = ns.Reverse().ToArray();
                     if (ns[0] >= ns[1])
                     {
                         var t = ns[1];
                         ns[1] = ns[0];
                         ns[0] = t;
                     }
                     if (ns[2] >= ns[3])
                     {
                         var t = ns[3];
                         ns[3] = ns[2];
                         ns[2] = t;
                     }
                     Vector2d intersection = Intersect(blobs.nodes[ns[0]], blobs.nodes[ns[1]], blobs.nodes[ns[2]], blobs.nodes[ns[3]]);
                     if (intersection != null)
                     {
                         long intersectionID;
                         if (uids.ContainsKey(intersectionKey))
                         {
                             intersectionID = uids[intersectionKey];
                         }
                         else
                         {
                             intersectionID = uidCounter--;
                             uids[intersectionKey] = intersectionID;
                         }
                         NewIntersection newNode1 = new NewIntersection() { nodeID = intersectionID, wayRef = n1.wayRef, nodePos = n1.nodePos + 1 };
                         NewIntersection newNode2 = new NewIntersection() { nodeID = intersectionID, wayRef = n2.wayRef, nodePos = n2.nodePos + 1 };
                         blobs.nodes[intersectionID] = intersection;
                         if (!intersections.ContainsKey(n1.wayRef)) intersections.Add(n1.wayRef, new List<NewIntersection>());
                         intersections[n1.wayRef].Add(newNode1);
                         if (!intersections.ContainsKey(n2.wayRef)) intersections.Add(n2.wayRef, new List<NewIntersection>());
                         intersections[n2.wayRef].Add(newNode2);
                         if (n1.wayRef.id == n2.wayRef.id)
                         {
                             n1.wayRef.selfIntersects = true; // mark for destruction, probably
                         }
                     }
                 }
             }
         }
     }
     List<long> wayids = new List<long>();
     foreach (var pair in intersections)
     {
         if (wayids.Contains(pair.Key.id)) throw new NotImplementedException();
         wayids.Add(pair.Key.id);
     }
     foreach (var pair in intersections)
     {
         foreach (var intersection in pair.Value)
         {
             intersection.sortRank = Sorter(intersection, blobs);
         }
     }
     foreach (var pair in intersections)
     {
         var sorted = pair.Value.OrderBy(x => x.sortRank).ToList();
         // get rid of duplicates
         for (int i = sorted.Count - 1; i > 0; i--)
         {
             if (sorted[i].nodeID == sorted[i - 1].nodeID) sorted.RemoveAt(i);
         }
         // now insert them
         for (int i = sorted.Count - 1; i >= 0; i--)
         {
             pair.Key.refs.Insert(sorted[i].nodePos, sorted[i].nodeID);
         }
     }
     RemoveDuplicates(blobs); // remove duplicates at the end to also remove duplicate intersections
 }
Beispiel #15
0
        public void TestKNearestNeighbors()
        {
            int topK               = 1000;
            int totalRecords       = 10000;
            var geometryFactory    = new GeometryFactory();
            var coordinate         = new Coordinate(10.1, -10.1);
            var queryCenter        = geometryFactory.CreatePoint(coordinate);
            int valueRange         = 1000;
            var testDataset        = new List <Geometry>();
            var correctData        = new List <Geometry>();
            var random             = new Random();
            var distanceComparator = new GeometryDistanceComparer(queryCenter, true);

            /*
             * Generate the random test data set
             */
            for (int i = 0; i < totalRecords; i++)
            {
                coordinate = new Coordinate(-100 + random.Next(valueRange) * 1.1, random.Next(valueRange) * (-5.1));
                var spatialObject = geometryFactory.CreatePoint(coordinate);
                testDataset.Add(spatialObject);
            }

            /*
             * Sort the original data set and make sure the elements are sorted in an ascending order
             */
            testDataset.Sort(distanceComparator);

            /*
             * Get the correct top K
             */
            for (int i = 0; i < topK; i++)
            {
                correctData.Add(testDataset[i]);
            }

            var strtree = new STRtree <Geometry>();

            for (int i = 0; i < totalRecords; i++)
            {
                strtree.Insert(testDataset[i].EnvelopeInternal, testDataset[i]);
            }

            /*
             * Shoot a random query to make sure the STR-Tree is built.
             */
            strtree.Query(new Envelope(1 + 0.1, 1 + 0.1, 2 + 0.1, 2 + 0.1));

            /*
             * Issue the KNN query.
             */
            var testTopK = strtree.NearestNeighbour(queryCenter.EnvelopeInternal, queryCenter,
                                                    new GeometryItemDistance(), topK);
            var topKList = new List <Geometry>(testTopK);

            topKList.Sort(distanceComparator);

            /*
             * Check the difference between correct result and test result. The difference should be 0.
             */
            int difference = 0;

            for (int i = 0; i < topK; i++)
            {
                if (distanceComparator.Compare(correctData[i], topKList[i]) != 0)
                {
                    difference++;
                }
            }

            Assert.That(difference, Is.Zero);
        }
Beispiel #16
0
 public IList <T> Query(Envelope searchEnv)
 {
     return(_index.Query(searchEnv));
 }
Beispiel #17
0
        // special case for streets

        public static bool CheckForIntersection(Geometry geometry, STRtree <Geometry> possibleIntersections)
        {
            return(possibleIntersections.Query(geometry.EnvelopeInternal).Any(x => x.Intersects(geometry)));
        }
 // TODO: somehow merge this with DoIntersections all in one pass elegantly
 // this method is called before all of these maps get added together
 // it will detect and remove shapes found inside each other
 public static void FixLoops(List<SectorConstrainedOSMAreaGraph> addingMaps, BlobCollection blobs)
 {
     // TODO: I think we still need to exclude intersecting loops from our thing
     STRtree<LoopRef> rtree = new STRtree<LoopRef>();
     List<MainLoopRef> mainLoopRefs = new List<MainLoopRef>();
     HashSet<AreaNode> explored = new HashSet<AreaNode>();
     foreach (var addingMap in addingMaps)
     {
         foreach (var nodeList in addingMap.nodes.Values)
         {
             foreach (var node in nodeList)
             {
                 if (explored.Contains(node)) continue;
                 List<AreaNode> newLoop = new List<AreaNode>();
                 AreaNode curr = node;
                 while (true)
                 {
                     newLoop.Add(curr);
                     explored.Add(curr);
                     if (curr.next == node)
                     {
                         mainLoopRefs.Add(new MainLoopRef() { nodes = newLoop, graph = addingMap });
                         break;
                     }
                     curr = curr.next;
                 }
             }
         }
     }
     foreach (var loopRef in mainLoopRefs)
     {
         loopRef.isCW = GetArea(loopRef.nodes, blobs) < 0;
     }
     foreach (var loopRef in mainLoopRefs)
     {
         for (int i = 0; i < loopRef.nodes.Count; i++)
         {
             Vector2d pos1 = blobs.nodes[loopRef.nodes[i].id];
             Vector2d pos2 = blobs.nodes[loopRef.nodes[(i + 1) % loopRef.nodes.Count].id];
             var env = new Envelope(Math.Min(pos1.X, pos2.X), Math.Max(pos1.X, pos2.X), Math.Min(pos1.Y, pos2.Y), Math.Max(pos1.Y, pos2.Y));
             rtree.Insert(env, new LoopRef() { nodes = loopRef.nodes, graph = loopRef.graph, v1 = pos1, v2 = pos2, n1 = loopRef.nodes[i].id, n2 = loopRef.nodes[(i + 1) % loopRef.nodes.Count].id, isCW = loopRef.isCW });
         }
     }
     rtree.Build();
     List<MainLoopRef> remove = new List<MainLoopRef>();
     foreach (var loopRef in mainLoopRefs)
     {
         HashSet<long> nodesLookup = new HashSet<long>();
         foreach (var n in loopRef.nodes) nodesLookup.Add(n.id);
         var node = blobs.nodes[loopRef.nodes.First().id];
         Vector2d v1 = new Vector2d(node.X, 10);
         Vector2d v2 = new Vector2d(node.X, -10);
         var env = new Envelope(node.X, node.X, -10, 10);
         var intersections = rtree.Query(env);
         bool doRemove = false;
         foreach (var group in intersections.GroupBy(x => x.graph))
         {
             bool isOwnGraph = group.First().graph == loopRef.graph;
             if (!isOwnGraph && group.Any(x => x.nodes.Any(y => nodesLookup.Contains(y.id)))) continue; // let's ignore WHOLE GRAPHS that intersect with us (might need to revise this thinking)
             var lessfiltered = group.Where(x => x.v1.X != x.v2.X).ToList(); // skip vertical intersections
             var sorted = lessfiltered.Where(x => Math.Min(x.v1.X, x.v2.X) != node.X).ToList(); // on perfect-overlap of adjoining lines, this will count things appropriately
             sorted = sorted.OrderBy(x => -Intersect(v1, v2, x.v1, x.v2).Y).ToList(); // order from bottom to top
             sorted = sorted.Where(x => x.nodes != loopRef.nodes).ToList(); // ignore our own loop
             List<int> prevSwaps = new List<int>();
             for (int i = 1; i < sorted.Count; i++)
             {
                 // swap perfectly adjacent if necessary
                 if ((sorted[i - 1].v2 == sorted[i].v1 && sorted[i - 1].v2.X == node.X && sorted[i - 1].V1Y() < sorted[i].V2Y()) || (sorted[i - 1].v1 == sorted[i].v2 && sorted[i - 1].v1.X == node.X && sorted[i - 1].V2Y() < sorted[i].V1Y()))
                 {
                     var temp = sorted[i - 1];
                     sorted[i - 1] = sorted[i];
                     sorted[i] = temp;
                     prevSwaps.Add(i - 1);
                 }
             }
             var validateStack = new List<LoopRef>();
             var contains = new List<LoopRef>();
             if (sorted.Count % 2 != 0) throw new NotImplementedException(); // malformed shape
             for (int i = 0; i < sorted.Count; i++)
             {
                 bool outer = !sorted[i].isCW; // we will no longer be checking for outers immediately within outers, etc. because sometimes people mess up
                 bool opens = outer == sorted[i].IsLeftToRight();
                 if (opens)
                 {
                     validateStack.Add(sorted[i]);
                 }
                 else
                 {
                     if (validateStack.Last().nodes != sorted[i].nodes) throw new NotImplementedException(); // malformed shape
                     if (Intersect(v1, v2, validateStack.Last().v1, validateStack.Last().v2).Y > node.Y && Intersect(v1, v2, sorted[i].v1, sorted[i].v2).Y < node.Y)
                     {
                         contains.Add(sorted[i]); // add the one that's above it, sure
                     }
                     validateStack.RemoveAt(validateStack.Count - 1);
                 }
             }
             if (validateStack.Count > 0) throw new NotImplementedException(); // malformed shape
             // remove duplicates that overlap perfectly
             for (int i = sorted.Count - 1; i > 0; i--)
             {
                 if (sorted[i].ContainsNode(sorted[i - 1].v1) || sorted[i].ContainsNode(sorted[i - 1].v2))
                 {
                     Vector2d inCommon = sorted[i].ContainsNode(sorted[i - 1].v1) ? sorted[i - 1].v1 : sorted[i - 1].v2;
                     if (inCommon.X == node.X)
                     {
                         sorted.RemoveRange(i - 1, 2);
                         i -= 2;
                     }
                 }
             }
             if (contains.Count == 0 && isOwnGraph && loopRef.isCW) doRemove = true;
             if (contains.Count == 0) continue;
             var immediate = contains.First();
             if (isOwnGraph && immediate.isCW == loopRef.isCW) doRemove = true; // remove the outermost invalid loop
             if (!isOwnGraph && !immediate.isCW) doRemove = true; // remove anything immediately contained by an outer
         }
         if (doRemove) remove.Add(loopRef);
     }
     foreach (var r in remove)
     {
         foreach (var node in r.nodes)
         {
             r.graph.nodes[node.id].Remove(node);
             if (r.graph.nodes[node.id].Count == 0) r.graph.nodes.Remove(node.id);
         }
     }
 }