mapcomplete/scripts/osm2pgsql/generateBuildDbScript.ts

326 lines
11 KiB
TypeScript
Raw Normal View History

import { TagsFilter } from "../../src/Logic/Tags/TagsFilter"
import { Tag } from "../../src/Logic/Tags/Tag"
import { And } from "../../src/Logic/Tags/And"
import Script from "../Script"
import fs from "fs"
import { Or } from "../../src/Logic/Tags/Or"
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
import { ValidateThemeEnsemble } from "../../src/Models/ThemeConfig/Conversion/Validation"
import { AllKnownLayouts } from "../../src/Customizations/AllKnownLayouts"
import { OsmObject } from "../../src/Logic/Osm/OsmObject"
2024-01-23 22:03:02 +01:00
class LuaSnippets {
2024-01-26 18:18:07 +01:00
public static helpers = [
"function countTbl(tbl)\n" +
" local c = 0\n" +
" for n in pairs(tbl) do \n" +
" c = c + 1 \n" +
" end\n" +
" return c\n" +
"end",
2024-01-26 18:18:07 +01:00
].join("\n")
2024-01-23 22:03:02 +01:00
public static isPolygonFeature(): { blacklist: TagsFilter; whitelisted: TagsFilter } {
const dict = OsmObject.polygonFeatures
const or: TagsFilter[] = []
const blacklisted: TagsFilter[] = []
dict.forEach(({ values, blacklist }, k) => {
if (blacklist) {
if (values === undefined) {
blacklisted.push(new RegexTag(k, /.+/is))
return
}
values.forEach((v) => {
blacklisted.push(new RegexTag(k, v))
})
return
}
if (values === undefined || values === null) {
or.push(new RegexTag(k, /.+/is))
return
}
values.forEach((v) => {
or.push(new RegexTag(k, v))
})
})
console.log(
"Polygon features are:",
or.map((t) => t.asHumanString(false, false, {}))
)
return { blacklist: new Or(blacklisted), whitelisted: new Or(or) }
}
2024-01-26 18:18:07 +01:00
public static toLuaFilter(tag: TagsFilter, useParens: boolean = false): string {
if (tag instanceof Tag) {
return `object.tags["${tag.key}"] == "${tag.value}"`
}
2024-01-26 18:18:07 +01:00
if (tag instanceof And) {
const expr = tag.and.map((t) => this.toLuaFilter(t, true)).join(" and ")
2024-01-26 18:18:07 +01:00
if (useParens) {
return "(" + expr + ")"
}
return expr
}
2024-01-26 18:18:07 +01:00
if (tag instanceof Or) {
const expr = tag.or.map((t) => this.toLuaFilter(t, true)).join(" or ")
2024-01-26 18:18:07 +01:00
if (useParens) {
return "(" + expr + ")"
}
return expr
}
if (tag instanceof RegexTag) {
let expr = LuaSnippets.regexTagToLua(tag)
if (useParens) {
expr = "(" + expr + ")"
}
return expr
}
let msg = "Could not handle" + tag.asHumanString(false, false, {})
console.error(msg)
throw msg
}
2024-01-26 18:18:07 +01:00
private static regexTagToLua(tag: RegexTag) {
2024-01-23 22:03:02 +01:00
if (typeof tag.value === "string" && tag.invert) {
return `object.tags["${tag.key}"] ~= "${tag.value}"`
}
if (typeof tag.value === "string" && !tag.invert) {
return `object.tags["${tag.key}"] == "${tag.value}"`
}
let v: string = (<RegExp>tag.value).source.replace(/\\\//g, "/")
2024-01-23 22:03:02 +01:00
if ("" + tag.value === "/.+/is" && !tag.invert) {
return `object.tags["${tag.key}"] ~= nil`
}
if ("" + tag.value === "/.+/is" && tag.invert) {
return `object.tags["${tag.key}"] == nil`
}
if (tag.matchesEmpty && !tag.invert) {
return `object.tags["${tag.key}"] == nil or object.tags["${tag.key}"] == ""`
}
if (tag.matchesEmpty && tag.invert) {
return `object.tags["${tag.key}"] ~= nil or object.tags["${tag.key}"] ~= ""`
}
let head = "^((.*;)?"
let tail = "(;.*)?)$"
if (v.startsWith(head)) {
v = "(" + v.substring(head.length)
}
if (v.endsWith(tail)) {
v = v.substring(0, v.length - tail.length) + ")"
// We basically remove the optional parts at the start and the end, as object.find has this freedom anyway.
// This might result in _some_ incorrect values that end up in the database (e.g. when matching 'friture', it might als match "abc;foo_friture_bar;xyz", but the frontend will filter this out
}
if (v.indexOf(")?") > 0) {
throw (
"LUA regexes have a bad support for (optional) capture groups, as such, " +
v +
" is not supported"
)
}
2024-01-23 22:03:02 +01:00
if (tag.invert) {
return `object.tags["${tag.key}"] == nil or not string.find(object.tags["${tag.key}"], "${v}")`
2024-01-23 22:03:02 +01:00
}
return `(object.tags["${tag.key}"] ~= nil and string.find(object.tags["${tag.key}"], "${v}"))`
2024-01-23 22:03:02 +01:00
}
2024-01-26 18:18:07 +01:00
}
class GenerateLayerLua {
private readonly _id: string
private readonly _tags: TagsFilter
private readonly _foundInThemes: string[]
constructor(id: string, tags: TagsFilter, foundInThemes: string[] = []) {
this._tags = tags
this._id = id
this._foundInThemes = foundInThemes
}
public generateTables(): string {
if (!this._tags) {
return undefined
}
2024-01-26 18:18:07 +01:00
return [
`db_tables.pois_${this._id} = osm2pgsql.define_table({`,
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
` name = 'pois_${this._id}',`,
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
" columns = {",
" { column = 'tags', type = 'jsonb' },",
" { column = 'geom', type = 'point', projection = 4326, not_null = true },",
" }",
"})",
"",
`db_tables.lines_${this._id} = osm2pgsql.define_table({`,
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
` name = 'lines_${this._id}',`,
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
" columns = {",
" { column = 'tags', type = 'jsonb' },",
" { column = 'geom', type = 'linestring', projection = 4326, not_null = true },",
" }",
"})",
`db_tables.polygons_${this._id} = osm2pgsql.define_table({`,
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
` name = 'polygons_${this._id}',`,
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
" columns = {",
" { column = 'tags', type = 'jsonb' },",
" { column = 'geom', type = 'polygon', projection = 4326, not_null = true },",
" }",
"})",
"",
].join("\n")
}
}
class GenerateBuildDbScript extends Script {
constructor() {
super("Generates a .lua-file to use with osm2pgsql")
}
async main(args: string[]) {
const allLayers = new ValidateThemeEnsemble().convertStrict(
AllKnownLayouts.allKnownLayouts.values()
)
if (allLayers.size === 0) {
throw "No layers found at all"
}
const notCounted: string[] = []
const allNeededLayers: Map<string, { tags: TagsFilter; foundInTheme: string[] }> = new Map<
string,
{ tags: TagsFilter; foundInTheme: string[] }
>()
for (const key of allLayers.keys()) {
const layer = allLayers.get(key)
if (layer.isCounted) {
allNeededLayers.set(key, layer)
} else {
notCounted.push(key)
}
}
const generators: GenerateLayerLua[] = []
allNeededLayers.forEach(({ tags, foundInTheme }, layerId) => {
generators.push(new GenerateLayerLua(layerId, tags, foundInTheme))
})
const script = [
2024-01-26 18:18:07 +01:00
"local db_tables = {}",
LuaSnippets.helpers,
...generators.map((g) => g.generateTables()),
2024-01-26 18:18:07 +01:00
this.generateProcessPoi(allNeededLayers),
this.generateProcessWay(allNeededLayers),
].join("\n\n\n")
const path = "build_db.lua"
2024-01-23 22:03:02 +01:00
fs.writeFileSync(path, script, "utf-8")
console.log("Written", path)
console.log(
"Following layers are _not_ indexed as they are not counted:",
notCounted.join(", ")
)
console.log(
allNeededLayers.size +
" layers will be created with 3 tables each. Make sure to set 'max_connections' to at least " +
(10 + 3 * allNeededLayers.size)
)
2024-01-26 18:18:07 +01:00
}
private earlyAbort() {
return [" if countTbl(object.tags) == 0 then", " return", " end", ""].join("\n")
2024-01-26 18:18:07 +01:00
}
private generateProcessPoi(
allNeededLayers: Map<string, { tags: TagsFilter; foundInTheme: string[] }>
) {
2024-01-26 18:18:07 +01:00
const body: string[] = []
allNeededLayers.forEach(({ tags }, layerId) => {
body.push(this.insertInto(tags, layerId, "pois_").join("\n"))
2024-01-26 18:18:07 +01:00
})
return [
"function osm2pgsql.process_node(object)",
this.earlyAbort(),
" local geom = object:as_point()",
" local matches_filter = false",
body.join("\n"),
"end",
].join("\n")
}
/**
* If matches_filter
* @param tags
* @param layerId
* @param tableprefix
* @private
*/
private insertInto(
tags: TagsFilter,
layerId: string,
tableprefix: "pois_" | "lines_" | "polygons_"
) {
2024-01-26 18:18:07 +01:00
const filter = LuaSnippets.toLuaFilter(tags)
return [
" matches_filter = " + filter,
" if matches_filter then",
" db_tables." + tableprefix + layerId + ":insert({",
" geom = geom,",
" tags = object.tags",
" })",
" end",
]
}
private generateProcessWay(allNeededLayers: Map<string, { tags: TagsFilter }>) {
const bodyLines: string[] = []
allNeededLayers.forEach(({ tags }, layerId) => {
bodyLines.push(this.insertInto(tags, layerId, "lines_").join("\n"))
})
const bodyPolygons: string[] = []
allNeededLayers.forEach(({ tags }, layerId) => {
bodyPolygons.push(this.insertInto(tags, layerId, "polygons_").join("\n"))
})
const isPolygon = LuaSnippets.isPolygonFeature()
2024-01-26 18:18:07 +01:00
return [
"function process_polygon(object, geom)",
" local matches_filter",
...bodyPolygons,
"end",
"function process_linestring(object, geom)",
" local matches_filter",
...bodyLines,
"end",
"",
"function osm2pgsql.process_way(object)",
this.earlyAbort(),
" local object_is_line = not object.is_closed or " +
LuaSnippets.toLuaFilter(isPolygon.blacklist),
` local object_is_area = object.is_closed and (object.tags["area"] == "yes" or (not object_is_line and ${LuaSnippets.toLuaFilter(
isPolygon.whitelisted,
true
)}))`,
" if object_is_area then",
2024-01-26 18:18:07 +01:00
" process_polygon(object, object:as_polygon())",
" else",
" process_linestring(object, object:as_linestring())",
" end",
"end",
].join("\n")
}
}
new GenerateBuildDbScript().run()