atlas/tools/calculate_center.py

151 lines
3.6 KiB
Python
Raw Normal View History

"""
From https://github.com/stefda/polylabel,
which is in turn implemented from https://github.com/mapbox/polylabel
"""
from queue import Queue
import math
SQRT2 = math.sqrt(2)
def polylabel(polygon, precision=0.5, debug=False):
"""
Computes the pole of inaccessibility coordinate in [x, y] format.
"""
# find the bounding box of the outer ring
minX = maxX = polygon[0][0]
minY = maxY = polygon[0][1]
for p in polygon[1:]:
if p[0] < minX: minX = p[0]
if p[1] < minY: minY = p[1]
if p[0] > maxX: maxX = p[0]
if p[1] > maxY: maxY = p[1]
width = maxX - minX
height = maxY - minY
cellSize = min(width, height)
h = cellSize / 2.0
# priority queue of cells in order of their "potential" (max distance to polygon)
cellQueue = Queue(0)
if cellSize == 0: return [minX, minY];
# cover polygon with initial cells
for x in range(math.floor(minX), math.ceil(maxX - 1), round(cellSize)):
for y in range(math.floor(minY), math.ceil(maxY - 1), round(cellSize)):
cellQueue.put(Cell(x + h, y + h, h, polygon))
# take centroid as the first best guess
bestCell = getCentroidCell(polygon)
# special case for rectangular polygons
bboxCell = Cell(minX + width / 2, minY + height / 2, 0, polygon)
if bboxCell.d > bestCell.d: bestCell = bboxCell
numProbes = cellQueue.qsize()
while not cellQueue.empty():
# pick the most promising cell from the queue
cell = cellQueue.get()
# update the best cell if we found a better one
if cell.d > bestCell.d:
bestCell = cell
if debug: print('found best %d after %d probes' % (round(1e4 * cell.d) / 1e4, numProbes))
# do not drill down further if there's no chance of a better solution
if cell.max - bestCell.d <= precision: continue
# split the cell into four cells
h = cell.h / 2
cellQueue.put(Cell(cell.x - h, cell.y - h, h, polygon))
cellQueue.put(Cell(cell.x + h, cell.y - h, h, polygon))
cellQueue.put(Cell(cell.x - h, cell.y + h, h, polygon))
cellQueue.put(Cell(cell.x + h, cell.y + h, h, polygon))
numProbes += 4
if debug:
print('num probes: %d' % numProbes)
print('best distance: %d' % bestCell.d)
return [bestCell.x, bestCell.y]
def compareMax(a, b):
return b.max - a.max
class Cell:
def __init__(self, x, y, h, polygon):
self.x = x # cell center x
self.y = y # cell center y
self.h = h # half the cell size
self.d = pointToPolygonDist(x, y, polygon) # distance from cell center to polygon
self.max = self.d + self.h * SQRT2 # max distance to polygon within a cell
def pointToPolygonDist(x, y, polygon):
"""
Computes the signed distance from point to polygon outline (negative if point is outside).
"""
inside = False
minDistSq = float('inf')
for a, b in zip(polygon, rotate(polygon)):
if ((a[1] > y) != (b[1] > y)) and (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0]):
inside = not inside
minDistSq = min(minDistSq, getSegDistSq(x, y, a, b))
return math.sqrt(minDistSq) * (1 if inside else -1)
def getCentroidCell(polygon):
"""
Gets the polygon centroid.
"""
area = 0
x = 0
y = 0
for a, b in zip(polygon, rotate(polygon)):
f = a[0] * b[1] - b[0] * a[1]
x += (a[0] + b[0]) * f
y += (a[1] + b[1]) * f
area += f * 3
if area == 0:
return Cell(polygon[0][0], polygon[0][1], 0, polygon)
return Cell(x / area, y / area, 0, polygon)
def getSegDistSq(px, py, a, b):
"""
Gets the squared distance from a point to a segment.
"""
x = a[0]
y = a[1]
dx = b[0] - x
dy = b[1] - y
if dx != 0 or dy != 0:
t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy)
if t > 1:
x = b[0]
y = b[1]
elif t > 0:
x += dx * t
y += dy * t
dx = px - x
dy = py - y
return dx * dx + dy * dy
def rotate(l):
return l[-1:] + l[:-1]