From f7430aacda2b1a75b7c67bdcbb1d55e1640a4a29 Mon Sep 17 00:00:00 2001 From: Jochen Topf Date: Fri, 11 Nov 2022 10:56:50 +0100 Subject: [PATCH] WIP: Add code to generalize data using different strategies This commit adds the code to generalize various types of data using different strategies. The following strategies work on a tile-by-tile basis and operate on polygons: The "vector-union" strategy buffers and unionizes polygons using vector operations. The "raster-union" strategy does a similar thing but does it in raster space which is much faster. First the polygons are rendered into a raster, an open/close operation is called (which basically does the same thing as the buffering in vector space) and finally the resulting raster is vectorized again. The "builtup" strategy is intended to derive a layer of builtup areas from landuse=residential/industrial etc. as well as building cover and dense road networks. This still needs some work... Also a new "discrete-isolation" strategy which rates places based on some importance metric. (This is not tile-based.) The new "rivers" strategy finds important rivers, this is still very much work in progress. For the raster support this adds two new library dependency: CImg and potrace. The functionality is accessed through a new command line program called osm2pgsql-gen. Call it with -h to get some usage information. This program is for testing only, eventually the functionality should be accessible from osm2pgsql itself (using the Lua config file for configuration). Some additional information is in README-gen.md . See also https://osm2pgsql.org/generalization/ . wip rivers --- .../actions/ubuntu-prerequisites/action.yml | 2 + .github/actions/win-install/action.yml | 3 +- .github/workflows/ci.yml | 4 +- .github/workflows/test-install.yml | 5 +- CMakeLists.txt | 16 + README-gen.md | 97 +++++ README.md | 5 +- src/canvas.cpp | 129 ++++++ src/canvas.hpp | 81 ++++ src/gen-base.cpp | 144 +++++++ src/gen-base.hpp | 138 +++++++ src/gen-discrete-isolation.cpp | 144 +++++++ src/gen-discrete-isolation.hpp | 37 ++ src/gen-rivers.cpp | 340 ++++++++++++++++ src/gen-rivers.hpp | 43 ++ src/gen-tile-builtup.cpp | 265 ++++++++++++ src/gen-tile-builtup.hpp | 51 +++ src/gen-tile-raster.cpp | 250 ++++++++++++ src/gen-tile-raster.hpp | 46 +++ src/gen-tile-vector.cpp | 100 +++++ src/gen-tile-vector.hpp | 36 ++ src/gen-tile.cpp | 49 +++ src/gen-tile.hpp | 48 +++ src/geom.hpp | 10 + src/osm2pgsql-gen.cpp | 385 ++++++++++++++++++ src/pgsql.hpp | 40 +- src/raster.cpp | 66 +++ src/raster.hpp | 64 +++ src/tile.hpp | 25 ++ src/tracer.cpp | 108 +++++ src/tracer.hpp | 76 ++++ src/util.hpp | 14 +- tests/test-pgsql.cpp | 12 - 33 files changed, 2811 insertions(+), 22 deletions(-) create mode 100644 README-gen.md create mode 100644 src/canvas.cpp create mode 100644 src/canvas.hpp create mode 100644 src/gen-base.cpp create mode 100644 src/gen-base.hpp create mode 100644 src/gen-discrete-isolation.cpp create mode 100644 src/gen-discrete-isolation.hpp create mode 100644 src/gen-rivers.cpp create mode 100644 src/gen-rivers.hpp create mode 100644 src/gen-tile-builtup.cpp create mode 100644 src/gen-tile-builtup.hpp create mode 100644 src/gen-tile-raster.cpp create mode 100644 src/gen-tile-raster.hpp create mode 100644 src/gen-tile-vector.cpp create mode 100644 src/gen-tile-vector.hpp create mode 100644 src/gen-tile.cpp create mode 100644 src/gen-tile.hpp create mode 100644 src/osm2pgsql-gen.cpp create mode 100644 src/raster.cpp create mode 100644 src/raster.hpp create mode 100644 src/tracer.cpp create mode 100644 src/tracer.hpp diff --git a/.github/actions/ubuntu-prerequisites/action.yml b/.github/actions/ubuntu-prerequisites/action.yml index 12743319a..8a7eb0c91 100644 --- a/.github/actions/ubuntu-prerequisites/action.yml +++ b/.github/actions/ubuntu-prerequisites/action.yml @@ -16,10 +16,12 @@ runs: - name: Install software run: | sudo apt-get install -yq --no-install-suggests --no-install-recommends \ + cimg-dev \ libboost-filesystem-dev \ libboost-system-dev \ libbz2-dev \ libexpat1-dev \ + libpotrace-dev \ libpq-dev \ libproj-dev \ pandoc \ diff --git a/.github/actions/win-install/action.yml b/.github/actions/win-install/action.yml index bf5171099..3ebc475b3 100644 --- a/.github/actions/win-install/action.yml +++ b/.github/actions/win-install/action.yml @@ -5,9 +5,8 @@ runs: steps: - name: Install packages - run: vcpkg install bzip2:x64-windows expat:x64-windows zlib:x64-windows proj4:x64-windows boost-geometry:x64-windows boost-system:x64-windows boost-filesystem:x64-windows boost-property-tree:x64-windows lua:x64-windows libpq:x64-windows + run: vcpkg install cimg:x64-windows bzip2:x64-windows expat:x64-windows zlib:x64-windows proj4:x64-windows boost-geometry:x64-windows boost-system:x64-windows boost-filesystem:x64-windows boost-property-tree:x64-windows lua:x64-windows libpq:x64-windows shell: bash - - name: Install psycopg2 and beahve run: python -m pip install psycopg2 behave shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66662ed32..60a144fed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Install prerequisites run: | - brew install lua boost postgis pandoc + brew install lua boost postgis pandoc cimg potrace pip3 install psycopg2 behave pg_ctl -D /usr/local/var/postgres init pg_ctl -D /usr/local/var/postgres start @@ -45,6 +45,7 @@ jobs: env: CC: gcc-7 CXX: g++-7 + EXTRA_FLAGS: -Wno-unused-but-set-parameter # workaround for GCC bug LUA_VERSION: 5.3 LUAJIT_OPTION: ON POSTGRESQL_VERSION: 9.6 @@ -79,6 +80,7 @@ jobs: env: CC: gcc-9 CXX: g++-9 + EXTRA_FLAGS: -Wno-unused-but-set-parameter # workaround for GCC bug LUA_VERSION: 5.3 LUAJIT_OPTION: OFF POSTGRESQL_VERSION: 10 diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 297cfb56c..1fce83fa4 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -11,7 +11,8 @@ jobs: POSTGRESQL_VERSION: 12 POSTGIS_VERSION: 3 BUILD_TYPE: Release - CXXFLAGS: -pedantic -Wextra -Werror + # -Wno-unused-but-set-parameter is a workaround for GCC bug + CXXFLAGS: -pedantic -Wextra -Werror -Wno-unused-but-set-parameter PREFIX: /usr/local OSMURL: https://download.geofabrik.de/europe/monaco-latest.osm.pbf OSMFILE: monaco-latest.osm.pbf @@ -26,12 +27,14 @@ jobs: run: | sudo apt-get purge -yq postgresql* sudo apt-get install -yq --no-install-suggests --no-install-recommends \ + cimg-dev \ libboost-filesystem-dev \ libboost-system-dev \ libbz2-dev \ libexpat1-dev \ liblua${LUA_VERSION}-dev \ libluajit-5.1-dev \ + libpotrace-dev \ libpq-dev \ libproj-dev \ lua${LUA_VERSION} \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 847f0a811..f17e90d8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,11 @@ include_directories(SYSTEM ${PostgreSQL_INCLUDE_DIRS}) find_package(Threads) +find_path(POTRACE_INCLUDE_DIR potracelib.h) +find_library(POTRACE_LIBRARY NAMES potrace) + +find_path(CIMG_INCLUDE_DIR CImg.h) + ############### Libraries are found now ######################## set(LIBS ${Boost_LIBRARIES} ${PostgreSQL_LIBRARY} ${OSMIUM_LIBRARIES}) @@ -276,6 +281,17 @@ add_subdirectory(src) add_executable(osm2pgsql src/osm2pgsql.cpp) target_link_libraries(osm2pgsql osm2pgsql_lib ${LIBS}) +if (${POTRACE_LIBRARY} STREQUAL "POTRACE_LIBRARY-NOTFOUND" OR ${CIMG_INCLUDE_DIR} STREQUAL "CIMG_INCLUDE_DIR-NOTFOUND") + message(STATUS "Did not find cimg or potrace library. Not building osm2pgsql-gen.") +else() + message(STATUS "Found cimg and potrace library. Building osm2pgsql-gen.") + include_directories(SYSTEM ${CIMG_INCLUDE_DIR}) + include_directories(SYSTEM ${POTRACE_INCLUDE_DIR}) + add_executable(osm2pgsql-gen src/osm2pgsql-gen.cpp src/canvas.cpp src/raster.cpp src/tracer.cpp + src/gen-base.cpp src/gen-discrete-isolation.cpp src/gen-tile.cpp src/gen-tile-builtup.cpp src/gen-tile-raster.cpp src/gen-rivers.cpp src/gen-tile-vector.cpp) + target_link_libraries(osm2pgsql-gen osm2pgsql_lib ${LIBS} ${POTRACE_LIBRARY}) +endif() + ############################################################# # Optional "clang-tidy" target ############################################################# diff --git a/README-gen.md b/README-gen.md new file mode 100644 index 000000000..310b3b39b --- /dev/null +++ b/README-gen.md @@ -0,0 +1,97 @@ + +# Generalization + +There is an experimental new program `osm2pgsql-gen`. It is only built if the +CImg and potrace libraries are available. + +This is part of a project to add generalization support to osm2pgsql. See +https://osm2pgsql.org/generalization/ for more information. + +Generalization is currently only supported for Web Mercator (EPSG 3857). This +is by far the most common use case, we can look at extending this later if +needed. + +## Usage + +Run osm2pgsql as usual. This example assumes you have a table called +`landcover` with a polygon geometry column called `geom` and a text column +called `type` with the type of the landcover (forest, grass, etc.). + +Create a table `landcover_z10` with (at least) columns `geom` and `type` as +well as integer columns `x`, and `y`. Then call this: + +``` +PGDATABASE=mydb ./osm2pgsql-gen -t landcover -T landcover_z10 -g geom -G type -z 10 -m 0.125 -s raster-union +``` + +The `landcover_z10` table will be filled with generalized polygons. + +Database connection parameters have to be specified using [environment +variables](https://www.postgresql.org/docs/current/libpq-envars.html). + +Call `osm2pgsql-gen -h` for more command line options. + +## Extent + +You can process a single tile by setting `-x`, and `-y`. If `-X` and `-Y` are +also set, all tiles between the arguments of `-x` and `-X`, and `-y` and `-Y` +are processed. Without any of these, the program gets the extent from the data +in the source table. + +You can also use the option `-e, --expire-list=TABLE` to read the list of tiles +from a database table. + +In any case `-z` or `--zoom` sets the zoom level. + +## Parameters + +For a full list of parameters see `osm2pgsql-gen -h`. + +* `-s, --strategy=STRATEGY`: Set the strategy used for generalization. See + below for available strategies. +* `-m, --margin=MARGIN`: This sets the margin around the tile as a fraction of + the tile size. So a value of 0.1 sets a 10% boundary on each side of the tile, + so as a result the tiles will overlap by 20% of their size. When using the + `raster-union` strategy the margin will be rounded up to the nearest multiple + of 64 pixels. +* `-w, --width=WIDTH`: Size of the imaged rendered when using the `raster-union` + strategy, not used in the `vector-union` strategy. +* `-b, --buffer=BUFFER`: The amount by which the polygons will be buffered. For + the `raster-union` strategy this is in pixels, for the `vector-union` strategy + this is in Mercator map units. +* `-g, --group-by-column=COL`: Set this to the column describing the type of + polygon if any, the program will group the data by this column before + generalization. If this is not specified, no grouping is performed and + the destination table also doesn't need a column for this. +* `--img-path=PATH`: Used to dump PNGs of the "before" and "after" images + generated with the `raster-union` strategy. Use something like this: + `--img-path=some/dir/path/img`. Resulting images will be in the + directory `some/dir/path` and are named `img-X-Y-TYPE-[io].png` for + input (`i`) or output (`o`) images. The `TYPE` is the value from the + column specified with the `-G` option. +* `--img-table=TABLE`: Used to dump "before" and "after" raster images to the + database. The table will be created if it doesn't exist already. +* `-e, --expire-list=TABLE`: Get list of tiles to expire from the specified + table. If this is set the options `-x`, `-X`, `-y`, and `-Y` are ignored. The + content of the table is not removed after processing! +* `-p, --param=KEY=VALUE`: Set any parameter. This allows for easier + experimentation with new parameters. + +## Strategies + +Some strategies work on a tile-by-tile basis (`vector-union`, `raster-union`, +`builtup`), some work for all data at once (`discrete-isolation`). + +* `vector-union`: Buffer and union polygons together in vector space to + form generalized polygons for landcover and similar use. +* `raster-union`: Buffer and union polygons together in raster space to + form generalized polygons for landcover and similar use. + See https://blog.jochentopf.com/2022-11-21-generalizing-polygons.html for + details. +* `builtup`: Aggregate data from several layers (such as landcover, buildings, + and roads) to derive built-up areas. +* `discrete-isolation`: Classify point geometries by importance to render + only the most important places but still don't leave areas too empty. + See https://blog.jochentopf.com/2022-12-19-selecting-settlements-to-display.html + for some background. + diff --git a/README.md b/README.md index 3264c0ad3..e9c801a2d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Required libraries are * [zlib](https://www.zlib.net/) * [Boost libraries](https://www.boost.org/), including geometry, system and filesystem +* [CImg](https://cimg.eu/) (Optional, see README-gen.md) +* [potrace](https://potrace.sourceforge.net/) (Optional, see README-gen.md) * [PostgreSQL](https://www.postgresql.org/) client libraries * [Lua](https://www.lua.org/) (Optional, used for Lua tag transforms and the flex output) @@ -80,7 +82,7 @@ On a Debian or Ubuntu system, this can be done with: ```sh sudo apt-get install make cmake g++ libboost-dev libboost-system-dev \ - libboost-filesystem-dev libexpat1-dev zlib1g-dev \ + libboost-filesystem-dev libexpat1-dev zlib1g-dev libpotrace-dev cimg-dev\ libbz2-dev libpq-dev libproj-dev lua5.3 liblua5.3-dev pandoc ``` @@ -88,6 +90,7 @@ On a Fedora system, use ```sh sudo dnf install cmake make gcc-c++ boost-devel expat-devel zlib-devel \ + potrace-devel cimg-devel\ bzip2-devel postgresql-devel proj-devel proj-epsg lua-devel pandoc ``` diff --git a/src/canvas.cpp b/src/canvas.cpp new file mode 100644 index 000000000..1e9332669 --- /dev/null +++ b/src/canvas.cpp @@ -0,0 +1,129 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "canvas.hpp" +#include "raster.hpp" + +cimg_library::CImg canvas_t::create_pointlist(geom::point_list_t const &pl, + tile_t const &tile) const +{ + cimg_library::CImg points{static_cast(pl.size()), 2}; + + int n = 0; + for (auto const point : pl) { + auto const tp = tile.to_tile_coords(point, m_extent); + points(n, 0) = static_cast(m_buffer) + tp.x(); + points(n, 1) = static_cast(m_buffer + m_extent) - tp.y(); + ++n; + } + + return points; +} + +std::size_t canvas_t::draw_polygon(geom::polygon_t const &polygon, + tile_t const &tile) +{ + if (polygon.inners().empty()) { + m_rast.draw_polygon(create_pointlist(polygon.outer(), tile), &White); + return polygon.outer().size(); + } + + std::size_t num_points = polygon.outer().size(); + m_temp.draw_polygon(create_pointlist(polygon.outer(), tile), &White); + for (auto const &inner : polygon.inners()) { + num_points += inner.size(); + m_temp.draw_polygon(create_pointlist(inner, tile), &Black); + } + m_rast |= m_temp; + + return num_points; +} + +std::size_t canvas_t::draw_linestring(geom::linestring_t const &linestring, + tile_t const &tile) +{ + m_rast.draw_line(create_pointlist(linestring, tile), &White); + return linestring.size(); +} + +std::size_t canvas_t::draw(geom::geometry_t const &geometry, tile_t const &tile) +{ + if (geometry.is_linestring()) { + auto const &linestring = geometry.get(); + return draw_linestring(linestring, tile); + } + + if (geometry.is_polygon()) { + auto const &polygon = geometry.get(); + return draw_polygon(polygon, tile); + } + + if (geometry.is_multipolygon()) { + auto const &mp = geometry.get(); + std::size_t num_points = 0; + for (auto const &p : mp) { + num_points += draw_polygon(p, tile); + } + return num_points; + } + + // XXX other geometry types? + + return 0; +} + +void canvas_t::save(std::string const &filename) const +{ + m_rast.save(filename.c_str()); +} + +std::string canvas_t::to_wkb(tile_t const &tile, double margin) const +{ + std::string wkb; + wkb.reserve(61 + 2 + m_rast.size()); + + // header + wkb_raster_header header{}; + header.nBands = 1; + header.scaleX = tile.extent() / m_extent; + header.scaleY = -header.scaleX; + header.ipX = tile.xmin(margin); + header.ipY = tile.ymax(margin); + header.width = m_extent + 2 * m_buffer; + header.height = header.width; + add_raster_header(&wkb, header); + + // band + wkb_raster_band band{}; + band.bits = 4; + add_raster_band(&wkb, band); + + // rasterdata + wkb.append(reinterpret_cast(m_rast.data()), m_rast.size()); + + assert(wkb.size() == 61 + 2 + m_rast.size()); + + return wkb; +} + +void canvas_t::merge(canvas_t const &other) { m_rast |= other.m_rast; } + +std::string to_hex(std::string const &in) +{ + std::string result; + char const *const lookup_hex = "0123456789ABCDEF"; + + for (const auto c : in) { + unsigned int const num = static_cast(c); + result += lookup_hex[(num >> 4U) & 0xfU]; + result += lookup_hex[num & 0xfU]; + } + + return result; +} diff --git a/src/canvas.hpp b/src/canvas.hpp new file mode 100644 index 000000000..480304a8b --- /dev/null +++ b/src/canvas.hpp @@ -0,0 +1,81 @@ +#ifndef OSM2PGSQL_CANVAS_HPP +#define OSM2PGSQL_CANVAS_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "geom.hpp" +#include "tile.hpp" + +#define cimg_display 0 // NOLINT(cppcoreguidelines-macro-usage) +#include "CImg.h" + +#include + +class canvas_t +{ +public: + static void info() { cimg_library::cimg::info(); } + + canvas_t(std::size_t extent, std::size_t buffer) + : m_extent(extent), + m_buffer(buffer), m_rast{size(), size(), 1, 1, 0}, m_temp{size(), size(), + 1, 1, 0} + {} + + unsigned int size() const noexcept + { + return static_cast(m_extent + 2 * m_buffer); + } + + unsigned char const *begin() const noexcept { return m_rast.begin(); } + unsigned char const *end() const noexcept { return m_rast.end(); } + + std::size_t draw(geom::geometry_t const &geometry, tile_t const &tile); + + unsigned char operator()(int x, int y) const noexcept + { + return m_rast(x, y, 0, 0); + } + + void open_close(unsigned int buffer_size) + { + m_rast.dilate(buffer_size).erode(buffer_size * 2).dilate(buffer_size); + } + + void save(std::string const &filename) const; + + std::string to_wkb(tile_t const &tile, double margin) const; + + void merge(canvas_t const &other); + +private: + constexpr static unsigned char const Black = 0; + constexpr static unsigned char const White = 255; + + using image_type = cimg_library::CImg; + + cimg_library::CImg create_pointlist(geom::point_list_t const &pl, + tile_t const &tile) const; + + std::size_t draw_polygon(geom::polygon_t const &polygon, + tile_t const &tile); + + std::size_t draw_linestring(geom::linestring_t const &linestring, + tile_t const &tile); + + std::size_t m_extent; + std::size_t m_buffer; + image_type m_rast; + image_type m_temp; +}; // class canvas_t + +std::string to_hex(std::string const &in); + +#endif // OSM2PGSQL_CANVAS_HPP diff --git a/src/gen-base.cpp b/src/gen-base.cpp new file mode 100644 index 000000000..f8a72eb86 --- /dev/null +++ b/src/gen-base.cpp @@ -0,0 +1,144 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-base.hpp" + +#include "format.hpp" +#include "gen-discrete-isolation.hpp" +#include "gen-rivers.hpp" +#include "gen-tile-builtup.hpp" +#include "gen-tile-raster.hpp" +#include "gen-tile-vector.hpp" + +pg_result_t gen_base_t::dbexec(std::string const &templ) +{ + fmt::dynamic_format_arg_store format_store; + for (auto const &[key, value] : params()) { + format_store.push_back(fmt::arg(key.c_str(), value)); + } + auto const sql = fmt::vformat(templ, format_store); + return connection().exec(sql); +} + +void gen_base_t::raster_table_preprocess(std::string const &table) +{ + tmp_param tp{¶ms()}; + tp.add("TABLE", table); + + dbexec("SELECT DropRasterConstraints('{schema}'::name," + " '{TABLE}'::name, 'rast'::name)"); +} + +void gen_base_t::raster_table_postprocess(std::string const &table) +{ + tmp_param tp{¶ms()}; + tp.add("TABLE", table); + + dbexec("SELECT AddRasterConstraints('{schema}'::name," + " '{TABLE}'::name, 'rast'::name)"); + dbexec( + R"(ALTER TABLE "{schema}"."{TABLE}" VALIDATE CONSTRAINT enforce_max_extent_rast)"); + dbexec(R"(ANALYZE "{schema}"."{TABLE}")"); +} + +std::unique_ptr create_generalizer(std::string const &strategy, + pg_conn_t *connection, + gen_params_t *params) +{ + auto const schema = get_identifier(*params, "schema"); + auto const src_table = get_identifier(*params, "src_table"); + auto const dest_table = get_identifier(*params, "dest_table"); + params->emplace("src", qualified_name(schema, src_table)); + params->emplace("dest", qualified_name(schema, dest_table)); + + std::unique_ptr generalizer; + if (strategy == "discrete-isolation") { + generalizer = std::make_unique(connection, params); + } else if (strategy == "builtup") { + generalizer = std::make_unique(connection, params); + } else if (strategy == "raster-union") { + generalizer = + std::make_unique(connection, params); + } else if (strategy == "rivers") { + generalizer = std::make_unique(connection, params); + } else if (strategy == "vector-union") { + generalizer = + std::make_unique(connection, params); + } else { + throw fmt_error("Unknown generalization strategy '{}'.", strategy); + } + + generalizer->init(); + + return generalizer; +} + +unsigned int get_uint(gen_params_t const ¶ms, std::string const &key, + int default_value) +{ + auto const it = params.find(key); + if (it == params.end()) { + return default_value; + } + + char *end = nullptr; + auto const value = std::strtoul(it->second.c_str(), &end, 10); + if (end == it->second.c_str() || *end != '\0') { + throw fmt_error("Could not parse '{}' as unsigned int.", it->second); + } + + return value; +} + +double get_double(gen_params_t const ¶ms, std::string const &key, + double default_value) +{ + auto const it = params.find(key); + if (it == params.end()) { + return default_value; + } + + char *end = nullptr; + double const value = strtod(it->second.c_str(), &end); + if (end == it->second.c_str() || *end != '\0') { + throw fmt_error("Could not parse '{}' as real number.", it->second); + } + + return value; +} + +std::string get_string(gen_params_t const ¶ms, std::string const &key) +{ + auto const it = params.find(key); + if (it == params.end()) { + return {}; + } + return it->second; +} + +std::string get_identifier(gen_params_t const ¶ms, std::string const &key) +{ + auto const it = params.find(key); + if (it == params.end()) { + return {}; + } + check_identifier(it->second, key.c_str()); + return it->second; +} + +void check_identifier_with_default(gen_params_t *params, std::string const &key, + std::string default_value) +{ + auto const it = params->find(key); + if (it == params->end()) { + params->emplace(key, std::move(default_value)); + } else { + check_identifier(it->second, key.c_str()); + } +} diff --git a/src/gen-base.hpp b/src/gen-base.hpp new file mode 100644 index 000000000..ff037511a --- /dev/null +++ b/src/gen-base.hpp @@ -0,0 +1,138 @@ +#ifndef OSM2PGSQL_GEN_BASE_HPP +#define OSM2PGSQL_GEN_BASE_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "pgsql.hpp" +#include "tile.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +using gen_params_t = std::map; + +/** + * Base class for generalizations. + */ +class gen_base_t +{ +public: + virtual ~gen_base_t() = default; + + /// Initialize derived object. + virtual void init() {} + + /// Process data. + virtual void process() {} + + /// Process one tile. + virtual void process(tile_t const & /*tile*/) {} + + /// Optional postprocessing after all tiles. + virtual void post() {} + + /// Get the name of the generalization strategy. + virtual std::string_view name() const noexcept = 0; + + virtual bool on_tiles() const noexcept { return false; } + + std::vector const &timers() const noexcept + { + return m_timers; + } + +protected: + class tmp_param + { + public: + explicit tmp_param(gen_params_t *params) : m_params(params) {} + + tmp_param(tmp_param const &) = delete; + tmp_param &operator=(tmp_param const &) = delete; + tmp_param(tmp_param &&) = delete; + tmp_param &operator=(tmp_param &&) = delete; + + ~tmp_param() + { + for (auto const &key : m_keys) { + m_params->erase(key); + } + } + + void add(std::string key, std::string value) + { + m_params->emplace(key, std::move(value)); + m_keys.push_back(std::move(key)); + } + + private: + gen_params_t *m_params; + std::vector m_keys; + }; + + gen_base_t(pg_conn_t *connection, gen_params_t *params) + : m_connection(connection), m_params(params) + {} + + pg_conn_t &connection() noexcept { return *m_connection; } + + std::size_t add_timer(char const *name) + { + m_timers.emplace_back(name); + return m_timers.size() - 1; + } + + util::timer_t &timer(std::size_t n) noexcept { return m_timers[n]; } + + gen_params_t ¶ms() noexcept { return *m_params; } + +#if 0 + template + void add_param(KEY &&key, VALUE &&value) + { + m_params->emplace(std::forward(key), std::forward(value)); + } +#endif + + pg_result_t dbexec(std::string const &templ); + + void raster_table_preprocess(std::string const &table); + void raster_table_postprocess(std::string const &table); + +private: + std::vector m_timers; + pg_conn_t *m_connection; + gen_params_t *m_params; +}; + +/** + * Instantiate a generalizer for the specified strategy. + */ +std::unique_ptr create_generalizer(std::string const &strategy, + pg_conn_t *connection, + gen_params_t *params); + +unsigned int get_uint(gen_params_t const ¶ms, std::string const &key, + int default_value = 0); + +double get_double(gen_params_t const ¶ms, std::string const &key, + double default_value = 0.0); + +std::string get_string(gen_params_t const ¶ms, std::string const &key); + +std::string get_identifier(gen_params_t const ¶ms, std::string const &key); + +void check_identifier_with_default(gen_params_t *params, std::string const &key, + std::string default_value); + +#endif // OSM2PGSQL_GEN_BASE_HPP diff --git a/src/gen-discrete-isolation.cpp b/src/gen-discrete-isolation.cpp new file mode 100644 index 000000000..f17286857 --- /dev/null +++ b/src/gen-discrete-isolation.cpp @@ -0,0 +1,144 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-discrete-isolation.hpp" + +#include "logging.hpp" +#include "pgsql.hpp" +#include "util.hpp" + +#include +#include + +gen_di_t::gen_di_t(pg_conn_t *connection, gen_params_t *params) +: gen_base_t(connection, params), m_timer_get(add_timer("get")), + m_timer_sort(add_timer("sort")), m_timer_di(add_timer("di")), + m_timer_reorder(add_timer("reorder")), m_timer_write(add_timer("write")) +{ + check_identifier_with_default(params, "id_column", "id"); + check_identifier_with_default(params, "importance_column", "importance"); +} + +void gen_di_t::process() +{ + struct feature + { + // input: unique id of the feature + uint64_t id; + + // input: importance of the feature (positive, larger is more imporant) + double importance; + + // input: x/y coordinate of the feature + double x; + double y; + + // output: discrete isolation + double di; + + // output: rank for importance + uint32_t irank; + }; + + log_debug("Reading data from database..."); + + std::vector data; + timer(m_timer_get).start(); + { + auto const result = dbexec(R"( +SELECT {id_column}, {importance_column}, + ST_X({geom_column}), ST_Y({geom_column}) +FROM {src} WHERE {importance_column} > 0 +)"); + + data.reserve(result.num_tuples()); + for (int i = 0; i < result.num_tuples(); ++i) { + data.push_back({std::strtoull(result.get_value(i, 0), nullptr, 10), + std::strtod(result.get_value(i, 1), nullptr), + std::strtod(result.get_value(i, 2), nullptr), + std::strtod(result.get_value(i, 3), nullptr), 0.0, + 0}); + } + } + timer(m_timer_get).stop(); + log_debug("Read {} features", data.size()); + + if (data.size() < 2) { + log_info("Found fewer than two features. Nothing to do."); + return; + } + + log_debug("Sorting data by importance..."); + timer(m_timer_sort).start(); + { + std::sort(data.begin(), data.end(), + [](feature const &a, feature const &b) { + return a.importance > b.importance; + }); + { + uint32_t n = 0; + for (auto &item : data) { + item.irank = n++; + } + } + } + timer(m_timer_sort).stop(); + + log_debug("Calculating discrete isolation..."); + timer(m_timer_di).start(); + { + std::vector> coords; + coords.reserve(data.size()); + for (auto const &d : data) { + coords.emplace_back(d.x, d.y); + } + + for (std::size_t n = 1; n < data.size(); ++n) { + if (n % 10000 == 0) { + log_debug(" {}", n); + } + double min = 100000000000000.0; + for (std::size_t m = 0; m < n; ++m) { + double const dx = coords[m].first - coords[n].first; + double const dy = coords[m].second - coords[n].second; + double const dist = dx * dx + dy * dy; + if (dist < min) { + min = dist; + } + } + data[n].di = sqrt(min); + } + data[0].di = data[1].di + 1; + } + timer(m_timer_di).stop(); + + log_debug("Sorting data by discrete isolation..."); + timer(m_timer_reorder).start(); + std::sort(data.begin(), data.end(), + [](feature const &a, feature const &b) { return a.di > b.di; }); + timer(m_timer_reorder).stop(); + + log_debug("Writing results to destination table..."); + dbexec("PREPARE update (int, real, int4, int8) AS" + " UPDATE {src} SET dirank = $1, discr_iso = $2, irank = $3" + " WHERE {id_column} = $4"); + + timer(m_timer_write).start(); + connection().exec("BEGIN"); + std::size_t n = 0; + for (auto const &d : data) { + connection().exec_prepared("update", n++, d.di, d.irank, d.id); + } + connection().exec("COMMIT"); + timer(m_timer_write).stop(); + + dbexec("ANALYZE {src}"); + + log_debug("Done."); +} diff --git a/src/gen-discrete-isolation.hpp b/src/gen-discrete-isolation.hpp new file mode 100644 index 000000000..02d4aea81 --- /dev/null +++ b/src/gen-discrete-isolation.hpp @@ -0,0 +1,37 @@ +#ifndef OSM2PGSQL_GEN_DISCRETE_ISOLATION_HPP +#define OSM2PGSQL_GEN_DISCRETE_ISOLATION_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-base.hpp" + +#include + +class gen_di_t : public gen_base_t +{ +public: + gen_di_t(pg_conn_t *connection, gen_params_t *params); + + void process() override; + + std::string_view name() const noexcept override + { + return "discrete-isolation"; + } + +private: + std::size_t m_timer_get; + std::size_t m_timer_sort; + std::size_t m_timer_di; + std::size_t m_timer_reorder; + std::size_t m_timer_write; +}; + +#endif // OSM2PGSQL_GEN_DISCRETE_ISOLATION_HPP diff --git a/src/gen-rivers.cpp b/src/gen-rivers.cpp new file mode 100644 index 000000000..96f57e26d --- /dev/null +++ b/src/gen-rivers.cpp @@ -0,0 +1,340 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-rivers.hpp" + +#include "geom-functions.hpp" +#include "logging.hpp" +#include "pgsql.hpp" +#include "util.hpp" +#include "wkb.hpp" + +#include +#include +#include +#include +#include + +gen_rivers_t::gen_rivers_t(pg_conn_t *connection, gen_params_t *params) +: gen_base_t(connection, params), m_timer_area(add_timer("area")), + m_timer_prep(add_timer("prep")), m_timer_get(add_timer("get")), + m_timer_sort(add_timer("sort")), m_timer_net(add_timer("net")), + m_timer_remove(add_timer("remove")), m_timer_width(add_timer("width")), + m_timer_write(add_timer("write")), + m_delete_existing(params->count("delete_existing")) +{ + check_identifier_with_default(params, "src_areas", "waterway_areas"); + check_identifier_with_default(params, "id_column", "way_id"); + check_identifier_with_default(params, "width_column", "width"); + check_identifier_with_default(params, "name_column", "name"); +} + +/// The data for a graph edge in the waterway network. +struct edge_t +{ + // All the points in this edge + geom::linestring_t points; + + // Edges can be made from (part) of one or more OSM ways, this is the id + // of one of them. + osmid_t id; + + // The width of the river along this edge + double width; +}; + +bool operator<(edge_t const &a, edge_t const &b) noexcept +{ + assert(a.points.size() > 1 && b.points.size() > 1); + if (a.points[0] == b.points[0]) { + return a.points[1] < b.points[1]; + } + return a.points[0] < b.points[0]; +} + +bool operator<(edge_t const &a, geom::point_t b) noexcept +{ + assert(!a.points.empty()); + return a.points[0] < b; +} + +bool operator<(geom::point_t a, edge_t const &b) noexcept +{ + assert(!b.points.empty()); + return a < b.points[0]; +} + +static void +follow_chain_and_set_width(edge_t const &edge, std::vector *edges, + std::map const &node_order, + geom::linestring_t *seen) +{ + assert(!edge.points.empty()); + + auto const seen_it = + std::find(seen->cbegin(), seen->cend(), edge.points[0]); + if (seen_it != seen->cend()) { + return; // loop detected + } + + seen->push_back(edge.points[0]); + + assert(edge.points.size() > 1); + auto const next_point = edge.points.back(); + if (node_order.at(next_point) > 1) { + auto const [s, e] = + std::equal_range(edges->begin(), edges->end(), next_point); + + if (std::next(s) == e) { + if (s->width < edge.width) { + s->width = edge.width; + follow_chain_and_set_width(*s, edges, node_order, seen); + } + } else { + for (auto it = s; it != e; ++it) { + assert(it->points[0] == next_point); + if (it->width < edge.width) { + it->width = edge.width; + auto seen2 = *seen; + follow_chain_and_set_width(*it, edges, node_order, &seen2); + } + } + } + } +} + +static void assemble_edge(edge_t *edge, std::vector *edges, + std::map const &node_order) + +{ + assert(edge); + assert(edges); + while (true) { + assert(edge->points.size() > 1); + geom::point_t const next_point = edge->points.back(); + + auto const count = node_order.at(next_point); + if (count != 2) { + return; + } + + auto const [s, e] = + std::equal_range(edges->begin(), edges->end(), next_point); + + if (s == e) { + return; + } + assert(e == std::next(s)); + + auto const it = s; + if (it->points.size() == 1 || &*it == edge) { + return; + } + + if (it->points[0] != next_point) { + return; + } + assert(it != edges->end()); + + edge->width = std::max(edge->width, it->width); + + if (it->points.size() == 2) { + edge->points.push_back(it->points.back()); + it->points.resize(1); + it->points.shrink_to_fit(); + } else { + edge->points.insert(edge->points.end(), + std::next(it->points.begin()), + it->points.end()); + it->points.resize(1); + it->points.shrink_to_fit(); + return; + } + } +} + +/// Get some stats from source table +void gen_rivers_t::get_stats() +{ + auto const result = + dbexec("SELECT count(*), sum(ST_NumPoints(geom)) FROM {src}"); + + m_num_waterways = strtoul(result.get_value(0, 0), nullptr, 10); + m_num_points = strtoul(result.get_value(0, 1), nullptr, 10); + + log_debug("Found {} waterways with {} points.", m_num_waterways, + m_num_points); +} + +static std::string const & +get_name(std::unordered_map const &names, osmid_t id) +{ + static std::string const empty; + auto const it = names.find(id); + if (it == names.end()) { + return empty; + } + return it->second; +} + +void gen_rivers_t::process() +{ + log_debug("Calculate waterway area width..."); + timer(m_timer_area).start(); + dbexec("UPDATE {src_areas}" + " SET width = (ST_MaximumInscribedCircle({geom_column})).radius * 2" + " WHERE width IS NULL"); + timer(m_timer_area).stop(); + + log_debug("Project 'width' from areas to lines..."); + timer(m_timer_prep).start(); + dbexec(R"( +WITH covered_lines AS ( + SELECT * FROM {src} w + WHERE ST_NumPoints(w.geom) > 2 AND ST_CoveredBy(w.geom, + (SELECT ST_Union(geom) FROM {src_areas} a WHERE ST_Intersects(w.geom, a.geom))) +), intersections AS ( + SELECT w.{id_column} AS wid, ST_Intersection(a.geom, w.geom) AS inters, + ST_Length(w.geom) AS wlength, a.width AS width + FROM covered_lines w, {src_areas} a + WHERE ST_Intersects(w.geom, a.geom) +), lines AS ( + SELECT wid, wlength, ST_Length(inters) * width AS lenwidth FROM intersections + WHERE ST_GeometryType(inters) IN ('ST_LineString', 'ST_MultiLineString') +), glines AS ( + SELECT wid, sum(lenwidth) / wlength AS width FROM lines + GROUP BY wid, wlength +) +UPDATE {src} a SET width = l.width + FROM glines l WHERE l.wid = a.{id_column} AND a.width IS NULL + )"); + timer(m_timer_prep).stop(); + + log_debug("Reading waterway lines from database..."); + get_stats(); + + // This vector will initially contain all segments (connection between + // two points) from waterway ways. They will later be assembled into + // graph edges connecting points where the waterways network branches. + std::vector edges; + edges.reserve(m_num_points - m_num_waterways); + + // This stores the order of each node in our graph, i.e. the number of + // connections this node has. Order 1 are beginning or end of a waterway, + // order 2 is just the continuing waterway, order >= 3 is a branching + // point. + std::map node_order; + + // This is where we keep the names of all waterways indexed by their + // way id. + std::unordered_map names; + + timer(m_timer_get).start(); + { + auto const result = dbexec(R"( +SELECT {id_column}, {width_column}, {name_column}, {geom_column} FROM {src}; +)"); + + for (int i = 0; i < result.num_tuples(); ++i) { + auto const id = std::strtoul(result.get_value(i, 0), nullptr, 10); + auto const width = std::strtod(result.get_value(i, 1), nullptr); + auto const name = result.get(i, 2); + if (!name.empty()) { + names.emplace(id, name); + } + auto const geom = ewkb_to_geom(decode_hex(result.get_value(i, 3))); + + if (geom.is_linestring()) { + auto const &ls = geom.get(); + geom::for_each_segment(ls, + [&](geom::point_t a, geom::point_t b) { + if (a != b) { + auto &f = edges.emplace_back(); + f.points.push_back(a); + f.points.push_back(b); + f.id = id; + f.width = width; + node_order[a]++; + node_order[b]++; + } + }); + } + } + } + timer(m_timer_get).stop(); + log_debug("Read {} segments, {} unique points, and {} names.", edges.size(), + node_order.size(), names.size()); + + if (edges.size() < 2) { + log_info("Found fewer than two segments. Nothing to do."); + return; + } + + log_debug("Sorting segments..."); + timer(m_timer_sort).start(); + std::sort(edges.begin(), edges.end()); + timer(m_timer_sort).stop(); + + log_debug("Assembling edges from segments..."); + timer(m_timer_net).start(); + for (auto &edge : edges) { + if (edge.points.size() > 1) { + assemble_edge(&edge, &edges, node_order); + } + } + timer(m_timer_net).stop(); + + log_debug("Removing now empty edges..."); + timer(m_timer_remove).start(); + { + auto const last = + std::remove_if(edges.begin(), edges.end(), [](edge_t const &edge) { + return edge.points.size() == 1; + }); + edges.erase(last, edges.end()); + std::sort(edges.begin(), edges.end()); + } + timer(m_timer_remove).stop(); + + log_debug("Network has {} edges.", edges.size()); + + log_debug("Propagating 'width' property downstream..."); + timer(m_timer_width).start(); + for (auto &edge : edges) { + assert(!edge.points.empty()); + geom::linestring_t seen; + follow_chain_and_set_width(edge, &edges, node_order, &seen); + } + timer(m_timer_width).stop(); + + if (m_delete_existing) { + dbexec("TRUNCATE {dest}"); + } + + log_debug("Writing results to destination table..."); + dbexec( + "PREPARE ins (int8, real, text, geometry) AS" + " INSERT INTO {dest} ({id_column}, width, name, geom)" + " VALUES ($1, $2, $3, $4)"); + + timer(m_timer_write).start(); + connection().exec("BEGIN"); + for (auto &edge : edges) { + geom::geometry_t geom{std::move(edge.points), 3857}; + auto const wkb = geom_to_ewkb(geom); + connection().exec_prepared("ins", edge.id, edge.width, + get_name(names, edge.id), binary_param(wkb)); + } + connection().exec("COMMIT"); + timer(m_timer_write).stop(); + + dbexec("ANALYZE {dest}"); + + log_debug("Done."); +} diff --git a/src/gen-rivers.hpp b/src/gen-rivers.hpp new file mode 100644 index 000000000..a500478a6 --- /dev/null +++ b/src/gen-rivers.hpp @@ -0,0 +1,43 @@ +#ifndef OSM2PGSQL_GEN_RIVERS_HPP +#define OSM2PGSQL_GEN_RIVERS_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-base.hpp" + +#include + +class gen_rivers_t : public gen_base_t +{ +public: + gen_rivers_t(pg_conn_t *connection, gen_params_t *params); + + void process() override; + + std::string_view name() const noexcept override { return "rivers"; } + +private: + void get_stats(); + + std::size_t m_timer_area; + std::size_t m_timer_prep; + std::size_t m_timer_get; + std::size_t m_timer_sort; + std::size_t m_timer_net; + std::size_t m_timer_remove; + std::size_t m_timer_width; + std::size_t m_timer_write; + + std::size_t m_num_waterways = 0; + std::size_t m_num_points = 0; + bool m_delete_existing; +}; + +#endif // OSM2PGSQL_GEN_RIVERS_HPP diff --git a/src/gen-tile-builtup.cpp b/src/gen-tile-builtup.cpp new file mode 100644 index 000000000..e4dc93b5c --- /dev/null +++ b/src/gen-tile-builtup.cpp @@ -0,0 +1,265 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile-builtup.hpp" + +#include "canvas.hpp" +#include "geom-functions.hpp" +#include "logging.hpp" +#include "pgsql.hpp" +#include "raster.hpp" +#include "tile.hpp" +#include "tracer.hpp" +#include "wkb.hpp" + +#include + +static std::size_t round_up(std::size_t value, std::size_t multiple) noexcept +{ + return ((value + multiple - 1U) / multiple) * multiple; +} + +gen_tile_builtup_t::gen_tile_builtup_t(pg_conn_t *connection, + gen_params_t *params) +: gen_tile_t(connection, params), m_timer_draw(add_timer("draw")), + m_timer_simplify(add_timer("simplify")), + m_timer_vectorize(add_timer("vectorize")), m_timer_write(add_timer("write")) +{ + m_schema = get_identifier(*params, "schema"); + m_source_tables = + osmium::split_string(get_string(*params, "src_tables"), ','); + + m_margin = get_double(*params, "margin"); + m_image_extent = get_uint(*params, "image_extent", 2048); + m_image_buffer = get_uint(*params, "image_buffer", 0); + + auto const buffer_sizes = + osmium::split_string(get_string(*params, "buffer_size"), ','); + for (auto const &bs : buffer_sizes) { + m_buffer_sizes.push_back(std::strtoul(bs.c_str(), nullptr, 10)); + } + + m_turdsize = static_cast(get_uint(*params, "turdsize", m_turdsize)); + m_min_area = get_double(*params, "min_area", 0.0); + + if (params->count("img_path")) { + m_image_path = get_string(*params, "img_path"); + } + + if (params->count("img_table")) { + m_image_table = get_string(*params, "img_table"); + + for (auto const &table : m_source_tables) { + for (char const variant : {'i', 'o'}) { + auto const table_name = + fmt::format("{}_{}_{}", m_image_table, table, variant); + connection->exec(R"( +CREATE TABLE IF NOT EXISTS "{}" ( + id SERIAL PRIMARY KEY NOT NULL, + zoom INT4, + x INT4, + y INT4, + rast RASTER +) +)", + table_name); + raster_table_preprocess(table_name); + } + } + } + + if (params->count("make_valid")) { + params->emplace( + "geom_sql", + "(ST_Dump(ST_CollectionExtract(ST_MakeValid($1), 3))).geom"); + } else { + params->emplace("geom_sql", "$1"); + } + + if (m_image_extent < 1024U) { + throw std::runtime_error{"width must be at least 1024"}; + } + + if ((m_image_extent & (m_image_extent - 1)) != 0) { + throw std::runtime_error{"width must be power of 2"}; + } + + m_image_buffer = + round_up(static_cast(m_margin * + static_cast(m_image_extent)), + 64U); + m_margin = static_cast(m_image_buffer) / + static_cast(m_image_extent); + + log_info("Image extent: {}px, buffer: {}px, margin: {}", m_image_extent, + m_image_buffer, m_margin); +} + +void gen_tile_builtup_t::init() +{ + init_common(params()); + + int n = 0; + for (auto const &src_table : m_source_tables) { + tmp_param tp{¶ms()}; + tp.add("N", std::to_string(n++)); + tp.add("SRC", qualified_name(m_schema, src_table)); + + dbexec(R"( +PREPARE get_geoms_{N} (real, real, real, real) AS + SELECT "{geom_column}", '' AS param + FROM {SRC} + WHERE "{geom_column}" && ST_MakeEnvelope($1, $2, $3, $4, 3857) +)"); + } + + dbexec(R"( +PREPARE insert_geoms (geometry, int, int) AS + INSERT INTO {dest} ("{geom_column}", x, y) VALUES ({geom_sql}, $2, $3) +)"); +} + +static void save_image_to_table(pg_conn_t *connection, canvas_t const &canvas, + tile_t const &tile, double margin, + std::string const &table, char const *variant, + std::string const &table_prefix) +{ + auto const wkb = to_hex(canvas.to_wkb(tile, margin)); + + connection->exec("INSERT INTO \"{}_{}_{}\" (zoom, x, y, rast)" + " VALUES ({}, {}, {}, '{}')", + table_prefix, table, variant, tile.zoom(), tile.x(), + tile.y(), wkb); +} + +namespace { + +struct param_canvas_t +{ + canvas_t canvas; + std::string table; +}; + +} // anonymous namespace + +using canvas_list_t = std::vector; + +static void draw_from_db(double margin, canvas_list_t *canvas_list, + pg_conn_t *conn, tile_t const &tile) +{ + int prep = 0; + for (auto &cc : *canvas_list) { + log_debug("Draw from {}", cc.table); + std::string statement = "get_geoms_" + fmt::to_string(prep++); + auto const result = conn->exec_prepared( + statement.c_str(), tile.xmin(margin), tile.ymin(margin), + tile.xmax(margin), tile.ymax(margin)); + + for (int n = 0; n < result.num_tuples(); ++n) { + auto const geom = ewkb_to_geom(decode_hex(result.get_value(n, 0))); + cc.canvas.draw(geom, tile); + } + } +} + +void gen_tile_builtup_t::process(tile_t const &tile) +{ + log_debug("Handle tile {}/{}/{}...", tile.zoom(), tile.x(), tile.y()); + delete_existing(tile); + + canvas_list_t canvas_list; + for (auto const &table : m_source_tables) { + canvas_list.push_back( + {canvas_t{m_image_extent, m_image_buffer}, table}); + } + + if (canvas_list.empty()) { + throw std::runtime_error{"No source tables?!"}; + } + + log_debug("Read from database and draw polygons..."); + timer(m_timer_draw).start(); + draw_from_db(m_margin, &canvas_list, &connection(), tile); + timer(m_timer_draw).stop(); + + std::size_t n = 0; + for (auto &[canvas, table] : canvas_list) { + log_debug("Handling table='{}'", table); + + if (!m_image_path.empty()) { + // Save input images for debugging + save_image_to_file(canvas, tile, m_image_path, table, "i", + m_image_extent, m_margin); + } + + if (!m_image_table.empty()) { + // Store input images in database for debugging + save_image_to_table(&connection(), canvas, tile, m_margin, table, + "i", m_image_table); + } + + if (m_buffer_sizes[n] > 0) { + log_debug("Generalize (buffer={} Mercator units)...", + m_buffer_sizes[n] * tile.extent() / + static_cast(m_image_extent)); + timer(m_timer_simplify).start(); + canvas.open_close(m_buffer_sizes[n]); + timer(m_timer_simplify).stop(); + } + + if (!m_image_path.empty()) { + // Save output image for debugging + save_image_to_file(canvas, tile, m_image_path, table, "o", + m_image_extent, m_margin); + } + + if (!m_image_table.empty()) { + // Store output image in database for debugging + save_image_to_table(&connection(), canvas, tile, m_margin, table, + "o", m_image_table); + } + + ++n; + } + + log_debug("Merge bitmaps..."); + for (std::size_t n = 1; n < canvas_list.size(); ++n) { + canvas_list[0].canvas.merge(canvas_list[n].canvas); + } + + tracer_t tracer{m_image_extent, m_image_buffer, m_turdsize}; + + log_debug("Vectorize..."); + timer(m_timer_vectorize).start(); + auto const geometries = + tracer.trace(canvas_list[0].canvas, tile, m_min_area); + timer(m_timer_vectorize).stop(); + + log_debug("Write geometries to destination table..."); + timer(m_timer_write).start(); + for (auto const &geom : geometries) { + auto const wkb = to_hex(geom_to_ewkb(geom)); + connection().exec_prepared("insert_geoms", wkb, tile.x(), tile.y()); + } + timer(m_timer_write).stop(); + log_debug("Inserted {} generalized polygons", geometries.size()); +} + +void gen_tile_builtup_t::post() +{ + if (!m_image_table.empty()) { + for (auto const &table : m_source_tables) { + for (char const variant : {'i', 'o'}) { + raster_table_postprocess( + fmt::format("{}_{}_{}", m_image_table, table, variant)); + } + } + } + dbexec("ANALYZE {dest}"); +} diff --git a/src/gen-tile-builtup.hpp b/src/gen-tile-builtup.hpp new file mode 100644 index 000000000..08447164b --- /dev/null +++ b/src/gen-tile-builtup.hpp @@ -0,0 +1,51 @@ +#ifndef OSM2PGSQL_GEN_TILE_BUILTUP_HPP +#define OSM2PGSQL_GEN_TILE_BUILTUP_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile.hpp" + +#include +#include +#include + +class gen_tile_builtup_t final : public gen_tile_t +{ +public: + gen_tile_builtup_t(pg_conn_t *connection, gen_params_t *params); + + ~gen_tile_builtup_t() override = default; + + void init() override; + void process(tile_t const &tile) override; + void post() override; + + std::string_view name() const noexcept override { return "builtup"; } + +private: + std::size_t m_timer_draw; + std::size_t m_timer_simplify; + std::size_t m_timer_vectorize; + std::size_t m_timer_write; + + std::vector m_source_tables; + std::string m_image_path; + std::string m_schema; + std::string m_dest_table; + std::string m_image_table; + double m_margin; + std::size_t m_image_extent = 2048; + std::size_t m_image_buffer = 0; + std::vector m_buffer_sizes; + int m_turdsize = 2; + double m_min_area = 0.0; +}; + +#endif // OSM2PGSQL_GEN_TILE_BUILTUP_HPP diff --git a/src/gen-tile-raster.cpp b/src/gen-tile-raster.cpp new file mode 100644 index 000000000..d32378724 --- /dev/null +++ b/src/gen-tile-raster.cpp @@ -0,0 +1,250 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile-raster.hpp" + +#include "canvas.hpp" +#include "logging.hpp" +#include "pgsql.hpp" +#include "raster.hpp" +#include "tile.hpp" +#include "tracer.hpp" +#include "wkb.hpp" + +static std::size_t round_up(std::size_t value, std::size_t multiple) noexcept +{ + return ((value + multiple - 1U) / multiple) * multiple; +} + +gen_tile_raster_union_t::gen_tile_raster_union_t(pg_conn_t *connection, + gen_params_t *params) +: gen_tile_t(connection, params), m_timer_draw(add_timer("draw")), + m_timer_simplify(add_timer("simplify")), + m_timer_vectorize(add_timer("vectorize")), m_timer_write(add_timer("write")) +{ + m_margin = get_double(*params, "margin"); + m_image_extent = get_uint(*params, "image_extent", 2048); + m_image_buffer = get_uint(*params, "image_buffer", 0); + m_buffer_size = get_uint(*params, "buffer_size", 10); + m_turdsize = static_cast(get_uint(*params, "turdsize", m_turdsize)); + + if (params->count("img_path")) { + m_image_path = get_string(*params, "img_path"); + } + + if (params->count("img_table")) { + m_image_table = get_string(*params, "img_table"); + + for (char const variant : {'i', 'o'}) { + auto const table_name = + fmt::format("{}_{}", m_image_table, variant); + connection->exec(R"( +CREATE TABLE IF NOT EXISTS "{}" ( + type TEXT, + zoom INT4, + x INT4, + y INT4, + rast RASTER +) +)", + table_name); + raster_table_preprocess(table_name); + } + } + + if (params->count("make_valid")) { + params->emplace( + "geom_sql", + "(ST_Dump(ST_CollectionExtract(ST_MakeValid($1), 3))).geom"); + } else { + params->emplace("geom_sql", "$1"); + } + + if (m_image_extent < 1024U) { + throw std::runtime_error{"width must be at least 1024"}; + } + + if ((m_image_extent & (m_image_extent - 1)) != 0) { + throw std::runtime_error{"width must be power of 2"}; + } + + m_image_buffer = + round_up(static_cast(m_margin * + static_cast(m_image_extent)), + 64U); + m_margin = static_cast(m_image_buffer) / + static_cast(m_image_extent); + + log_info("Image extent: {}px, buffer: {}px, margin: {}", m_image_extent, + m_image_buffer, m_margin); +} + +void gen_tile_raster_union_t::init() +{ + init_common(params()); + + if (with_group_by()) { + dbexec(R"( +PREPARE get_geoms (real, real, real, real) AS + SELECT "{geom_column}", "{group_by_column}" + FROM {src} + WHERE "{geom_column}" && ST_MakeEnvelope($1, $2, $3, $4, 3857) + ORDER BY "{group_by_column}" +)"); + dbexec(R"( +PREPARE insert_geoms (geometry, int, int, text) AS + INSERT INTO {dest} ("{geom_column}", x, y, "{group_by_column}") + VALUES ({geom_sql}, $2, $3, $4) +)"); + } else { + dbexec(R"( +PREPARE get_geoms (real, real, real, real) AS + SELECT "{geom_column}", NULL AS param + FROM {src} + WHERE "{geom_column}" && ST_MakeEnvelope($1, $2, $3, $4, 3857) +)"); + dbexec(R"( +PREPARE insert_geoms (geometry, int, int, text) AS + INSERT INTO {dest} ("{geom_column}", x, y) VALUES ({geom_sql}, $2, $3) +)"); + } +} + +static void save_image_to_table(pg_conn_t *connection, canvas_t const &canvas, + tile_t const &tile, double margin, + std::string const ¶m, char const *variant, + std::string const &table_prefix) +{ + auto const wkb = to_hex(canvas.to_wkb(tile, margin)); + + connection->exec("INSERT INTO \"{}_{}\" (type, zoom, x, y, rast)" + " VALUES ('{}', {}, {}, {}, '{}')", + table_prefix, variant, param, tile.zoom(), tile.x(), + tile.y(), wkb); +} + +namespace { + +struct param_canvas_t +{ + canvas_t canvas; + std::string param; + std::size_t points; +}; + +} // anonymous namespace + +using canvas_list_t = std::vector; + +static void draw_from_db(double margin, unsigned int image_extent, + unsigned int image_buffer, canvas_list_t *canvas_list, + pg_conn_t *conn, tile_t const &tile) +{ + auto const result = + conn->exec_prepared("get_geoms", tile.xmin(margin), tile.ymin(margin), + tile.xmax(margin), tile.ymax(margin)); + + param_canvas_t *current = nullptr; + for (int n = 0; n < result.num_tuples(); ++n) { + char const *new_param = result.get_value(n, 1); + if (!current || (new_param && (new_param != current->param))) { + param_canvas_t pc{canvas_t{image_extent, image_buffer}, new_param, + 0}; + canvas_list->push_back(std::move(pc)); + current = &canvas_list->back(); + } + + auto const geom = ewkb_to_geom(decode_hex(result.get_value(n, 0))); + current->points += current->canvas.draw(geom, tile); + } +} + +void gen_tile_raster_union_t::process(tile_t const &tile) +{ + log_debug("Handle tile {}/{}/{}...", tile.zoom(), tile.x(), tile.y()); + delete_existing(tile); + + canvas_list_t canvas_list; + + log_debug("Read from database and draw polygons..."); + timer(m_timer_draw).start(); + draw_from_db(m_margin, m_image_extent, m_image_buffer, &canvas_list, + &connection(), tile); + timer(m_timer_draw).stop(); + + for (auto &[canvas, param, points] : canvas_list) { + log_debug("Handling param='{}'", param); + + if (!m_image_path.empty()) { + // Save input image for debugging + save_image_to_file(canvas, tile, m_image_path, param, "i", + m_image_extent, m_margin); + } + + if (!m_image_table.empty()) { + // Store input image in database for debugging + save_image_to_table(&connection(), canvas, tile, m_margin, param, + "i", m_image_table); + } + + if (m_buffer_size > 0) { + log_debug("Generalize (buffer={} Mercator units)...", + m_buffer_size * tile.extent() / + static_cast(m_image_extent)); + timer(m_timer_simplify).start(); + canvas.open_close(m_buffer_size); + timer(m_timer_simplify).stop(); + } + + if (!m_image_path.empty()) { + // Save output image for debugging + save_image_to_file(canvas, tile, m_image_path, param, "o", + m_image_extent, m_margin); + } + + if (!m_image_table.empty()) { + // Store output image in database for debugging + save_image_to_table(&connection(), canvas, tile, m_margin, param, + "o", m_image_table); + } + + tracer_t tracer{m_image_extent, m_image_buffer, m_turdsize}; + + log_debug("Vectorize..."); + timer(m_timer_vectorize).start(); + auto const geometries = tracer.trace(canvas, tile); + timer(m_timer_vectorize).stop(); + + log_debug("Reduced from {} points to {} points ({:.1f} %)", points, + tracer.num_points(), + static_cast(tracer.num_points()) / + static_cast(points) * 100); + + log_debug("Write geometries to destination table..."); + timer(m_timer_write).start(); + for (auto const &geom : geometries) { + auto const wkb = geom_to_ewkb(geom); + connection().exec_prepared("insert_geoms", binary_param{wkb}, + tile.x(), tile.y(), param); + } + timer(m_timer_write).stop(); + log_debug("Inserted {} generalized polygons", geometries.size()); + } +} + +void gen_tile_raster_union_t::post() +{ + if (!m_image_table.empty()) { + for (char const variant : {'i', 'o'}) { + raster_table_postprocess( + fmt::format("{}_{}", m_image_table, variant)); + } + } + dbexec("ANALYZE {dest}"); +} diff --git a/src/gen-tile-raster.hpp b/src/gen-tile-raster.hpp new file mode 100644 index 000000000..b663cdcb0 --- /dev/null +++ b/src/gen-tile-raster.hpp @@ -0,0 +1,46 @@ +#ifndef OSM2PGSQL_GEN_TILE_RASTER_HPP +#define OSM2PGSQL_GEN_TILE_RASTER_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile.hpp" + +#include +#include + +class gen_tile_raster_union_t final : public gen_tile_t +{ +public: + gen_tile_raster_union_t(pg_conn_t *connection, gen_params_t *params); + + ~gen_tile_raster_union_t() override = default; + + void init() override; + void process(tile_t const &tile) override; + void post() override; + + std::string_view name() const noexcept override { return "raster-union"; } + +private: + std::size_t m_timer_draw; + std::size_t m_timer_simplify; + std::size_t m_timer_vectorize; + std::size_t m_timer_write; + + std::string m_image_path; + std::string m_image_table; + double m_margin; + std::size_t m_image_extent = 2048; + std::size_t m_image_buffer = 0; + unsigned int m_buffer_size = 10; + int m_turdsize = 2; +}; + +#endif // OSM2PGSQL_GEN_TILE_RASTER_HPP diff --git a/src/gen-tile-vector.cpp b/src/gen-tile-vector.cpp new file mode 100644 index 000000000..dc847e67f --- /dev/null +++ b/src/gen-tile-vector.cpp @@ -0,0 +1,100 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile-vector.hpp" + +#include "logging.hpp" +#include "pgsql.hpp" +#include "tile.hpp" + +gen_tile_vector_union_t::gen_tile_vector_union_t(pg_conn_t *connection, + gen_params_t *params) +: gen_tile_t(connection, params), m_timer_simplify(add_timer("simplify")) +{ + if (params->count("margin") == 0) { + params->emplace("margin", "0.0"); + } else { + // We don't need the result, just checking that this is a real number + get_double(*params, "margin"); + } + + if (params->count("buffer_size") == 0) { + params->emplace("buffer_size", "10"); + } else { + // We don't need the result, just checking that this is an integer + get_uint(*params, "buffer_size"); + } +} + +void gen_tile_vector_union_t::init() +{ + init_common(params()); + + if (with_group_by()) { + dbexec(R"( +PREPARE gen_geoms (int, int, int) AS + WITH gen_tile_input AS ( + SELECT "{group_by_column}" AS col, "{geom_column}" AS geom FROM {src} + WHERE "{geom_column}" && ST_TileEnvelope($1, $2, $3, margin => {margin}) + ), + buffered AS ( + SELECT col, ST_Buffer(geom, {buffer_size}) AS geom + FROM gen_tile_input + ), + merged AS ( + SELECT col, ST_Union(geom) AS geom + FROM buffered GROUP BY col + ), + unbuffered AS ( + SELECT col, ST_Buffer(ST_Buffer(geom, -2 * {buffer_size}), {buffer_size}) AS geom + FROM merged + ) + INSERT INTO {dest} (x, y, "{group_by_column}", "{geom_column}") + SELECT $2, $3, col, (ST_Dump(geom)).geom FROM unbuffered +)"); + } else { + dbexec(R"( +PREPARE gen_geoms (int, int, int) AS + WITH gen_tile_input AS ( + SELECT "{geom_column}" AS geom FROM {src} + WHERE "{geom_column}" && ST_TileEnvelope($1, $2, $3, margin => {margin}) + ), + buffered AS ( + SELECT ST_Buffer(geom, {buffer_size}) AS geom + FROM gen_tile_input + ), + merged AS ( + SELECT ST_Union(geom) AS geom + FROM buffered + ), + unbuffered AS ( + SELECT ST_Buffer(ST_Buffer(geom, -2 * {buffer_size}), {buffer_size}) AS geom + FROM merged + ) + INSERT INTO {dest} (x, y, "{geom_column}") + SELECT $2, $3, (ST_Dump(geom)).geom FROM unbuffered +)"); + } +} + +void gen_tile_vector_union_t::process(tile_t const &tile) +{ + log_debug("Handle tile {}/{}/{}...", tile.zoom(), tile.x(), tile.y()); + + delete_existing(tile); + + log_debug("Generalize..."); + timer(m_timer_simplify).start(); + auto const result = connection().exec_prepared("gen_geoms", tile.zoom(), + tile.x(), tile.y()); + timer(m_timer_simplify).stop(); + log_debug("Inserted {} generalized polygons", result.affected_rows()); +} + +void gen_tile_vector_union_t::post() { dbexec("ANALYZE {dest}"); } diff --git a/src/gen-tile-vector.hpp b/src/gen-tile-vector.hpp new file mode 100644 index 000000000..0594d1d38 --- /dev/null +++ b/src/gen-tile-vector.hpp @@ -0,0 +1,36 @@ +#ifndef OSM2PGSQL_GEN_TILE_VECTOR_HPP +#define OSM2PGSQL_GEN_TILE_VECTOR_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile.hpp" + +#include + +class gen_tile_vector_union_t final : public gen_tile_t +{ +public: + gen_tile_vector_union_t(pg_conn_t *connection, gen_params_t *params); + + ~gen_tile_vector_union_t() override = default; + + void init() override; + + void process(tile_t const &tile) override; + + void post() override; + + std::string_view name() const noexcept override { return "vector-union"; } + +private: + std::size_t m_timer_simplify; +}; + +#endif // OSM2PGSQL_GEN_TILE_VECTOR_HPP diff --git a/src/gen-tile.cpp b/src/gen-tile.cpp new file mode 100644 index 000000000..08de6c766 --- /dev/null +++ b/src/gen-tile.cpp @@ -0,0 +1,49 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "gen-tile.hpp" + +#include "format.hpp" +#include "gen-tile-builtup.hpp" +#include "gen-tile-raster.hpp" +#include "gen-tile-vector.hpp" +#include "logging.hpp" +#include "pgsql.hpp" +#include "tile.hpp" + +#include + +gen_tile_t::gen_tile_t(pg_conn_t *connection, gen_params_t *params) +: gen_base_t(connection, params), m_timer_delete(add_timer("delete")) +{} + +void gen_tile_t::init_common(gen_params_t const ¶ms) +{ + m_with_group_by = !get_identifier(params, "group_by_column").empty(); + + if (params.count("delete_existing")) { + m_delete_existing = true; + dbexec("PREPARE del_geoms (int, int) AS" + " DELETE FROM {dest} WHERE x=$1 AND y=$2"); + } +} + +void gen_tile_t::delete_existing(tile_t const &tile) +{ + if (!m_delete_existing) { + return; + } + + log_debug("Delete geometries from destination table..."); + timer(m_timer_delete).start(); + auto const result = + connection().exec_prepared("del_geoms", tile.x(), tile.y()); + timer(m_timer_delete).stop(); + log_debug("Deleted {} rows.", result.affected_rows()); +} diff --git a/src/gen-tile.hpp b/src/gen-tile.hpp new file mode 100644 index 000000000..27d4f0f61 --- /dev/null +++ b/src/gen-tile.hpp @@ -0,0 +1,48 @@ +#ifndef OSM2PGSQL_GEN_TILE_HPP +#define OSM2PGSQL_GEN_TILE_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "format.hpp" +#include "gen-base.hpp" +#include "util.hpp" + +#include + +#include + +class pg_conn_t; +class tile_t; + +/** + * Base class for generalizations based on tiles. Use the create() function + * to instantiate a subclass. + */ +class gen_tile_t : public gen_base_t +{ +public: + bool on_tiles() const noexcept override { return true; } + +protected: + gen_tile_t(pg_conn_t *connection, gen_params_t *params); + + void init_common(gen_params_t const ¶ms); + + void delete_existing(tile_t const &tile); + + bool with_group_by() const noexcept { return m_with_group_by; } + +private: + std::size_t m_timer_delete; + bool m_delete_existing = false; + bool m_with_group_by = false; +}; + +#endif // OSM2PGSQL_GEN_TILE_HPP diff --git a/src/geom.hpp b/src/geom.hpp index 8d4fb5928..dde5f5ac8 100644 --- a/src/geom.hpp +++ b/src/geom.hpp @@ -84,6 +84,14 @@ class point_t return !(a == b); } + [[nodiscard]] constexpr friend bool operator<(point_t a, point_t b) noexcept + { + if (a.x() == b.x()) { + return a.y() < b.y(); + } + return a.x() < b.x(); + } + private: double m_x = 0.0; double m_y = 0.0; @@ -153,6 +161,8 @@ class polygon_t [[nodiscard]] ring_t &outer() noexcept { return m_outer; } + void set_outer_ring(ring_t &&ring) noexcept { m_outer = std::move(ring); } + [[nodiscard]] std::vector const &inners() const noexcept { return m_inners; diff --git a/src/osm2pgsql-gen.cpp b/src/osm2pgsql-gen.cpp new file mode 100644 index 000000000..d6481ee69 --- /dev/null +++ b/src/osm2pgsql-gen.cpp @@ -0,0 +1,385 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +/** + * \file + * + * This program is used for testing the tile generalization code. Eventually + * the functionality should be integrated into osm2pgsql. + */ + +#include "canvas.hpp" +#include "format.hpp" +#include "gen-base.hpp" +#include "logging.hpp" +#include "pgsql.hpp" +#include "tile.hpp" +#include "util.hpp" +#include "version.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void show_help() +{ + fmt::print(R"(osm2pgsql-gen [OPTIONS] +Generalizes OSM data. + +Use PG* environment variables to specify database connection. +Without x,y,X,Y the extent will be read from the database table. + +Options: + -h, --help Print this help text and stop + -b, --buffer=SIZE Buffer size + -D, --no-delete Do not DELETE existing features + -e, --expire-list=TABLE Read tiles to render from database table + -g, --geom-column=COLUMN Geometry column (default: 'geom') + -G, --group-by-column=COLUMN Column name to group table contents on + -l, --log-level=LEVEL Log level (debug, info (default), warn, error) + -m, --margin=MARGIN Margin as a fraction of the tile size + -p, --param=KEY=VALUE Add parameter for strategy (can be used multiple times) + -s, --strategy=STRATEGY Simplification strategy + -t, --table=TABLE Table that should be simplified + -T, --dest-table=TABLE Destination table (must have geom, x, y) + -w, --width=SIZE Size (width/height) of the rendered image (default: 2048) + -x Minimum tile X + -y Minimum tile Y + -X Maximum tile X + -Y Maximum tile Y + -z, --zoom=ZOOM Zoom level + --img-path=PATH Dump rendered images to PATH + --img-table=TABLE Dump rendered images to TABLE + --log-sql Log SQL commands + --cimg-info Call info() function of CImg library +)"); +} + +static char const *const short_options = "hb:De:g:G:l:m:p:s:t:T:w:x:y:X:Y:z:"; + +static std::array const long_options = { + {{"help", no_argument, nullptr, 'h'}, + {"buffer", required_argument, nullptr, 'b'}, + {"no-delete", no_argument, nullptr, 'D'}, + {"expire-list", required_argument, nullptr, 'e'}, + {"geom-column", required_argument, nullptr, 'g'}, + {"group-by-column", required_argument, nullptr, 'G'}, + {"log-level", required_argument, nullptr, 'l'}, + {"margin", required_argument, nullptr, 'm'}, + {"param", required_argument, nullptr, 'p'}, + {"strategy", required_argument, nullptr, 's'}, + {"table", required_argument, nullptr, 't'}, + {"dest-table", required_argument, nullptr, 'T'}, + {"width", required_argument, nullptr, 'w'}, + {"zoom", required_argument, nullptr, 'z'}, + {"cimg-info", no_argument, nullptr, 200}, + {"img-path", required_argument, nullptr, 201}, + {"img-table", required_argument, nullptr, 202}, + {"log-sql", no_argument, nullptr, 203}, + {nullptr, 0, nullptr, 0}}}; + +static std::array get_extent_from_db(pg_conn_t *connection, + gen_params_t const ¶ms, + uint32_t zoom) +{ + auto const it = params.find("schema"); + std::string const schema = (it == params.end()) ? "public" : it->second; + + auto const table = params.at("src_table"); + auto const geom_column = params.at("geom_column"); + + auto const result = + connection->exec("SELECT ST_XMin(e), ST_YMin(e), ST_XMax(e), ST_YMax(e)" + " FROM ST_EstimatedExtent('{}', '{}', '{}') AS e", + schema, table, geom_column); + + char *end = nullptr; + double const extent_xmin = strtod(result.get_value(0, 0), &end); + double const extent_ymin = strtod(result.get_value(0, 1), &end); + double const extent_xmax = strtod(result.get_value(0, 2), &end); + double const extent_ymax = strtod(result.get_value(0, 3), &end); + log_info("Extent: ({} {}, {} {})", extent_xmin, extent_ymin, extent_xmax, + extent_ymax); + + return {osmium::geom::mercx_to_tilex(zoom, extent_xmin), + osmium::geom::mercy_to_tiley(zoom, extent_ymax), + osmium::geom::mercx_to_tilex(zoom, extent_xmax), + osmium::geom::mercy_to_tiley(zoom, extent_ymin)}; +} + +static std::vector get_tiles_from_table(pg_conn_t *connection, + std::string const &table, + uint32_t zoom) +{ + std::vector tiles; + + auto const result = connection->exec(R"(SELECT x, y FROM "{}")", table); + + for (int n = 0; n < result.num_tuples(); ++n) { + char *end = nullptr; + auto const x = std::strtoul(result.get_value(n, 0), &end, 10); + auto const y = std::strtoul(result.get_value(n, 1), &end, 10); + tiles.emplace_back(zoom, x, y); + } + + return tiles; +} + +static uint32_t parse_uint32(char const *str, char const *context) +{ + char *end = nullptr; + uint32_t const result = std::strtoul(str, &end, 10); + if (*end != '\0') { + throw fmt_error("Invalid value '{}' for {}.", str, context); + } + return result; +} + +static void add_param(gen_params_t *params, char const *arg) +{ + auto const *p = std::strchr(arg, '='); + if (p) { + std::string key(arg, p - arg); + params->insert_or_assign(std::move(key), p + 1); + } else { + params->insert_or_assign(arg, ""); + } +} + +static void show_params(gen_params_t const ¶ms) +{ + for (auto const &[key, value] : params) { + if (value.empty()) { + log_debug(" {}", key); + } else { + log_debug(" {}={}", key, value); + } + } +} + +int main(int argc, char *argv[]) +{ + constexpr const uint32_t none = std::numeric_limits::max(); + + try { + gen_params_t params{{"delete_existing", ""}}; + std::string log_level; + std::string strategy; + uint32_t zoom = 10; + uint32_t x = none; + uint32_t y = none; + uint32_t xmax = none; + uint32_t ymax = none; + + int c = 0; + optind = 0; + while (-1 != (c = getopt_long(argc, argv, short_options, + long_options.data(), nullptr))) { + switch (c) { + case 'h': + show_help(); + return 0; + case 'b': + params["buffer_size"] = optarg; + break; + case 'D': + params.erase("delete_existing"); + break; + case 'e': + params["expire_list"] = optarg; + break; + case 'g': + params["geom_column"] = optarg; + break; + case 'G': + params["group_by_column"] = optarg; + break; + case 'l': + log_level = optarg; + break; + case 'm': + params["margin"] = optarg; + break; + case 'p': + add_param(¶ms, optarg); + break; + case 's': + strategy = optarg; + break; + case 't': + params["src_table"] = optarg; + break; + case 'T': + params["dest_table"] = optarg; + break; + case 'w': + params["image_extent"] = optarg; + break; + case 'x': + x = parse_uint32(optarg, "x"); + break; + case 'y': + y = parse_uint32(optarg, "y"); + break; + case 'X': + xmax = parse_uint32(optarg, "X"); + break; + case 'Y': + ymax = parse_uint32(optarg, "Y"); + break; + case 'z': + zoom = parse_uint32(optarg, "zoom"); + break; + case 200: + canvas_t::info(); + return 0; + case 201: + params["img_path"] = optarg; + break; + case 202: + params["img_table"] = optarg; + break; + case 203: + get_logger().enable_sql(); + break; + default: + log_error("Unknown argument"); + return 2; + } + } + +#if 0 + if (params.count("src_table") == 0) { + log_error("Missing required -t, --table option"); + return 2; + } + + if (params.count("dest_table") == 0) { + log_error("Missing required -T, --dest-table option"); + return 2; + } +#endif + + if (log_level == "debug") { + get_logger().set_level(log_level::debug); + } else if (log_level == "info" || log_level.empty()) { + get_logger().set_level(log_level::info); + } else if (log_level == "warn") { + get_logger().set_level(log_level::warn); + } else if (log_level == "error") { + get_logger().set_level(log_level::error); + } else { + log_error("Unknown log level: {}. " + "Use 'debug', 'info', 'warn', or 'error'.", + log_level); + return 2; + } + + util::timer_t timer_overall; + + log_info("osm2pgsql-gen version {}", get_osm2pgsql_version()); + pg_conn_t conn{""}; + + log_debug("Params (command line):"); + show_params(params); + + check_identifier_with_default(¶ms, "geom_column", "geom"); + check_identifier_with_default(¶ms, "schema", "public"); + + auto generalizer = create_generalizer(strategy, &conn, ¶ms); + + log_debug("Generalizer '{}' initialized.", generalizer->name()); + + log_debug("Params:"); + show_params(params); + + if (generalizer->on_tiles()) { + if (params.count("expire_list")) { + log_info("Get tiles to reprocess from table '{}'", + params["expire_list"]); + x = none; + y = none; + xmax = none; + ymax = none; + } else { + if (x == none && y == none && xmax == none && ymax == none) { + log_info("No extent given, get it from database..."); + auto const extent = get_extent_from_db(&conn, params, zoom); + + x = extent[0]; + y = extent[1]; + xmax = extent[2]; + ymax = extent[3]; + + log_info("Extent tiles: ({} {}, {} {})", x, y, xmax, ymax); + } + } + + if (xmax != none && ymax != none) { + assert(x <= xmax); + assert(y <= ymax); + log_debug("Running generalizer for bounding box x{}-{}, y{}-{}" + " on zoom={}...", + x, xmax, y, ymax, zoom); + for (unsigned i = x; i <= xmax; ++i) { + for (unsigned j = y; j <= ymax; ++j) { + generalizer->process({zoom, i, j}); + } + } + } else if (params.count("expire_list")) { + log_debug( + "Running generalizer for expire list from table '{}'...", + params["expire_list"]); + auto const tiles = + get_tiles_from_table(&conn, params["expire_list"], zoom); + for (auto const &tile : tiles) { + generalizer->process(tile); + } + } else { + log_debug("Running generalizer for tile {}/{}/{}...", zoom, x, + y); + generalizer->process({zoom, x, y}); + } + } else { + generalizer->process(); + } + + log_debug("Running generalizer postprocessing..."); + generalizer->post(); + + log_debug("Processing done."); + + log_info("Timers:"); + for (auto const &timer : generalizer->timers()) { + log_info(fmt::format(" {:10} {:>10L}", timer.name() + ":", + timer.elapsed_ms())); + } + + osmium::MemoryUsage const mem; + log_info("Memory: {}MB current, {}MB peak", mem.current(), mem.peak()); + + log_info("osm2pgsql-gen took {} overall.", + util::human_readable_duration(timer_overall.stop())); + + } catch (std::exception const &e) { + log_error("{}", e.what()); + return 1; + } + + return 0; +} diff --git a/src/pgsql.hpp b/src/pgsql.hpp index 27f4f7a0f..61080f7a6 100644 --- a/src/pgsql.hpp +++ b/src/pgsql.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,14 @@ class pg_result_t return PQgetisnull(m_result.get(), row, col) != 0; } + /// Return the number of INSERTed, UPDATEd, or DELETEed rows. + std::size_t affected_rows() const noexcept + { + char const *s = PQcmdTuples(m_result.get()); + char *ptr = nullptr; + return std::strtoull(s, &ptr, 10); + } + /** * The length of the field at (row, col) in bytes. * @@ -123,6 +132,16 @@ class pg_result_t std::unique_ptr m_result; }; +class binary_param : public std::string_view +{ +public: + using std::string_view::string_view; + + binary_param(std::string const &str) + : std::string_view(str.data(), str.size()) + {} +}; + /** * PostgreSQL connection. * @@ -226,6 +245,8 @@ class pg_conn_t return 0; } else if constexpr (std::is_same_v) { return 0; + } else if constexpr (std::is_same_v) { + return 0; } return 1; } @@ -237,12 +258,18 @@ class pg_conn_t * strings. */ template - static char const *to_str(std::vector *data, T const ¶m) + static char const *to_str(std::vector *data, int *length, + int *bin, T const ¶m) { if constexpr (std::is_same_v) { return param; } else if constexpr (std::is_same_v) { + *length = param.size(); return param.c_str(); + } else if constexpr (std::is_same_v) { + *length = param.size(); + *bin = 1; + return param.data(); } return data->emplace_back(fmt::to_string(param)).c_str(); } @@ -272,16 +299,21 @@ class pg_conn_t std::vector exec_params; exec_params.reserve(total_buffers_needed); + std::array lengths = {0}; + std::array bins = {0}; + // This array holds the pointers to all parameter strings, either // to the original string parameters or to the recently converted // in the exec_params vector. + std::size_t n = 0; + std::size_t m = 0; std::array param_ptrs = { - to_str>(&exec_params, + to_str>(&exec_params, &lengths[n++], &bins[m++], std::forward(params))...}; return exec_prepared_internal(stmt, sizeof...(params), - param_ptrs.data(), nullptr, nullptr, - result_as_binary ? 1 : 0); + param_ptrs.data(), lengths.data(), + bins.data(), result_as_binary ? 1 : 0); } struct pg_conn_deleter_t diff --git a/src/raster.cpp b/src/raster.cpp new file mode 100644 index 000000000..dc0641505 --- /dev/null +++ b/src/raster.cpp @@ -0,0 +1,66 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "raster.hpp" + +#include "canvas.hpp" +#include "format.hpp" +#include "pgsql.hpp" +#include "tile.hpp" + +#include + +template +void append(std::string *str, T value) +{ + str->append(reinterpret_cast(&value), sizeof(T)); +} + +void add_raster_header(std::string *wkb, wkb_raster_header const &data) +{ + append(wkb, data.endianness); + append(wkb, data.version); + append(wkb, data.nBands); + append(wkb, data.scaleX); + append(wkb, data.scaleY); + append(wkb, data.ipX); + append(wkb, data.ipY); + append(wkb, data.skewX); + append(wkb, data.skewY); + append(wkb, data.srid); + append(wkb, data.width); + append(wkb, data.height); +} + +void add_raster_band(std::string *wkb, wkb_raster_band const &data) +{ + append(wkb, data.bits); + append(wkb, data.nodata); +} + +void save_image_to_file(canvas_t const &canvas, tile_t const &tile, + std::string const &path, std::string const ¶m, + char const *variant, unsigned int image_extent, + double margin) +{ + std::string name{fmt::format("{}-{}-{}-{}{}{}.", path, tile.x(), tile.y(), + param, param.empty() ? "" : "-", variant)}; + + // write image file + canvas.save(name + "png"); + + // write world file + auto const pixel_size = tile.extent() / image_extent; + name += "wld"; + auto *file = std::fopen(name.c_str(), "w"); + fmt::print(file, "{0}\n0.0\n0.0\n-{0}\n{1}\n{2}\n", pixel_size, + tile.xmin(margin) + pixel_size / 2, + tile.ymax(margin) - pixel_size / 2); + std::fclose(file); +} diff --git a/src/raster.hpp b/src/raster.hpp new file mode 100644 index 000000000..82a86ea0b --- /dev/null +++ b/src/raster.hpp @@ -0,0 +1,64 @@ +#ifndef OSM2PGSQL_RASTER_HPP +#define OSM2PGSQL_RASTER_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include +#include + +class canvas_t; +class pg_conn_t; +class tile_t; + +/** + * \file + * + * Helper functions for creating raster images in PostgreSQL/PostGIS. + * https://trac.osgeo.org/postgis/wiki/WKTRaster/RFC/RFC2_V0WKBFormat + */ + +struct wkb_raster_header +{ + uint8_t endianness = +#if __BYTE_ORDER == __LITTLE_ENDIAN + 1 // Little Endian +#else + 0 // Big Endian +#endif + ; + uint16_t version = 0; + uint16_t nBands = 0; + double scaleX = 0.0; + double scaleY = 0.0; + double ipX = 0.0; + double ipY = 0.0; + double skewX = 0.0; + double skewY = 0.0; + int32_t srid = 3857; + uint16_t width = 0; + uint16_t height = 0; +}; + +struct wkb_raster_band +{ + uint8_t bits = 0; + uint8_t nodata = 0; +}; + +void add_raster_header(std::string *wkb, wkb_raster_header const &data); + +void add_raster_band(std::string *wkb, wkb_raster_band const &data); + +void save_image_to_file(canvas_t const &canvas, tile_t const &tile, + std::string const &path, std::string const ¶m, + char const *variant, unsigned int image_extent, + double margin); + +#endif // OSM2PGSQL_RASTER_HPP diff --git a/src/tile.hpp b/src/tile.hpp index 29864174e..255e963e0 100644 --- a/src/tile.hpp +++ b/src/tile.hpp @@ -12,6 +12,7 @@ #include "geom.hpp" +#include #include #include @@ -143,6 +144,30 @@ class tile_t return half_earth_circumference - m_y * extent(); } + double xmin(double margin) const noexcept + { + return std::clamp(xmin() - margin * extent(), -half_earth_circumference, + half_earth_circumference); + } + + double xmax(double margin) const noexcept + { + return std::clamp(xmax() + margin * extent(), -half_earth_circumference, + half_earth_circumference); + } + + double ymin(double margin) const noexcept + { + return std::clamp(ymin() - margin * extent(), -half_earth_circumference, + half_earth_circumference); + } + + double ymax(double margin) const noexcept + { + return std::clamp(ymax() + margin * extent(), -half_earth_circumference, + half_earth_circumference); + } + /** * Convert a point from web mercator (EPSG:3857) coordinates to * coordinates in the tile assuming a tile extent of `pixel_extent`. diff --git a/src/tracer.cpp b/src/tracer.cpp new file mode 100644 index 000000000..8e2aea9ad --- /dev/null +++ b/src/tracer.cpp @@ -0,0 +1,108 @@ +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "tracer.hpp" + +#include "geom-boost-adaptor.hpp" + +#include +#include +#include + +geom::point_t tracer_t::make_point(potrace_dpoint_t const &p) const noexcept +{ + return {p.x - static_cast(m_buffer), + static_cast(m_extent + m_buffer) - p.y}; +} + +std::vector +tracer_t::trace(canvas_t const &canvas, tile_t const &tile, double min_area) +{ + prepare(canvas); + + m_state.reset(potrace_trace(m_param.get(), &m_bitmap)); + if (!m_state || m_state->status != POTRACE_STATUS_OK) { + throw std::runtime_error{"potrace failed"}; + } + + return build_geometries(tile, m_state->plist, min_area); +} + +void tracer_t::reset() +{ + m_bits.clear(); + m_state.reset(); + m_num_points = 0; +} + +void tracer_t::prepare(canvas_t const &canvas) noexcept +{ + unsigned int size = canvas.size(); + assert(size % bits_per_word == 0); + + m_bits.reserve((size * size) / bits_per_word); + + unsigned char const *d = canvas.begin(); + while (d != canvas.end()) { + potrace_word w = 0x1U & *d++; + for (unsigned int n = 1; n < bits_per_word; ++n) { + w <<= 1U; + assert(d != canvas.end()); + w |= 0x1U & *d++; + } + m_bits.push_back(w); + } + + m_bitmap = {int(size), int(size), int(size / bits_per_word), m_bits.data()}; +} + +std::vector +tracer_t::build_geometries(tile_t const &tile, potrace_path_t const *plist, + double min_area) noexcept +{ + std::vector geometries; + if (!plist) { + return geometries; + } + + for (potrace_path_t const *path = plist; path != nullptr; + path = path->next) { + + geom::ring_t ring; + + auto const n = path->curve.n; + assert(path->curve.tag[n - 1] == POTRACE_CORNER); + ring.push_back(tile.to_world_coords(make_point(path->curve.c[n - 1][2]), + m_extent)); + for (int i = 0; i < n; ++i) { + assert(path->curve.tag[i] == POTRACE_CORNER); + auto const &c = path->curve.c[i]; + ring.push_back(tile.to_world_coords(make_point(c[1]), m_extent)); + ring.push_back(tile.to_world_coords(make_point(c[2]), m_extent)); + } + + auto const ring_area = + std::abs(static_cast(boost::geometry::area(ring))); + if (ring_area >= min_area) { + m_num_points += ring.size(); + + if (path->sign == '+') { + geometries.emplace_back(geom::polygon_t{}, 3857) + .get() + .set_outer_ring(std::move(ring)); + } else { + assert(!geometries.empty()); + geometries.back().get().add_inner_ring( + std::move(ring)); + } + } + } + + return geometries; +} diff --git a/src/tracer.hpp b/src/tracer.hpp new file mode 100644 index 000000000..6026070e6 --- /dev/null +++ b/src/tracer.hpp @@ -0,0 +1,76 @@ +#ifndef OSM2PGSQL_TRACER_HPP +#define OSM2PGSQL_TRACER_HPP + +/** + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This file is part of osm2pgsql (https://osm2pgsql.org/). + * + * Copyright (C) 2006-2023 by the osm2pgsql developer community. + * For a full list of authors see the git log. + */ + +#include "canvas.hpp" +#include "geom.hpp" +#include "tile.hpp" + +#include + +#include +#include + +class tracer_t +{ +public: + tracer_t(std::size_t extent, std::size_t buffer, int turdsize) + : m_param(potrace_param_default()), m_extent(extent), m_buffer(buffer) + { + m_param->alphamax = 0.0; + m_param->turdsize = turdsize; + } + + std::vector + trace(canvas_t const &canvas, tile_t const &tile, double min_area = 0.0); + + void reset(); + + std::size_t num_points() const noexcept { return m_num_points; } + +private: + static constexpr auto const bits_per_word = sizeof(potrace_word) * 8; + + geom::point_t make_point(potrace_dpoint_t const &p) const noexcept; + + struct potrace_param_deleter + { + void operator()(potrace_param_t *ptr) const noexcept + { + potrace_param_free(ptr); + } + }; + + struct potrace_state_deleter + { + void operator()(potrace_state_t *ptr) const noexcept + { + potrace_state_free(ptr); + } + }; + + void prepare(canvas_t const &canvas) noexcept; + + std::vector build_geometries(tile_t const &tile, + potrace_path_t const *plist, + double min_area) noexcept; + + std::vector m_bits; + potrace_bitmap_t m_bitmap{}; + std::unique_ptr m_param; + std::unique_ptr m_state; + std::size_t m_extent; + std::size_t m_buffer; + std::size_t m_num_points = 0; + +}; // class tracer_t + +#endif // OSM2PGSQL_TRACER_HPP diff --git a/src/util.hpp b/src/util.hpp index 97c8af8d5..e8e3630a6 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -52,7 +52,11 @@ class double_to_buffer class timer_t { public: - timer_t() noexcept : m_start(clock::now()) {} + explicit timer_t(char const *name = "") + : m_name(name), m_start(clock::now()) + {} + + std::string const &name() const noexcept { return m_name; } void start() noexcept { m_start = clock::now(); } @@ -66,6 +70,12 @@ class timer_t std::chrono::microseconds elapsed() const noexcept { return m_duration; } + std::chrono::milliseconds elapsed_ms() const noexcept + { + return std::chrono::duration_cast( + m_duration); + } + /** * Calculate ratio: value divided by elapsed time. * @@ -84,6 +94,8 @@ class timer_t private: using clock = std::chrono::steady_clock; + + std::string m_name; std::chrono::time_point m_start; std::chrono::microseconds m_duration{}; diff --git a/tests/test-pgsql.cpp b/tests/test-pgsql.cpp index bb9657d65..12f26805b 100644 --- a/tests/test-pgsql.cpp +++ b/tests/test-pgsql.cpp @@ -56,18 +56,6 @@ TEST_CASE("exec with invalid SQL should fail") REQUIRE_THROWS(conn.exec("XYZ")); } -TEST_CASE("exec_prepared without parameters should work") -{ - auto conn = db.db().connect(); - conn.exec("PREPARE test AS SELECT 42"); - - auto const result = conn.exec_prepared("test"); - REQUIRE(result.status() == PGRES_TUPLES_OK); - REQUIRE(result.num_fields() == 1); - REQUIRE(result.num_tuples() == 1); - REQUIRE(result.get(0, 0) == "42"); -} - TEST_CASE("exec_prepared with single string parameters should work") { auto conn = db.db().connect();