forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			324 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			324 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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"
 | |
| 
 | |
| class LuaSnippets {
 | |
|     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",
 | |
|     ].join("\n")
 | |
| 
 | |
|     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))
 | |
|             })
 | |
|         })
 | |
|         const features = or.map((t) => t.asHumanString(false, false, {}))
 | |
|         features.sort()
 | |
|         console.log("Polygon features are:", features)
 | |
|         return { blacklist: new Or(blacklisted), whitelisted: new Or(or) }
 | |
|     }
 | |
| 
 | |
|     public static toLuaFilter(tag: TagsFilter, useParens: boolean = false): string {
 | |
|         if (tag instanceof Tag) {
 | |
|             return `object.tags["${tag.key}"] == "${tag.value}"`
 | |
|         }
 | |
|         if (tag instanceof And) {
 | |
|             const expr = tag.and.map((t) => this.toLuaFilter(t, true)).join(" and ")
 | |
|             if (useParens) {
 | |
|                 return "(" + expr + ")"
 | |
|             }
 | |
|             return expr
 | |
|         }
 | |
|         if (tag instanceof Or) {
 | |
|             const expr = tag.or.map((t) => this.toLuaFilter(t, true)).join(" or ")
 | |
|             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
 | |
|     }
 | |
| 
 | |
|     private static regexTagToLua(tag: RegexTag) {
 | |
|         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, "/")
 | |
| 
 | |
|         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"
 | |
|             )
 | |
|         }
 | |
| 
 | |
|         if (tag.invert) {
 | |
|             return `object.tags["${tag.key}"] == nil or not string.find(object.tags["${tag.key}"], "${v}")`
 | |
|         }
 | |
| 
 | |
|         return `(object.tags["${tag.key}"] ~= nil and string.find(object.tags["${tag.key}"], "${v}"))`
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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
 | |
|         }
 | |
|         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 = [
 | |
|             "local db_tables = {}",
 | |
|             LuaSnippets.helpers,
 | |
|             ...generators.map((g) => g.generateTables()),
 | |
|             this.generateProcessPoi(allNeededLayers),
 | |
|             this.generateProcessWay(allNeededLayers),
 | |
|         ].join("\n\n\n")
 | |
|         const path = "build_db.lua"
 | |
|         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)
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     private earlyAbort() {
 | |
|         return ["  if countTbl(object.tags) == 0 then", "    return", "  end", ""].join("\n")
 | |
|     }
 | |
| 
 | |
|     private generateProcessPoi(
 | |
|         allNeededLayers: Map<string, { tags: TagsFilter; foundInTheme: string[] }>
 | |
|     ) {
 | |
|         const body: string[] = []
 | |
|         allNeededLayers.forEach(({ tags }, layerId) => {
 | |
|             body.push(this.insertInto(tags, layerId, "pois_").join("\n"))
 | |
|         })
 | |
| 
 | |
|         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_"
 | |
|     ) {
 | |
|         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()
 | |
|         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",
 | |
|             "    process_polygon(object, object:as_polygon())",
 | |
|             "  else",
 | |
|             "    process_linestring(object, object:as_linestring())",
 | |
|             "  end",
 | |
|             "end",
 | |
|         ].join("\n")
 | |
|     }
 | |
| }
 | |
| 
 | |
| new GenerateBuildDbScript().run()
 |