Skip to content

Commit e615668

Browse files
authored
Avoid placing polygon labels in holes (#62)
* Avoid placing polygon labels in holes * Traverse vertices in hilbert order to find potential label points * Update test to reflect new label placements * Sorting by Y coordinate is better and easier than Hilbert order * Give a bonus for being near the center of mass * Make label points a little more border-shy * Tall places, not just wide places; border lines, not just border points * Clean up * Try diagonals through the features too * Limit the search for label points to prevent slowing down too much * At this point the center of mass bonus is doing more harm than good * Forgot to update test
1 parent c58a8e3 commit e615668

4 files changed

Lines changed: 264 additions & 97 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.21.0
2+
3+
* Improve label placement to avoid placing labels in polygon holes
4+
15
## 2.20.0
26

37
* Round coordinates instead of truncating them, for better precision when overzooming

‎geometry.cpp‎

Lines changed: 198 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "main.hpp"
2222
#include "options.hpp"
2323
#include "errors.hpp"
24+
#include "projection.hpp"
2425

2526
static int clip(double *x0, double *y0, double *x1, double *y1, double xmin, double ymin, double xmax, double ymax);
2627

@@ -1481,25 +1482,117 @@ draw centerOfMass(const drawvec &dv, size_t start, size_t end, draw centre) {
14811482
}
14821483
}
14831484

