Skip to content

Commit 38e4efc

Browse files
committed
Add pole_of_inaccessibility() function in Lua for polygons
Implementation of the "polylabel" algorithm for finding the "pole of inaccessibility" or center of the maximum inscribed circle of a polygon. This is accessible from Lua as the pole_of_inaccessibility() function. The current implementation is only defined for polygons, because it is unclear how it should behave for other types of geometries. We can extend this later if needed.
1 parent 60aa90f commit 38e4efc

File tree

8 files changed

+473
-0
lines changed

8 files changed

+473
-0
lines changed

flex-config/labelpoint.lua

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-- This config example file is released into the Public Domain.
2+
--
3+
-- This example shows the use of the centroid() and pole_of_inaccessibility()
4+
-- functions. For all named polygons several types of "center" points are
5+
-- calculated that can be used for labelling.
6+
7+
local tables = {}
8+
9+
tables.polygons = osm2pgsql.define_area_table('polygons', {
10+
{ column = 'name', type = 'text' },
11+
{ column = 'tags', type = 'jsonb' },
12+
{ column = 'geom', type = 'polygon', not_null = true },
13+
{ column = 'centroid', type = 'point', not_null = true },
14+
{ column = 'poi1', type = 'point', not_null = true },
15+
{ column = 'poi2', type = 'point', not_null = true },
16+
})
17+
18+
function add(tags, geom)
19+
tables.polygons:insert({
20+
name = tags.name,
21+
tags = tags,
22+
geom = geom,
23+
-- The centroid is easy and fast to calculate
24+
centroid = geom:centroid(),
25+
-- The pole of inaccessibility can take a bit longer. It calculates
26+
-- the place in the polygon that's farthest away from the boundary.
27+
-- In other words it finds the center of the largest circle that fits
28+
-- completely into the polygon. (Note that due to the time required
29+
-- to calculate this, only an approximation will be calculated.)
30+
poi1 = geom:pole_of_inaccessibility(),
31+
-- The function allows a Lua table with options as parameter.
32+
-- Currently only a single option "stretch" is defined. If you set
33+
-- this to something other than 1 (the default), the functions doesn't
34+
-- use a circle but an ellipse stretched horizontally by the specified
35+
-- factor when calculating the result. This can make sense, because
36+
-- labels are usually written horizontally so you need more space in
37+
-- that direction. (Use a value > 1 to stretch horizontally or values
38+
-- between 0 and 1 to stretch vertically. The value 0 ist not allowed.)
39+
poi2 = geom:pole_of_inaccessibility({ stretch = 3 }),
40+
})
41+
end
42+
43+
function osm2pgsql.process_way(object)
44+
if object.is_closed and object.tags.name then
45+
local geom = object:as_polygon()
46+
add(object.tags, geom)
47+
end
48+
end
49+
50+
function osm2pgsql.process_relation(object)
51+
local relation_type = object:grab_tag('type')
52+
53+
if relation_type == 'multipolygon' and object.tags.name then
54+
local geom = object:as_multipolygon()
55+
-- The pole_of_inaccessibility() function only works for polygons,
56+
-- not multipolygons. So we split up the multipolygons here and
57+
-- calculate the pole for each part separately.
58+
for g in geom:geometries() do
59+
add(object.tags, g)
60+
end
61+
end
62+
end
63+

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ target_sources(osm2pgsql_lib PRIVATE
1010
geom-box.cpp
1111
geom-from-osm.cpp
1212
geom-functions.cpp
13+
geom-pole-of-inaccessibility.cpp
1314
input.cpp
1415
logging.cpp
1516
middle.cpp

src/flex-lua-geom.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "flex-lua-geom.hpp"
1111
#include "geom-functions.hpp"
12+
#include "geom-pole-of-inaccessibility.hpp"
1213
#include "lua-utils.hpp"
1314

1415
extern "C"
@@ -167,6 +168,34 @@ static int geom_num_geometries(lua_State *lua_state)
167168
return 1;
168169
}
169170

