Skip to content

Commit eefe9af

Browse files
committed
feat: expire tiles for geometry columns in any single projection
The command-line expire options only picked up tables whose geometry column is Web Mercator, so a flex config storing EPSG:4326 produced an empty expire list. Expiry now takes the SRID from the geometry columns being expired, builds the tile conversion with osm2pgsql's own reprojection for it, and refuses to start if those columns disagree. The expire output stays in the Web Mercator tile scheme. The SRID is stored on the expire output, so the main output and its per-worker clones build their projection from the same value, at setup and never per row. Lua-defined expire outputs keep the Web Mercator default. Refs: MAPCO-11657
1 parent c80fcda commit eefe9af

5 files changed

Lines changed: 197 additions & 12 deletions

File tree

‎src/expire-output.hpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* For a full list of authors see the git log.
1111
*/
1212

13+
#include "projection.hpp"
1314
#include "tile.hpp"
1415

1516
#include <cassert>
@@ -59,6 +60,9 @@ class expire_output_t
5960
uint32_t maxzoom() const noexcept { return m_maxzoom; }
6061
void set_maxzoom(uint32_t maxzoom) noexcept { m_maxzoom = maxzoom; }
6162

63+
int srid() const noexcept { return m_srid; }
64+
void set_srid(int srid) noexcept { m_srid = srid; }
65+
6266
std::size_t max_tiles_geometry() const noexcept
6367
{
6468
return m_max_tiles_geometry;
@@ -142,6 +146,9 @@ class expire_output_t
142146
/// Zoom level we capture tiles on
143147
uint32_t m_maxzoom = 0;
144148

149+
/// Projection of the geometries expired into this output
150+
int m_srid = PROJ_SPHERE_MERC;
151+
145152
/**
146153
* The following two settings are for protecting osm2pgsql from overload as
147154
* well as downstream tile expiry mechanisms in case of large changes to

‎src/flex-table-column.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ std::string flex_table_column_t::sql_create() const
209209
void flex_table_column_t::add_expire(expire_config_t const &config)
210210
{
211211
assert(is_geometry_column());
212-
assert(srid() == PROJ_SPHERE_MERC);
212+
assert(srid() > 0);
213213
m_expires.push_back(config);
214214
}
215215

‎src/output-flex.cpp‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ output_flex_t::output_flex_t(output_flex_t const *other,
13151315
for (auto &expire_output : *m_expire_outputs) {
13161316
m_expire_tiles.emplace_back(
13171317
expire_output.maxzoom(),
1318-
reprojection_t::create_projection(PROJ_SPHERE_MERC),
1318+
reprojection_t::create_projection(expire_output.srid()),
13191319
expire_output.max_tiles_geometry());
13201320
}
13211321
}
@@ -1358,17 +1358,32 @@ output_flex_t::output_flex_t(std::shared_ptr<middle_query_t> const &mid,
13581358
eo.set_minzoom(options.expire_tiles_zoom_min);
13591359
eo.set_maxzoom(options.expire_tiles_zoom);
13601360

1361+
// Expiry reprojects from the geometry columns' projection, so all
1362+
// tables feeding this output must agree on it.
1363+
flex_table_t const *srid_table = nullptr;
13611364
for (auto &table : *m_tables) {
1362-
if (table.has_geom_column() &&
1363-
table.geom_column().srid() == PROJ_SPHERE_MERC) {
1364-
expire_config_t config{};
1365-
config.expire_output = m_expire_outputs->size() - 1;
1366-
if (options.expire_tiles_max_bbox > 0.0) {
1367-
config.mode = expire_mode::hybrid;
1368-
config.full_area_limit = options.expire_tiles_max_bbox;
1369-
}
1370-
table.geom_column().add_expire(config);
1365+
if (!table.has_geom_column()) {
1366+
continue;
1367+
}
1368+
auto const srid = table.geom_column().srid();
1369+
if (!srid_table) {
1370+
srid_table = &table;
1371+
eo.set_srid(srid);
1372+
} else if (srid != eo.srid()) {
1373+
throw fmt_error(
1374+
"Tile expiry needs all tables with a geometry column to use"
1375+
" the same projection, but table '{}' uses SRID {} and"
1376+
" table '{}' uses SRID {}.",
1377+
srid_table->name(), eo.srid(), table.name(), srid);
1378+
}
1379+
1380+
expire_config_t config{};
1381+
config.expire_output = m_expire_outputs->size() - 1;
1382+
if (options.expire_tiles_max_bbox > 0.0) {
1383+
config.mode = expire_mode::hybrid;
1384+
config.full_area_limit = options.expire_tiles_max_bbox;
13711385
}
1386+
table.geom_column().add_expire(config);
13721387
}
13731388
}
13741389

@@ -1382,7 +1397,7 @@ output_flex_t::output_flex_t(std::shared_ptr<middle_query_t> const &mid,
13821397
for (auto const &expire_output : *m_expire_outputs) {
13831398
m_expire_tiles.emplace_back(
13841399
expire_output.maxzoom(),
1385-
reprojection_t::create_projection(PROJ_SPHERE_MERC),
1400+
reprojection_t::create_projection(expire_output.srid()),
13861401
expire_output.max_tiles_geometry());
13871402
}
13881403

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Feature: Expire with command line options on geometries not in Web Mercator
2+
3+
Scenario: Tables in EPSG:4326 are expired
4+
Given the lua style
5+
"""
6+
local points = osm2pgsql.define_node_table('osm2pgsql_test_points', {
7+
{ column = 'geom', type = 'point', projection = 4326 },
8+
})
9+
10+
function osm2pgsql.process_node(object)
11+
points:insert({ geom = object:as_point() })
12+
end
13+
"""
14+
And the OSM data
15+
"""
16+
n10 v1 dV Tamenity=restaurant x10.0 y10.0
17+
"""
18+
When running osm2pgsql flex with parameters
19+
| --slim | -c |
20+
Then execution is successful
21+
22+
Given the OSM data
23+
"""
24+
n10 v2 dV Tamenity=restaurant x10.5 y10.5
25+
"""
26+
When running osm2pgsql flex with parameters
27+
| --slim | -a | --expire-tiles=12 | --expire-output=expire.list |
28+
Then execution is successful
29+
And the error output contains
30+
"""
31+
entries to expire output [0].
32+
"""
33+
34+
Scenario: Tables in different projections can not share command line expire
35+
Given the lua style
36+
"""
37+
local merc = osm2pgsql.define_node_table('osm2pgsql_test_merc', {
38+
{ column = 'geom', type = 'point', projection = 3857 },
39+
})
40+
41+
local latlon = osm2pgsql.define_node_table('osm2pgsql_test_latlon', {
42+
{ column = 'geom', type = 'point', projection = 4326 },
43+
})
44+
45+
function osm2pgsql.process_node(object)
46+
merc:insert({ geom = object:as_point() })
47+
latlon:insert({ geom = object:as_point() })
48+
end
49+
"""
50+
And the OSM data
51+
"""
52+
n10 v1 dV Tamenity=restaurant x10.0 y10.0
53+
"""
54+
When running osm2pgsql flex with parameters
55+
| --slim | -c |
56+
Then execution is successful
57+
58+
Given the OSM data
59+
"""
60+
n10 v2 dV Tamenity=restaurant x10.5 y10.5
61+
"""
62+
When running osm2pgsql flex with parameters
63+
| --slim | -a | --expire-tiles=12 |
64+
Then execution fails
65+
And the error output contains
66+
"""
67+
Tile expiry needs all tables with a geometry column to use the same projection
68+
"""

‎tests/test-expire-from-geometry.cpp‎

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

1010
#include <catch.hpp>
1111

12+
#include <osmium/geom/mercator_projection.hpp>
13+
14+
#include <algorithm>
1215
#include <memory>
1316
#include <set>
1417
#include <utility>
@@ -24,10 +27,39 @@ namespace {
2427
std::shared_ptr<reprojection_t> defproj{
2528
reprojection_t::create_projection(PROJ_SPHERE_MERC)};
2629

30+
std::shared_ptr<reprojection_t> latlonproj{
31+
reprojection_t::create_projection(PROJ_LATLONG)};
32+
2733
// We are using zoom level 12 here, because at that level a tile is about
2834
// 10,000 units wide/high which gives us easy numbers to work with.
2935
constexpr uint32_t ZOOM = 12;
3036

37+
geom::point_t merc_to_latlon(geom::point_t const &point)
38+
{
39+
auto const c = osmium::geom::mercator_to_lonlat(
40+
osmium::geom::Coordinates{point.x(), point.y()});
41+
return {c.x, c.y};
42+
}
43+
44+
template <typename TLIST>
45+
TLIST merc_to_latlon(TLIST const &list)
46+
{
47+
TLIST result;
48+
for (auto const &point : list) {
49+
result.push_back(merc_to_latlon(point));
50+
}
51+
return result;
52+
}
53+
54+
template <typename TGEOM>
55+
quadkey_list_t expire(std::shared_ptr<reprojection_t> const &projection,
56+
TGEOM const &geom, expire_config_t const &expire_config)
57+
{
58+
expire_tiles_t et{ZOOM, projection};
59+
et.from_geometry(geom, expire_config);
60+
return et.get_tiles();
61+
}
62+
3163
} // anonymous namespace
3264

3365
TEST_CASE("expire null geometry does nothing", "[NoDB]")
@@ -506,3 +538,66 @@ TEST_CASE("expire doesn't do anything if not in 3857", "[NoDB]")
506538
auto const tiles = et.get_tiles();
507539
REQUIRE(tiles.empty());
508540
}
541+
542+
TEST_CASE("expire point in 4326 matches web mercator", "[NoDB]")
543+
{
544+
expire_config_t const expire_config;
545+
geom::point_t const pt{5000.0, 5000.0};
546+
547+
auto const merc_tiles = expire(defproj, pt, expire_config);
548+
REQUIRE(merc_tiles.size() == 1);
549+
REQUIRE(expire(latlonproj, merc_to_latlon(pt), expire_config) ==
550+
merc_tiles);
551+
}
552+
553+
TEST_CASE("expire linestring in 4326 matches web mercator", "[NoDB]")
554+
{
555+
expire_config_t const expire_config;
556+
geom::linestring_t const line{{5000.0, 5000.0}, {5000.0, 15000.0}};
557+
558+
auto const merc_tiles = expire(defproj, line, expire_config);
559+
REQUIRE(merc_tiles.size() == 2);
560+
REQUIRE(expire(latlonproj, merc_to_latlon(line), expire_config) ==
561+
merc_tiles);
562+
}
563+
564+
TEST_CASE("expire polygon boundary in 4326 matches web mercator", "[NoDB]")
565+
{
566+
expire_config_t expire_config;
567+
expire_config.mode = expire_mode::boundary_only;
568+
geom::ring_t const ring{{5000.0, 5000.0},
569+
{25000.0, 5000.0},
570+
{25000.0, 25000.0},
571+
{5000.0, 25000.0},
572+
{5000.0, 5000.0}};
573+
574+
auto const merc_tiles =
575+
expire(defproj, geom::polygon_t{geom::ring_t{ring}}, expire_config);
576+
REQUIRE(merc_tiles.size() == 8);
577+
REQUIRE(expire(latlonproj, geom::polygon_t{merc_to_latlon(ring)},
578+
expire_config) == merc_tiles);
579+
}
580+
581+
TEST_CASE("expire polygon interior in 4326 matches web mercator", "[NoDB]")
582+
{
583+
expire_config_t expire_config;
584+
expire_config.mode = expire_mode::full_area;
585+
geom::ring_t const ring{{5000.0, 5000.0},
586+
{25000.0, 5000.0},
587+
{25000.0, 25000.0},
588+
{5000.0, 25000.0},
589+
{5000.0, 5000.0}};
590+
591+
auto const merc_tiles =
592+
expire(defproj, geom::polygon_t{geom::ring_t{ring}}, expire_config);
593+
auto const latlon_tiles = expire(
594+
latlonproj, geom::polygon_t{merc_to_latlon(ring)}, expire_config);
595+
596+
REQUIRE(merc_tiles.size() == 9);
597+
REQUIRE(latlon_tiles == merc_tiles);
598+
599+
// The one tile no part of the boundary touches.
600+
auto const interior = tile_t{ZOOM, 2049, 2046}.quadkey();
601+
REQUIRE(std::find(latlon_tiles.cbegin(), latlon_tiles.cend(), interior) !=
602+
latlon_tiles.cend());
603+
}

0 commit comments

Comments
 (0)