1484-
double label_goodness(const drawvec &dv, size_t start, size_t count, long long x, long long y) {
1485-
if (!pnpoly(dv, start, count, x, y)) {
1485+
double label_goodness(const drawvec &dv, long long x, long long y) {
1486+
int nesting = 0;
1487+
1488+
for (size_t i = 0; i < dv.size(); i++) {
1489+
if (dv[i].op == VT_MOVETO) {
1490+
size_t j;
1491+
for (j = i + 1; j < dv.size(); j++) {
1492+
if (dv[j].op != VT_LINETO) {
1493+
break;
1494+
}
1495+
}
1496+
1497+
// if it's inside the ring, and it's an outer ring,
1498+
// we are nested more; if it's an inner ring, we are
1499+
// nested less.
1500+
if (pnpoly(dv, i, j - i, x, y)) {
1501+
if (get_area(dv, i, j) >= 0) {
1502+
nesting++;
1503+
} else {
1504+
nesting--;
1505+
}
1506+
}
1507+
1508+
i = j - 1;
1509+
}
1510+
}
1511+
1512+
if (nesting < 1) {
14861513
return 0; // outside the polygon is as bad as it gets
14871514
}
14881515

14891516
double closest = INFINITY; // square of closest distance to the border
14901517

1491-
for (size_t i = start; i < start + count; i++) {
1518+
for (size_t i = 0; i < dv.size(); i++) {
14921519
double dx = dv[i].x - x;
14931520
double dy = dv[i].y - y;
14941521
double squared = dx * dx + dy * dy;
14951522
if (squared < closest) {
14961523
closest = squared;
14971524
}
1525+
1526+
if (i > 0 && dv[i].op == VT_LINETO) {
1527+
squared = square_distance_from_line(x, y, dv[i - 1].x, dv[i - 1].y, dv[i].x, dv[i].y);
1528+
if (squared < closest) {
1529+
closest = squared;
1530+
}
1531+
}
14981532
}
14991533

15001534
return sqrt(closest);
15011535
}
15021536

1537+
struct sorty {
1538+
long long x;
1539+
long long y;
1540+
};
1541+
1542+
struct sorty_sorter {
1543+
int kind;
1544+
sorty_sorter(int k) : kind(k) {};
1545+
1546+
bool operator()(const sorty &a, const sorty &b) const {
1547+
long long xa, ya, xb, yb;
1548+
1549+
if (kind == 0) { // Y first
1550+
xa = a.x;
1551+
ya = a.y;
1552+
1553+
xb = b.x;
1554+
yb = b.y;
1555+
} else if (kind == 1) { // X first
1556+
xa = a.y;
1557+
ya = a.x;
1558+
1559+
xb = b.y;
1560+
yb = b.x;
1561+
} else if (kind == 2) { // diagonal
1562+
xa = a.x + a.y;
1563+
ya = a.x - a.y;
1564+
1565+
xb = b.x + b.y;
1566+
yb = b.x - b.y;
1567+
} else { // other diagonal
1568+
xa = a.x - a.y;
1569+
ya = a.x + a.y;
1570+
1571+
xb = b.x - b.y;
1572+
yb = b.x + b.y;
1573+
}
1574+
1575+
if (ya < yb) {
1576+
return true;
1577+
} else if (ya == yb && xa < xb) {
1578+
return true;
1579+
} else {
1580+
return false;
1581+
}
1582+
};
1583+
};
1584+
1585+
struct candidate {
1586+
long long x;
1587+
long long y;
1588+
double dist;
1589+
1590+
bool operator<(const candidate &c) const {
1591+
// largest distance sorts first
1592+
return dist > c.dist;
1593+
};
1594+
};
1595+
15031596
// Generate a label point for a polygon feature.
15041597
//
15051598
// A good label point will be near the center of the feature and far from any border.
@@ -1521,6 +1614,10 @@ double label_goodness(const drawvec &dv, size_t start, size_t count, long long x
15211614
drawvec polygon_to_anchor(const drawvec &geom) {
15221615
size_t start = 0, end = 0;
15231616
size_t best_area = 0;
1617+
std::vector<sorty> points;
1618+
1619+
// find the largest outer ring, which will be the best thing
1620+
// to label if we can do it.
15241621

15251622
for (size_t i = 0; i < geom.size(); i++) {
15261623
if (geom[i].op == VT_MOVETO) {
@@ -1529,6 +1626,11 @@ drawvec polygon_to_anchor(const drawvec &geom) {
15291626
if (geom[j].op != VT_LINETO) {
15301627
break;
15311628
}
1629+
1630+
sorty sy;
1631+
sy.x = geom[j].x;
1632+
sy.y = geom[j].y;
1633+
points.push_back(sy);
15321634
}
15331635

15341636
double area = get_area(geom, i, j);
@@ -1542,13 +1644,15 @@ drawvec polygon_to_anchor(const drawvec &geom) {
15421644
}
15431645
}
15441646

1647+
// If there are no outer rings, don't generate a label point
1648+
15451649
if (best_area > 0) {
15461650
long long xsum = 0;
15471651
long long ysum = 0;
15481652
size_t count = 0;
15491653
long long xmin = LLONG_MAX, ymin = LLONG_MAX, xmax = LLONG_MIN, ymax = LLONG_MIN;
15501654

1551-
// Calculate centroid and bounding box.
1655+
// Calculate centroid and bounding box of biggest ring.
15521656
// start + 1 to exclude the first point, which is duplicated as the last
15531657
for (size_t k = start + 1; k < end; k++) {
15541658
xsum += geom[k].x;
@@ -1562,17 +1666,79 @@ drawvec polygon_to_anchor(const drawvec &geom) {
15621666
}
15631667

15641668
if (count > 0) {
1565-
draw centroid(VT_MOVETO, xsum / count, ysum / count);
1566-
draw d = centerOfMass(geom, start, end, centroid);
1669+
// We want label points that are at least a moderate distance from
1670+
// the edge of the feature. The threshold for what is too close
1671+
// is derived from the area of the feature.
15671672

15681673
double radius = sqrt(best_area / M_PI);
15691674
double goodness_threshold = radius / 5;
15701675

1571-
double goodness = label_goodness(geom, start, end - start - 1, d.x, d.y);
1676+
// First choice: Turf's center of mass.
1677+
1678+
draw centroid(VT_MOVETO, xsum / count, ysum / count);
1679+
draw d = centerOfMass(geom, start, end, centroid);
1680+
double goodness = label_goodness(geom, d.x, d.y);
1681+
const char *kind = "mass";
1682+
15721683
if (goodness < goodness_threshold) {
15731684
// Label is too close to the border or outside it,
1574-
// so try some other possible points
1685+
// so try some other possible points. Sort the vertices
1686+
// both by Y and X coordinate and then by diagonals,
1687+
// and walk through each set
1688+
// in sorted order. Adjacent pairs of coordinates should
1689+
// tend to bounce back and forth between rings, so the
1690+
// midpoint of each pair will hopefully be somewhere in the
1691+
// interior of the polygon.
1692+
1693+
std::vector<candidate> candidates;
15751694

1695+
for (size_t pass = 0; pass < 4; pass++) {
1696+
std::sort(points.begin(), points.end(), sorty_sorter(pass));
1697+
1698+
for (size_t i = 1; i < points.size(); i++) {
1699+
double dx = points[i].x - points[i - 1].x;
1700+
double dy = points[i].y - points[i - 1].y;
1701+
1702+
double dist = sqrt(dx * dx + dy * dy);
1703+
if (dist > 2 * goodness_threshold) {
1704+
candidate c;
1705+
1706+
c.x = (points[i].x + points[i - 1].x) / 2;
1707+
c.y = (points[i].y + points[i - 1].y) / 2;
1708+
c.dist = dist;
1709+
1710+
candidates.push_back(c);
1711+
}
1712+
}
1713+
}
1714+
1715+
// Now sort the accumulate list of segment midpoints by the lengths
1716+
// of the segments. Starting from the longest
1717+
// segment, if we find one whose midpoint is inside the polygon and
1718+
// far enough from any edge to be good enough, stop looking.
1719+
1720+
std::sort(candidates.begin(), candidates.end());
1721+
// only check the top 50 stride midpoints, since this list can be quite large
1722+
for (size_t i = 0; i < candidates.size() && i < 50; i++) {
1723+
double maybe_goodness = label_goodness(geom, candidates[i].x, candidates[i].y);
1724+
1725+
if (maybe_goodness > goodness) {
1726+
d.x = candidates[i].x;
1727+
d.y = candidates[i].y;
1728+
1729+
goodness = maybe_goodness;
1730+
kind = "diagonal";
1731+
if (goodness > goodness_threshold) {
1732+
break;
1733+
}
1734+
}
1735+
}
1736+
}
1737+
1738+
// We may still not have anything decent, so the next thing to look at
1739+
// is points from gridding the bounding box of the largest ring.
1740+
1741+
if (goodness < goodness_threshold) {
15761742
for (long long sub = 2;
15771743
sub < 32 && (xmax - xmin) > 2 * sub && (ymax - ymin) > 2 * sub;
15781744
sub *= 2) {
@@ -1582,11 +1748,12 @@ drawvec polygon_to_anchor(const drawvec &geom) {
15821748
xmin + x * (xmax - xmin) / sub,
15831749
ymin + y * (ymax - ymin) / sub);
15841750

1585-
double maybe_goodness = label_goodness(geom, start, end - start, maybe.x, maybe.y);
1751+
double maybe_goodness = label_goodness(geom, maybe.x, maybe.y);
15861752
if (maybe_goodness > goodness) {
15871753
// better than the previous
15881754
d = maybe;
15891755
goodness = maybe_goodness;
1756+
kind = "grid";
15901757
}
15911758
}
15921759
}
@@ -1598,8 +1765,17 @@ drawvec polygon_to_anchor(const drawvec &geom) {
15981765

15991766
// There is nothing really good. Is the centroid maybe better?
16001767
// If not, we're stuck with whatever the best we found was.
1601-
if (label_goodness(geom, start, end - start, centroid.x, centroid.y) > goodness) {
1768+
double maybe_goodness = label_goodness(geom, centroid.x, centroid.y);
1769+
if (maybe_goodness > goodness) {
16021770
d = centroid;
1771+
goodness = maybe_goodness;
1772+
kind = "centroid";
1773+
}
1774+
1775+
if (goodness <= 0) {
1776+
double lon, lat;
1777+
tile2lonlat(d.x, d.y, 32, &lon, &lat);
1778+
fprintf(stderr, "could not find label point: %s %f,%f\n", kind, lat, lon);
16031779
}
16041780
}
16051781

@@ -1676,31 +1852,18 @@ drawvec checkerboard_anchors(drawvec const &geom, int tx, int ty, int z, unsigne
16761852
continue;
16771853
}
16781854

1679-
for (size_t a = 0; a < geom.size(); a++) {
1680-
if (geom[a].op == VT_MOVETO) {
1681-
size_t b;
1682-
for (b = a + 1; b < geom.size(); b++) {
1683-
if (geom[b].op != VT_LINETO) {
1684-
break;
1685-
}
1686-
}
1687-
1688-
// If it's the central label, it's the best we've got,
1689-
// so accept it in any case. If it's from the outer spiral,
1690-
// don't use it if it's too close to a border.
1691-
if (lx == 0 && ly == 0) {
1692-
out.push_back(draw(VT_MOVETO, x - tx1, y - ty1));
1693-
break;
1694-
} else {
1695-
double tilesize = 1LL << (32 - z);
1696-
double goodness_threshold = tilesize / 100;
1697-
if (label_goodness(geom, a, b - a, x - tx1, y - ty1) > goodness_threshold) {
1698-
out.push_back(draw(VT_MOVETO, x - tx1, y - ty1));
1699-
break;
1700-
}
1701-
}
1702-
1703-
a = b - 1;
1855+
// If it's the central label, it's the best we've got,
1856+
// so accept it in any case. If it's from the outer spiral,
1857+
// don't use it if it's too close to a border.
1858+
if (lx == 0 && ly == 0) {
1859+
out.push_back(draw(VT_MOVETO, x - tx1, y - ty1));
1860+
break;
1861+
} else {
1862+
double tilesize = 1LL << (32 - z);
1863+
double goodness_threshold = tilesize / 100;
1864+
if (label_goodness(geom, x - tx1, y - ty1) > goodness_threshold) {
1865+
out.push_back(draw(VT_MOVETO, x - tx1, y - ty1));
1866+
break;
17041867
}
17051868
}
17061869
}

0 commit comments

Comments
 (0)