171+
static int geom_pole_of_inaccessibility(lua_State *lua_state)
172+
{
173+
auto const *const input_geometry = unpack_geometry(lua_state);
174+
175+
double stretch = 1.0;
176+
if (lua_gettop(lua_state) > 1) {
177+
if (lua_type(lua_state, 2) != LUA_TTABLE) {
178+
throw std::runtime_error{
179+
"Argument #2 to 'pole_of_inaccessibility' must be a table."};
180+
}
181+
182+
lua_getfield(lua_state, 2, "stretch");
183+
if (lua_isnumber(lua_state, -1)) {
184+
stretch = lua_tonumber(lua_state, -1);
185+
if (stretch <= 0.0) {
186+
throw std::runtime_error{"The 'stretch' factor must be > 0."};
187+
}
188+
} else {
189+
throw std::runtime_error{"The 'stretch' factor must be a number."};
190+
}
191+
}
192+
193+
auto *geom = create_lua_geometry_object(lua_state);
194+
geom::pole_of_inaccessibility(geom, *input_geometry, 0, stretch);
195+
196+
return 1;
197+
}
198+
170199
static int geom_segmentize(lua_State *lua_state)
171200
{
172201
auto const *const input_geometry = unpack_geometry(lua_state);
@@ -257,6 +286,8 @@ void init_geometry_class(lua_State *lua_state)
257286
luaX_add_table_func(lua_state, "line_merge", geom_line_merge);
258287
luaX_add_table_func(lua_state, "reverse", geom_reverse);
259288
luaX_add_table_func(lua_state, "num_geometries", geom_num_geometries);
289+
luaX_add_table_func(lua_state, "pole_of_inaccessibility",
290+
geom_pole_of_inaccessibility);
260291
luaX_add_table_func(lua_state, "segmentize", geom_segmentize);
261292
luaX_add_table_func(lua_state, "simplify", geom_simplify);
262293
luaX_add_table_func(lua_state, "srid", geom_srid);

src/geom-box.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class box_t
4848
constexpr double width() const noexcept { return m_max_x - m_min_x; }
4949
constexpr double height() const noexcept { return m_max_y - m_min_y; }
5050

51+
constexpr point_t center() const noexcept
52+
{
53+
return {m_min_x + width() / 2.0, m_min_y + height() / 2.0};
54+
}
55+
5156
constexpr point_t min() const noexcept { return {m_min_x, m_min_y}; }
5257
constexpr point_t max() const noexcept { return {m_max_x, m_max_y}; }
5358

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* SPDX-License-Identifier: GPL-2.0-or-later
3+
*
4+
* This file is part of osm2pgsql (https://osm2pgsql.org/).
5+
*
6+
* Copyright (C) 2006-2023 by the osm2pgsql developer community.
7+
* For a full list of authors see the git log.
8+
*/
9+
10+
#include "geom-pole-of-inaccessibility.hpp"
11+
#include "geom-boost-adaptor.hpp"
12+
#include "geom-box.hpp"
13+
#include "logging.hpp"
14+
15+
#include <algorithm>
16+
#include <cassert>
17+
#include <cmath>
18+
#include <iostream>
19+
#include <queue>
20+
21+
/**
22+
* \file
23+
*
24+
* Implementation of the "Polylabel" algorithm for finding the "pole of
25+
* inaccessibility", the internal point most distant from the polygon outline,
26+
* or center of the maximum inscribed circle, described in
27+
* https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc
28+
*
29+
* Adapted from https://github.com/mapbox/polylabel and
30+
* https://github.com/libgeos/geos/blob/main/src/algorithm/construct/MaximumInscribedCircle.cpp
31+
* including the change from https://github.com/mapbox/polylabel/issues/82 .
32+
*
33+
* Forcing the precision to be no smaller than max(width, height) / 1000 of the
34+
* envelope makes sure the algorithm terminates in sensible run-time and without
35+
* taking too much memory. The value of 1000.0 was taken from the PostGIS
36+
* implementation, but unlike the PostGIS implementation you can set a higher
37+
* value.
38+
*/
39+
40+
namespace geom {
41+
42+
/// Get squared distance from a point p to a segment (a, b).
43+
static double point_to_segment_distance_squared(point_t p, point_t a, point_t b,
44+
double stretch) noexcept
45+
{
46+
double x = a.x();
47+
double y = a.y() * stretch;
48+
double dx = b.x() - x;
49+
double dy = b.y() * stretch - y;
50+
51+
if (dx != 0 || dy != 0) {
52+
double const t =
53+
((p.x() - x) * dx + (p.y() - y) * dy) / (dx * dx + dy * dy);
54+
55+
if (t > 1) {
56+
x = b.x();
57+
y = b.y() * stretch;
58+
} else if (t > 0) {
59+
x += dx * t;
60+
y += dy * t;
61+
}
62+
}
63+
64+
dx = p.x() - x;
65+
dy = p.y() - y;
66+
67+
return dx * dx + dy * dy;
68+
}
69+
70+
/// Get squared distance from a point p to ring.
71+
static bool point_to_ring_distance_squared(point_t point, ring_t const &ring,
72+
bool inside, double stretch,
73+
double *min_dist_squared) noexcept
74+
{
75+
std::size_t const len = ring.size();
76+
77+
for (std::size_t i = 0, j = len - 1; i < len; j = i++) {
78+
auto const &a = ring[i];
79+
auto const &b = ring[j];
80+
81+
if (((a.y() * stretch) > point.y()) !=
82+
((b.y() * stretch) > point.y()) &&
83+
(point.x() < (b.x() - a.x()) * (point.y() - (a.y() * stretch)) /
84+
((b.y() - a.y()) * stretch) +
85+
a.x())) {
86+
inside = !inside;
87+
}
88+
89+
double const d =
90+
point_to_segment_distance_squared(point, a, b, stretch);
91+
if (d < *min_dist_squared) {
92+
*min_dist_squared = d;
93+
}
94+
}
95+
96+
return inside;
97+
}
98+
99+
/**
100+
* Signed distance from point to polygon boundary. The result is negative if
101+
* the point is outside.
102+
*/
103+
static auto point_to_polygon_distance(point_t point, polygon_t const &polygon,
104+
double stretch)
105+
{
106+
double min_dist_squared = std::numeric_limits<double>::infinity();
107+
108+
bool inside = point_to_ring_distance_squared(point, polygon.outer(), false,
109+
stretch, &min_dist_squared);
110+
111+
for (auto const &ring : polygon.inners()) {
112+
inside = point_to_ring_distance_squared(point, ring, inside, stretch,
113+
&min_dist_squared);
114+
}
115+
116+
return (inside ? 1 : -1) * std::sqrt(min_dist_squared);
117+
}
118+
119+
namespace {
120+
121+
struct Cell
122+
{
123+
static constexpr double const SQRT2 = 1.4142135623730951;
124+
125+
Cell(point_t c, double h, polygon_t const &polygon, double stretch)
126+
: center(c), half_size(h),
127+
dist(point_to_polygon_distance(center, polygon, stretch)),
128+
max(dist + half_size * SQRT2)
129+
{}
130+
131+
point_t center; // cell center
132+
double half_size; // half the cell size
133+
double dist; // distance from cell center to polygon
134+
double max; // max distance to polygon within a cell
135+
136+
friend bool operator<(Cell const &a, Cell const &b) noexcept
137+
{
138+
return a.max < b.max;
139+
}
140+
};
141+
142+
} // anonymous namespace
143+
144+
static Cell make_centroid_cell(polygon_t const &polygon, double stretch)
145+
{
146+
point_t centroid{0, 0};
147+
boost::geometry::centroid(polygon, centroid);
148+
centroid.set_y(stretch * centroid.y());
149+
return {centroid, 0, polygon, stretch};
150+
}
151+
152+
point_t pole_of_inaccessibility(const polygon_t &polygon, double precision,
153+
double stretch)
154+
{
155+
assert(stretch > 0);
156+
157+
box_t const envelope = geom::envelope(polygon);
158+
159+
double const min_precision =
160+
std::max(envelope.width(), envelope.height()) / 1000.0;
161+
if (min_precision > precision) {
162+
precision = min_precision;
163+
}
164+
165+
box_t const stretched_envelope{envelope.min_x(), envelope.min_y() * stretch,
166+
envelope.max_x(),
167+
envelope.max_y() * stretch};
168+
169+
double const cell_size =
170+
std::min(stretched_envelope.width(), stretched_envelope.height());
171+
if (cell_size == 0) {
172+
return envelope.min();
173+
}
174+
175+
std::priority_queue<Cell, std::vector<Cell>> cell_queue;
176+
177+
// cover polygon with initial cells
178+
{
179+
double const h = cell_size / 2.0;
180+
for (double x = stretched_envelope.min_x();
181+
x < stretched_envelope.max_x(); x += cell_size) {
182+
for (double y = stretched_envelope.min_y();
183+
y < stretched_envelope.max_y(); y += cell_size) {
184+
cell_queue.emplace(point_t{x + h, y + h}, h, polygon, stretch);
185+
}
186+
}
187+
}
188+
189+
// take centroid as the first best guess
190+
auto best_cell = make_centroid_cell(polygon, stretch);
191+
192+
// second guess: bounding box centroid
193+
Cell const bbox_cell{stretched_envelope.center(), 0, polygon, stretch};
194+
if (bbox_cell.dist > best_cell.dist) {
195+
best_cell = bbox_cell;
196+
}
197+
198+
auto num_probes = cell_queue.size();
199+
while (!cell_queue.empty()) {
200+
// pick the most promising cell from the queue
201+
auto cell = cell_queue.top();
202+
cell_queue.pop();
203+
204+
// update the best cell if we found a better one
205+
if (cell.dist > best_cell.dist) {
206+
best_cell = cell;
207+
log_debug("polyline: found best {} after {} probes",
208+
::round(1e4 * cell.dist) / 1e4, num_probes);
209+
}
210+
211+
// do not drill down further if there's no chance of a better solution
212+
if (cell.max - best_cell.dist <= precision) {
213+
continue;
214+
}
215+
216+
// split the cell into four cells
217+
auto const h = cell.half_size / 2.0;
218+
auto const center = cell.center;
219+
220+
for (auto dy : {-h, h}) {
221+
for (auto dx : {-h, h}) {
222+
Cell c{point_t{center.x() + dx, center.y() + dy}, h, polygon,
223+
stretch};
224+
if (c.max > best_cell.dist) {
225+
cell_queue.push(c);
226+
}
227+
}
228+
}
229+
230+
num_probes += 4;
231+
}
232+
233+
log_debug("polyline: num probes: {}", num_probes);
234+
log_debug("polyline: best distance: {}", best_cell.dist);
235+
236+
return {best_cell.center.x(), best_cell.center.y() / stretch};
237+
}
238+
239+
void pole_of_inaccessibility(geometry_t *output, geometry_t const &input,
240+
double precision, double stretch)
241+
{
242+
if (input.is_polygon()) {
243+
output->set<geom::point_t>() = pole_of_inaccessibility(
244+
input.get<geom::polygon_t>(), precision, stretch);
245+
output->set_srid(input.srid());
246+
} else {
247+
output->reset();
248+
}
249+
}
250+
251+
geometry_t pole_of_inaccessibility(geometry_t const &input, double precision,
252+
double stretch)
253+
{
254+
geometry_t geom;
255+
pole_of_inaccessibility(&geom, input, precision, stretch);
256+
return geom;
257+
}
258+
259+
} // namespace geom

0 commit comments

Comments
 (0)