Skip to content

Commit 6ce2bc0

Browse files
authored
Merge pull request #2453 from joto/expire-polygon
Do area expire based on the actual polygon instead of the bbox
2 parents 4fed109 + d57ba40 commit 6ce2bc0

File tree

11 files changed

+8284
-59
lines changed

11 files changed

+8284
-59
lines changed

scripts/osm2pgsql-test-style

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ class ReplicationServerMock:
9393
numdiffs = int((max_size + 1023)/1024)
9494
return min(self.state_infos[-1].sequence, start_id + numdiffs - 1)
9595

96-
9796
# Replication module is optional
9897
_repfl_spec = importlib.util.spec_from_loader(
9998
'osm2pgsql_replication',
@@ -510,6 +509,30 @@ def execute_osm2pgsql(context, output):
510509
context.osm2pgsql_returncode = proc.returncode
511510
context.osm2pgsql_params = {'-d': context.user_args.test_db}
512511

512+
@when("running osm2pgsql-expire with parameters")
513+
def execute_osm2pgsql_expire(context):
514+
515+
cmdline = [context.user_args.osm2pgsql_binary + '-expire']
516+
517+
test_dir = (context.user_args.test_data_dir or Path('.')).resolve()
518+
def _template(param):
519+
return param.replace('{TEST_DATA_DIR}', str(test_dir))
520+
521+
if context.table:
522+
assert not any('<' in h for h in context.table.headings), \
523+
"Substition in the first line of a table are not supported."
524+
cmdline.extend(_template(h) for h in context.table.headings if h)
525+
for row in context.table:
526+
cmdline.extend(_template(c) for c in row if c)
527+
528+
proc = Popen(cmdline, cwd=str(context.workdir),
529+
stdout=PIPE, stderr=PIPE)
530+
531+
outdata = proc.communicate()
532+
context.osm2pgsql_cmdline = ' '.join(cmdline)
533+
context.osm2pgsql_outdata = [d.decode('utf-8').replace('\\n', '\n') for d in outdata]
534+
context.osm2pgsql_returncode = proc.returncode
535+
513536
@then("execution is successful")
514537
def osm2pgsql_check_success(context):
515538
assert context.osm2pgsql_returncode == 0, \
@@ -541,6 +564,21 @@ def check_program_output(context, kind):
541564
assert line in s,\
542565
f"Output '{line}' not found in {kind} output:\n{s}\n"
543566

567+
@then(r"the (?P<kind>\w+) output matches contents of (?P<result_file>.*)")
568+
def check_program_output_against_file(context, kind, result_file):
569+
if kind == 'error':
570+
s = context.osm2pgsql_outdata[1]
571+
elif kind == 'standard':
572+
s = context.osm2pgsql_outdata[0]
573+
else:
574+
assert not "Expect one of error, standard"
575+
576+
test_dir = (context.user_args.test_data_dir or Path('.')).resolve()
577+
with open(test_dir / result_file, 'r') as fd:
578+
expected = fd.read()
579+
assert s == expected, \
580+
f"Output does not match contents of {result_file}. Actual output:\n{s}"
581+
544582
################### Steps: Running Replication #####################
545583

546584
@given("the replication service at (?P<base_url>.*)")

src/expire-tiles.cpp

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -136,28 +136,90 @@ expire_mode decide_expire_mode(TGEOM const &geom,
136136

137137
} // anonymous namespace
138138

139-
void expire_tiles_t::from_geometry(geom::polygon_t const &geom,
140-
expire_config_t const &expire_config)
139+
void expire_tiles_t::build_tile_list(std::vector<uint32_t> *tile_x_list,
140+
geom::ring_t const &ring, double tile_y)
141141
{
142-
geom::box_t box;
143-
auto const mode = decide_expire_mode(geom, expire_config, &box);
144-
145-
if (mode == expire_mode::boundary_only) {
146-
from_polygon_boundary(geom, expire_config);
147-
return;
142+
assert(!ring.empty());
143+
for (std::size_t i = 1; i < ring.size(); ++i) {
144+
auto const t1 = coords_to_tile(ring[i]);
145+
auto const t2 = coords_to_tile(ring[i - 1]);
146+
147+
if ((t1.y() < tile_y && t2.y() >= tile_y) ||
148+
(t2.y() < tile_y && t1.y() >= tile_y)) {
149+
auto const pos =
150+
(tile_y - t1.y()) / (t2.y() - t1.y()) * (t2.x() - t1.x());
151+
tile_x_list->push_back(static_cast<uint32_t>(std::clamp(
152+
t1.x() + pos, 0.0, static_cast<double>(m_map_width - 1))));
153+
}
148154
}
155+
}
149156

157+
void expire_tiles_t::from_polygon_area(geom::polygon_t const &geom,
158+
geom::box_t box)
159+
{
150160
if (!box.valid()) {
151161
box = geom::envelope(geom);
152162
}
153-
from_bbox(box, expire_config);
163+
164+
// This uses a variation on a simple polygon fill algorithm, for instance
165+
// described on https://alienryderflex.com/polygon_fill/ . For each row
166+
// of tiles we find the intersections with the area boundary and "fill" in
167+
// the tiles between them. Note that we don't need to take particular care
168+
// about the boundary, because we simply use the algorithm we use for
169+
// expiry along a line to do that, which will also take care of the buffer.
170+
171+
// Coordinates are numbered from bottom to top, tiles are numbered from top
172+
// to bottom, so "min" and "max" are switched here.
173+
auto const max_tile_y = static_cast<std::uint32_t>(
174+
m_map_width * (0.5 - box.min().y() / tile_t::EARTH_CIRCUMFERENCE));
175+
auto const min_tile_y = static_cast<std::uint32_t>(
176+
m_map_width * (0.5 - box.max().y() / tile_t::EARTH_CIRCUMFERENCE));
177+
178+
std::vector<uint32_t> tile_x_list;
179+
180+
// Loop through the tile rows from top to bottom
181+
for (std::uint32_t tile_y = min_tile_y; tile_y < max_tile_y; ++tile_y) {
182+
183+
// Build a list of tiles crossed by the area boundary
184+
tile_x_list.clear();
185+
build_tile_list(&tile_x_list, geom.outer(),
186+
static_cast<double>(tile_y));
187+
for (auto const &ring : geom.inners()) {
188+
build_tile_list(&tile_x_list, ring, static_cast<double>(tile_y));
189+
}
190+
191+
// Sort list of tiles from left to right
192+
std::sort(tile_x_list.begin(), tile_x_list.end());
193+
194+
// Add the tiles between entering and leaving the area to expire list
195+
assert(tile_x_list.size() % 2 == 0);
196+
for (std::size_t i = 0; i < tile_x_list.size(); i += 2) {
197+
if (tile_x_list[i] >= static_cast<uint32_t>(m_map_width - 1)) {
198+
break;
199+
}
200+
if (tile_x_list[i + 1] > 0) {
201+
for (std::uint32_t tile_x = tile_x_list[i];
202+
tile_x < tile_x_list[i + 1]; ++tile_x) {
203+
expire_tile(tile_x, tile_y);
204+
}
205+
}
206+
}
207+
}
154208
}
155209

156-
void expire_tiles_t::from_polygon_boundary(geom::multipolygon_t const &geom,
157-
expire_config_t const &expire_config)
210+
void expire_tiles_t::from_geometry(geom::polygon_t const &geom,
211+
expire_config_t const &expire_config)
158212
{
159-
for (auto const &sgeom : geom) {
160-
from_polygon_boundary(sgeom, expire_config);
213+
geom::box_t box;
214+
auto const mode = decide_expire_mode(geom, expire_config, &box);
215+
216+
from_polygon_boundary(geom, expire_config);
217+
218+
// Only need to expire area if in full_area mode. If there is only a
219+
// single tile expired the whole polygon is inside that tile and we
220+
// don't need to do the polygon expire.
221+
if (mode == expire_mode::full_area && m_dirty_tiles.size() > 1) {
222+
from_polygon_area(geom, box);
161223
}
162224
}
163225

@@ -167,15 +229,18 @@ void expire_tiles_t::from_geometry(geom::multipolygon_t const &geom,
167229
geom::box_t box;
168230
auto const mode = decide_expire_mode(geom, expire_config, &box);
169231

170-
if (mode == expire_mode::boundary_only) {
171-
from_polygon_boundary(geom, expire_config);
172-
return;
232+
for (auto const &sgeom : geom) {
233+
from_polygon_boundary(sgeom, expire_config);
173234
}
174235

175-
if (!box.valid()) {
176-
box = geom::envelope(geom);
236+
// Only need to expire area if in full_area mode. If there is only a
237+
// single tile expired the whole polygon is inside that tile and we
238+
// don't need to do the polygon expire.
239+
if (mode == expire_mode::full_area && m_dirty_tiles.size() > 1) {
240+
for (auto const &sgeom : geom) {
241+
from_polygon_area(sgeom, geom::box_t{});
242+
}
177243
}
178-
from_bbox(box, expire_config);
179244
}
180245

181246
// False positive: Apparently clang-tidy can not see through the visit()
@@ -258,35 +323,6 @@ void expire_tiles_t::from_line_segment(geom::point_t const &a,
258323
}
259324
}
260325

261-
/*
262-
* Expire tiles within a bounding box
263-
*/
264-
int expire_tiles_t::from_bbox(geom::box_t const &box,
265-
expire_config_t const &expire_config)
266-
{
267-
/* Convert the box's Mercator coordinates into tile coordinates */
268-
auto const tmp_min = coords_to_tile({box.min_x(), box.max_y()});
269-
int const min_tile_x =
270-
std::clamp(int(tmp_min.x() - expire_config.buffer), 0, m_map_width - 1);
271-
int const min_tile_y =
272-
std::clamp(int(tmp_min.y() - expire_config.buffer), 0, m_map_width - 1);
273-
274-
auto const tmp_max = coords_to_tile({box.max_x(), box.min_y()});
275-
int const max_tile_x =
276-
std::clamp(int(tmp_max.x() + expire_config.buffer), 0, m_map_width - 1);
277-
int const max_tile_y =
278-
std::clamp(int(tmp_max.y() + expire_config.buffer), 0, m_map_width - 1);
279-
280-
for (int iterator_x = min_tile_x; iterator_x <= max_tile_x; ++iterator_x) {
281-
uint32_t const norm_x = normalise_tile_x_coord(iterator_x);
282-
for (int iterator_y = min_tile_y; iterator_y <= max_tile_y;
283-
++iterator_y) {
284-
expire_tile(norm_x, static_cast<uint32_t>(iterator_y));
285-
}
286-
}
287-
return 0;
288-
}
289-
290326
quadkey_list_t expire_tiles_t::get_tiles()
291327
{
292328
quadkey_list_t tiles;

src/expire-tiles.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ class expire_tiles_t
4040
void from_polygon_boundary(geom::polygon_t const &geom,
4141
expire_config_t const &expire_config);
4242

43-
void from_polygon_boundary(geom::multipolygon_t const &geom,
44-
expire_config_t const &expire_config);
43+
void from_polygon_area(geom::polygon_t const &geom, geom::box_t box);
4544

4645
void from_geometry(geom::nullgeom_t const & /*geom*/,
4746
expire_config_t const & /*expire_config*/)
@@ -74,8 +73,6 @@ class expire_tiles_t
7473
void from_geometry_if_3857(geom::geometry_t const &geom,
7574
expire_config_t const &expire_config);
7675

77-
int from_bbox(geom::box_t const &box, expire_config_t const &expire_config);
78-
7976
/**
8077
* Get tiles as a vector of quadkeys and remove them from the expire_tiles_t
8178
* object.
@@ -107,6 +104,9 @@ class expire_tiles_t
107104

108105
uint32_t normalise_tile_x_coord(int x) const;
109106

107+
void build_tile_list(std::vector<uint32_t> *tile_x_list,
108+
geom::ring_t const &ring, double tile_y);
109+
110110
void from_line_segment(geom::point_t const &a, geom::point_t const &b,
111111
expire_config_t const &expire_config);
112112

tests/bdd/expire/expire.feature

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Feature: Test osm2pgsql-expire
2+
3+
Scenario: Test with invalid options
4+
When running osm2pgsql-expire with parameters
5+
| -z18 | -m | abc | {TEST_DATA_DIR}/expire/test-data.osm |
6+
Then execution fails
7+
Then the error output contains
8+
"""
9+
Value for --mode must be 'boundary_only', 'full_area', or 'hybrid'
10+
"""
11+
12+
Scenario: Test with invalid options
13+
When running osm2pgsql-expire with parameters
14+
| -z18 | -m | full_area | -f | foo | {TEST_DATA_DIR}/expire/test-data.osm |
15+
Then execution fails
16+
Then the error output contains
17+
"""
18+
Value for --format must be 'tiles' or 'geojson'
19+
"""
20+
21+
Scenario: Test expire for various objects on zoom 18 (geojson format)
22+
When running osm2pgsql-expire with parameters
23+
| -z18 | -m | full_area | -f | geojson | {TEST_DATA_DIR}/expire/test-data.osm |
24+
Then execution is successful
25+
Then the standard output matches contents of expire/test-z18-b0.geojson
26+
27+
Scenario: Test expire for various objects on zoom 18 (tiles format)
28+
When running osm2pgsql-expire with parameters
29+
| -z18 | -m | full_area | -f | tiles | {TEST_DATA_DIR}/expire/test-data.osm |
30+
Then execution is successful
31+
Then the standard output matches contents of expire/test-z18-b0.tiles
32+
33+
Scenario: Test expire for various objects on zoom 18 (geojson format)
34+
When running osm2pgsql-expire with parameters
35+
| -z18 | -m | full_area | -f | geojson | -b | 0.5 | {TEST_DATA_DIR}/expire/test-data.osm |
36+
Then execution is successful
37+
Then the standard output matches contents of expire/test-z18-b05.geojson
38+
39+
Scenario: Test expire for various objects on zoom 18 (tiles format)
40+
When running osm2pgsql-expire with parameters
41+
| -z18 | -m | full_area | -f | tiles | -b | 0.5 | {TEST_DATA_DIR}/expire/test-data.osm |
42+
Then execution is successful
43+
Then the standard output matches contents of expire/test-z18-b05.tiles
44+

0 commit comments

Comments
 (0)