diff --git a/.travis.yml b/.travis.yml index 7b94109b3..c94cfdd22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ addons: # Here we install only packages that are the same for all builds on ubuntu. apt: packages: ['python3-psycopg2', 'libexpat1-dev', 'libpq-dev', 'libbz2-dev', - 'libproj-dev', 'libluajit-5.1-dev', + 'libproj-dev', 'libluajit-5.1-dev', 'lua-messagepack', 'libboost-dev', 'libboost-system-dev', 'libboost-filesystem-dev'] # env: T="...." // please set an unique test id (T="..") diff --git a/flex-config/README.md b/flex-config/README.md new file mode 100644 index 000000000..0b03d829c --- /dev/null +++ b/flex-config/README.md @@ -0,0 +1,150 @@ +# Flex Backend Configuration + +The "Flex" backend is configured through a Lua file which defines the structure +of the output tables and is used to map OSM data to the data format to be used +in the database. This way you have a lot of control over how the data should +look like in the database. + +## Lua config file + +All configuration is done through the `osm2pgsql` object in Lua. It has the +following fields: + +* `osm2pgsql.version`: The version of osm2pgsql as string. +* `osm2pgsql.srid`: The SRID set on the command line (with `-l`, `-m`, or `-E`). +* `osm2pgsql.mode`: Either `create` or `append` depending on the command line options. +* `osm2pgsql.stage`: Either 1 or 2 (1st/2nd stage processing the data). See below. +* `osm2pgsql.userdata`: To store your user data. See below. + +The following functions are defined: + +* `osm2pgsql.define_node_table(name, columns)`: Define a node table with the + specified name and columns. +* `osm2pgsql.define_way_table(name, columns)`: Define a way table with the + specified name and columns. +* `osm2pgsql.define_relation_table(name, columns)`: Define a relation table + with the specified name and columns. +* `osm2pgsql.define_area_table(name, columns)`: Define an area table + with the specified name and columns. +* `osm2pgsql.define_table(data)`: Define a table. +* `osm2pgsql.mark(type, id)`: Mark the OSM object of the specified type ('w' + or 'r') with the specified id. The OSM object will trigger a call to the + processing function again in the second stage. +* `osm2pgsql.get_bbox()`: Get the bounding box of the current node or way. Only + works inside the `osm2pgsql.process_node()` and `osm2pgsql.process_way()` + functions. + +You are expected to define one or more of the following functions: + +* `osm2pgsql.process_node(data)`: Called for each node. +* `osm2pgsql.process_way(data)`: Called for each way. +* `osm2pgsql.process_relation(data)`: Called for each relation. + +Any fields starting with an underscore (`_`) are reserved for internal use +of osm2pgsql and must not be accessed in any way. + +### Defining a table + +You have to define one or more tables where your data should end up. This +is done with the `osm2pgsql.define_table()` function or one of the slightly +more convenient functions `osm2pgsql.define_(node|way|relation|area_table()`. + +Each table is either a *node table*, *way table*, *relation table*, or *area +table*. This means that the data for that table comes primarily from a node, +way, relation, or area respectively. Osm2pgsql makes sure that the OSM object +id will be stored in the table so that later updates to those OSM objects (or +deletions) will be properly reflected in the tables. Area tables are special, +they can contain data derived from ways or from relations. Way ids will be +stored as is, relation ids will be stored as negative numbers. (You can define +tables that don't have any ids, but those tables will never be updated by +osm2pgsql.) + +If you are using the `osm2pgsql.define_(node|way|relation|area_table()` +convenience functions, osm2pgsql will automatically create an id column named +`(node|way|relation|area)_id`, respectively. If you want more control over +the id column(s), use the `osm2pgsql.define_table()` function. + +Most tables will have a geometry column. (Currently only zero or one geometry +columns are supported.) The types of the geometry column possible depend on +the type of the input data. For node tables you are pretty much restricted +to point geometries, but there is a variety of options for relation tables +for instance. + +Supported geometry types: +* `geometry`: Any kind of geometry. Also used for area tables that should hold + both polygon and multipolygon geometries. +* `point`: Point geometry, usually created from nodes. +* `linestring`: Linestring geometry, usually created from ways. +* `polygon`: Polygon geometry for area tables, created from ways or relations. +* `multipoint`: Currently not used. +* `multilinestring`: Created from (possibly split up) ways. +* `multipolygon`: For area tables, created from ways or relations. + +The only thing you have to do here is to define the geometry type you want and +osm2pgsql will create the right geometry for you from the OSM data and fill it +in. + +In addition to id and geometry columns, each table can have any number of +"normal" columns using any type supported be PostgreSQL. Some types are +specially recognized by osm2pgsql and it adds some support for them. But +you can use any SQL type you want, in which case you have to make sure are +creating the right text format for these columns. + +Available column types: +* `text`: Text string +* `boolean`: Interprets values `"true"`, `"yes"` as `true` and everything else + as `"false"` +* `int2`, `smallint`: 16bit signed integer +* `int4`, `int`, `integer`: 32bit signed integer +* `int8`, `bigint`: 64bit signed integer +* `real`: A real number +* `hstore`: Can be created automatically from a Lua table +* `json` and `jsonb`: Not supported yet +* `direction`: Interprets values `"true"`, `"yes"`, and `"1"` as 1, `"-1"` as + `-1`, and everything else as `0`. Useful for `oneway` tags etc. +* `area`: The area of the (polygon) geometry. + + +## Command line options + +Use the command line option `-O flex` or `--output=flex` to enable the flex +backend and the `-S|--style` option to set the Lua config file. + +The following command line options have a somewhat different meaning when +using the flex backend: + +* `-p|--prefix`: The table names you are setting in your Lua config files + will *not* get this prefix. +* `-S|--style`: Use this to specify the Lua config file. Without it, osm2pgsql + will not work, because it will try to read the default style file which + the flex backend doesn't understand. +* `-G|--multi-geometry` is not used. Set the column type of the output table + to the type you want instead, for instance `polygon` vs. `multipolygon`. + +The following command line options are ignored by `osm2pgsl` when using the +flex backend, because they don't make sense in that context: + +* `-k|--hstore` +* `-j|--hstore-all` +* `-z|--hstore-column` +* `--hstore-match-only` +* `--hstore-add-index` +* `-K|--keep-coastlines` (Coastline tags are not handled specially in the + flex backend.) +* `--tag-transform-script` (Set the Lua config file with the `-S|--style` + option.) + +## Example config files + +This directory contains example config files for the flex backend. All config +files contain comments as documentation. + +If you are learning about the flex backend, read the config files in the +following order (from easiest to understand to the more complex ones): + +1. [simple.lua](simple.lua) +2. [multipolygons.lua](multipolygons.lua) +3. [advanced.lua](advanced.lua) +4. [highway-shields.lua](highway-shields.lua) +5. [unitable.lua](unitable.lua) + diff --git a/flex-config/advanced.lua b/flex-config/advanced.lua new file mode 100644 index 000000000..a2c97ea7a --- /dev/null +++ b/flex-config/advanced.lua @@ -0,0 +1,166 @@ + +-- Read and understand simple.lua and multipolygons.lua first, before you try +-- to understand this file. + +inspect = require('inspect') + +print("osm2pgsql version: " .. osm2pgsql.version) + +-- Are we running in "create" or "append" mode? +print("osm2pgsql mode: " .. osm2pgsql.mode) + +-- Which stage in the data processing is this? +print("osm2pgsql stage: " .. osm2pgsql.stage) + +-- Uncomment the following line to see the userdata (but, careful, it might be +-- a lot of data) +-- print("osm2pgsql userdata: " .. inspect(osm2pgsql.userdata)) + +tables = {} + +tables.pois = osm2pgsql.define_node_table("pois", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'point' }, +}) + +tables.ways = osm2pgsql.define_way_table("ways", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'linestring' }, +}) + +-- Using the define_table function allows some more control over the id columns +-- than the more convenient define_(node|way|relation|area)_table functions. +-- In this case we are setting the name of the id column to "osm_id". +tables.polygons = osm2pgsql.define_table{ + name = "polygons", + ids = { type = 'area', id_column = 'osm_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'geometry' }, + } +} + +-- A table for all route relations +tables.routes = osm2pgsql.define_relation_table("routes", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'multilinestring' }, +}) + +-- A table for all individual members of route relations +-- (Note that this script doesn't handle ways in multiple relations correctly.) +tables.route_members = osm2pgsql.define_table{ + name = "route_members", + ids = { type = 'way', id_column = 'way_id' }, + columns = { + { column = 'rel_id', type = 'int8' }, -- not a specially handled id column + { column = 'tags', type = 'hstore' }, -- tags from member way + { column = 'role', type = 'text' }, -- role in the relation + { column = 'rtags', type = 'hstore' }, -- tags from relation + { column = 'geom', type = 'linestring' }, + } +} + +function is_empty(some_table) + return next(some_table) == nil +end + +function clean_tags(tags) + tags.odbl = nil + tags.created_by = nil + tags.source = nil + tags["source:ref"] = nil + tags["source:name"] = nil +end + +function osm2pgsql.process_node(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + tables.pois:add_row({ + tags = data.tags + }) +end + +function osm2pgsql.process_way(data) +-- print(inspect(data)) + + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + -- osm2pgsql.stage: either 1 or 2 for first or second pass through the data + if osm2pgsql.stage == 2 then + local row = { + rel_id = 0, + tags = data.tags, + role = '', + rtags = {}, + } + member_data = osm2pgsql.userdata.w2r[data.id] + if member_data then + row.rel_id = member_data.rel_id + row.role = member_data.role + row.rtags = osm2pgsql.userdata.route_tags[row.rel_id] + end + -- print(inspect(row)) + tables.route_members:add_row(row) + return + end + + if data.is_closed then + tables.polygons:add_row({ + tags = data.tags + }) + else + tables.ways:add_row({ + tags = data.tags + }) + end +end + +function osm2pgsql.process_relation(data) +-- print(inspect(data)) + + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + if data.tags.type == 'multipolygon' or data.tags.type == 'boundary' then + tables.polygons:add_row({ + tags = data.tags + }) + elseif data.tags.type == 'route' and data.tags.route == 'hiking' then + tables.routes:add_row({ + tags = data.tags + }) + + if not osm2pgsql.userdata.route_tags then + osm2pgsql.userdata.route_tags = {} + end + + if not osm2pgsql.userdata.w2r then + osm2pgsql.userdata.w2r = {} + end + + osm2pgsql.userdata.route_tags[data.id] = data.tags + + -- Go through all the members... + for i, member in ipairs(data.members) do + if member.type == 'w' then + -- Mark the member way as "interesting", the "process_way" + -- callback will be triggered again in the second stage + osm2pgsql.mark('w', member.ref) + -- print("mark way id " .. member.ref) + osm2pgsql.userdata.w2r[member.ref] = { + rel_id = data.id, + role = member.role, + } + end + end + end +end + diff --git a/flex-config/compatible.lua b/flex-config/compatible.lua new file mode 100644 index 000000000..497fdf5e9 --- /dev/null +++ b/flex-config/compatible.lua @@ -0,0 +1,571 @@ + +-- This configuration for the flex backend tries to be compatible with the +-- original pgsql backend. + +-- Objects with any of the following keys will be treated as polygon +polygon_keys = { + 'aeroway', + 'amenity', + 'building', + 'harbour', + 'historic', + 'landuse', + 'leisure', + 'man_made', + 'military', + 'natural', + 'office', + 'place', + 'power', + 'public_transport', + 'shop', + 'sport', + 'tourism', + 'water', + 'waterway', + 'wetland' +} + +-- Objects without any of the following keys will be deleted +generic_keys = { + 'access', + 'addr:housename', + 'addr:housenumber', + 'addr:interpolation', + 'admin_level', + 'aerialway', + 'aeroway', + 'amenity', + 'area', + 'barrier', + 'bicycle', + 'boundary', + 'brand', + 'bridge', + 'building', + 'capital', + 'construction', + 'covered', + 'culvert', + 'cutting', + 'denomination', + 'disused', + 'ele', + 'embarkment', + 'foot', + 'generation:source', + 'harbour', + 'highway', + 'historic', + 'hours', + 'intermittent', + 'junction', + 'landuse', + 'layer', + 'leisure', + 'lock', + 'man_made', + 'military', + 'motor_car', + 'name', + 'natural', + 'office', + 'oneway', + 'operator', + 'place', + 'population', + 'power', + 'power_source', + 'public_transport', + 'railway', + 'ref', + 'religion', + 'route', + 'service', + 'shop', + 'sport', + 'surface', + 'toll', + 'tourism', + 'tower:type', + 'tracktype', + 'tunnel', + 'type', + 'water', + 'waterway', + 'wetland', + 'width', + 'wood' +} + +-- The following keys will be deleted +delete_tags = { + 'FIXME', + 'note', + 'source', + 'way', + 'way_area', + 'z_order', +} + +-- Array used to specify z_order per key/value combination. +-- Each element has the form {key, value, z_order, is_road}. +-- If is_road=1, the object will be added to planet_osm_roads. +zordering_tags = {{ 'railway', nil, 5, 1}, { 'boundary', 'administrative', 0, 1}, + { 'bridge', 'yes', 10, 0 }, { 'bridge', 'true', 10, 0 }, { 'bridge', 1, 10, 0 }, + { 'tunnel', 'yes', -10, 0}, { 'tunnel', 'true', -10, 0}, { 'tunnel', 1, -10, 0}, + { 'highway', 'minor', 3, 0}, { 'highway', 'road', 3, 0 }, { 'highway', 'unclassified', 3, 0 }, + { 'highway', 'residential', 3, 0 }, { 'highway', 'tertiary_link', 4, 0}, { 'highway', 'tertiary', 4, 0}, + { 'highway', 'secondary_link', 6, 1}, { 'highway', 'secondary', 6, 1}, + { 'highway', 'primary_link', 7, 1}, { 'highway', 'primary', 7, 1}, + { 'highway', 'trunk_link', 8, 1}, { 'highway', 'trunk', 8, 1}, + { 'highway', 'motorway_link', 9, 1}, { 'highway', 'motorway', 9, 1}, +} + +tables = {} + +tables.point = osm2pgsql.define_table{ + name = 'planet_osm_point', + ids = { type = 'node', id_column = 'osm_id' }, + columns = { + { column = 'access', type = 'text' }, + { column = 'addr:housename', type = 'text' }, + { column = 'addr:housenumber', type = 'text' }, + { column = 'addr:interpolation', type = 'text' }, + { column = 'admin_level', type = 'text' }, + { column = 'aerialway', type = 'text' }, + { column = 'aeroway', type = 'text' }, + { column = 'amenity', type = 'text' }, + { column = 'area', type = 'text' }, + { column = 'barrier', type = 'text' }, + { column = 'bicycle', type = 'text' }, + { column = 'brand', type = 'text' }, + { column = 'bridge', type = 'text' }, + { column = 'boundary', type = 'text' }, + { column = 'building', type = 'text' }, + { column = 'capital', type = 'text' }, + { column = 'construction', type = 'text' }, + { column = 'covered', type = 'text' }, + { column = 'culvert', type = 'text' }, + { column = 'cutting', type = 'text' }, + { column = 'denomination', type = 'text' }, + { column = 'disused', type = 'text' }, + { column = 'ele', type = 'text' }, + { column = 'embankment', type = 'text' }, + { column = 'foot', type = 'text' }, + { column = 'generator:source', type = 'text' }, + { column = 'harbour', type = 'text' }, + { column = 'highway', type = 'text' }, + { column = 'historic', type = 'text' }, + { column = 'horse', type = 'text' }, + { column = 'intermittent', type = 'text' }, + { column = 'junction', type = 'text' }, + { column = 'landuse', type = 'text' }, + { column = 'layer', type = 'text' }, + { column = 'leisure', type = 'text' }, + { column = 'lock', type = 'text' }, + { column = 'man_made', type = 'text' }, + { column = 'military', type = 'text' }, + { column = 'motorcar', type = 'text' }, + { column = 'name', type = 'text' }, + { column = 'natural', type = 'text' }, + { column = 'office', type = 'text' }, + { column = 'oneway', type = 'text' }, + { column = 'operator', type = 'text' }, + { column = 'place', type = 'text' }, + { column = 'population', type = 'text' }, + { column = 'power', type = 'text' }, + { column = 'power_source', type = 'text' }, + { column = 'public_transport', type = 'text' }, + { column = 'railway', type = 'text' }, + { column = 'ref', type = 'text' }, + { column = 'religion', type = 'text' }, + { column = 'route', type = 'text' }, + { column = 'service', type = 'text' }, + { column = 'shop', type = 'text' }, + { column = 'sport', type = 'text' }, + { column = 'surface', type = 'text' }, + { column = 'toll', type = 'text' }, + { column = 'tourism', type = 'text' }, + { column = 'tower:type', type = 'text' }, + { column = 'tunnel', type = 'text' }, + { column = 'water', type = 'text' }, + { column = 'waterway', type = 'text' }, + { column = 'wetland', type = 'text' }, + { column = 'width', type = 'text' }, + { column = 'wood', type = 'text' }, + { column = 'z_order', type = 'int' }, + { column = 'way', type = 'point' }, + } +} + +tables.line = osm2pgsql.define_table{ + name = 'planet_osm_line', + ids = { type = 'way', id_column = 'osm_id' }, + columns = { + { column = 'access', type = 'text' }, + { column = 'addr:housename', type = 'text' }, + { column = 'addr:housenumber', type = 'text' }, + { column = 'addr:interpolation', type = 'text' }, + { column = 'admin_level', type = 'text' }, + { column = 'aerialway', type = 'text' }, + { column = 'aeroway', type = 'text' }, + { column = 'amenity', type = 'text' }, + { column = 'area', type = 'text' }, + { column = 'barrier', type = 'text' }, + { column = 'bicycle', type = 'text' }, + { column = 'brand', type = 'text' }, + { column = 'bridge', type = 'text' }, + { column = 'boundary', type = 'text' }, + { column = 'building', type = 'text' }, + { column = 'construction', type = 'text' }, + { column = 'covered', type = 'text' }, + { column = 'culvert', type = 'text' }, + { column = 'cutting', type = 'text' }, + { column = 'denomination', type = 'text' }, + { column = 'disused', type = 'text' }, + { column = 'embankment', type = 'text' }, + { column = 'foot', type = 'text' }, + { column = 'generator:source', type = 'text' }, + { column = 'harbour', type = 'text' }, + { column = 'highway', type = 'text' }, + { column = 'historic', type = 'text' }, + { column = 'horse', type = 'text' }, + { column = 'intermittent', type = 'text' }, + { column = 'junction', type = 'text' }, + { column = 'landuse', type = 'text' }, + { column = 'layer', type = 'text' }, + { column = 'leisure', type = 'text' }, + { column = 'lock', type = 'text' }, + { column = 'man_made', type = 'text' }, + { column = 'military', type = 'text' }, + { column = 'motorcar', type = 'text' }, + { column = 'name', type = 'text' }, + { column = 'natural', type = 'text' }, + { column = 'office', type = 'text' }, + { column = 'oneway', type = 'text' }, + { column = 'operator', type = 'text' }, + { column = 'place', type = 'text' }, + { column = 'population', type = 'text' }, + { column = 'power', type = 'text' }, + { column = 'power_source', type = 'text' }, + { column = 'public_transport', type = 'text' }, + { column = 'railway', type = 'text' }, + { column = 'ref', type = 'text' }, + { column = 'religion', type = 'text' }, + { column = 'route', type = 'text' }, + { column = 'service', type = 'text' }, + { column = 'shop', type = 'text' }, + { column = 'sport', type = 'text' }, + { column = 'surface', type = 'text' }, + { column = 'toll', type = 'text' }, + { column = 'tourism', type = 'text' }, + { column = 'tower:type', type = 'text' }, + { column = 'tracktype', type = 'text' }, + { column = 'tunnel', type = 'text' }, + { column = 'water', type = 'text' }, + { column = 'waterway', type = 'text' }, + { column = 'wetland', type = 'text' }, + { column = 'width', type = 'text' }, + { column = 'wood', type = 'text' }, + { column = 'z_order', type = 'int' }, + { column = 'way_area', type = 'area' }, + { column = 'way', type = 'linestring', split_at = 100000 }, + } +} + +tables.polygon = osm2pgsql.define_table{ + name = 'planet_osm_polygon', + ids = { type = 'area', id_column = 'osm_id' }, + columns = { + { column = 'access', type = 'text' }, + { column = 'addr:housename', type = 'text' }, + { column = 'addr:housenumber', type = 'text' }, + { column = 'addr:interpolation', type = 'text' }, + { column = 'admin_level', type = 'text' }, + { column = 'aerialway', type = 'text' }, + { column = 'aeroway', type = 'text' }, + { column = 'amenity', type = 'text' }, + { column = 'area', type = 'text' }, + { column = 'barrier', type = 'text' }, + { column = 'bicycle', type = 'text' }, + { column = 'brand', type = 'text' }, + { column = 'bridge', type = 'text' }, + { column = 'boundary', type = 'text' }, + { column = 'building', type = 'text' }, + { column = 'construction', type = 'text' }, + { column = 'covered', type = 'text' }, + { column = 'culvert', type = 'text' }, + { column = 'cutting', type = 'text' }, + { column = 'denomination', type = 'text' }, + { column = 'disused', type = 'text' }, + { column = 'embankment', type = 'text' }, + { column = 'foot', type = 'text' }, + { column = 'generator:source', type = 'text' }, + { column = 'harbour', type = 'text' }, + { column = 'highway', type = 'text' }, + { column = 'historic', type = 'text' }, + { column = 'horse', type = 'text' }, + { column = 'intermittent', type = 'text' }, + { column = 'junction', type = 'text' }, + { column = 'landuse', type = 'text' }, + { column = 'layer', type = 'text' }, + { column = 'leisure', type = 'text' }, + { column = 'lock', type = 'text' }, + { column = 'man_made', type = 'text' }, + { column = 'military', type = 'text' }, + { column = 'motorcar', type = 'text' }, + { column = 'name', type = 'text' }, + { column = 'natural', type = 'text' }, + { column = 'office', type = 'text' }, + { column = 'oneway', type = 'text' }, + { column = 'operator', type = 'text' }, + { column = 'place', type = 'text' }, + { column = 'population', type = 'text' }, + { column = 'power', type = 'text' }, + { column = 'power_source', type = 'text' }, + { column = 'public_transport', type = 'text' }, + { column = 'railway', type = 'text' }, + { column = 'ref', type = 'text' }, + { column = 'religion', type = 'text' }, + { column = 'route', type = 'text' }, + { column = 'service', type = 'text' }, + { column = 'shop', type = 'text' }, + { column = 'sport', type = 'text' }, + { column = 'surface', type = 'text' }, + { column = 'toll', type = 'text' }, + { column = 'tourism', type = 'text' }, + { column = 'tower:type', type = 'text' }, + { column = 'tracktype', type = 'text' }, + { column = 'tunnel', type = 'text' }, + { column = 'water', type = 'text' }, + { column = 'waterway', type = 'text' }, + { column = 'wetland', type = 'text' }, + { column = 'width', type = 'text' }, + { column = 'wood', type = 'text' }, + { column = 'z_order', type = 'int' }, + { column = 'way_area', type = 'area' }, + { column = 'way', type = 'geometry' }, + } +} + +tables.roads = osm2pgsql.define_table{ + name = 'planet_osm_roads', + ids = { type = 'way', id_column = 'osm_id' }, + columns = { + { column = 'access', type = 'text' }, + { column = 'addr:housename', type = 'text' }, + { column = 'addr:housenumber', type = 'text' }, + { column = 'addr:interpolation', type = 'text' }, + { column = 'admin_level', type = 'text' }, + { column = 'aerialway', type = 'text' }, + { column = 'aeroway', type = 'text' }, + { column = 'amenity', type = 'text' }, + { column = 'area', type = 'text' }, + { column = 'barrier', type = 'text' }, + { column = 'bicycle', type = 'text' }, + { column = 'brand', type = 'text' }, + { column = 'bridge', type = 'text' }, + { column = 'boundary', type = 'text' }, + { column = 'building', type = 'text' }, + { column = 'construction', type = 'text' }, + { column = 'covered', type = 'text' }, + { column = 'culvert', type = 'text' }, + { column = 'cutting', type = 'text' }, + { column = 'denomination', type = 'text' }, + { column = 'disused', type = 'text' }, + { column = 'embankment', type = 'text' }, + { column = 'foot', type = 'text' }, + { column = 'generator:source', type = 'text' }, + { column = 'harbour', type = 'text' }, + { column = 'highway', type = 'text' }, + { column = 'historic', type = 'text' }, + { column = 'horse', type = 'text' }, + { column = 'intermittent', type = 'text' }, + { column = 'junction', type = 'text' }, + { column = 'landuse', type = 'text' }, + { column = 'layer', type = 'text' }, + { column = 'leisure', type = 'text' }, + { column = 'lock', type = 'text' }, + { column = 'man_made', type = 'text' }, + { column = 'military', type = 'text' }, + { column = 'motorcar', type = 'text' }, + { column = 'name', type = 'text' }, + { column = 'natural', type = 'text' }, + { column = 'office', type = 'text' }, + { column = 'oneway', type = 'text' }, + { column = 'operator', type = 'text' }, + { column = 'place', type = 'text' }, + { column = 'population', type = 'text' }, + { column = 'power', type = 'text' }, + { column = 'power_source', type = 'text' }, + { column = 'public_transport', type = 'text' }, + { column = 'railway', type = 'text' }, + { column = 'ref', type = 'text' }, + { column = 'religion', type = 'text' }, + { column = 'route', type = 'text' }, + { column = 'service', type = 'text' }, + { column = 'shop', type = 'text' }, + { column = 'sport', type = 'text' }, + { column = 'surface', type = 'text' }, + { column = 'toll', type = 'text' }, + { column = 'tourism', type = 'text' }, + { column = 'tower:type', type = 'text' }, + { column = 'tracktype', type = 'text' }, + { column = 'tunnel', type = 'text' }, + { column = 'water', type = 'text' }, + { column = 'waterway', type = 'text' }, + { column = 'wetland', type = 'text' }, + { column = 'width', type = 'text' }, + { column = 'wood', type = 'text' }, + { column = 'z_order', type = 'int' }, + { column = 'way_area', type = 'area' }, + { column = 'way', type = 'linestring', split_at = 100000 }, + } +} + +function get_z_order(keyvalues) + -- The default z_order is 0 + local z_order = 0 + local roads = false + + -- Add the value of the layer key times 10 to z_order + if (keyvalues["layer"] ~= nil and tonumber(keyvalues["layer"])) then + z_order = 10*keyvalues["layer"] + end + + -- Increase or decrease z_order based on the specific key/value combination as specified in zordering_tags + for i,k in ipairs(zordering_tags) do + -- If the value in zordering_tags is specified, match key and value. Otherwise, match key only. + if ((k[2] and keyvalues[k[1]] == k[2]) or (k[2] == nil and keyvalues[k[1]] ~= nil)) then + -- If the fourth component of the element of zordering_tags is 1, add the object to planet_osm_roads + if (k[4] == 1) then + roads = true + end + z_order = z_order + k[3] + end + end + + return z_order, roads +end + +-- Helper function to check whether a table is empty +function is_empty(some_table) + return next(some_table) == nil +end + +function has_generic_tag(tags) + for k, v in pairs(tags) do + for j, k2 in ipairs(generic_keys) do + if k == k2 then + return true + end + end + end + return false +end + +function osm2pgsql.process_node(data) + if is_empty(data.tags) then + return + end + + for i,k in ipairs(delete_tags) do + data.tags[k] = nil + end + + if not has_generic_tag(data.tags) then + return + end + + tables.point:add_row(data.tags) +end + +-- Treat objects with a key in polygon_keys as polygon +function is_polygon(tags) + for i,k in ipairs(polygon_keys) do + if tags[k] then + return true + end + end + return false +end + +function osm2pgsql.process_way(data) + if is_empty(data.tags) then + return + end + + for i,k in ipairs(delete_tags) do + data.tags[k] = nil + end + + if not has_generic_tag(data.tags) then + return + end + + local polygon = is_polygon(data.tags) + -- Treat objects tagged as area=yes, area=1, or area=true as polygon, + -- and treat objects tagged as area=no, area=0, or area=false not as polygon + local area_tag = data.tags.area + if area_tag == "yes" or area_tag == "1" or area_tag == "true" then + polygon = true + elseif area_tag == "no" or area_tag == "0" or area_tag == "false" then + polygon = false + end + + local z_order, roads = get_z_order(data.tags) + data.tags.z_order = z_order + + if polygon then + tables.polygon:add_row(data.tags) + else + tables.line:add_row(data.tags) + end + + if roads then + tables.roads:add_row(data.tags) + end +end + +function osm2pgsql.process_relation(data) + if is_empty(data.tags) then + return + end + + local type = data.tags.type + if (type ~= "route") and (type ~= "multipolygon") and (type ~= "boundary") then + return + end + + for i,k in ipairs(delete_tags) do + data.tags[k] = nil + end + + if not has_generic_tag(data.tags) then + return + end + + local z_order, roads = get_z_order(data.tags) + data.tags.z_order = z_order + + local linestring = false + if type == 'boundary' then + linestring = true + elseif type == 'multipolygon' then + if data.tags.boundary then + linestring = true + else + tables.polygon:add_row(data.tags) + end + end + + if linestring then + tables.line:add_row(data.tags) + end + + if roads then + tables.roads:add_row(data.tags) + end +end + diff --git a/flex-config/highway-shields.lua b/flex-config/highway-shields.lua new file mode 100644 index 000000000..bfe67555a --- /dev/null +++ b/flex-config/highway-shields.lua @@ -0,0 +1,107 @@ + +-- Read and understand simple.lua and multipolygons.lua first, before you try +-- to understand this file. + +-- This will import highways only. The 'refs' column will contain a +-- comma-separated list of all refs found in relations with type=route and +-- route=road. + +inspect = require('inspect') + +print("osm2pgsql version: " .. osm2pgsql.version) + +-- Are we running in "create" or "append" mode? +print("osm2pgsql mode: " .. osm2pgsql.mode) + +-- Which stage in the data processing is this? +print("osm2pgsql stage: " .. osm2pgsql.stage) + +-- Uncomment the following line to see the userdata (but, careful, it might be +-- a lot of data) +-- print("osm2pgsql userdata: " .. inspect(osm2pgsql.userdata)) + +tables = {} + +tables.highways = osm2pgsql.define_way_table("highways", { + { column = 'tags', type = 'hstore' }, + { column = 'refs', type = 'text' }, + { column = 'oneway', type = 'direction' }, -- the 'direction' type maps "yes", "true", and "1" to 1, "-1" to -1, everything else to 0 + { column = 'rel_ids', type = 'int8[]' }, -- array with integers (for relation IDs) + { column = 'geom', type = 'linestring' }, +}) + +-- tables don't have to have a geometry column +tables.routes = osm2pgsql.define_relation_table("routes", { + { column = 'tags', type = 'hstore' }, +}) + +function clean_tags(tags) + tags.odbl = nil + tags.created_by = nil + tags.source = nil + tags["source:ref"] = nil + tags["source:name"] = nil +end + +function osm2pgsql.process_way(data) + -- we are only interested in highways + if not data.tags.highway then + return + end + + -- mark all remaining ways so we will see them again in the second stage + if osm2pgsql.stage == 1 then + osm2pgsql.mark('w', data.id) + return + end + + -- we are now in second stage + + clean_tags(data.tags) + + local row = { + tags = data.tags, + oneway = data.tags.oneway, + } + + -- if there is any data from relations, add it in + local d = osm2pgsql.userdata.by_way_id[data.id] + if d then + row.refs = table.concat(d.refs, ',') + row.rel_ids = '{' .. table.concat(d.ids, ',') .. '}' + end + + -- print(inspect(row)) + + tables.highways:add_row(row) +end + +function osm2pgsql.process_relation(data) + -- only interested in relations with type=route, route=road and a ref + if data.tags.type == 'route' and data.tags.route == 'road' and data.tags.ref then + if not osm2pgsql.userdata.by_way_id then + osm2pgsql.userdata.by_way_id = {} + end + + tables.routes:add_row({ + tags = data.tags + }) + + -- Go through all the members and store relation ids and refs so it + -- can be found by the way id. + for i, member in ipairs(data.members) do + if member.type == 'w' then + if not osm2pgsql.userdata.by_way_id[member.ref] then + osm2pgsql.userdata.by_way_id[member.ref] = { + ids = {}, + refs = {} + } + end + local d = osm2pgsql.userdata.by_way_id[member.ref] + table.insert(d.ids, data.id) + table.insert(d.refs, data.tags.ref) + end + end + end +end + diff --git a/flex-config/multipolygons.lua b/flex-config/multipolygons.lua new file mode 100644 index 000000000..6767be8c6 --- /dev/null +++ b/flex-config/multipolygons.lua @@ -0,0 +1,201 @@ + +inspect = require('inspect') + +print("osm2pgsql version: " .. osm2pgsql.version) + +-- The projection configured on the command line is available in Lua: +print("osm2pgsql srid: " .. osm2pgsql.srid) + +tables = {} + +tables.pois = osm2pgsql.define_node_table("pois", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'point' }, +}) + +tables.ways = osm2pgsql.define_way_table("ways", { + { column = 'tags', type = 'hstore' }, + -- If you want to split long linestrings, you can set "split_at" to the + -- maximum length the pieces should have. This length is in map units, + -- so it depends on the projection used. "Traditional" osm2pgsql sets + -- this to 1 for 4326 geometries and 100000 for 3857 (web mercator) + -- geometries. The default is 0.0, which means no splitting. + { column = 'geom', type = 'linestring', split_at = 100000 }, +}) + +tables.polygons = osm2pgsql.define_area_table("polygons", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'geometry' }, + -- The 'area' type is used to store the calculated area of a polygon feature + { column = 'area', type = 'area' }, +}) + +function is_empty(some_table) + return next(some_table) == nil +end + +function starts_with(str, start) + return str:sub(1, #start) == start +end + +function remove_with_prefix(tags, prefix) + for k, v in pairs(tags) do + if starts_with(k, prefix) then + tags.k = nil + end + end +end + +function clean_tags(tags) + -- These are tags that are generally regarded as useless for most + -- rendering. Most of them are from imports or intended as internal + -- information for mappers Some of them are automatically deleted by + -- editors. If you want some of them, perhaps for a debugging layer, just + -- delete the lines. + + -- These tags are used by mappers to keep track of data. + -- They aren't very useful for rendering. + tags['note'] = nil + remove_with_prefix(tags, 'note:') + tags['source'] = nil + tags['source_ref'] = nil + remove_with_prefix(tags, 'source:') + tags['attribution'] = nil + tags['comment'] = nil + tags['fixme'] = nil + + -- Tags generally dropped by editors, not otherwise covered + tags['created_by'] = nil + tags['odbl'] = nil + tags['odbl:note'] = nil + tags['SK53_bulk:load'] = nil + + -- Lots of import tags + -- TIGER (US) + remove_with_prefix(tags, 'tiger:') + + -- NHD (US) + -- NHD has been converted every way imaginable + remove_with_prefix(tags, 'NHD:') + remove_with_prefix(tags, 'nhd:') + + -- GNIS (US) + remove_with_prefix(tags, 'gnis:') + + -- Geobase (CA) + remove_with_prefix(tags, 'geobase:') + -- NHN (CA) + tags['accuracy:meters'] = nil + tags['sub_sea:type'] = nil + tags['waterway:type'] = nil + + -- KSJ2 (JA) + -- See also note:ja and source_ref above + remove_with_prefix(tags, 'KSJ2:') + -- Yahoo/ALPS (JA) + remove_with_prefix(tags, 'yh:') + + -- osak (DK) + remove_with_prefix(tags, 'osak:') + + -- kms (DK) + remove_with_prefix(tags, 'kms:') + + -- ngbe (ES) + -- See also note:es and source:file above + remove_with_prefix(tags, 'ngbe:') + + -- naptan (UK) + remove_with_prefix(tags, 'naptan:') + + -- Corine (CLC) (Europe) + remove_with_prefix(tags, 'CLC:') + + -- misc + tags['3dshapes:ggmodelk'] = nil + tags['AND_nosr_r'] = nil + tags['import'] = nil + remove_with_prefix(tags, 'it:fvg:') +end + +-- Helper function that looks at the tags and decides if this is possibly +-- an area. +function has_area_tags(tags) + if tags.area == 'yes' then + return true + end + if tags.area == 'no' then + return false + end + + return tags.aeroway + or tags.amenity + or tags.building + or tags.harbour + or tags.historic + or tags.landuse + or tags.leisure + or tags.man_made + or tags.military + or tags.natural + or tags.office + or tags.place + or tags.power + or tags.public_transport + or tags.shop + or tags.sport + or tags.tourism + or tags.water + or tags.waterway + or tags.wetland + or tags['abandoned:aeroway'] + or tags['abandoned:amenity'] + or tags['abandoned:building'] + or tags['abandoned:landuse'] + or tags['abandoned:power'] + or tags['area:highway'] +end + +function osm2pgsql.process_node(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + tables.pois:add_row({ + tags = data.tags + }) +end + +function osm2pgsql.process_way(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + -- A closed way that also has the right tags for an area is a polygon. + if data.is_closed and has_area_tags(data.tags) then + tables.polygons:add_row({ + tags = data.tags + }) + else + tables.ways:add_row({ + tags = data.tags + }) + end +end + +function osm2pgsql.process_relation(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + -- Only handle multipolygons + if data.tags.type == 'multipolygon' or data.tags.type == 'boundary' then + tables.polygons:add_row({ + tags = data.tags + }) + end +end + diff --git a/flex-config/simple.lua b/flex-config/simple.lua new file mode 100644 index 000000000..e4a579737 --- /dev/null +++ b/flex-config/simple.lua @@ -0,0 +1,143 @@ + +-- For debugging +inspect = require('inspect') + +-- The global variable "osm2pgsql" is used to talk to the main osm2pgsql code. +print("osm2pgsql version: " .. osm2pgsql.version) + +-- A place to store the SQL tables we will define shortly. +tables = {} + +-- Create a new table called "pois" with the given columns. When running in +-- "create" mode, this will do the `CREATE TABLE`, when running in "append" +-- mode, this will only declare the table for use. +-- +-- This is a "node table", it can only contain data derived from nodes and will +-- contain a "node_id" column (SQL type INT8) as first column. When running in +-- "append" mode, osm2pgsql will automatically update this table using the node +-- ids. +tables.pois = osm2pgsql.define_node_table("pois", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'point' }, -- translates to `GEOMETRY(POINT, srid)` in SQL +}) + +-- A special table for restaurants to demonstrate that we can have any and +-- all tables we want. +tables.restaurants = osm2pgsql.define_node_table("restaurants", { + { column = 'name', type = 'text' }, + { column = 'cuisine', type = 'text' }, + { column = 'geom', type = 'point' }, +}) + +-- This is a "way table", it can only contain data derived from ways and will +-- contain a "way_id" column. When running in "append" mode, osm2pgsql will +-- automatically update this table using the way ids. +tables.ways = osm2pgsql.define_way_table("ways", { + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'linestring' }, +}) + +-- This is an "area table", it can contain data derived from ways or relations +-- and will contain an "area_id" column. Way ids will be stored "as is" in the +-- "area_id" column, for relations the negative id will be stored. When +-- running in "append" mode, osm2pgsql will automatically update this table +-- using the way/relation ids. +tables.polygons = osm2pgsql.define_area_table("polygons", { + { column = 'tags', type = 'hstore' }, + -- The type of the `geom` column is `geometry`, because we need to store + -- polygons AND multipolygons + { column = 'geom', type = 'geometry' }, +}) + +-- Debug output: Show definition of tables +for name, table in pairs(tables) do + print("table '" .. name .. "': name='" .. table:name() .. "' columns=" .. inspect(table:columns())) +end + +-- Helper function to check whether a table is empty +function is_empty(some_table) + return next(some_table) == nil +end + +-- Helper function to remove some of the tags we usually are not interested in +function clean_tags(tags) + tags.odbl = nil + tags.created_by = nil + tags.source = nil + tags["source:ref"] = nil + tags["source:name"] = nil +end + +-- Called for every node in the input. The `data` argument contains all the +-- attributes of the node like `id`, `version`, etc. as well as all tags as a +-- Lua table (`data.tags`). +function osm2pgsql.process_node(data) + -- Uncomment next line to look at the data: + -- print(inspect(data)) + + if data.tags.amenity == "restaurant" then + -- Add a row to the SQL table. The keys in the parameter table + -- correspond to the table columns, if one is missing the column will + -- be NULL. Id and geometry columns will be filled automatically. + tables.restaurants:add_row({ + name = data.tags.name, + cuisine = data.tags.cuisine + }) + else + clean_tags(data.tags) + + if not is_empty(data.tags) then + tables.pois:add_row({ + -- We know `tags` is of type `hstore` so this will do the + -- right thing. + tags = data.tags + }) + end + end +end + +-- Called for every way in the input. The `data` argument contains the same +-- information as with nodes and additionally a boolean `is_closed` flag and +-- the list of node IDs referenced by the way (`data.nodes`). +function osm2pgsql.process_way(data) + -- Uncomment next line to look at the data: + -- print(inspect(data)) + + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + -- Very simple check to decide whether a way is a polygon or not, in a + -- real stylesheet we'd have to also look at the tags... + if data.is_closed then + tables.polygons:add_row({ + tags = data.tags + }) + else + tables.ways:add_row({ + tags = data.tags + }) + end +end + +-- Called for every relation in the input. The `data` argument contains the +-- same information as with nodes and additionally an array of members +-- (`data.members`). +function osm2pgsql.process_relation(data) + -- Uncomment next line to look at the data: + -- print(inspect(data)) + + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + -- Only handle multipolygons + if data.tags.type == 'multipolygon' or data.tags.type == 'boundary' then + tables.polygons:add_row({ + tags = data.tags + }) + end +end + diff --git a/flex-config/unitable.lua b/flex-config/unitable.lua new file mode 100644 index 000000000..ccb5ad08c --- /dev/null +++ b/flex-config/unitable.lua @@ -0,0 +1,68 @@ + +-- Read and understand simple.lua and advanced.lua first, before you try +-- to understand this file. + +inspect = require('inspect') + +-- We define a single table that can take any OSM object and any geometry. +-- XXX Updates/expire will currently not work on these tables. +dtable = osm2pgsql.define_table{ + name = "data", + -- This will generate a column "osm_id INT8" for the id, and a column + -- "osm_type CHAR(1)" for the type of object: N(ode), W(way), R(relation) + ids = { type = 'any', id_column = 'osm_id', type_column = 'osm_type' }, + columns = { + { column = 'attrs', type = 'hstore' }, + { column = 'tags', type = 'hstore' }, + { column = 'geom', type = 'geometry' }, + } +} + +print("columns=" .. inspect(dtable:columns())) + +function is_empty(some_table) + return next(some_table) == nil +end + +function clean_tags(tags) + tags.odbl = nil + tags.created_by = nil + tags.source = nil + tags["source:ref"] = nil + tags["source:name"] = nil +end + +function process(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + dtable:add_row({ + attrs = { + version = data.version, + timestamp = data.timestamp, + }, + tags = data.tags + }) +end + +osm2pgsql.process_node = process +osm2pgsql.process_way = process + +function osm2pgsql.process_relation(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + if data.tags.type == 'multipolygon' or data.tags.type == 'boundary' then + dtable:add_row({ + attrs = { + version = data.version, + timestamp = data.timestamp, + }, + tags = data.tags + }) + end +end + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 783b8e14f..687557186 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,9 @@ set(osm2pgsql_lib_SOURCES db-copy.cpp expire-tiles.cpp + flex-lua.cpp + flex-table.cpp + flex-table-column.cpp gazetteer-style.cpp geometry-processor.cpp id-tracker.cpp @@ -12,6 +15,7 @@ set(osm2pgsql_lib_SOURCES options.cpp osmdata.cpp osmium-builder.cpp + output-flex.cpp output-gazetteer.cpp output-multi.cpp output-null.cpp @@ -32,6 +36,9 @@ set(osm2pgsql_lib_SOURCES wildcmp.cpp db-copy.hpp expire-tiles.hpp + flex-lua.hpp + flex-table.hpp + flex-table-column.hpp gazetteer-style.hpp geometry-processor.hpp id-tracker.hpp @@ -44,6 +51,7 @@ set(osm2pgsql_lib_SOURCES osmdata.hpp osmium-builder.hpp osmtypes.hpp + output-flex.hpp output-gazetteer.hpp output-multi.hpp output-null.hpp diff --git a/src/expire-tiles.cpp b/src/expire-tiles.cpp index 1bc2e20dd..e28f613f1 100644 --- a/src/expire-tiles.cpp +++ b/src/expire-tiles.cpp @@ -419,6 +419,25 @@ int expire_tiles::from_db(table_t *table, osmid_t osm_id) return wkbs.get_count(); } +int expire_tiles::from_result(pg_result_t const &result, osmid_t osm_id) +{ + //bail if we dont care about expiry + if (maxzoom == 0) { + return -1; + } + + //dirty the stuff + auto const num_tuples = result.num_tuples(); + for (int i = 0; i < num_tuples; ++i) { + char const *const wkb = result.get_value(i, 0); + auto const binwkb = ewkb::parser_t::wkb_from_hex(wkb); + from_wkb(binwkb.c_str(), osm_id); + } + + //return how many rows were affected + return num_tuples; +} + void expire_tiles::merge_and_destroy(expire_tiles &other) { if (map_width != other.map_width) { diff --git a/src/expire-tiles.hpp b/src/expire-tiles.hpp index 626257ad6..d167ff9a6 100644 --- a/src/expire-tiles.hpp +++ b/src/expire-tiles.hpp @@ -5,6 +5,7 @@ #include #include "osmtypes.hpp" +#include "pgsql.hpp" class reprojection; class table_t; @@ -55,6 +56,7 @@ struct expire_tiles double max_lat); void from_wkb(const char *wkb, osmid_t osm_id); int from_db(table_t *table, osmid_t osm_id); + int from_result(pg_result_t const &result, osmid_t osm_id); /** * Write the list of expired tiles to a file. diff --git a/src/flex-lua.cpp b/src/flex-lua.cpp new file mode 100644 index 000000000..533669462 --- /dev/null +++ b/src/flex-lua.cpp @@ -0,0 +1,116 @@ +#include "flex-lua.hpp" +#include "format.hpp" + +extern "C" +{ +#include +} + +#include + +// The lua_getextraspace() function is only available from Lua 5.3. For +// earlier versions we fall back to storing the context pointer in the +// Lua registry which is somewhat more effort so will be slower. +#if LUA_VERSION_NUM >= 503 + +void luaX_set_context(lua_State *lua_state, void *ptr) noexcept +{ + assert(lua_state); + assert(ptr); + *static_cast(lua_getextraspace(lua_state)) = ptr; +} + +void *luaX_get_context(lua_State *lua_state) noexcept +{ + assert(lua_state); + return *static_cast(lua_getextraspace(lua_state)); +} + +#else + +// Unique key for lua registry +static char const *osm2pgsql_output_flex = "osm2pgsql_output_flex"; + +void luaX_set_context(lua_State *lua_state, void *ptr) noexcept +{ + assert(lua_state); + assert(ptr); + lua_pushlightuserdata(lua_state, (void *)osm2pgsql_output_flex); + lua_pushlightuserdata(lua_state, ptr); + lua_settable(lua_state, LUA_REGISTRYINDEX); +} + +void *luaX_get_context(lua_State *lua_state) noexcept +{ + assert(lua_state); + lua_pushlightuserdata(lua_state, (void *)osm2pgsql_output_flex); + lua_gettable(lua_state, LUA_REGISTRYINDEX); + auto *const ptr = lua_touserdata(lua_state, -1); + assert(ptr); + lua_pop(lua_state, 1); + return ptr; +} + +#endif + +void luaX_add_table_str(lua_State *lua_state, char const *key, + char const *value) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushstring(lua_state, value); + lua_rawset(lua_state, -3); +} + +void luaX_add_table_str(lua_State *lua_state, char const *key, + char const *value, std::size_t size) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushlstring(lua_state, value, size); + lua_rawset(lua_state, -3); +} + +void luaX_add_table_int(lua_State *lua_state, char const *key, + int64_t value) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushinteger(lua_state, value); + lua_rawset(lua_state, -3); +} + +void luaX_add_table_num(lua_State *lua_state, char const *key, + double value) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushnumber(lua_state, value); + lua_rawset(lua_state, -3); +} + +void luaX_add_table_bool(lua_State *lua_state, char const *key, + bool value) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushboolean(lua_state, value); + lua_rawset(lua_state, -3); +} + +void luaX_add_table_func(lua_State *lua_state, char const *key, + lua_CFunction func) noexcept +{ + lua_pushstring(lua_state, key); + lua_pushcfunction(lua_state, func); + lua_rawset(lua_state, -3); +} + +char const *luaX_get_table_string(lua_State *lua_state, char const *key, + int table_index, char const *error_msg) +{ + assert(lua_state); + assert(key); + assert(error_msg); + lua_getfield(lua_state, table_index, key); + if (!lua_isstring(lua_state, -1)) { + throw std::runtime_error{ + "{} must contain a '{}' string field"_format(error_msg, key)}; + } + return lua_tostring(lua_state, -1); +} diff --git a/src/flex-lua.hpp b/src/flex-lua.hpp new file mode 100644 index 000000000..2b5bcea71 --- /dev/null +++ b/src/flex-lua.hpp @@ -0,0 +1,49 @@ +#ifndef OSM2PGSQL_FLEX_LUA_HPP +#define OSM2PGSQL_FLEX_LUA_HPP + +// This file contains helper functions for talking to Lua. It is used from +// the flex output backend. All functions start with "luaX_". + +extern "C" +{ +#include +} + +#include +#include + +void luaX_set_context(lua_State *lua_state, void *ptr) noexcept; +void *luaX_get_context(lua_State *lua_state) noexcept; + +void luaX_add_table_str(lua_State *lua_state, char const *key, + char const *value) noexcept; +void luaX_add_table_str(lua_State *lua_state, char const *key, + char const *value, std::size_t size) noexcept; +void luaX_add_table_int(lua_State *lua_state, char const *key, + int64_t value) noexcept; +void luaX_add_table_num(lua_State *lua_state, char const *key, + double value) noexcept; +void luaX_add_table_bool(lua_State *lua_state, char const *key, + bool value) noexcept; +void luaX_add_table_func(lua_State *lua_state, char const *key, + lua_CFunction func) noexcept; + +template +void luaX_add_table_array(lua_State *lua_state, char const *key, + COLLECTION const &collection, FUNC &&func) +{ + lua_pushstring(lua_state, key); + lua_createtable(lua_state, (int)collection.size(), 0); + int n = 0; + for (auto const &member : collection) { + lua_pushinteger(lua_state, ++n); + std::forward(func)(member); + lua_rawset(lua_state, -3); + } + lua_rawset(lua_state, -3); +} + +char const *luaX_get_table_string(lua_State *lua_state, char const *key, + int table_index, char const *error_msg); + +#endif // OSM2PGSQL_FLEX_LUA_HPP diff --git a/src/flex-table-column.cpp b/src/flex-table-column.cpp new file mode 100644 index 000000000..1c81f65d5 --- /dev/null +++ b/src/flex-table-column.cpp @@ -0,0 +1,172 @@ +#include "flex-table-column.hpp" + +#include +#include +#include +#include +#include +#include + +using column_type_lookup = std::pair; + +static std::vector column_types = { + {"sql", table_column_type::sql}, + + {"text", table_column_type::text}, + + {"boolean", table_column_type::boolean}, + {"bool", table_column_type::boolean}, + + {"int2", table_column_type::int2}, + {"smallint", table_column_type::int2}, + {"int4", table_column_type::int4}, + {"int", table_column_type::int4}, + {"integer", table_column_type::int4}, + {"int8", table_column_type::int8}, + {"bigint", table_column_type::int8}, + + {"real", table_column_type::real}, + + {"hstore", table_column_type::hstore}, + {"json", table_column_type::json}, + {"jsonb", table_column_type::jsonb}, + + {"direction", table_column_type::direction}, + + {"geometry", table_column_type::geometry}, + {"point", table_column_type::point}, + {"linestring", table_column_type::linestring}, + {"polygon", table_column_type::polygon}, + {"multipoint", table_column_type::multipoint}, + {"multilinestring", table_column_type::multilinestring}, + {"multipolygon", table_column_type::multipolygon}, + + {"area", table_column_type::area}, + + {"id_type", table_column_type::id_type}, + {"id_num", table_column_type::id_num}}; + +static table_column_type +get_column_type_from_string(std::string const &type) noexcept +{ + auto const it = std::find_if(column_types.cbegin(), column_types.cend(), + [&type](column_type_lookup name_type) { + return type == name_type.first; + }); + + if (it != column_types.cend()) { + return it->second; + } + + // If we don't recognize the column type, we just assume its a valid SQL + // type and use it "as is". + return table_column_type::sql; +} + +static std::string lowercase(std::string const &str) +{ + std::string result; + + for (char c : str) { + result += + static_cast(std::tolower(static_cast(c))); + } + + return result; +} + +flex_table_column_t::flex_table_column_t(std::string name, + std::string const &type) +: m_name(std::move(name)), m_type_name(lowercase(type)), + m_type(get_column_type_from_string(m_type_name)) +{} + +std::string flex_table_column_t::sql_type_name(int srid) const +{ + switch (m_type) { + case table_column_type::sql: + return m_type_name; + break; + case table_column_type::text: + return "TEXT"; + break; + case table_column_type::boolean: + return "BOOLEAN"; + break; + case table_column_type::int2: + return "INT2"; + break; + case table_column_type::int4: + return "INT4"; + break; + case table_column_type::int8: + return "INT8"; + break; + case table_column_type::real: + return "REAL"; + break; + case table_column_type::hstore: + return "HSTORE"; + break; + case table_column_type::json: + return "JSON"; + break; + case table_column_type::jsonb: + return "JSONB"; + break; + case table_column_type::direction: + return "INT4"; + break; + case table_column_type::geometry: + return "GEOMETRY(GEOMETRY, {})"_format(srid); + break; + case table_column_type::point: + return "GEOMETRY(POINT, {})"_format(srid); + break; + case table_column_type::linestring: + return "GEOMETRY(LINESTRING, {})"_format(srid); + break; + case table_column_type::polygon: + return "GEOMETRY(POLYGON, {})"_format(srid); + break; + case table_column_type::multipoint: + return "GEOMETRY(MULTIPOINT, {})"_format(srid); + break; + case table_column_type::multilinestring: + return "GEOMETRY(MULTILINESTRING, {})"_format(srid); + break; + case table_column_type::multipolygon: + return "GEOMETRY(MULTIPOLYGON, {})"_format(srid); + break; + case table_column_type::area: + return "REAL"; + break; + case table_column_type::id_type: + return "CHAR(1)"; + break; + case table_column_type::id_num: + return "INT8"; + break; + } + throw std::runtime_error{"Unknown column type"}; +} + +std::string flex_table_column_t::sql_modifiers() const +{ + std::string modifiers; + + if ((m_flags & table_column_flags::not_null) != 0U) { + modifiers += "NOT NULL "; + } + + if (!modifiers.empty()) { + modifiers.resize(modifiers.size() - 1); + } + + return modifiers; +} + +std::string flex_table_column_t::sql_create(int srid) const +{ + return "\"{}\" {} {},"_format(m_name, sql_type_name(srid), sql_modifiers()); +} diff --git a/src/flex-table-column.hpp b/src/flex-table-column.hpp new file mode 100644 index 000000000..b521e31aa --- /dev/null +++ b/src/flex-table-column.hpp @@ -0,0 +1,131 @@ +#ifndef OSM2PGSQL_FLEX_TABLE_COLUMN_HPP +#define OSM2PGSQL_FLEX_TABLE_COLUMN_HPP + +#include "format.hpp" + +#include +#include + +enum class table_column_type : uint8_t +{ + sql, + + text, + + boolean, + + int2, + int4, + int8, + + real, + + hstore, + json, + jsonb, + + direction, + + geometry, + point, + linestring, + polygon, + multipoint, + multilinestring, + multipolygon, + + area, + + id_type, + id_num +}; + +enum table_column_flags : uint8_t +{ + none = 0, + not_null = 1 +}; + +/** + * A column in a flex_table_t. + */ +class flex_table_column_t +{ +public: + flex_table_column_t(std::string name, std::string const &type); + + std::string const &name() const noexcept { return m_name; } + + table_column_type type() const noexcept { return m_type; } + + table_column_flags flags() const noexcept { return m_flags; } + + bool is_point_column() const noexcept + { + return (m_type == table_column_type::point) || + (m_type == table_column_type::multipoint); + } + + bool is_linestring_column() const noexcept + { + return (m_type == table_column_type::linestring) || + (m_type == table_column_type::multilinestring); + } + + bool is_polygon_column() const noexcept + { + return (m_type == table_column_type::geometry) || + (m_type == table_column_type::polygon) || + (m_type == table_column_type::multipolygon); + } + + bool is_geometry_column() const noexcept + { + return (m_type >= table_column_type::geometry) && + (m_type <= table_column_type::multipolygon); + } + + std::string const &type_name() const noexcept { return m_type_name; } + + double split_at() const noexcept { return m_split_at; } + + void set_split_at(double split_at) noexcept + { + assert(is_linestring_column()); + m_split_at = split_at; + } + + void set_not_null_constraint() noexcept + { + m_flags = static_cast(m_flags | + table_column_flags::not_null); + } + + std::string sql_type_name(int srid) const; + std::string sql_modifiers() const; + std::string sql_create(int srid) const; + +private: + /// The name of the database table column. + std::string m_name; + + /** + * The type name of the database table column. Either a name we recognize + * or just an SQL snippet. + */ + std::string m_type_name; + + /** + * The type of database column. Use table_column_type::sql as fallback + * in which case m_type_name is the SQL type used in the database. + */ + table_column_type m_type; + + /// Flags like NOT NULL. + table_column_flags m_flags = table_column_flags::none; + + /// Maximum length of linestrings. + double m_split_at = 0.0; +}; + +#endif // OSM2PGSQL_FLEX_TABLE_COLUMN_HPP diff --git a/src/flex-table.cpp b/src/flex-table.cpp new file mode 100644 index 000000000..fabc6c47b --- /dev/null +++ b/src/flex-table.cpp @@ -0,0 +1,249 @@ +#include "flex-table.hpp" +#include "format.hpp" + +#include +#include +#include + +flex_table_column_t &flex_table_t::add_column(std::string const &name, + std::string const &type) +{ + m_columns.emplace_back(name, type); + auto &column = m_columns.back(); + + if (column.is_geometry_column()) { + m_geom_column = m_columns.size() - 1; + column.set_not_null_constraint(); + } + + return column; +} + +std::string flex_table_t::build_sql_prepare_get_wkb() const +{ + if (has_geom_column()) { + return "PREPARE get_wkb (BIGINT) AS" + " SELECT \"{}\" FROM \"{}\" WHERE \"{}\" = $1"_format( + geom_column().name(), name(), id_column_name()); + } + return "PREPARE get_wkb (BIGINT) AS SELECT ''"; +} + +std::string flex_table_t::build_sql_create_table() const +{ + assert(!m_columns.empty()); + + std::string sql = + "CREATE UNLOGGED TABLE IF NOT EXISTS \"{}\" ("_format(m_name); + + for (auto const &column : m_columns) { + sql += column.sql_create(m_srid); + } + + assert(sql.back() == ','); + sql.back() = ')'; + + // The final tables are created with CREATE TABLE AS ... SELECT * FROM ... + // This means that they won't get this autovacuum setting, so it + // doesn't need to be RESET on these tables + sql += " WITH ( autovacuum_enabled = FALSE )"; + + sql += m_table_space; + + return sql; +} + +std::string flex_table_t::build_sql_column_list() const +{ + assert(!m_columns.empty()); + + std::string result; + for (auto const &column : m_columns) { + result += '"'; + result += column.name(); + result += '"'; + result += ','; + } + result.resize(result.size() - 1); + + return result; +} + +void flex_table_t::connect(std::string const &conninfo) +{ + m_db_connection.reset(new pg_conn_t{conninfo}); + m_db_connection->exec("SET synchronous_commit TO off"); +} + +void flex_table_t::start(std::string const &conninfo, + std::string const &table_space) +{ + if (m_db_connection) { + throw std::runtime_error(name() + " cannot start, its already started"); + } + + connect(conninfo); + if (!table_space.empty()) { + m_table_space = " TABLESPACE \"{}\" "_format(table_space); + } + + m_db_connection->exec("SET client_min_messages = WARNING"); + + if (!m_append) { + m_db_connection->exec( + "DROP TABLE IF EXISTS \"{}\" CASCADE"_format(name())); + } + + // These _tmp tables can be left behind if we run out of disk space. + m_db_connection->exec("DROP TABLE IF EXISTS \"{}_tmp\""_format(name())); + m_db_connection->exec("RESET client_min_messages"); + + if (!m_append) { + auto const sql = build_sql_create_table(); + m_db_connection->exec(sql); + } else { + //check the columns against those in the existing table + auto const res = m_db_connection->query( + PGRES_TUPLES_OK, "SELECT * FROM \"{}\" LIMIT 0"_format(name())); + + for (auto const &column : m_columns) { + if (res.get_column_number(column.name()) < 0) { + fmt::print(stderr, "Adding new column \"{}\" to \"{}\"\n", + column.name(), name()); + m_db_connection->exec( + "ALTER TABLE \"{}\" ADD COLUMN \"{}\" {}"_format( + name(), column.name(), column.sql_type_name(m_srid))); + } + // Note: we do not verify the type or delete unused columns + } + + //TODO: change the type of the geometry column if needed - this can only change to a more permissive type + } + + prepare(); +} + +void flex_table_t::stop(bool updateable, std::string const &table_space_index) +{ + m_copy_mgr.sync(); + + if (m_append) { + teardown(); + return; + } + + std::time_t const start = std::time(nullptr); + + std::string const tblspc_sql = + table_space_index.empty() + ? "" + : " TABLESPACE \"{}\""_format(table_space_index); + + if (has_geom_column()) { + fmt::print(stderr, "Clustering table '{}' by geometry...\n", name()); + + // Notices about invalid geometries are expected and can be ignored + // because they say nothing about the validity of the geometry in OSM. + m_db_connection->exec("SET client_min_messages = WARNING"); + + std::string sql = + "CREATE TABLE \"{0}_tmp\" {1} AS SELECT * FROM \"{0}\""_format( + name(), m_table_space); + + if (m_srid != 4326) { + // libosmium assures validity of geometries in 4326. + // Transformation to another projection could make the geometry + // invalid. Therefore add a filter to drop those. + sql += " WHERE ST_IsValid(\"{}\")"_format(geom_column().name()); + } + + auto const res = m_db_connection->query( + PGRES_TUPLES_OK, + "SELECT regexp_split_to_table(postgis_lib_version(), '\\.')"); + auto const postgis_major = std::stoi(res.get_value_as_string(0, 0)); + auto const postgis_minor = std::stoi(res.get_value_as_string(1, 0)); + + sql += " ORDER BY "; + if (postgis_major == 2 && postgis_minor < 4) { + fmt::print(stderr, "Using GeoHash for clustering\n"); + if (m_srid == 4326) { + sql += "ST_GeoHash({},10)"_format(geom_column().name()); + } else { + sql += + "ST_GeoHash(ST_Transform(ST_Envelope({}),4326),10)"_format( + geom_column().name()); + } + sql += " COLLATE \"C\""; + } else { + fmt::print(stderr, "Using native order for clustering\n"); + // Since Postgis 2.4 the order function for geometries gives + // useful results. + sql += geom_column().name(); + } + + m_db_connection->exec(sql); + + m_db_connection->exec("DROP TABLE \"{}\""_format(name())); + m_db_connection->exec( + "ALTER TABLE \"{0}_tmp\" RENAME TO \"{0}\""_format(name())); + + fmt::print(stderr, "Creating geometry index on table '{}'...\n", + name()); + + // Use fillfactor 100 for un-updateable imports + m_db_connection->exec( + "CREATE INDEX ON \"{}\" USING GIST (\"{}\") {} {}"_format( + name(), geom_column().name(), + (updateable ? "" : "WITH (FILLFACTOR=100)"), tblspc_sql)); + } + + if (updateable && m_has_id_column) { + fmt::print(stderr, "Creating id index on table '{}'...\n", name()); + m_db_connection->exec( + "CREATE INDEX ON \"{}\" USING BTREE (\"{}\") {}"_format( + name(), id_column_name(), tblspc_sql)); + + if (m_srid != 4326 && has_geom_column()) { + m_db_connection->exec( + "CREATE OR REPLACE FUNCTION {}_osm2pgsql_valid()\n" + "RETURNS TRIGGER AS $$\n" + "BEGIN\n" + " IF ST_IsValid(NEW.{}) THEN \n" + " RETURN NEW;\n" + " END IF;\n" + " RETURN NULL;\n" + "END;" + "$$ LANGUAGE plpgsql;"_format(name(), geom_column().name())); + + m_db_connection->exec("CREATE TRIGGER \"{0}_osm2pgsql_valid\"" + " BEFORE INSERT OR UPDATE" + " ON \"{0}\"" + " FOR EACH ROW EXECUTE PROCEDURE" + " {0}_osm2pgsql_valid();"_format(name())); + } + } + + fmt::print(stderr, "Analyzing table '{}'...\n", name()); + m_db_connection->exec("ANALYZE \"{}\""_format(name())); + + std::time_t const end = std::time(nullptr); + fmt::print(stderr, "All postprocessing on table '{}' done in {}s.\n", + name(), end - start); + + teardown(); +} + +void flex_table_t::delete_rows_with(osmid_t id) +{ + m_copy_mgr.new_line(m_target); + m_copy_mgr.delete_object(id); +} + +pg_result_t flex_table_t::get_geom_by_id(osmid_t id) const +{ + assert(has_geom_column()); + assert(m_db_connection); + std::string const id_str = fmt::to_string(id); + char const *param_values[] = {id_str.c_str()}; + return m_db_connection->exec_prepared("get_wkb", 1, param_values); +} diff --git a/src/flex-table.hpp b/src/flex-table.hpp new file mode 100644 index 000000000..046e81e82 --- /dev/null +++ b/src/flex-table.hpp @@ -0,0 +1,191 @@ +#ifndef OSM2PGSQL_FLEX_TABLE_HPP +#define OSM2PGSQL_FLEX_TABLE_HPP + +#include "db-copy-mgr.hpp" +#include "flex-table-column.hpp" +#include "pgsql.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +/** + * An output table (in the SQL sense) for the flex backend. + */ +class flex_table_t +{ +public: + flex_table_t(std::string const &name, int srid, + std::shared_ptr const ©_thread, + bool append) + : m_name(name), m_srid(srid), m_copy_mgr(copy_thread), m_append(append) + {} + + std::string const &name() const noexcept { return m_name; } + + osmium::item_type id_type() const noexcept { return m_id_type; } + + void set_id_type(osmium::item_type type) noexcept + { + m_id_type = type; + m_has_id_column = true; + } + + std::size_t num_columns() const noexcept { return m_columns.size(); } + + std::vector::const_iterator begin() const noexcept + { + return m_columns.begin(); + } + + std::vector::const_iterator end() const noexcept + { + return m_columns.end(); + } + + char const *id_column_name() const noexcept + { + if (!m_has_id_column) { + return ""; + } + + std::size_t index = + m_columns[0].type() == table_column_type::id_num ? 0 : 1; + return m_columns[index].name().c_str(); + } + + bool has_geom_column() const noexcept + { + return m_geom_column != std::numeric_limits::max(); + } + + // XXX should we allow several geometry columns? + flex_table_column_t const &geom_column() const noexcept + { + assert(has_geom_column()); + return m_columns[m_geom_column]; + } + + int srid() const noexcept { return m_srid; } + + std::string build_sql_prepare_get_wkb() const; + + std::string build_sql_create_table() const; + + std::string build_sql_column_list() const; + + /// Does this table take objects of the specified type? + bool matches_type(osmium::item_type type) const noexcept + { + if (type == m_id_type) { + return true; + } + return m_id_type == osmium::item_type::area && + type != osmium::item_type::node; + } + + /// Map way/node/relation ID to id value used in database table column + osmid_t map_id(osmium::item_type type, osmid_t id) const noexcept + { + if (m_id_type == osmium::item_type::area && + type == osmium::item_type::relation) { + return -id; + } + return id; + } + + flex_table_column_t &add_column(std::string const &name, + std::string const &type); + + void init() + { + std::string const columns = build_sql_column_list(); + m_target = std::make_shared( + name().c_str(), id_column_name(), columns.c_str()); + } + + void connect(std::string const &conninfo); + + void commit() { m_copy_mgr.sync(); } + + void new_line() { m_copy_mgr.new_line(m_target); } + + void teardown() { m_db_connection.reset(); } + + void prepare() + { + assert(m_db_connection); + if (m_has_id_column) { + m_db_connection->exec(build_sql_prepare_get_wkb()); + } + } + + void start(std::string const &conninfo, std::string const &table_space); + + void stop(bool updateable, std::string const &table_space_index); + + void delete_rows_with(osmid_t id); + + pg_result_t get_geom_by_id(osmid_t id) const; + + db_copy_mgr_t ©_mgr() noexcept + { + return m_copy_mgr; + } + +private: + /// The name of the table + std::string m_name; + + /** + * Either empty for the default PostgreSQL tablespace, or " TABLESPACE " + * plus a tablespace name. + */ + std::string m_table_space; + + /** + * The columns in this table (The first zero, one or two columns are always + * the id columns). + */ + std::vector m_columns; + + /// Index of the geometry column in m_columns. Default means no geometry. + std::size_t m_geom_column = std::numeric_limits::max(); + + /** + * Type of Id stored in this table (node, way, relation, area, or + * undefined for any type). + */ + osmium::item_type m_id_type = osmium::item_type::undefined; + + /// The SRID all geometries in this table use. + int m_srid; + + /** + * The copy manager responsible for sending data through the COPY mechanism + * to the database server. + */ + db_copy_mgr_t m_copy_mgr; + + /// The connection to the database server. + std::unique_ptr m_db_connection; + + std::shared_ptr m_target; + + /// Are we in append mode? + bool m_append; + + /** + * Does this table have an idea column? Tables without id columns are + * possible, but can not be updated. + */ + bool m_has_id_column = false; + +}; // class flex_table_t + +#endif // OSM2PGSQL_FLEX_TABLE_HPP diff --git a/src/output-flex.cpp b/src/output-flex.cpp new file mode 100644 index 000000000..9f7986961 --- /dev/null +++ b/src/output-flex.cpp @@ -0,0 +1,1241 @@ + +#include "config.h" + +#include "expire-tiles.hpp" +#include "flex-lua.hpp" +#include "middle.hpp" +#include "options.hpp" +#include "osmtypes.hpp" +#include "output-flex.hpp" +#include "pgsql.hpp" +#include "reprojection.hpp" +#include "taginfo-impl.hpp" +#include "wkb.hpp" + +extern "C" +{ +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +// Lua can't call functions on C++ objects directly. This macro defines simple +// C "trampoline" functions which are called from Lua which get the current +// context (the output_flex_t object) and call the respective function on the +// context object. +#define TRAMPOLINE(func_name, lua_name) \ + static int lua_trampoline_##func_name(lua_State *lua_state) noexcept \ + { \ + try { \ + return static_cast(luaX_get_context(lua_state)) \ + ->func_name(); \ + } catch (std::exception const &e) { \ + return luaL_error(lua_state, "Error in '" #lua_name "': %s\n", \ + e.what()); \ + } catch (...) { \ + return luaL_error(lua_state, \ + "Unknown error in '" #lua_name "'.\n"); \ + } \ + } + +TRAMPOLINE(app_define_table, define_table) +TRAMPOLINE(app_mark, mark) +TRAMPOLINE(app_get_bbox, get_bbox) +TRAMPOLINE(table_name, name) +TRAMPOLINE(table_add_row, add_row) +TRAMPOLINE(table_columns, columns) +TRAMPOLINE(table_tostring, __tostring) + +static char const osm2pgsql_table_name[] = "osm2pgsql.table"; + +static char const *type_to_char(osmium::item_type type) noexcept +{ + switch (type) { + case osmium::item_type::node: + return "N"; + case osmium::item_type::way: + return "W"; + case osmium::item_type::relation: + return "R"; + default: + break; + } + return "X"; +} + +static void push_osm_object_to_lua_stack(lua_State *lua_state, + osmium::OSMObject const &object) +{ + assert(lua_state); + bool const has_changeset = object.changeset() != 0u; + bool const has_uid = object.uid() != 0u; + bool const has_user = object.user()[0] != '\0'; + + int size = 4 /* id,version,timestamp,tags */ + + static_cast(has_changeset) + static_cast(has_uid) + + static_cast(has_user); + + if (object.type() == osmium::item_type::way) { + size += 2; // for is_closed, nodes + } + + if (object.type() == osmium::item_type::relation) { + size += 1; // for members + } + + lua_createtable(lua_state, 0, size); + + luaX_add_table_int(lua_state, "id", object.id()); + luaX_add_table_int(lua_state, "version", object.version()); + luaX_add_table_int(lua_state, "timestamp", + object.timestamp().seconds_since_epoch()); + + if (has_changeset) { + luaX_add_table_int(lua_state, "changeset", object.changeset()); + } + + if (has_uid) { + luaX_add_table_int(lua_state, "uid", object.uid()); + } + + if (has_user) { + luaX_add_table_str(lua_state, "user", object.user()); + } + + if (object.type() == osmium::item_type::way) { + auto const &way = static_cast(object); + luaX_add_table_bool(lua_state, "is_closed", way.is_closed()); + luaX_add_table_array(lua_state, "nodes", way.nodes(), + [&](osmium::NodeRef const &wn) { + lua_pushinteger(lua_state, wn.ref()); + }); + } else if (object.type() == osmium::item_type::relation) { + auto const &relation = static_cast(object); + luaX_add_table_array( + lua_state, "members", relation.members(), + [&](osmium::RelationMember const &member) { + lua_createtable(lua_state, 0, 3); + std::array tmp{"x"}; + tmp[0] = osmium::item_type_to_char(member.type()); + luaX_add_table_str(lua_state, "type", &tmp[0]); + luaX_add_table_int(lua_state, "ref", member.ref()); + luaX_add_table_str(lua_state, "role", member.role()); + }); + } + + lua_pushliteral(lua_state, "tags"); + lua_createtable(lua_state, 0, (int)object.tags().size()); + for (auto const &tag : object.tags()) { + luaX_add_table_str(lua_state, tag.key(), tag.value()); + } + lua_rawset(lua_state, -3); +} + +void output_flex_t::write_row(flex_table_t *table, osmium::item_type id_type, + osmid_t id, std::string const &geom) +{ + assert(table); + auto ©_mgr = table->copy_mgr(); + table->new_line(); + + for (auto const &column : *table) { + if (column.is_geometry_column()) { + copy_mgr.add_hex_geom(geom); + } else if ((column.type() == table_column_type::sql) || + (column.type() == table_column_type::text)) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + auto const *const str = lua_tolstring(m_lua_state, -1, nullptr); + if (str) { + copy_mgr.add_column(str); + } else { + copy_mgr.add_null_column(); + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::boolean) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + int const ltype = lua_type(m_lua_state, -1); + switch (ltype) { + case LUA_TNIL: + copy_mgr.add_null_column(); + break; + case LUA_TBOOLEAN: + copy_mgr.add_column(lua_toboolean(m_lua_state, -1) != 0); + break; + case LUA_TNUMBER: + copy_mgr.add_column(lua_tointeger(m_lua_state, -1) != 0); + break; + case LUA_TSTRING: { + auto const *str = lua_tolstring(m_lua_state, -1, nullptr); + bool const value = (std::strcmp(str, "yes") == 0) || + (std::strcmp(str, "true") == 0); + copy_mgr.add_column(value); + } break; + default: + throw std::runtime_error{ + "Invalid type '{}' for boolean column"_format( + lua_typename(m_lua_state, ltype))}; + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::int2) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + if (lua_type(m_lua_state, -1) == LUA_TNIL) { + copy_mgr.add_null_column(); + } else { + // cast here is on okay, because the database column is only 16bit + copy_mgr.add_column((int16_t)lua_tointeger(m_lua_state, -1)); + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::int4) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + if (lua_type(m_lua_state, -1) == LUA_TNIL) { + copy_mgr.add_null_column(); + } else { + // cast here is on okay, because the database column is only 32bit + copy_mgr.add_column((int32_t)lua_tointeger(m_lua_state, -1)); + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::int8) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + if (lua_type(m_lua_state, -1) == LUA_TNIL) { + copy_mgr.add_null_column(); + } else { + copy_mgr.add_column(lua_tointeger(m_lua_state, -1)); + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::real) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + if (lua_type(m_lua_state, -1) == LUA_TNIL) { + copy_mgr.add_null_column(); + } else { + double const value = lua_tonumber(m_lua_state, -1); + copy_mgr.add_column(value); + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::hstore) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + auto const ltype = lua_type(m_lua_state, -1); + if (ltype == LUA_TNIL) { + copy_mgr.add_null_column(); + } else if (lua_type(m_lua_state, -1) == LUA_TTABLE) { + copy_mgr.new_hash(); + + lua_pushnil(m_lua_state); + while (lua_next(m_lua_state, -2) != 0) { + const char *key = lua_tostring(m_lua_state, -2); + const char *value = lua_tostring(m_lua_state, -1); + if (key == nullptr) { + int const ltype_key = lua_type(m_lua_state, -2); + throw std::runtime_error{ + "NULL key for hstore. Possibly this is due to" + "an incorrect data type '{}'."_format( + lua_typename(m_lua_state, ltype_key))}; + } + if (value == nullptr) { + int const ltype_value = lua_type(m_lua_state, -1); + throw std::runtime_error{ + "NULL key for hstore. Possibly this is due to" + "an incorrect data type '{}'."_format( + lua_typename(m_lua_state, ltype_value))}; + } + copy_mgr.add_hash_elem(key, value); + lua_pop(m_lua_state, 1); + } + + copy_mgr.finish_hash(); + } else { + throw std::runtime_error{ + "Invalid type '{}' for hstore column"_format( + lua_typename(m_lua_state, ltype))}; + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::direction) { + lua_getfield(m_lua_state, -1, column.name().c_str()); + int const ltype = lua_type(m_lua_state, -1); + switch (ltype) { + case LUA_TNIL: + copy_mgr.add_null_column(); + break; + case LUA_TBOOLEAN: + copy_mgr.add_column(lua_toboolean(m_lua_state, -1)); + break; + case LUA_TNUMBER: { + auto value = (int)lua_tointeger(m_lua_state, -1); + if (value > 1) { + value = 1; + } else if (value < -1) { + value = -1; + } + copy_mgr.add_column(value); + } break; + case LUA_TSTRING: { + auto const *str = lua_tolstring(m_lua_state, -1, nullptr); + int value = 0; + if ((std::strcmp(str, "yes") == 0) || + (std::strcmp(str, "true") == 0) || + (std::strcmp(str, "1") == 0)) { + value = 1; + } else if (std::strcmp(str, "-1") == 0) { + value = -1; + } + copy_mgr.add_column(value); + } break; + default: + throw std::runtime_error{ + "Invalid type '{}' for direction column"_format( + lua_typename(m_lua_state, ltype))}; + } + lua_pop(m_lua_state, 1); + } else if (column.type() == table_column_type::id_type) { + copy_mgr.add_column(type_to_char(id_type)); + } else if (column.type() == table_column_type::id_num) { + copy_mgr.add_column(id); + } else if (column.type() == table_column_type::area) { + if (geom.empty()) { + copy_mgr.add_null_column(); + } else { + double const area = + get_options()->reproject_area + ? ewkb::parser_t(geom).get_area( + get_options()->projection.get()) + : ewkb::parser_t(geom) + .get_area(); + copy_mgr.add_column(area); + } + } else { + throw std::runtime_error{ + "XXX Column type {} not implemented yet."_format( + column.type())}; + } + } + + copy_mgr.finish_line(); +} + +int output_flex_t::app_mark() +{ + char const *type_name = luaL_checkstring(m_lua_state, 1); + if (!type_name) { + return 0; + } + + osmium::object_id_type id = luaL_checkinteger(m_lua_state, 2); + + // fmt::print(stderr, "mark {} id:{}\n", type_name, id); + + if (type_name[0] == 'w') { + m_ways_pending_tracker.mark(id); + return 0; + } + if (type_name[0] == 'r') { + m_rels_pending_tracker.mark(id); + } + + return 0; +} + +// Gets all way nodes from the middle the first time this is called. +std::size_t output_flex_t::get_way_nodes() +{ + assert(m_context_way); + if (m_num_way_nodes == std::numeric_limits::max()) { + m_num_way_nodes = m_mid->nodes_get_list(&m_context_way->nodes()); + } + + return m_num_way_nodes; +} + +int output_flex_t::app_get_bbox() +{ + if (lua_gettop(m_lua_state) != 0) { + throw std::runtime_error{"No parameter(s) needed for get_box()"}; + } + + if (m_context_node) { + lua_pushnumber(m_lua_state, m_context_node->location().lon()); + lua_pushnumber(m_lua_state, m_context_node->location().lat()); + lua_pushnumber(m_lua_state, m_context_node->location().lon()); + lua_pushnumber(m_lua_state, m_context_node->location().lat()); + return 4; + } else if (m_context_way) { + get_way_nodes(); + auto const bbox = m_context_way->envelope(); + if (bbox.valid()) { + lua_pushnumber(m_lua_state, bbox.bottom_left().lon()); + lua_pushnumber(m_lua_state, bbox.bottom_left().lat()); + lua_pushnumber(m_lua_state, bbox.top_right().lon()); + lua_pushnumber(m_lua_state, bbox.top_right().lat()); + return 4; + } + } + + return 0; +} + +flex_table_t &output_flex_t::create_flex_table() +{ + char const *const table_name = + luaX_get_table_string(m_lua_state, "name", -1, "The table"); + + if (std::strchr(table_name, '"') != nullptr) { + throw std::runtime_error{ + "Quote characters are not allowed in table names: '{}'"_format( + table_name)}; + } + + auto const it = std::find_if(m_tables.cbegin(), m_tables.cend(), + [&table_name](flex_table_t const &table) { + return table.name() == table_name; + }); + if (it != m_tables.cend()) { + throw std::runtime_error{ + "Table with that name already exists: '{}'"_format(table_name)}; + } + + m_tables.emplace_back(table_name, get_options()->projection->target_srs(), + m_copy_thread, get_options()->append); + auto &new_table = m_tables.back(); + + lua_remove(m_lua_state, -1); + + return new_table; +} + +static void check_column_name(char const *name) +{ + if (std::strchr(name, '"') == nullptr) { + return; + } + throw std::runtime_error{ + "Quote characters (\") are not allowed in column names: '{}'"_format( + name)}; +} + +void output_flex_t::setup_id_columns(flex_table_t *table) +{ + assert(table); + lua_getfield(m_lua_state, -1, "ids"); + if (lua_type(m_lua_state, -1) != LUA_TTABLE) { + fmt::print(stderr, + "WARNING! Table '{}' doesn't have an 'ids' column. Updates" + " and expire will not work!\n", + table->name()); + lua_pop(m_lua_state, 1); // ids + return; + } + + std::string const type{ + luaX_get_table_string(m_lua_state, "type", -1, "The ids field")}; + + if (type == "node") { + table->set_id_type(osmium::item_type::node); + } else if (type == "way") { + table->set_id_type(osmium::item_type::way); + } else if (type == "relation") { + table->set_id_type(osmium::item_type::relation); + } else if (type == "area") { + table->set_id_type(osmium::item_type::area); + } else if (type == "any") { + std::string type_column_name{"osm_type"}; + lua_getfield(m_lua_state, -1, "type_column"); + if (lua_isstring(m_lua_state, -1)) { + type_column_name = lua_tolstring(m_lua_state, -1, nullptr); + check_column_name(type_column_name.c_str()); + } + lua_pop(m_lua_state, 1); // type_column + auto &column = table->add_column(type_column_name, "id_type"); + column.set_not_null_constraint(); + table->set_id_type(osmium::item_type::undefined); + } else { + throw std::runtime_error{"Unknown ids type: " + type}; + } + + char const *const name = + luaX_get_table_string(m_lua_state, "id_column", -2, "The ids field"); + check_column_name(name); + + auto &column = table->add_column(name, "id_num"); + column.set_not_null_constraint(); + lua_pop(m_lua_state, 3); // id_column, type, ids +} + +void output_flex_t::setup_flex_table_columns(flex_table_t *table) +{ + assert(table); + lua_getfield(m_lua_state, -1, "columns"); + if (lua_type(m_lua_state, -1) != LUA_TTABLE) { + throw std::runtime_error{ + "No columns defined for table '{}'."_format(table->name())}; + } + + std::size_t num_columns = 0; + lua_pushnil(m_lua_state); + while (lua_next(m_lua_state, -2) != 0) { + if (!lua_isnumber(m_lua_state, -2)) { + throw std::runtime_error{ + "The 'columns' field must contain an array"}; + } + if (!lua_istable(m_lua_state, -1)) { + throw std::runtime_error{ + "The entries in the 'columns' array must be tables"}; + } + + char const *const type = + luaX_get_table_string(m_lua_state, "type", -1, "Column entry"); + char const *const name = + luaX_get_table_string(m_lua_state, "column", -2, "Column entry"); + check_column_name(name); + + auto &new_column = table->add_column(name, type); + + if (new_column.is_linestring_column()) { + lua_getfield(m_lua_state, -3, "split_at"); + if (lua_type(m_lua_state, -1) == LUA_TNUMBER) { + new_column.set_split_at(lua_tonumber(m_lua_state, -1)); + } else if (lua_type(m_lua_state, -1) != LUA_TNIL) { + throw std::runtime_error{ + "Value of 'split_at' must be a number."}; + } + lua_pop(m_lua_state, 1); // split_at + } + + lua_pop(m_lua_state, 3); // column, type, table + ++num_columns; + } + + if (num_columns == 0) { + throw std::runtime_error{ + "No columns defined for table '{}'."_format(table->name())}; + } +} + +int output_flex_t::app_define_table() +{ + luaL_checktype(m_lua_state, 1, LUA_TTABLE); + + auto &new_table = create_flex_table(); + setup_id_columns(&new_table); + setup_flex_table_columns(&new_table); + + lua_pushlightuserdata(m_lua_state, (void *)(m_tables.size())); + luaL_getmetatable(m_lua_state, osm2pgsql_table_name); + lua_setmetatable(m_lua_state, -2); + + return 1; +} + +// Check function parameters of all osm2pgsql.table functions and return the +// flex table this function is on. +flex_table_t &output_flex_t::table_func_params(int n) +{ + if (lua_gettop(m_lua_state) != n) { + throw std::runtime_error{"Need {} parameter(s)"_format(n)}; + } + + void *user_data = lua_touserdata(m_lua_state, 1); + if (user_data == nullptr || !lua_getmetatable(m_lua_state, 1)) { + throw std::runtime_error{ + "first parameter must be of type osm2pgsql.table"}; + } + + luaL_getmetatable(m_lua_state, osm2pgsql_table_name); + if (!lua_rawequal(m_lua_state, -1, -2)) { + throw std::runtime_error{ + "first parameter must be of type osm2pgsql.table"}; + } + lua_pop(m_lua_state, 2); + + auto &table = m_tables.at(reinterpret_cast(user_data) - 1); + lua_remove(m_lua_state, 1); + return table; +} + +int output_flex_t::table_tostring() +{ + auto const &table = table_func_params(1); + + std::string const str{"osm2pgsql.table[{}]"_format(table.name())}; + lua_pushstring(m_lua_state, str.c_str()); + + return 1; +} + +int output_flex_t::table_add_row() +{ + auto &table = table_func_params(2); + luaL_checktype(m_lua_state, 1, LUA_TTABLE); + + if (m_context_node) { + add_row(&table, *m_context_node); + } else if (m_context_way) { + add_row(&table, m_context_way); + } else if (m_context_relation) { + add_row(&table, *m_context_relation); + } + + return 0; +} + +int output_flex_t::table_columns() +{ + auto const &table = table_func_params(1); + + lua_createtable(m_lua_state, (int)table.num_columns(), 0); + + int n = 0; + for (auto const &column : table) { + lua_pushinteger(m_lua_state, ++n); + lua_newtable(m_lua_state); + + luaX_add_table_str(m_lua_state, "name", column.name().c_str()); + luaX_add_table_str(m_lua_state, "type", column.type_name().c_str()); + luaX_add_table_str(m_lua_state, "sql_type", + column.sql_type_name(table.srid()).c_str()); + luaX_add_table_str(m_lua_state, "sql_modifiers", + column.sql_modifiers().c_str()); + + if (column.is_linestring_column() && column.split_at() != 0.0) { + luaX_add_table_num(m_lua_state, "split_at", column.split_at()); + } + + lua_rawset(m_lua_state, -3); + } + return 1; +} + +int output_flex_t::table_name() +{ + auto const &table = table_func_params(1); + + lua_pushstring(m_lua_state, table.name().c_str()); + + return 1; +} + +void output_flex_t::add_row(flex_table_t *table, osmium::Node const &node) +{ + assert(table); + std::string const wkb = m_builder.get_wkb_node(node.location()); + + if (wkb.empty()) { + return; + } + + m_expire.from_wkb(wkb.c_str(), node.id()); + write_row(table, osmium::item_type::node, node.id(), wkb); +} + +void output_flex_t::add_row(flex_table_t *table, osmium::Way *way) +{ + assert(table); + assert(way); + + if (get_way_nodes() <= 1U) { + return; + } + + if (!table->has_geom_column()) { + write_row(table, osmium::item_type::way, way->id(), ""); + return; + } + + if (table->geom_column().is_polygon_column()) { + if (way->is_closed()) { + auto const wkb = m_builder.get_wkb_polygon(*way); + if (!wkb.empty()) { + m_expire.from_wkb(wkb.c_str(), way->id()); + write_row(table, osmium::item_type::way, way->id(), wkb); + } + } + return; + } + + double const split_at = table->geom_column().split_at(); + auto const wkbs = m_builder.get_wkb_line(way->nodes(), split_at); + + for (auto const &wkb : wkbs) { + m_expire.from_wkb(wkb.c_str(), way->id()); + write_row(table, osmium::item_type::way, way->id(), wkb); + } +} + +void output_flex_t::add_row(flex_table_t *table, + osmium::Relation const &relation) +{ + assert(table); + + osmid_t const id = table->map_id(osmium::item_type::relation, relation.id()); + + if (!table->has_geom_column()) { + write_row(table, osmium::item_type::relation, id, ""); + return; + } + + m_buffer.clear(); + auto const num_ways = + m_mid->rel_way_members_get(relation, nullptr, m_buffer); + + if (num_ways == 0) { + return; + } + + for (auto &way : m_buffer.select()) { + m_mid->nodes_get_list(&(way.nodes())); + } + + if (table->geom_column().is_polygon_column()) { + bool const want_mp = + table->geom_column().type() == table_column_type::multipolygon; + auto const wkbs = + m_builder.get_wkb_multipolygon(relation, m_buffer, want_mp); + + for (auto const &wkb : wkbs) { + m_expire.from_wkb(wkb.c_str(), id); + write_row(table, osmium::item_type::relation, id, wkb); + } + return; + } + + if (table->geom_column().is_linestring_column()) { + double const split_at = table->geom_column().split_at(); + auto const wkbs = m_builder.get_wkb_multiline(m_buffer, split_at); + + for (auto const &wkb : wkbs) { + m_expire.from_wkb(wkb.c_str(), id); + write_row(table, osmium::item_type::relation, id, wkb); + } + } +} + +void output_flex_t::call_process_function(int index, + osmium::OSMObject const &object) +{ + // get function to call + lua_pushvalue(m_lua_state, index); + + push_osm_object_to_lua_stack(m_lua_state, object); + + if (lua_pcall(m_lua_state, 1, 0, 0)) { + throw std::runtime_error{"Failed to execute lua processing function:" + " {}"_format(lua_tostring(m_lua_state, -1))}; + } +} + +void output_flex_t::enqueue_ways(pending_queue_t &job_queue, osmid_t id, + size_t output_id, size_t &added) +{ + fmt::print(stderr, "enqueue_ways: {}/{}\n", id, output_id); + osmid_t const prev = m_ways_pending_tracker.last_returned(); + if (id_tracker::is_valid(prev) && prev >= id) { + if (prev > id) { + job_queue.push(pending_job_t(id, output_id)); + } + // already done the job + return; + } + + //make sure we get the one passed in + if (!m_ways_done_tracker->is_marked(id) && id_tracker::is_valid(id)) { + job_queue.push(pending_job_t(id, output_id)); + added++; + } + + //grab the first one or bail if its not valid + osmid_t popped = m_ways_pending_tracker.pop_mark(); + if (!id_tracker::is_valid(popped)) { + return; + } + + //get all the ones up to the id that was passed in + while (popped < id) { + if (!m_ways_done_tracker->is_marked(popped)) { + job_queue.push(pending_job_t(popped, output_id)); + added++; + } + popped = m_ways_pending_tracker.pop_mark(); + } + + //make sure to get this one as well and move to the next + if (popped > id) { + if (!m_ways_done_tracker->is_marked(popped) && + id_tracker::is_valid(popped)) { + job_queue.push(pending_job_t(popped, output_id)); + added++; + } + } +} + +void output_flex_t::pending_way(osmid_t id, int exists) +{ + if (!m_has_process_way) { + return; + } + + m_buffer.clear(); + if (!m_mid->ways_get(id, m_buffer)) { + return; + } + + /* If the flag says this object may exist already, delete it first */ + if (exists) { + way_delete(id); + // TODO: this now only has an effect when called from the iterate_ways + // call-back, so we need some alternative way to trigger this within + // osmdata_t. + const idlist_t rel_ids = m_mid->relations_using_way(id); + for (auto &mid : rel_ids) { + m_rels_pending_tracker.mark(mid); + } + } + + auto &way = m_buffer.get(0); + + m_context_way = &way; + call_process_function(2, way); + m_context_way = nullptr; + m_num_way_nodes = std::numeric_limits::max(); +} + +void output_flex_t::enqueue_relations(pending_queue_t &job_queue, osmid_t id, + size_t output_id, size_t &added) +{ + if (!m_has_process_relation) { + return; + } + + osmid_t const prev = m_rels_pending_tracker.last_returned(); + if (id_tracker::is_valid(prev) && prev >= id) { + if (prev > id) { + job_queue.push(pending_job_t(id, output_id)); + } + // already done the job + return; + } + + //make sure we get the one passed in + if (id_tracker::is_valid(id)) { + job_queue.push(pending_job_t(id, output_id)); + added++; + } + + //grab the first one or bail if its not valid + osmid_t popped = m_rels_pending_tracker.pop_mark(); + if (!id_tracker::is_valid(popped)) { + return; + } + + //get all the ones up to the id that was passed in + while (popped < id) { + job_queue.push(pending_job_t(popped, output_id)); + added++; + popped = m_rels_pending_tracker.pop_mark(); + } + + //make sure to get this one as well and move to the next + if (popped > id) { + if (id_tracker::is_valid(popped)) { + job_queue.push(pending_job_t(popped, output_id)); + added++; + } + } +} + +void output_flex_t::pending_relation(osmid_t id, int exists) +{ + if (!m_has_process_relation) { + return; + } + + // Try to fetch the relation from the DB + // Note that we cannot use the global buffer here because + // we cannot keep a reference to the relation and an autogrow buffer + // might be relocated when more data is added. + m_rels_buffer.clear(); + if (!m_mid->relations_get(id, m_rels_buffer)) { + return; + } + + // If the flag says this object may exist already, delete it first. + if (exists) { + delete_from_tables(osmium::item_type::relation, id); + } + + auto const &relation = m_rels_buffer.get(0); + + m_buffer.clear(); + auto const num_ways = + m_mid->rel_way_members_get(relation, nullptr, m_buffer); + + if (num_ways == 0) { + return; + } + + for (auto &w : m_buffer.select()) { + m_mid->nodes_get_list(&(w.nodes())); + } + + m_context_relation = &relation; + call_process_function(3, relation); + m_context_relation = nullptr; +} + +void output_flex_t::commit() +{ + for (auto &table : m_tables) { + table.commit(); + } + + // Run Lua garbage collection + lua_gc(m_lua_state, LUA_GCCOLLECT, 0); +} + +std::shared_ptr output_flex_t::read_userdata() const +{ + // If a previous run of this function already got the userdata, return it. + if (m_userdata) { + return m_userdata; + } + + // If there is no userdata, return an empty string. + lua_getglobal(m_lua_state, "osm2pgsql"); + lua_getfield(m_lua_state, -1, "userdata"); + if (lua_type(m_lua_state, -1) == LUA_TNIL) { + lua_pop(m_lua_state, 2); // userdata, osm2pgsql + m_userdata.reset(new std::string{}); + return m_userdata; + } + + lua_pop(m_lua_state, 1); // userdata + + luaL_dostring( + m_lua_state, + "osm2pgsql._userdata = osm2pgsql.messagepack.pack(osm2pgsql.userdata)"); + lua_getfield(m_lua_state, -1, "_userdata"); + + std::size_t len = 0; + char const *str = lua_tolstring(m_lua_state, -1, &len); + m_userdata.reset(new std::string(str, len)); + + // Now that we have read the user data we can clean it up and recover the + // memory. + lua_pushnil(m_lua_state); + lua_rawset(m_lua_state, -3); + lua_pushnil(m_lua_state); + lua_setfield(m_lua_state, -2, "userdata"); + lua_gc(m_lua_state, LUA_GCCOLLECT, 0); + + lua_pop(m_lua_state, 1); // osm2pgsql + + return m_userdata; +} + +void output_flex_t::stop(osmium::thread::Pool *pool) +{ + for (auto &table : m_tables) { + pool->submit([&]() { + table.stop(m_options.slim & !m_options.droptemp, + m_options.tblsmain_data.get_value_or("")); + }); + } + + if (m_options.expire_tiles_zoom_min > 0) { + m_expire.output_and_destroy(m_options.expire_tiles_filename.c_str(), + m_options.expire_tiles_zoom_min); + } +} + +void output_flex_t::node_add(osmium::Node const &node) +{ + if (!m_has_process_node) { + return; + } + + m_context_node = &node; + call_process_function(1, node); + m_context_node = nullptr; +} + +void output_flex_t::way_add(osmium::Way *way) +{ + assert(way); + + if (!m_has_process_way) { + return; + } + + m_context_way = way; + call_process_function(2, *way); + m_context_way = nullptr; + m_num_way_nodes = std::numeric_limits::max(); +} + +void output_flex_t::relation_add(osmium::Relation const &relation) +{ + if (!m_has_process_relation) { + return; + } + + m_context_relation = &relation; + call_process_function(3, relation); + m_context_relation = nullptr; +} + +void output_flex_t::delete_from_table(flex_table_t *table, + osmium::item_type type, osmid_t osm_id) +{ + assert(table); + auto const id = table->map_id(type, osm_id); + auto const result = table->get_geom_by_id(id); + if (m_expire.from_result(result, id) != 0) { + table->delete_rows_with(id); + } +} + +void output_flex_t::delete_from_tables(osmium::item_type type, osmid_t osm_id) +{ + for (auto &table : m_tables) { + if (table.matches_type(type)) { + delete_from_table(&table, type, osm_id); + } + } +} + +/* Delete is easy, just remove all traces of this object. We don't need to + * worry about finding objects that depend on it, since the same diff must + * contain the change for that also. */ +void output_flex_t::node_delete(osmid_t osm_id) +{ + delete_from_tables(osmium::item_type::node, osm_id); +} + +void output_flex_t::way_delete(osmid_t osm_id) +{ + delete_from_tables(osmium::item_type::way, osm_id); +} + +void output_flex_t::relation_delete(osmid_t osm_id) +{ + delete_from_tables(osmium::item_type::relation, osm_id); +} + +void output_flex_t::node_modify(osmium::Node const &node) +{ + node_delete(node.id()); + node_add(node); +} + +void output_flex_t::way_modify(osmium::Way *way) +{ + way_delete(way->id()); + way_add(way); +} + +void output_flex_t::relation_modify(osmium::Relation const &rel) +{ + relation_delete(rel.id()); + relation_add(rel); +} + +void output_flex_t::init_clone() +{ + for (auto &table : m_tables) { + table.connect(m_options.database_options.conninfo()); + table.prepare(); + } +} + +void output_flex_t::start() +{ + for (auto &table : m_tables) { + table.start(m_options.database_options.conninfo(), + m_options.tblsmain_data.get_value_or("")); + } +} + +std::shared_ptr +output_flex_t::clone(std::shared_ptr const &mid, + std::shared_ptr const ©_thread) const +{ + return std::shared_ptr(new output_flex_t{ + mid, *get_options(), copy_thread, true, read_userdata()}); +} + +output_flex_t::output_flex_t( + std::shared_ptr const &mid, options_t const &o, + std::shared_ptr const ©_thread, bool is_clone, + std::shared_ptr userdata) +: output_t(mid, o), m_builder(o.projection), + m_expire(o.expire_tiles_zoom, o.expire_tiles_max_bbox, o.projection), + m_ways_done_tracker(new id_tracker{}), + m_buffer(32768, osmium::memory::Buffer::auto_grow::yes), + m_rels_buffer(1024, osmium::memory::Buffer::auto_grow::yes), + m_copy_thread(copy_thread), m_stage(is_clone ? 2U : 1U) +{ + init_lua(m_options.style, userdata); + + for (auto &table : m_tables) { + table.init(); + } + + if (is_clone) { + init_clone(); + } +} + +void output_flex_t::init_lua(std::string const &filename, + std::shared_ptr userdata) +{ + m_lua_state = luaL_newstate(); + + // Set up global lua libs + luaL_openlibs(m_lua_state); + + // Set up global "osm2pgsql" object + lua_newtable(m_lua_state); + + luaX_add_table_str(m_lua_state, "version", VERSION); + luaX_add_table_int(m_lua_state, "srid", + get_options()->projection->target_srs()); + luaX_add_table_str(m_lua_state, "mode", + m_options.append ? "append" : "create"); + luaX_add_table_int(m_lua_state, "stage", m_stage); + + if (userdata) { + luaX_add_table_str(m_lua_state, "_userdata", userdata->data(), + userdata->size()); + } + + // Add empty "userdata" table + lua_pushliteral(m_lua_state, "userdata"); + lua_newtable(m_lua_state); + lua_rawset(m_lua_state, -3); + + luaX_add_table_func(m_lua_state, "define_table", + lua_trampoline_app_define_table); + luaX_add_table_func(m_lua_state, "mark", lua_trampoline_app_mark); + luaX_add_table_func(m_lua_state, "get_bbox", lua_trampoline_app_get_bbox); + + lua_setglobal(m_lua_state, "osm2pgsql"); + + if (luaL_dostring(m_lua_state, R"( + +function osm2pgsql._define_table_impl(_type, _name, _columns) + return osm2pgsql.define_table{ + name = _name, + ids = { type = _type, id_column = _type .. '_id' }, + columns = _columns, + } +end + +function osm2pgsql.define_node_table(_name, _columns) + return osm2pgsql._define_table_impl('node', _name, _columns) +end + +function osm2pgsql.define_way_table(_name, _columns) + return osm2pgsql._define_table_impl('way', _name, _columns) +end + +function osm2pgsql.define_relation_table(_name, _columns) + return osm2pgsql._define_table_impl('relation', _name, _columns) +end + +function osm2pgsql.define_area_table(_name, _columns) + return osm2pgsql._define_table_impl('area', _name, _columns) +end + + )")) { + throw std::runtime_error{"Internal error: Lua setup"}; + } + + if (luaL_dostring(m_lua_state, + "osm2pgsql.messagepack = require('MessagePack')")) { + throw std::runtime_error{"loading MessagePack failed"}; + } + + if (userdata && !userdata->empty()) { + if (luaL_dostring( + m_lua_state, + "osm2pgsql.userdata = " + "osm2pgsql.messagepack.unpack(osm2pgsql._userdata)")) { + throw std::runtime_error{"setting userdata failed: {}"_format( + lua_tostring(m_lua_state, -1))}; + } + lua_getglobal(m_lua_state, "osm2pgsql"); + lua_pushnil(m_lua_state); + lua_setfield(m_lua_state, -2, "_userdata"); + lua_pop(m_lua_state, 1); // osm2pgsql + userdata.reset(); + } + + luaX_set_context(m_lua_state, this); + + // Define "osmpgsql.table" metatable + if (luaL_newmetatable(m_lua_state, osm2pgsql_table_name) != 1) { + throw std::runtime_error{"newmetatable failed"}; + } + lua_pushvalue(m_lua_state, -1); + lua_setfield(m_lua_state, -2, "__index"); + luaX_add_table_func(m_lua_state, "__tostring", + lua_trampoline_table_tostring); + luaX_add_table_func(m_lua_state, "add_row", lua_trampoline_table_add_row); + luaX_add_table_func(m_lua_state, "name", lua_trampoline_table_name); + luaX_add_table_func(m_lua_state, "columns", lua_trampoline_table_columns); + lua_pop(m_lua_state, 1); + + // Load user config file + if (luaL_dofile(m_lua_state, filename.c_str())) { + throw std::runtime_error{"Error loading lua config: {}"_format( + lua_tostring(m_lua_state, -1))}; + } + + // Check that the process_* functions are available and store them in the + // Lua stack for fast access later. + lua_getglobal(m_lua_state, "osm2pgsql"); + lua_getfield(m_lua_state, 1, "process_node"); + if (lua_type(m_lua_state, -1) == LUA_TFUNCTION) { + m_has_process_node = true; + } + lua_getfield(m_lua_state, 1, "process_way"); + if (lua_type(m_lua_state, -1) == LUA_TFUNCTION) { + m_has_process_way = true; + } + lua_getfield(m_lua_state, 1, "process_relation"); + if (lua_type(m_lua_state, -1) == LUA_TFUNCTION) { + m_has_process_relation = true; + } + lua_remove(m_lua_state, 1); +} + +output_flex_t::~output_flex_t() noexcept { lua_close(m_lua_state); } + +size_t output_flex_t::pending_count() const +{ + return m_ways_pending_tracker.size() + m_rels_pending_tracker.size(); +} + +void output_flex_t::merge_pending_relations(output_t *other) +{ + auto *opgsql = dynamic_cast(other); + if (opgsql) { + osmid_t id; + while (id_tracker::is_valid( + (id = opgsql->m_rels_pending_tracker.pop_mark()))) { + m_rels_pending_tracker.mark(id); + } + } +} + +void output_flex_t::merge_expire_trees(output_t *other) +{ + auto *opgsql = dynamic_cast(other); + if (opgsql) { + m_expire.merge_and_destroy(opgsql->m_expire); + } +} diff --git a/src/output-flex.hpp b/src/output-flex.hpp new file mode 100644 index 000000000..061eb87a8 --- /dev/null +++ b/src/output-flex.hpp @@ -0,0 +1,146 @@ +#ifndef OSM2PGSQL_OUTPUT_FLEX_HPP +#define OSM2PGSQL_OUTPUT_FLEX_HPP + +#include "db-copy.hpp" +#include "expire-tiles.hpp" +#include "flex-table-column.hpp" +#include "flex-table.hpp" +#include "format.hpp" +#include "id-tracker.hpp" +#include "osmium-builder.hpp" +#include "output.hpp" +#include "table.hpp" +#include "tagtransform.hpp" + +#include + +extern "C" +{ +#include +} + +#include +#include +#include +#include +#include + +class output_flex_t : public output_t +{ +public: + output_flex_t(std::shared_ptr const &mid, + options_t const &options, + std::shared_ptr const ©_thread, + bool is_clone = false, + std::shared_ptr userdata = nullptr); + + output_flex_t(output_flex_t const &) = delete; + output_flex_t &operator=(output_flex_t const &) = delete; + + output_flex_t(output_flex_t &&) = delete; + output_flex_t &operator=(output_flex_t &&) = delete; + + virtual ~output_flex_t() noexcept; + + std::shared_ptr + clone(std::shared_ptr const &mid, + std::shared_ptr const ©_thread) const override; + + void start() override; + void stop(osmium::thread::Pool *pool) override; + void commit() override; + + void enqueue_ways(pending_queue_t &job_queue, osmid_t id, size_t output_id, + size_t &added) override; + void pending_way(osmid_t id, int exists) override; + + void enqueue_relations(pending_queue_t &job_queue, osmid_t id, + size_t output_id, size_t &added) override; + void pending_relation(osmid_t id, int exists) override; + + void node_add(osmium::Node const &node) override; + void way_add(osmium::Way *way) override; + void relation_add(osmium::Relation const &rel) override; + + void node_modify(osmium::Node const &node) override; + void way_modify(osmium::Way *way) override; + void relation_modify(osmium::Relation const &rel) override; + + void node_delete(osmid_t id) override; + void way_delete(osmid_t id) override; + void relation_delete(osmid_t id) override; + + size_t pending_count() const override; + + void merge_pending_relations(output_t *other) override; + void merge_expire_trees(output_t *other) override; + + int app_define_table(); + int app_mark(); + int app_get_bbox(); + + int table_tostring(); + int table_add_row(); + int table_name(); + int table_columns(); + +private: + void init_clone(); + + void call_process_function(int index, osmium::OSMObject const &object); + + std::shared_ptr read_userdata() const; + + void init_lua(std::string const &filename, + std::shared_ptr userdata); + + flex_table_t &create_flex_table(); + void setup_id_columns(flex_table_t *table); + void setup_flex_table_columns(flex_table_t *table); + + flex_table_t &table_func_params(int n); + + void write_row(flex_table_t *table, osmium::item_type id_type, osmid_t id, + std::string const &geom); + + void add_row(flex_table_t *table, osmium::Node const &node); + void add_row(flex_table_t *table, osmium::Way *way); + void add_row(flex_table_t *table, osmium::Relation const &relation); + + void delete_from_table(flex_table_t *table, osmium::item_type type, + osmid_t osm_id); + void delete_from_tables(osmium::item_type type, osmid_t osm_id); + + std::size_t get_way_nodes(); + + std::vector m_tables; + + geom::osmium_builder_t m_builder; + expire_tiles m_expire; + + id_tracker m_ways_pending_tracker; + id_tracker m_rels_pending_tracker; + std::shared_ptr m_ways_done_tracker; + + osmium::memory::Buffer m_buffer; + osmium::memory::Buffer m_rels_buffer; + + std::shared_ptr m_copy_thread; + lua_State *m_lua_state = nullptr; + + osmium::Node const *m_context_node = nullptr; + osmium::Way *m_context_way = nullptr; + osmium::Relation const *m_context_relation = nullptr; + + mutable std::shared_ptr m_userdata = nullptr; + + std::size_t m_num_way_nodes = std::numeric_limits::max(); + + bool m_has_process_node = false; + bool m_has_process_way = false; + bool m_has_process_relation = false; + + uint8_t m_stage; +}; + +#endif // OSM2PGSQL_OUTPUT_FLEX_HPP diff --git a/src/output.cpp b/src/output.cpp index 1040cb09c..2ed41dfb9 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -1,5 +1,6 @@ #include "db-copy.hpp" #include "format.hpp" +#include "output-flex.hpp" #include "output-gazetteer.hpp" #include "output-multi.hpp" #include "output-null.hpp" @@ -143,6 +144,10 @@ output_t::create_outputs(std::shared_ptr const &mid, outputs.push_back( std::make_shared(mid, options, copy_thread)); + } else if (options.output_backend == "flex") { + outputs.push_back( + std::make_shared(mid, options, copy_thread)); + } else if (options.output_backend == "gazetteer") { outputs.push_back( std::make_shared(mid, options, copy_thread)); @@ -156,7 +161,7 @@ output_t::create_outputs(std::shared_ptr const &mid, } else { throw std::runtime_error{ "Output backend `{}' not recognised. Should be one " - "of [pgsql, gazetteer, null, multi].\n"_format( + "of [pgsql, flex, gazetteer, null, multi].\n"_format( options.output_backend)}; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 90eee6aec..896863aad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,6 +45,12 @@ set_test(test-middle) set_test(test-options-database LABELS NoDB) set_test(test-options-parse LABELS NoDB) set_test(test-options-projection) +set_test(test-output-flex) +set_test(test-output-flex-area) +set_test(test-output-flex-extra) +set_test(test-output-flex-tablespace LABELS Tablespace) +set_test(test-output-flex-update) +set_test(test-output-flex-validgeom) set_test(test-output-gazetteer) set_test(test-output-multi-line) set_test(test-output-multi-point) diff --git a/tests/common-options.hpp b/tests/common-options.hpp index 64a0cfad4..4c875044e 100644 --- a/tests/common-options.hpp +++ b/tests/common-options.hpp @@ -55,6 +55,14 @@ class opt_t return *this; } + opt_t &flex(char const *style) + { + m_opt.output_backend = "flex"; + m_opt.style = TESTDATA_DIR; + m_opt.style += style; + return *this; + } + opt_t &flatnodes() { m_opt.flat_node_file = diff --git a/tests/data/test_output_flex.lua b/tests/data/test_output_flex.lua new file mode 100644 index 000000000..4afb881aa --- /dev/null +++ b/tests/data/test_output_flex.lua @@ -0,0 +1,142 @@ + +if osm2pgsql.srid == 3857 then + max_length = 100000 +else + max_length = 1 +end + +tables = {} + +tables.point = osm2pgsql.define_node_table("osm2pgsql_test_point", { + { column = 'tags', type = 'hstore' }, + { column = 'way', type = 'point' }, +}) + +tables.line = osm2pgsql.define_table{ + name = "osm2pgsql_test_line", + ids = { type = 'way', id_column = 'osm_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'name', type = 'text' }, + { column = 'way', type = 'linestring', split_at = max_length }, + } +} + +tables.polygon = osm2pgsql.define_table{ + name = "osm2pgsql_test_polygon", + ids = { type = 'area', id_column = 'osm_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'name', type = 'text' }, + { column = 'way', type = 'geometry' }, + { column = 'way_area', type = 'area' }, + } +} + +tables.route = osm2pgsql.define_table{ + name = "osm2pgsql_test_route", + ids = { type = 'relation', id_column = 'osm_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'way', type = 'multilinestring' }, + } +} + +function is_empty(some_table) + return next(some_table) == nil +end + +function clean_tags(tags) + tags.odbl = nil + tags.created_by = nil + tags.source = nil + tags["source:ref"] = nil + tags["source:name"] = nil +end + +function is_polygon(tags) + if tags.aeroway + or tags.amenity + or tags.area + or tags.building + or tags.harbour + or tags.historic + or tags.landuse + or tags.leisure + or tags.man_made + or tags.military + or tags.natural + or tags.office + or tags.place + or tags.power + or tags.public_transport + or tags.shop + or tags.sport + or tags.tourism + or tags.water + or tags.waterway + or tags.wetland + or tags['abandoned:aeroway'] + or tags['abandoned:amenity'] + or tags['abandoned:building'] + or tags['abandoned:landuse'] + or tags['abandoned:power'] + or tags['area:highway'] + then + return true + else + return false + end +end + +function osm2pgsql.process_node(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + tables.point:add_row({ + tags = data.tags, + }) +end + +function osm2pgsql.process_way(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + if is_polygon(data.tags) then + tables.polygon:add_row({ + tags = data.tags, + name = data.tags.name, + }) + else + tables.line:add_row({ + tags = data.tags, + name = data.tags.name, + }) + end +end + +function osm2pgsql.process_relation(data) + clean_tags(data.tags) + if is_empty(data.tags) then + return + end + + if data.tags.type == 'multipolygon' or data.tags.type == 'boundary' then + tables.polygon:add_row({ + tags = data.tags, + name = data.tags.name, + }) + return + end + + if data.tags.type == 'route' then + tables.route:add_row({ + tags = data.tags, + }) + end +end + diff --git a/tests/data/test_output_flex_extra.lua b/tests/data/test_output_flex_extra.lua new file mode 100644 index 000000000..a9cd65d5f --- /dev/null +++ b/tests/data/test_output_flex_extra.lua @@ -0,0 +1,85 @@ + +tables = {} + +tables.highways = osm2pgsql.define_table{ + name = "osm2pgsql_test_highways", + ids = { type = 'way', id_column = 'way_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'refs', type = 'text' }, + { column = 'min_x', type = 'real' }, + { column = 'min_y', type = 'real' }, + { column = 'max_x', type = 'real' }, + { column = 'max_y', type = 'real' }, + { column = 'geom', type = 'linestring' }, + } +} + +tables.routes = osm2pgsql.define_table{ + name = "osm2pgsql_test_routes", + ids = { type = 'relation', id_column = 'rel_id' }, + columns = { + { column = 'tags', type = 'hstore' }, + { column = 'members', type = 'text' }, + { column = 'geom', type = 'multilinestring' }, + } +} + +function osm2pgsql.process_way(data) + if osm2pgsql.stage == 1 then + osm2pgsql.mark('w', data.id) + return + end + + local row = { + tags = data.tags, + } + + row.min_x, row.min_y, row.max_x, row.max_y = osm2pgsql.get_bbox() + + if not osm2pgsql.userdata.by_way_id then + osm2pgsql.userdata.by_way_id = {} + end + + -- if there is any data from relations, add it in + local d = osm2pgsql.userdata.by_way_id[data.id] + if d then + row.refs = table.concat(d.refs, ',') + -- row.rel_ids = '{' .. table.concat(d.ids, ',') .. '}' + end + + tables.highways:add_row(row) +end + +function osm2pgsql.process_relation(data) + if data.tags.type ~= 'route' then + return + end + + if not osm2pgsql.userdata.by_way_id then + osm2pgsql.userdata.by_way_id = {} + end + + local mlist = {} + for i, member in ipairs(data.members) do + if member.type == 'w' then + osm2pgsql.mark('w', member.ref) + if not osm2pgsql.userdata.by_way_id[member.ref] then + osm2pgsql.userdata.by_way_id[member.ref] = { + ids = {}, + refs = {} + } + end + local d = osm2pgsql.userdata.by_way_id[member.ref] + table.insert(d.ids, data.id) + table.insert(d.refs, data.tags.ref) + mlist[#mlist + 1] = member.ref + end + end + + tables.routes:add_row({ + tags = data.tags, + members = table.concat(mlist, ',') + }) +end + diff --git a/tests/test-output-flex-area.cpp b/tests/test-output-flex-area.cpp new file mode 100644 index 000000000..b56361994 --- /dev/null +++ b/tests/test-output-flex-area.cpp @@ -0,0 +1,59 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +TEST_CASE("default projection") +{ + options_t const options = + testing::opt_t().slim().flex("test_output_flex.lua"); + + REQUIRE_NOTHROW(db.run_file(options, "test_output_pgsql_area.osm")); + + auto conn = db.db().connect(); + + REQUIRE(2 == conn.get_count("osm2pgsql_test_polygon")); + conn.assert_double( + 1.23927e+10, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='poly'"); + conn.assert_double( + 9.91828e+10, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='multi'"); +} + +TEST_CASE("latlon projection") +{ + options_t const options = + testing::opt_t().slim().flex("test_output_flex.lua").srs(PROJ_LATLONG); + + REQUIRE_NOTHROW(db.run_file(options, "test_output_pgsql_area.osm")); + + auto conn = db.db().connect(); + + REQUIRE(2 == conn.get_count("osm2pgsql_test_polygon")); + conn.assert_double( + 1, "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='poly'"); + conn.assert_double( + 8, "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='multi'"); +} + +TEST_CASE("latlon projection with way_area reprojection") +{ + options_t options = + testing::opt_t().slim().flex("test_output_flex.lua").srs(PROJ_LATLONG); + options.reproject_area = true; + + REQUIRE_NOTHROW(db.run_file(options, "test_output_pgsql_area.osm")); + + auto conn = db.db().connect(); + + REQUIRE(2 == conn.get_count("osm2pgsql_test_polygon")); + conn.assert_double( + 1.23927e+10, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='poly'"); + conn.assert_double( + 9.91828e+10, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE name='multi'"); +} diff --git a/tests/test-output-flex-extra.cpp b/tests/test-output-flex-extra.cpp new file mode 100644 index 000000000..b610a090d --- /dev/null +++ b/tests/test-output-flex-extra.cpp @@ -0,0 +1,91 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +TEST_CASE("relation data on ways") +{ + // create database with three ways and a relation on two of them + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().flex("test_output_flex_extra.lua"), + "n10 v1 dV x10.0 y10.0\n" + "n11 v1 dV x10.0 y10.2\n" + "n12 v1 dV x10.2 y10.2\n" + "n13 v1 dV x10.2 y10.0\n" + "n14 v1 dV x10.3 y10.0\n" + "n15 v1 dV x10.4 y10.0\n" + "w20 v1 dV Thighway=primary Nn10,n11,n12\n" + "w21 v1 dV Thighway=secondary Nn12,n13\n" + "w22 v1 dV Thighway=secondary Nn13,n14,n15\n" + "r30 v1 dV Ttype=route,ref=X11 Mw20@,w21@\n")); + + auto conn = db.db().connect(); + + CHECK(3 == conn.get_count("osm2pgsql_test_highways")); + CHECK(1 == conn.get_count("osm2pgsql_test_routes")); + + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'primary'")); + CHECK(2 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'secondary'")); + + CHECK(2 == conn.get_count("osm2pgsql_test_highways", "refs = 'X11'")); + CHECK(1 == conn.get_count("osm2pgsql_test_highways", "refs IS NULL")); + + CHECK(1 == conn.get_count("osm2pgsql_test_highways", "abs(min_x - 10.0) < 0.01 AND abs(min_y - 10.0) < 0.01 AND abs(max_x - 10.2) < 0.01 AND abs(max_y - 10.2) < 0.01")); + + CHECK(1 == conn.get_count("osm2pgsql_test_routes", "members = '20,21'")); + + // add the third way to the relation + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex_extra.lua"), + "r30 v2 dV Ttype=route,ref=X11 Mw20@,w21@,w22@\n")); + + CHECK(3 == conn.get_count("osm2pgsql_test_highways")); + CHECK(1 == conn.get_count("osm2pgsql_test_routes")); + + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'primary'")); + CHECK(2 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'secondary'")); + + CHECK(3 == conn.get_count("osm2pgsql_test_highways", "refs = 'X11'")); + CHECK(0 == conn.get_count("osm2pgsql_test_highways", "refs IS NULL")); + CHECK(1 == conn.get_count("osm2pgsql_test_routes", "members = '20,21,22'")); + + // remove the second way from the relation and delete it + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex_extra.lua"), + "w21 v2 dD\n" + "r30 v3 dV Ttype=route,ref=X11 Mw20@,w22@\n")); + + CHECK(2 == conn.get_count("osm2pgsql_test_highways")); + CHECK(1 == conn.get_count("osm2pgsql_test_routes")); + + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'primary'")); + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'secondary'")); + + CHECK(2 == conn.get_count("osm2pgsql_test_highways", "refs = 'X11'")); + CHECK(0 == conn.get_count("osm2pgsql_test_highways", "refs IS NULL")); + CHECK(1 == conn.get_count("osm2pgsql_test_routes", "members = '20,22'")); + + // delete the relation, leaving two ways + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex_extra.lua"), + "r30 v4 dD\n")); + + CHECK(2 == conn.get_count("osm2pgsql_test_highways")); + CHECK(0 == conn.get_count("osm2pgsql_test_routes")); + + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'primary'")); + CHECK(1 == conn.get_count("osm2pgsql_test_highways", + "tags->'highway' = 'secondary'")); + + CHECK(0 == conn.get_count("osm2pgsql_test_highways", "refs = 'X11'")); + CHECK(2 == conn.get_count("osm2pgsql_test_highways", "refs IS NULL")); +} diff --git a/tests/test-output-flex-tablespace.cpp b/tests/test-output-flex-tablespace.cpp new file mode 100644 index 000000000..194d98941 --- /dev/null +++ b/tests/test-output-flex-tablespace.cpp @@ -0,0 +1,36 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +static void require_tables(pg::conn_t const &conn) +{ + conn.require_has_table("osm2pgsql_test_point"); + conn.require_has_table("osm2pgsql_test_line"); + conn.require_has_table("osm2pgsql_test_polygon"); +} + +TEST_CASE("simple import with tables spaces") +{ + { + auto conn = db.db().connect(); + REQUIRE(1 == + conn.get_count("pg_tablespace", "spcname = 'tablespacetest'")); + } + + options_t options = testing::opt_t().slim().flex("test_output_flex.lua"); + options.tblsslim_index = "tablespacetest"; + options.tblsslim_data = "tablespacetest"; + + REQUIRE_NOTHROW(db.run_file(options, "liechtenstein-2013-08-03.osm.pbf")); + + auto conn = db.db().connect(); + require_tables(conn); + + REQUIRE(1362 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(2932 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(4136 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(35 == conn.get_count("osm2pgsql_test_route")); +} diff --git a/tests/test-output-flex-update.cpp b/tests/test-output-flex-update.cpp new file mode 100644 index 000000000..babe485f1 --- /dev/null +++ b/tests/test-output-flex-update.cpp @@ -0,0 +1,224 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +TEST_CASE("updating a node") +{ + // import a node... + REQUIRE_NOTHROW( + db.run_import(testing::opt_t().slim().flex("test_output_flex.lua"), + "n10 v1 dV x10 y10\n")); + + auto conn = db.db().connect(); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + + // give the node a tag... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "n10 v2 dV x10 y10 Tamenity=restaurant\n")); + + REQUIRE(1 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == + conn.get_count("osm2pgsql_test_point", + "node_id = 10 AND tags->'amenity' = 'restaurant'")); + + SECTION("remove the tag from node") + { + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "n10 v3 dV x10 y10\n")); + } + + SECTION("delete the node") + { + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "n10 v3 dD\n")); + } + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); +} + +TEST_CASE("updating a way") +{ + // import a simple way... + REQUIRE_NOTHROW( + db.run_import(testing::opt_t().slim().flex("test_output_flex.lua"), + "n10 v1 dV x10.0 y10.1\n" + "n11 v1 dV x10.1 y10.2\n" + "w20 v1 dV Thighway=primary Nn10,n11\n")); + + auto conn = db.db().connect(); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = 'primary' " + "AND ST_NumPoints(way) = 2")); + + // now change the way itself... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v2 dV Thighway=secondary Nn10,n11\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = " + "'secondary' AND ST_NumPoints(way) = 2")); + + // now change a node in the way... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "n10 v2 dV x10.0 y10.3\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = " + "'secondary' AND ST_NumPoints(way) = 2")); + + // now add a node to the way... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "n12 v1 dV x10.2 y10.1\n" + "w20 v3 dV Thighway=residential Nn10,n11,n12\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = " + "'residential' AND ST_NumPoints(way) = 3")); + + // now delete the way... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v4 dD\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); +} + +TEST_CASE("ways as linestrings and polygons") +{ + // import a simple way... + REQUIRE_NOTHROW( + db.run_import(testing::opt_t().slim().flex("test_output_flex.lua"), + "n10 v1 dV x10.0 y10.0\n" + "n11 v1 dV x10.0 y10.2\n" + "n12 v1 dV x10.2 y10.2\n" + "n13 v1 dV x10.2 y10.0\n" + "w20 v1 dV Tbuilding=yes Nn10,n11,n12,n13,n14,n10\n")); + + auto conn = db.db().connect(); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon", + "osm_id = 20 AND tags->'building' = 'yes' AND " + "ST_GeometryType(way) = 'ST_Polygon'")); + + // now change the way tags... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v2 dV Thighway=secondary Nn10,n11,n12,n13,n14,n10\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == + conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = 'secondary' AND " + "ST_GeometryType(way) = 'ST_LineString'")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon")); + + // now remove a node from the way... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v3 dV Thighway=secondary Nn10,n11,n12,n13,n14\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == + conn.get_count("osm2pgsql_test_line", + "osm_id = 20 AND tags->'highway' = 'secondary' AND " + "ST_GeometryType(way) = 'ST_LineString'")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon")); + + // now change the tag back to an area tag (but the way is not closed)... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v4 dV Tbuilding=yes Nn10,n11,n12,n13,n14\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon")); + + // now close the way again + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "w20 v5 dV Tbuilding=yes Nn10,n11,n12,n13,n14,n10\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon", + "osm_id = 20 AND tags->'building' = 'yes' AND " + "ST_GeometryType(way) = 'ST_Polygon'")); +} + +TEST_CASE("multipolygons") +{ + // import a simple multipolygon relation... + REQUIRE_NOTHROW( + db.run_import(testing::opt_t().slim().flex("test_output_flex.lua"), + "n10 v1 dV x10.0 y10.0\n" + "n11 v1 dV x10.0 y10.2\n" + "n12 v1 dV x10.2 y10.2\n" + "n13 v1 dV x10.2 y10.0\n" + "w20 v1 dV Nn10,n11,n12,n13,n14,n10\n" + "r30 v1 dV Ttype=multipolygon,building=yes Mw20@\n")); + + auto conn = db.db().connect(); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon", + "osm_id = -30 AND tags->'building' = 'yes' AND " + "ST_GeometryType(way) = 'ST_Polygon'")); + + // change tags on that relation... + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "r30 v2 dV Ttype=multipolygon,building=yes,name=Shed Mw20@\n")); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon", + "osm_id = -30 AND tags->'building' = 'yes' AND " + "ST_GeometryType(way) = 'ST_Polygon'")); + + SECTION("remove relation") + { + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "r30 v3 dD\n")); + } + + SECTION("remove multipolygon tag") + { + REQUIRE_NOTHROW(db.run_import( + testing::opt_t().slim().append().flex("test_output_flex.lua"), + "r30 v3 dV Tbuilding=yes,name=Shed Mw20@\n")); + } + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon")); +} diff --git a/tests/test-output-flex-validgeom.cpp b/tests/test-output-flex-validgeom.cpp new file mode 100644 index 000000000..eec75bdfd --- /dev/null +++ b/tests/test-output-flex-validgeom.cpp @@ -0,0 +1,25 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +TEST_CASE("no invalid geometries") +{ + REQUIRE_NOTHROW( + db.run_file(testing::opt_t().slim().flex("test_output_flex.lua"), + "test_output_pgsql_validgeom.osm")); + + auto conn = db.db().connect(); + + conn.require_has_table("osm2pgsql_test_point"); + conn.require_has_table("osm2pgsql_test_line"); + conn.require_has_table("osm2pgsql_test_polygon"); + conn.require_has_table("osm2pgsql_test_route"); + + REQUIRE(12 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(0 == + conn.get_count("osm2pgsql_test_polygon", "NOT ST_IsValid(way)")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon", "ST_IsEmpty(way)")); +} diff --git a/tests/test-output-flex.cpp b/tests/test-output-flex.cpp new file mode 100644 index 000000000..75d2d93a6 --- /dev/null +++ b/tests/test-output-flex.cpp @@ -0,0 +1,133 @@ +#include + +#include "common-import.hpp" +#include "common-options.hpp" + +static testing::db::import_t db; + +static void require_tables(pg::conn_t const &conn) +{ + conn.require_has_table("osm2pgsql_test_point"); + conn.require_has_table("osm2pgsql_test_line"); + conn.require_has_table("osm2pgsql_test_polygon"); + conn.require_has_table("osm2pgsql_test_route"); +} + +TEST_CASE("liechtenstein slim regression simple") +{ + REQUIRE_NOTHROW( + db.run_file(testing::opt_t().slim().flex("test_output_flex.lua"), + "liechtenstein-2013-08-03.osm.pbf")); + + auto conn = db.db().connect(); + require_tables(conn); + + CHECK(1362 == conn.get_count("osm2pgsql_test_point")); + CHECK(2932 == conn.get_count("osm2pgsql_test_line")); + CHECK(4136 == conn.get_count("osm2pgsql_test_polygon")); + CHECK(35 == conn.get_count("osm2pgsql_test_route")); + + // Check size of lines + conn.assert_double( + 1696.04, + "SELECT ST_Length(way) FROM osm2pgsql_test_line WHERE osm_id = 1101"); + conn.assert_double(1151.26, + "SELECT ST_Length(ST_Transform(way,4326)::geography) " + "FROM osm2pgsql_test_line WHERE osm_id = 1101"); + + conn.assert_double( + 311.289, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE osm_id = 3265"); + conn.assert_double( + 311.289, + "SELECT ST_Area(way) FROM osm2pgsql_test_polygon WHERE osm_id = 3265"); + conn.assert_double(143.845, + "SELECT ST_Area(ST_Transform(way,4326)::geography) FROM " + "osm2pgsql_test_polygon WHERE osm_id = 3265"); + + // Check a point's location + REQUIRE(1 == conn.get_count("osm2pgsql_test_point", + "ST_DWithin(way, 'SRID=3857;POINT(1062645.12 " + "5972593.4)'::geometry, 0.1)")); +} + +TEST_CASE("liechtenstein slim latlon") +{ + REQUIRE_NOTHROW(db.run_file( + testing::opt_t().slim().flex("test_output_flex.lua").srs(PROJ_LATLONG), + "liechtenstein-2013-08-03.osm.pbf")); + + auto conn = db.db().connect(); + require_tables(conn); + + REQUIRE(1362 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(2932 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(4136 == conn.get_count("osm2pgsql_test_polygon")); + + // Check size of lines + conn.assert_double( + 0.0105343, + "SELECT ST_Length(way) FROM osm2pgsql_test_line WHERE osm_id = 1101"); + conn.assert_double(1151.26, + "SELECT ST_Length(ST_Transform(way,4326)::geography) " + "FROM osm2pgsql_test_line WHERE osm_id = 1101"); + + conn.assert_double( + 1.70718e-08, + "SELECT way_area FROM osm2pgsql_test_polygon WHERE osm_id = 3265"); + conn.assert_double( + 1.70718e-08, + "SELECT ST_Area(way) FROM osm2pgsql_test_polygon WHERE osm_id = 3265"); + conn.assert_double(143.845, + "SELECT ST_Area(ST_Transform(way,4326)::geography) FROM " + "osm2pgsql_test_polygon WHERE osm_id = 3265"); + + // Check a point's location + REQUIRE(1 == conn.get_count("osm2pgsql_test_point", + "ST_DWithin(way, 'SRID=4326;POINT(9.5459035 " + "47.1866494)'::geometry, 0.00001)")); +} + +TEST_CASE("way area slim flatnode") +{ + REQUIRE_NOTHROW(db.run_file( + testing::opt_t().slim().flex("test_output_flex.lua").flatnodes(), + "test_output_pgsql_way_area.osm")); + + auto conn = db.db().connect(); + require_tables(conn); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_polygon")); +} + +TEST_CASE("route relation slim flatnode") +{ + REQUIRE_NOTHROW(db.run_file( + testing::opt_t().slim().flex("test_output_flex.lua").flatnodes(), + "test_output_pgsql_route_rel.osm")); + + auto conn = db.db().connect(); + require_tables(conn); + + REQUIRE(0 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(0 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(1 == conn.get_count("osm2pgsql_test_route")); +} + +TEST_CASE("liechtenstein slim bz2 parsing regression") +{ + REQUIRE_NOTHROW( + db.run_file(testing::opt_t().slim().flex("test_output_flex.lua"), + "liechtenstein-2013-08-03.osm.bz2")); + + auto conn = db.db().connect(); + require_tables(conn); + + REQUIRE(1362 == conn.get_count("osm2pgsql_test_point")); + REQUIRE(2932 == conn.get_count("osm2pgsql_test_line")); + REQUIRE(4136 == conn.get_count("osm2pgsql_test_polygon")); + REQUIRE(35 == conn.get_count("osm2pgsql_test_route")); +}