136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
|
from ..geometry import Point, LineString
|
||
|
from ..geos import TopologicalError
|
||
|
from heapq import heappush, heappop
|
||
|
|
||
|
|
||
|
class Cell(object):
|
||
|
"""A `Cell`'s centroid property is a potential solution to finding the pole
|
||
|
of inaccessibility for a given polygon. Rich comparison operators are used
|
||
|
for sorting `Cell` objects in a priority queue based on the potential
|
||
|
maximum distance of any theoretical point within a cell to a given
|
||
|
polygon's exterior boundary.
|
||
|
"""
|
||
|
def __init__(self, x, y, h, polygon):
|
||
|
self.x = x
|
||
|
self.y = y
|
||
|
self.h = h # half of cell size
|
||
|
self.centroid = Point(x, y) # cell centroid, potential solution
|
||
|
|
||
|
# distance from cell centroid to polygon exterior
|
||
|
self.distance = self._dist(polygon)
|
||
|
|
||
|
# max distance to polygon exterior within a cell
|
||
|
self.max_distance = self.distance + h * 1.4142135623730951 # sqrt(2)
|
||
|
|
||
|
# rich comparison operators for sorting in minimum priority queue
|
||
|
def __lt__(self, other):
|
||
|
return self.max_distance > other.max_distance
|
||
|
|
||
|
def __le__(self, other):
|
||
|
return self.max_distance >= other.max_distance
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return self.max_distance == other.max_distance
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return self.max_distance != other.max_distance
|
||
|
|
||
|
def __gt__(self, other):
|
||
|
return self.max_distance < other.max_distance
|
||
|
|
||
|
def __ge__(self, other):
|
||
|
return self.max_distance <= other.max_distance
|
||
|
|
||
|
def _dist(self, polygon):
|
||
|
"""Signed distance from Cell centroid to polygon outline. The returned
|
||
|
value is negative if the point is outside of the polygon exterior
|
||
|
boundary.
|
||
|
"""
|
||
|
inside = polygon.contains(self.centroid)
|
||
|
distance = self.centroid.distance(LineString(polygon.exterior.coords))
|
||
|
if inside:
|
||
|
return distance
|
||
|
return -distance
|
||
|
|
||
|
|
||
|
def polylabel(polygon, tolerance=1.0):
|
||
|
"""Finds pole of inaccessibility for a given polygon. Based on
|
||
|
Vladimir Agafonkin's https://github.com/mapbox/polylabel
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
polygon : shapely.geometry.Polygon
|
||
|
tolerance : int or float, optional
|
||
|
`tolerance` represents the highest resolution in units of the
|
||
|
input geometry that will be considered for a solution. (default
|
||
|
value is 1.0).
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
shapely.geometry.Point
|
||
|
A point representing the pole of inaccessibility for the given input
|
||
|
polygon.
|
||
|
|
||
|
Raises
|
||
|
------
|
||
|
shapely.geos.TopologicalError
|
||
|
If the input polygon is not a valid geometry.
|
||
|
|
||
|
Example
|
||
|
-------
|
||
|
>>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50),
|
||
|
... (-100, -20), (-150, -200)]).buffer(100)
|
||
|
>>> label = polylabel(polygon, tolerance=10)
|
||
|
>>> label.wkt
|
||
|
'POINT (59.35615556364569 121.8391962974644)'
|
||
|
"""
|
||
|
if not polygon.is_valid:
|
||
|
raise TopologicalError('Invalid polygon')
|
||
|
minx, miny, maxx, maxy = polygon.bounds
|
||
|
width = maxx - minx
|
||
|
height = maxy - miny
|
||
|
cell_size = min(width, height)
|
||
|
h = cell_size / 2.0
|
||
|
cell_queue = []
|
||
|
|
||
|
# First best cell approximation is one constructed from the centroid
|
||
|
# of the polygon
|
||
|
x, y = polygon.centroid.coords[0]
|
||
|
best_cell = Cell(x, y, 0, polygon)
|
||
|
|
||
|
# Special case for rectangular polygons avoiding floating point error
|
||
|
bbox_cell = Cell(minx + width / 2.0, miny + height / 2, 0, polygon)
|
||
|
if bbox_cell.distance > best_cell.distance:
|
||
|
best_cell = bbox_cell
|
||
|
|
||
|
# build a regular square grid covering the polygon
|
||
|
x = minx
|
||
|
while x < maxx:
|
||
|
y = miny
|
||
|
while y < maxy:
|
||
|
heappush(cell_queue, Cell(x + h, y + h, h, polygon))
|
||
|
y += cell_size
|
||
|
x += cell_size
|
||
|
|
||
|
# minimum priority queue
|
||
|
while cell_queue:
|
||
|
cell = heappop(cell_queue)
|
||
|
|
||
|
# update the best cell if we find a better one
|
||
|
if cell.distance > best_cell.distance:
|
||
|
best_cell = cell
|
||
|
|
||
|
# continue to the next iteration if we cant find a better solution
|
||
|
# based on tolerance
|
||
|
if cell.max_distance - best_cell.distance <= tolerance:
|
||
|
continue
|
||
|
|
||
|
# split the cell into quadrants
|
||
|
h = cell.h / 2.0
|
||
|
heappush(cell_queue, Cell(cell.x - h, cell.y - h, h, polygon))
|
||
|
heappush(cell_queue, Cell(cell.x + h, cell.y - h, h, polygon))
|
||
|
heappush(cell_queue, Cell(cell.x - h, cell.y + h, h, polygon))
|
||
|
heappush(cell_queue, Cell(cell.x + h, cell.y + h, h, polygon))
|
||
|
|
||
|
return best_cell.centroid
|