summaryrefslogtreecommitdiff
path: root/lib/graph.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/graph.py')
-rw-r--r--lib/graph.py909
1 files changed, 0 insertions, 909 deletions
diff --git a/lib/graph.py b/lib/graph.py
deleted file mode 100644
index d8ccf75..0000000
--- a/lib/graph.py
+++ /dev/null
@@ -1,909 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2011 Association of Universities for Research in
-# Astronomy (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above
-# copyright notice, this list of conditions and the following
-# disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials
-# provided with the distribution.
-#
-# 3. The name of AURA and its representatives may not be used to
-# endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT,
-# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-# OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-This contains the code that does the actual unioning of regions.
-"""
-# TODO: Weak references for memory management problems?
-from __future__ import absolute_import, division, unicode_literals, print_function
-
-# STDLIB
-import itertools
-import weakref
-
-# THIRD-PARTY
-import numpy as np
-
-# LOCAL
-from . import great_circle_arc
-from . import vector
-
-# Set to True to enable some sanity checks
-DEBUG = True
-
-
-class Graph:
- """
- A graph of nodes connected by edges. The graph is used to build
- unions between polygons.
-
- .. note::
- This class is not meant to be used directly. Instead, use
- `~sphere.polygon.SphericalPolygon.union` and
- `~sphere.polygon.SphericalPolygon.intersection`.
- """
-
- class Node:
- """
- A `~Graph.Node` represents a single point, connected by an arbitrary
- number of `~Graph.Edge` objects to other `~Graph.Node` objects.
- """
- def __init__(self, point, source_polygons=[]):
- """
- Parameters
- ----------
- point : 3-sequence (*x*, *y*, *z*) coordinate
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon(s) this node came from. Used for bookkeeping.
- """
- point = vector.normalize_vector(*point)
-
- self._point = np.asanyarray(point)
- self._source_polygons = set(source_polygons)
- self._edges = weakref.WeakSet()
-
- def __repr__(self):
- return "Node(%s %d)" % (str(self._point), len(self._edges))
-
- def follow(self, edge):
- """
- Follows from one edge to another across this node.
-
- Parameters
- ----------
- edge : `~Graph.Edge` instance
- The edge to follow away from.
-
- Returns
- -------
- other : `~Graph.Edge` instance
- The other edge.
- """
- edges = list(self._edges)
- assert len(edges) == 2
- try:
- return edges[not edges.index(edge)]
- except IndexError:
- raise ValueError("Following from disconnected edge")
-
- def equals(self, other, thres=2e-8):
- """
- Returns `True` if the location of this and the *other*
- `~Graph.Node` are the same.
-
- Parameters
- ----------
- other : `~Graph.Node` instance
- The other node.
-
- thres : float
- If difference is smaller than this, points are equal.
- The default value of 2e-8 radians is set based on
- emphirical test cases. Relative threshold based on
- the actual sizes of polygons is not implemented.
- """
- # return np.array_equal(self._point, other._point)
- return great_circle_arc.length(self._point, other._point,
- degrees=False) < thres
-
-
- class Edge:
- """
- An `~Graph.Edge` represents a connection between exactly two
- `~Graph.Node` objects. This `~Graph.Edge` class has no direction.
- """
- def __init__(self, A, B, source_polygons=[]):
- """
- Parameters
- ----------
- A, B : `~Graph.Node` instances
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon this edge came from. Used for bookkeeping.
- """
- self._nodes = [A, B]
- for node in self._nodes:
- node._edges.add(self)
- self._source_polygons = set(source_polygons)
-
- def __repr__(self):
- nodes = self._nodes
- return "Edge(%s -> %s)" % (nodes[0]._point, nodes[1]._point)
-
- def follow(self, node):
- """
- Follow along the edge from the given *node* to the other
- node.
-
- Parameters
- ----------
- node : `~Graph.Node` instance
-
- Returns
- -------
- other : `~Graph.Node` instance
- """
- nodes = self._nodes
- try:
- return nodes[not nodes.index(node)]
- except IndexError:
- raise ValueError("Following from disconnected node")
-
- def equals(self, other):
- """
- Returns `True` if the other edge is between the same two nodes.
-
- Parameters
- ----------
- other : `~Graph.Edge` instance
-
- Returns
- -------
- equals : bool
- """
- if (self._nodes[0].equals(other._nodes[0]) and
- self._nodes[1].equals(other._nodes[1])):
- return True
- if (self._nodes[1].equals(other._nodes[0]) and
- self._nodes[0].equals(other._nodes[1])):
- return True
- return False
-
-
- def __init__(self, polygons):
- """
- Parameters
- ----------
- polygons : sequence of `~sphere.polygon.SphericalPolygon` instances
- Build a graph from this initial set of polygons.
- """
- self._nodes = set()
- self._edges = set()
- self._source_polygons = set()
- self._start_node = None
-
- self.add_polygons(polygons)
-
- def add_polygons(self, polygons):
- """
- Add more polygons to the graph.
-
- .. note::
- Must be called before `union` or `intersection`.
-
- Parameters
- ----------
- polygons : sequence of `~sphere.polygon.SphericalPolygon` instances
- Set of polygons to add to the graph
- """
- for polygon in polygons:
- self.add_polygon(polygon)
-
- def add_polygon(self, polygon):
- """
- Add a single polygon to the graph.
-
- .. note::
- Must be called before `union` or `intersection`.
-
- Parameters
- ----------
- polygon : `~sphere.polygon.SphericalPolygon` instance
- Polygon to add to the graph
- """
- points = polygon._points
-
- if len(points) < 3:
- raise ValueError("Too few points in polygon")
-
- self._source_polygons.add(polygon)
-
- start_node = nodeA = self.add_node(points[0], [polygon])
- if self._start_node is None:
- self._start_node = start_node
- for i in range(1, len(points) - 1):
- nodeB = self.add_node(points[i], [polygon])
- # Don't create self-pointing edges
- if nodeB is not nodeA:
- self.add_edge(nodeA, nodeB, [polygon])
- nodeA = nodeB
- # Close the polygon
- self.add_edge(nodeA, start_node, [polygon])
-
- def add_node(self, point, source_polygons=[]):
- """
- Add a node to the graph. It will be disconnected until used
- in a call to `add_edge`.
-
- Parameters
- ----------
- point : 3-sequence (*x*, *y*, *z*) coordinate
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon this node came from. Used for bookkeeping.
-
- Returns
- -------
- node : `~Graph.Node` instance
- The new node
- """
- new_node = self.Node(point, source_polygons)
-
- # Don't add nodes that already exist. Update the existing
- # node's source_polygons list to include the new polygon.
- for node in self._nodes:
- if node.equals(new_node):
- node._source_polygons.update(source_polygons)
- return node
-
- self._nodes.add(new_node)
- return new_node
-
- def remove_node(self, node):
- """
- Removes a node and all of the edges that touch it.
-
- .. note::
- It is assumed that *Node* is already a part of the graph.
-
- Parameters
- ----------
- node : `~Graph.Node` instance
- """
- assert node in self._nodes
-
- for edge in list(node._edges):
- nodeB = edge.follow(node)
- nodeB._edges.remove(edge)
- self._edges.remove(edge)
- self._nodes.remove(node)
-
- def add_edge(self, A, B, source_polygons=[]):
- """
- Add an edge between two nodes.
-
- .. note::
- It is assumed both nodes already belong to the graph.
-
- Parameters
- ----------
- A, B : `~Graph.Node` instances
-
- source_polygons : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon(s) this edge came from. Used for bookkeeping.
-
- Returns
- -------
- edge : `~Graph.Edge` instance
- The new edge
- """
- assert A in self._nodes
- assert B in self._nodes
-
- # Don't add any edges that already exist. Update the edge's
- # source polygons list to include the new polygon. Care needs
- # to be taken here to not create an Edge until we know we need
- # one, otherwise the Edge will get hooked up to the nodes but
- # be orphaned.
- for edge in self._edges:
- if ((A.equals(edge._nodes[0]) and
- B.equals(edge._nodes[1])) or
- (B.equals(edge._nodes[0]) and
- A.equals(edge._nodes[1]))):
- edge._source_polygons.update(source_polygons)
- return edge
-
- new_edge = self.Edge(A, B, source_polygons)
- self._edges.add(new_edge)
- return new_edge
-
- def remove_edge(self, edge):
- """
- Remove an edge from the graph. The nodes it points to remain intact.
-
- .. note::
- It is assumed that *edge* is already a part of the graph.
-
- Parameters
- ----------
- edge : `~Graph.Edge` instance
- """
- assert edge in self._edges
-
- A, B = edge._nodes
- A._edges.remove(edge)
- if len(A._edges) == 0:
- self.remove_node(A)
- if A is not B:
- B._edges.remove(edge)
- if len(B._edges) == 0:
- self.remove_node(B)
- self._edges.remove(edge)
-
- def split_edge(self, edge, node):
- """
- Splits an `~Graph.Edge` *edge* at `~Graph.Node` *node*, removing
- *edge* and replacing it with two new `~Graph.Edge` instances.
- It is intended that *E* is along the original edge, but that is
- not enforced.
-
- Parameters
- ----------
- edge : `~Graph.Edge` instance
- The edge to split
-
- node : `~Graph.Node` instance
- The node to insert
-
- Returns
- -------
- edgeA, edgeB : `~Graph.Edge` instances
- The two new edges on either side of *node*.
- """
- assert edge in self._edges
- assert node in self._nodes
-
- A, B = edge._nodes
- edgeA = self.add_edge(A, node, edge._source_polygons)
- edgeB = self.add_edge(node, B, edge._source_polygons)
- if edge not in (edgeA, edgeB):
- self.remove_edge(edge)
- return [edgeA, edgeB]
-
- def _sanity_check(self, title, node_is_2=False):
- """
- For debugging purposes: assert that edges and nodes are
- connected to each other correctly and there are no orphaned
- edges or nodes.
- """
- if not DEBUG:
- return
-
- try:
- unique_edges = set()
- for edge in self._edges:
- for node in edge._nodes:
- assert edge in node._edges
- assert node in self._nodes
- edge_repr = [tuple(x._point) for x in edge._nodes]
- edge_repr.sort()
- edge_repr = tuple(edge_repr)
- # assert edge_repr not in unique_edges
- unique_edges.add(edge_repr)
-
- for node in self._nodes:
- if node_is_2:
- assert len(node._edges) == 2
- else:
- assert len(node._edges) >= 2
- for edge in node._edges:
- assert node in edge._nodes
- assert edge in self._edges
- except AssertionError as e:
- import traceback
- traceback.print_exc()
- self._dump_graph(title=title)
- raise
-
- def _dump_graph(self, title=None, lon_0=0, lat_0=90,
- projection='vandg', func=lambda x: len(x._edges)):
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
- fig = plt.figure()
- m = Basemap()
-
- counts = {}
- for node in self._nodes:
- count = func(node)
- counts.setdefault(count, [])
- counts[count].append(list(node._point))
-
- minx = np.inf
- miny = np.inf
- maxx = -np.inf
- maxy = -np.inf
- for k, v in counts.items():
- v = np.array(v)
- ra, dec = vector.vector_to_radec(v[:, 0], v[:, 1], v[:, 2])
- x, y = m(ra, dec)
- m.plot(x, y, 'o', label=str(k))
- for x0 in x:
- minx = min(x0, minx)
- maxx = max(x0, maxx)
- for y0 in y:
- miny = min(y0, miny)
- maxy = max(y0, maxy)
-
- for edge in list(self._edges):
- A, B = [x._point for x in edge._nodes]
- r0, d0 = vector.vector_to_radec(A[0], A[1], A[2])
- r1, d1 = vector.vector_to_radec(B[0], B[1], B[2])
- m.drawgreatcircle(r0, d0, r1, d1, color='blue')
-
- plt.xlim(minx, maxx)
- plt.ylim(miny, maxy)
- if title:
- plt.title("%s, %d v, %d e" % (
- title, len(self._nodes), len(self._edges)))
- plt.legend()
- plt.show()
-
- def union(self):
- """
- Once all of the polygons have been added to the graph,
- join the polygons together.
-
- Returns
- -------
- points : Nx3 array of (*x*, *y*, *z*) points
- This is a list of points outlining the union of the
- polygons that were given to the constructor. If the
- original polygons are disjunct or contain holes, cut lines
- will be included in the output.
- """
- self._remove_cut_lines()
- self._sanity_check("union - remove cut lines")
- self._find_all_intersections()
- self._sanity_check("union - find all intersections")
- self._remove_interior_edges()
- self._sanity_check("union - remove interior edges")
- self._remove_3ary_edges()
- self._sanity_check("union - remove 3ary edges")
- self._remove_orphaned_nodes()
- self._sanity_check("union - remove orphan nodes", True)
- return self._trace()
-
- def intersection(self):
- """
- Once all of the polygons have been added to the graph,
- calculate the intersection.
-
- Returns
- -------
- points : Nx3 array of (*x*, *y*, *z*) points
- This is a list of points outlining the intersection of the
- polygons that were given to the constructor. If the
- resulting polygons are disjunct or contain holes, cut lines
- will be included in the output.
- """
- self._remove_cut_lines()
- self._sanity_check("intersection - remove cut lines")
- self._find_all_intersections()
- self._sanity_check("intersection - find all intersections")
- self._remove_exterior_edges()
- self._sanity_check("intersection - remove exterior edges")
- self._remove_3ary_edges(large_first=True)
- self._sanity_check("intersection - remove 3ary edges")
- self._remove_orphaned_nodes()
- self._sanity_check("intersection - remove orphan nodes", True)
- return self._trace()
-
- def _remove_cut_lines(self):
- """
- Removes any cutlines that may already have existed in the
- input polygons. This is so any cutlines in the final result
- will be optimized to be as short as possible and won't
- intersect each other.
-
- This works by finding coincident edges that are reverse to
- each other, and then splicing around them.
- """
- # As this proceeds, edges are removed from the graph. It
- # iterates over a static list of all edges that exist at the
- # start, so each time one is selected, we need to ensure it
- # still exists as part of the graph.
-
- # This transforms the following (where = is the cut line)
- #
- # \ /
- # A' + + B'
- # | |
- # A +====================+ B
- #
- # D +====================+ C
- # | |
- # D' + + C'
- # / \
- #
- # to this:
- #
- # \ /
- # A' + + B'
- # | |
- # A + + C
- # | |
- # D' + + C'
- # / \
- #
-
- edges = list(self._edges)
- for i in xrange(len(edges)):
- AB = edges[i]
- if AB not in self._edges:
- continue
- A, B = AB._nodes
- for j in xrange(i + 1, len(edges)):
- CD = edges[j]
- if CD not in self._edges:
- continue
- C, D = CD._nodes
- # To be a cutline, the candidate edges need to run in
- # the opposite direction, hence A == D and B == C, not
- # A == C and B == D.
- if (A.equals(D) and B.equals(C)):
- # Create new edges A -> D' and C -> B'
- self.add_edge(
- A, D.follow(CD).follow(D),
- AB._source_polygons | CD._source_polygons)
- self.add_edge(
- C, B.follow(AB).follow(B),
- AB._source_polygons | CD._source_polygons)
-
- # Remove B and D which are identical to C and A
- # respectively. We do not need to remove AB and
- # CD because this will remove it for us.
- self.remove_node(D)
- self.remove_node(B)
-
- break
-
- def _find_all_intersections(self):
- """
- Find all the intersecting edges in the graph. For each
- intersecting pair, four new edges are created around the
- intersection point.
- """
- def get_edge_points(edges):
- return (np.array([x._nodes[0]._point for x in edges]),
- np.array([x._nodes[1]._point for x in edges]))
-
- # For speed, we want to vectorize all of the intersection
- # calculations. Therefore, there is a list of edges, and two
- # arrays containing the end points of those edges. They all
- # need to have things added and removed from them at the same
- # time to keep them in sync, but of course the interface for
- # doing so is different between Python lists and numpy arrays.
-
- edges = list(self._edges)
- starts, ends = get_edge_points(edges)
-
- # First, split all edges by any nodes that intersect them
- while len(edges) > 1:
- AB = edges.pop(0)
- A = starts[0]; starts = starts[1:] # numpy equiv of "pop(0)"
- B = ends[0]; ends = ends[1:] # numpy equiv of "pop(0)"
-
- distance = great_circle_arc.length(A, B)
- for node in self._nodes:
- if node not in AB._nodes:
- distanceA = great_circle_arc.length(node._point, A)
- distanceB = great_circle_arc.length(node._point, B)
- if np.abs((distanceA + distanceB) - distance) < 1e-8:
- newA, newB = self.split_edge(AB, node)
-
- new_edges = [
- edge for edge in (newA, newB)
- if edge not in edges]
-
- edges = new_edges + edges
- new_starts, new_ends = get_edge_points(new_edges)
- starts = np.vstack(
- (new_starts, starts))
- ends = np.vstack(
- (new_ends, ends))
- break
-
- edges = list(self._edges)
- starts, ends = get_edge_points(edges)
-
- # Next, calculate edge-to-edge intersections and break
- # edges on the intersection point.
- while len(edges) > 1:
- AB = edges.pop(0)
- A = starts[0]; starts = starts[1:] # numpy equiv of "pop(0)"
- B = ends[0]; ends = ends[1:] # numpy equiv of "pop(0)"
-
- # Calculate the intersection points between AB and all
- # other remaining edges
- with np.errstate(invalid='ignore'):
- intersections = great_circle_arc.intersection(
- A, B, starts, ends)
- # intersects is `True` everywhere intersections has an
- # actual intersection
- intersects = np.isfinite(intersections[..., 0])
-
- intersection_indices = np.nonzero(intersects)[0]
-
- # Iterate through the candidate intersections, if any --
- # we want to eliminate intersections that only intersect
- # at the end points
- for j in intersection_indices:
- C = starts[j]
- D = ends[j]
- CD = edges[j]
- E = intersections[j]
-
- # This is a bona-fide intersection, and E is the
- # point at which the two lines intersect. Make a
- # new node for it -- this must belong to the all
- # of the source polygons of both of the edges that
- # crossed.
-
- # A
- # |
- # C--E--D
- # |
- # B
-
- E = self.add_node(
- E, AB._source_polygons | CD._source_polygons)
- newA, newB = self.split_edge(AB, E)
- newC, newD = self.split_edge(CD, E)
-
- new_edges = [
- edge for edge in (newA, newB, newC, newD)
- if edge not in edges]
-
- # Delete CD, and push the new edges to the
- # front so they will be tested for intersection
- # against all remaining edges.
- del edges[j] # CD
- edges = new_edges + edges
- new_starts, new_ends = get_edge_points(new_edges)
- starts = np.vstack(
- (new_starts, starts[:j], starts[j+1:]))
- ends = np.vstack(
- (new_ends, ends[:j], ends[j+1:]))
- break
-
- def _remove_interior_edges(self):
- """
- Removes any nodes that are contained inside other polygons.
- What's left is the (possibly disjunct) outline.
- """
- polygons = self._source_polygons
-
- for edge in self._edges:
- edge._count = 0
- for polygon in polygons:
- if (not polygon in edge._source_polygons and
- polygon.intersects_arc(
- edge._nodes[0]._point, edge._nodes[1]._point)):
- edge._count += 1
-
- for edge in list(self._edges):
- if edge._count >= 1:
- self.remove_edge(edge)
-
- def _remove_exterior_edges(self):
- """
- Removes any edges that are not contained in all of the source
- polygons. What's left is the (possibly disjunct) outline.
- """
- polygons = self._source_polygons
-
- for edge in self._edges:
- edge._count = 0
- for polygon in polygons:
- if (polygon in edge._source_polygons or
- polygon.intersects_arc(
- edge._nodes[0]._point, edge._nodes[1]._point)):
- edge._count += 1
-
- for edge in list(self._edges):
- if edge._count < len(polygons):
- self.remove_edge(edge)
-
- def _remove_3ary_edges(self, large_first=False):
- """
- Remove edges between pairs of nodes that have 3 or more edges.
- This removes triangles that can't be traced.
- """
- if large_first:
- max_ary = 0
- for node in self._nodes:
- max_ary = max(len(node._edges), max_ary)
- order = range(max_ary + 1, 2, -1)
- else:
- order = [3]
-
- for i in order:
- removals = []
- for edge in list(self._edges):
- if (len(edge._nodes[0]._edges) >= i and
- len(edge._nodes[1]._edges) >= i):
- removals.append(edge)
-
- for edge in removals:
- if edge in self._edges:
- self.remove_edge(edge)
-
- def _remove_orphaned_nodes(self):
- """
- Remove nodes with fewer than 2 edges.
- """
- while True:
- removes = []
- for node in list(self._nodes):
- if len(node._edges) < 2:
- removes.append(node)
- if len(removes):
- for node in removes:
- self.remove_node(node)
- else:
- break
-
- def _trace(self):
- """
- Given a graph that has had cutlines removed and all
- intersections found, traces it to find a resulting single
- polygon.
- """
- polygons = []
- edges = set(self._edges) # copy
- seen_nodes = set()
- while len(edges):
- polygon = []
- # Carefully pick out an "original" edge first. Synthetic
- # edges may not be pointing in the right direction to
- # properly calculate the area.
- for edge in edges:
- if len(edge._source_polygons) == 1:
- break
- edges.remove(edge)
- start_node = node = edge._nodes[0]
- while True:
- # TODO: Do we need this if clause any more?
- if len(polygon):
- if not np.array_equal(polygon[-1], node._point):
- polygon.append(node._point)
- else:
- polygon.append(node._point)
- edge = node.follow(edge)
- edges.discard(edge)
- node = edge.follow(node)
- if node is start_node:
- polygon.append(node._point)
- break
-
- polygons.append(np.asarray(polygon))
-
- if len(polygons) == 1:
- return polygons[0]
- elif len(polygons) == 0:
- return []
- else:
- return self._join(polygons)
-
- def _join(self, polygons):
- """
- If the graph is disjunct, joins the parts with cutlines.
-
- The closest nodes between each pair that don't intersect
- any other edges are used as cutlines.
-
- TODO: This is not optimal, because the closest distance
- between two polygons may not in fact be between two vertices,
- but may be somewhere along an edge.
- """
- def do_join(polygons):
- all_polygons = polygons[:]
-
- skipped = 0
-
- polyA = polygons.pop(0)
- while len(polygons):
- polyB = polygons.pop(0)
-
- # If fewer than 3 edges, it's not a polygon,
- # just throw it out
- if len(polyB) < 4:
- continue
-
- # Find the closest set of vertices between polyA and
- # polyB that don't cross any of the edges in *any* of
- # the polygons
- closest = np.inf
- closest_pair_idx = (None, None)
- for a in xrange(len(polyA) - 1):
- A = polyA[a]
- distances = great_circle_arc.length(A, polyB[:-1])
- b = np.argmin(distances)
- distance = distances[b]
- if distance < closest:
- B = polyB[b]
- # Does this candidate line cross other edges?
- crosses = False
- for poly in all_polygons:
- if np.any(
- great_circle_arc.intersects(
- A, B, poly[:-1], poly[1:])):
- crosses = True
- break
- if not crosses:
- closest = distance
- closest_pair_idx = (a, b)
-
- if not np.isfinite(closest):
- # We didn't find a pair of points that don't cross
- # something else, so we want to try to join another
- # polygon. Defer the current polygon until later.
- if len(polygons) in (0, skipped):
- return None
- polygons.append(polyB)
- skipped += 1
- else:
- # Splice the two polygons together using a cut
- # line
- a, b = closest_pair_idx
- new_poly = np.vstack((
- # polyA up to and including the cut point
- polyA[:a+1],
-
- # polyB starting with the cut point and
- # wrapping around back to the cut point.
- # Ignore the last point in polyB, because it
- # is the same as the first
- np.roll(polyB[:-1], -b, 0),
-
- # The cut point on polyB
- polyB[b:b+1],
-
- # the rest of polyA, starting with the cut
- # point
- polyA[a:]
- ))
-
- skipped = 0
- polyA = new_poly
-
- return polyA
-
- for permutation in itertools.permutations(polygons):
- poly = do_join(list(permutation))
- if poly is not None:
- return poly
-
- raise RuntimeError("Could not find cut points")