Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bbox plugin #14

Merged
merged 3 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ how to create one yourself.
The framework has support for plugins adding some functionality. They are in
the `lua/themepark/plugins` directory.

Available plugins are `taginfo`, `tilekiln`, and `t-rex`.
Available plugins are `taginfo`, `tilekiln`, `t-rex` and `bbox`.

### Plugin `taginfo`

Expand Down Expand Up @@ -80,6 +80,27 @@ with options. Available options are:
* `extra_layers`: Extra layers that should be added to the config file. Use
the same structure as the T-Rex config file would use.

### Plugin `bbox`

This plugin can be used to create a config file for the
[BBOX](https://www.bbox.earth/) tile server.

Call like this from your config file to create a file called
`bbox-config.toml`:

```
themepark:plugin('bbox'):write_config('bbox-config.toml')
```

A second argument on the `write_config()` function can contain a Lua table
with options. Available options are:

* `tileset`: Name of the tileset, defaults to `osm`.
* `attribution`: Set attribution string, defaults to the setting from the
themepark config file.
* `extra_layers`: Extra layers that should be added to the config file. Use
the same structure as the BBOX config file would use.

## Themes

Themes provide building blocks for map data transformations. They usually
Expand Down
5 changes: 5 additions & 0 deletions config/shortbread.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
-- If you are creating a tilekiln config you must also create
-- the 'shortbread_config' directory.
local TREX = false
local BBOX = false
local TILEKILN = false
local TAGINFO = false

Expand Down Expand Up @@ -70,6 +71,10 @@ if osm2pgsql.mode == 'create' then
themepark:plugin('t-rex'):write_config('t-rex-config.toml', {})
end

if BBOX then
themepark:plugin('bbox'):write_config('bbox-config.toml', {})
end

if TILEKILN then
themepark:plugin('tilekiln'):write_config('shortbread_config', {
tileset = 'shortbread_v1',
Expand Down
32 changes: 32 additions & 0 deletions config/shortbread_gen.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
-- If you are creating a tilekiln config you must also create
-- the 'shortbread_config' directory.
local TREX = false
local BBOX = false
local TILEKILN = false

local themepark = require('themepark')
Expand Down Expand Up @@ -100,6 +101,37 @@ SELECT "name","name_de","name_en","kind","layer","ref","ref_rows","ref_cols","z_
}
})
end
if BBOX then
themepark:plugin('bbox'):write_config('bbox-config.toml', {
tileset = 'osm',
extra_layers = {
{
buffer_size = 10,
name = 'street_labels',
geometry_type = 'LINESTRING',
query = {
{
minzoom = 14,
sql = [[
SELECT "name","name_de","name_en","kind","layer","ref","ref_rows","ref_cols","z_order","geom"
FROM "streets"
WHERE !zoom! >= "minzoom"
ORDER BY "z_order" asc]]
},
{
minzoom = 11,
maxzoom = 13,
sql = [[
SELECT "name","name_de","name_en","kind","layer","ref","ref_rows","ref_cols","z_order","geom"
FROM "streets_med"
WHERE !zoom! >= "minzoom"
ORDER BY "z_order" asc]]
},
}
}
}
})
end
if TILEKILN then
themepark:plugin('tilekiln'):write_config('shortbread_config', {
tileset = 'shortbread_v1',
Expand Down
282 changes: 282 additions & 0 deletions lua/themepark/plugins/bbox.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
-- ---------------------------------------------------------------------------
--
-- Osm2pgsql Themepark
--
-- A framework for pluggable osm2pgsql config files.
--
-- ---------------------------------------------------------------------------
--
-- lib/themepark/plugins/bbox.lua
--
-- ---------------------------------------------------------------------------
--
-- Copyright 2024 Jochen Topf <[email protected]>
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- https://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- ---------------------------------------------------------------------------

local plugin = {}

local utils = require 'themepark/utils'

local min_max_zoom_columns = function(columns)
local column_names = {}
local minzoom
local maxzoom

for _, column in ipairs(columns) do
if column.tiles == 'minzoom' then
minzoom = column.column
elseif column.tiles == 'maxzoom' then
maxzoom = column.column
elseif column.tiles ~= false and column.column ~= 'all_tags' then
table.insert(column_names, '"' .. column.column .. '"')
end
end

return minzoom, maxzoom, column_names
end

local function assemble_conditions(minzoom, maxzoom, xycondition)
local conditions = {}

if minzoom then
table.insert(conditions, '!zoom! >= "' .. minzoom .. '"')
end
if maxzoom then
table.insert(conditions, '!zoom! <= "' .. maxzoom .. '"')
end

if xycondition then
table.insert(conditions, '!x! = x AND !y! = y')
end

return conditions
end

local function build_layer_config(info)
local schema_prefix = ""
if plugin.themepark.options.schema then
schema_prefix = plugin.themepark.options.schema .. "."
end
local data = {
name = info.name,
geometry_field = info.geom_column,
geometry_type = string.upper(info.geom_type),
srid = plugin.themepark.options.srid,
-- simplify = true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't set simplify by default, escpecially when using osm2pgsql-gen.

buffer_size = 10,
}

local tiles = info.tiles or {}
if tiles.group then
return
end

if tiles.simplify ~= nil then
data.simplify = tiles.simplify
end

if tiles.make_valid ~= nil then
data.make_valid = tiles.make_valid
end

if tiles.buffer_size ~= nil then
data.buffer_size = tiles.buffer_size
end

if not plugin.themepark.layer_groups[info.name] then
if tiles.minzoom then
data.minzoom = tiles.minzoom
end
if tiles.maxzoom then
data.maxzoom = tiles.maxzoom
end
end

if tiles.sql then
data.query = {{
sql = info.sql
}}
else
local minzoom, maxzoom, columns = min_max_zoom_columns(info.columns)
local from = schema_prefix .. info.name
if plugin.themepark.layer_groups[info.name] then
local conditions = assemble_conditions(minzoom, maxzoom, tiles.xycondition)
local sql = utils.build_select_query(columns, from, conditions, tiles.order_by, tiles.order_dir)

data.query = {{
sql = sql,
}}
if tiles.minzoom then
data.query[1].minzoom = tiles.minzoom
end
if tiles.maxzoom then
data.query[1].maxzoom = tiles.maxzoom
end
else
local conditions = assemble_conditions(minzoom, maxzoom, tiles.xycondition)
local sql = utils.build_select_query(columns, from, conditions, tiles.order_by, tiles.order_dir)
data.query = {{
sql = sql,
}}
end

if plugin.themepark.layer_groups[info.name] then
for _, group_table in ipairs(plugin.themepark.layer_groups[info.name]) do
local sublayer = utils.find_name_in_array(plugin.themepark.layers, group_table.name)
minzoom, maxzoom, columns = min_max_zoom_columns(sublayer.columns)

local conditions = assemble_conditions(minzoom, maxzoom, sublayer.tiles.xycondition)
local subfrom = schema_prefix .. group_table.name
local sql = utils.build_select_query(columns, subfrom,
conditions, sublayer.tiles.order_by,
sublayer.tiles.order_dir)
table.insert(data.query, {
sql = sql,
minzoom = group_table.minzoom,
maxzoom = group_table.maxzoom,
})
end
end
end

return data
end

-- ---------------------------------------------------------------------------
-- ---------------------------------------------------------------------------
local function build_tileset_config(extra_layers)
local layers = {}

for _, layer in ipairs(plugin.themepark.layers) do
if layer.tiles ~= false then
local layer_config = build_layer_config(layer)
if layer_config then
table.insert(layers, layer_config)
end
end
end
for _, data in ipairs(extra_layers) do
if not data.geometry_field then
data.geometry_field = 'geom'
end
if not data.srid then
data.srid = plugin.themepark.options.srid
end
table.insert(layers, data)
end
return layers
end

local function ordered_keys(o)
local keys1 = {}
local keys2 = {}
for k, v in pairs(o) do
if type(v) == 'table' then
table.insert(keys2, k)
else
table.insert(keys1, k)
end
end
table.sort(keys1)
table.sort(keys2)
for _,v in ipairs(keys2) do
pka marked this conversation as resolved.
Show resolved Hide resolved
table.insert(keys1, v)
end
return keys1
end

local function dump_toml(o, indent_size, parents)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to pull this out later and re-use it in the t-rex config plugin also.

indent_size = indent_size or 2
parents = parents or {}
if type(o) == 'table' then
local s = ''
local indent = string.rep(" ", #parents * indent_size)
-- sort keys for stable output order
for _, k in ipairs(ordered_keys(o)) do
local v = o[k]
if type(v) == 'table' then
local array_val = v[1] ~= nil
if array_val then
table.insert(parents, k)
s = s .. dump_toml(v, indent_size, parents)
table.remove(parents)
elseif type(k) == 'number' then
local tag = table.concat(parents, ".")
s = s .. '\n\n' .. indent .. '[[' .. tag ..']]' .. dump_toml(v, indent_size, parents)
else
table.insert(parents, k)
indent = string.rep(" ", #parents * indent_size)
local tag = table.concat(parents, ".")
s = s .. '\n\n' .. indent .. '[' .. tag ..']' .. dump_toml(v, indent_size, parents)
table.remove(parents)
end
else
if type(v) == 'string' then
if string.find(v, '"') then
v = '"""'..v..'"""'
pka marked this conversation as resolved.
Show resolved Hide resolved
else
v = '"'..v..'"'
end
end
s = s .. '\n' .. indent .. k ..' = ' .. dump_toml(v, indent_size, parents)
end
end
return s
else
return tostring(o)
end
end

-- ---------------------------------------------------------------------------
-- ---------------------------------------------------------------------------
function plugin:write_config(filename, options)
if not options then
options = {}
end

local config = {
webserver = { server_addr = '0.0.0.0:8080' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default should probably be 127.0.0.1 to not open up a server to the world.

This is probably something we want to make configurable. The t-rex plugin also needs those settings. But okay for now. We can do that after this has been merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to 127.0.0.1 (0.0.0.0 is needed for Docker setups).

datasource = {
{
name = 'db',
postgis = {
url = "postgres:///db"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this hardcoding a database name? Not sure how this works with bbox, but maybe leave this empty so it picks up database connection from PG* env vars if they are available?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BBOX currently requires a configuraton, but it can be overwritten with env vars. A possbile way is to add comments at the begin of the config file explaining this.

Is it possible to get the used db parameters for adding a matching PG url?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The libpq library from PostgreSQL looks at some PG* env variables, this should work more or less everwhere unless that library is not used. So having no database config options is totally okay for osm2pgsql and should be for bbox also.

So there are several different ways of specifiying which database to use to osm2pgsql, maybe we can expose this information to Lua. But should we expose the original command line option or what's used in the end (taking env vars into account)? I have to think about this some more.

}
}
},
tileset = {
{
name = options.tileset or 'osm',
tms = 'WebMercatorQuad',
postgis = {
datasource = 'db',
attribution = options.attribution or plugin.themepark.options.attribution,
layer = build_tileset_config(options.extra_layers or {})
}
}
}
}

if plugin.themepark.options.extent then
config.tileset[1].postgis.extent = plugin.themepark.options.extent
end

utils.write_to_file(filename, dump_toml(config, 0) .. "\n")
end

return plugin

-- ---------------------------------------------------------------------------
4 changes: 3 additions & 1 deletion lua/themepark/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ function utils.write_to_file(filename, content)
end

function utils.build_select_query(columns, from, conditions, order_by, order_dir)
-- Handle from with schema prefix
local quoted_from = '"' .. string.gsub(from, '%.', '"."') .. '"'
local sql = 'SELECT ' .. table.concat(columns, ',')
.. ' FROM "' .. from .. '"'
.. ' FROM ' .. quoted_from

if conditions and #conditions > 0 then
sql = sql .. ' WHERE ' .. table.concat(conditions, ' AND ')
Expand Down