forked from MapComplete/MapComplete
		
	Logic: better support for tag optimization and simplifying expressions
This commit is contained in:
		
							parent
							
								
									bd228a6129
								
							
						
					
					
						commit
						a3d26db84a
					
				
					 11 changed files with 430 additions and 260 deletions
				
			
		| 
						 | 
					@ -1,9 +1,14 @@
 | 
				
			||||||
import Script from "./Script"
 | 
					import Script from "./Script"
 | 
				
			||||||
import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
 | 
					import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
 | 
				
			||||||
import * as nsiWD from "../node_modules/name-suggestion-index/dist/wikidata.min.json"
 | 
					import * as nsiWD from "../node_modules/name-suggestion-index/dist/wikidata.min.json"
 | 
				
			||||||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs"
 | 
					import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"
 | 
				
			||||||
import ScriptUtils from "./ScriptUtils"
 | 
					import ScriptUtils from "./ScriptUtils"
 | 
				
			||||||
import { Utils } from "../src/Utils"
 | 
					import { Utils } from "../src/Utils"
 | 
				
			||||||
 | 
					import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
 | 
				
			||||||
 | 
					import FilterConfigJson, { FilterConfigOptionJson } from "../src/Models/ThemeConfig/Json/FilterConfigJson"
 | 
				
			||||||
 | 
					import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson"
 | 
				
			||||||
 | 
					import { TagUtils } from "../src/Logic/Tags/TagUtils"
 | 
				
			||||||
 | 
					import { And } from "../src/Logic/Tags/And"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DownloadNsiLogos extends Script {
 | 
					class DownloadNsiLogos extends Script {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
| 
						 | 
					@ -43,7 +48,7 @@ class DownloadNsiLogos extends Script {
 | 
				
			||||||
            await ScriptUtils.DownloadFileTo(logos.facebook, path)
 | 
					            await ScriptUtils.DownloadFileTo(logos.facebook, path)
 | 
				
			||||||
            // Validate
 | 
					            // Validate
 | 
				
			||||||
            const content = readFileSync(path, "utf8")
 | 
					            const content = readFileSync(path, "utf8")
 | 
				
			||||||
            if (content.startsWith('{"error"')) {
 | 
					            if (content.startsWith("{\"error\"")) {
 | 
				
			||||||
                unlinkSync(path)
 | 
					                unlinkSync(path)
 | 
				
			||||||
                console.error("Attempted to fetch", logos.facebook, " but this gave an error")
 | 
					                console.error("Attempted to fetch", logos.facebook, " but this gave an error")
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
| 
						 | 
					@ -86,12 +91,8 @@ class DownloadNsiLogos extends Script {
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async main(): Promise<void> {
 | 
					 | 
				
			||||||
        await this.downloadFor("operator")
 | 
					 | 
				
			||||||
        await this.downloadFor("brand")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async downloadFor(type: "brand" | "operator"): Promise<void> {
 | 
					    async downloadFor(type: string): Promise<void> {
 | 
				
			||||||
        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
					        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
				
			||||||
        const items = nsi.allPossible(type)
 | 
					        const items = nsi.allPossible(type)
 | 
				
			||||||
        const basePath = "./public/assets/data/nsi/logos/"
 | 
					        const basePath = "./public/assets/data/nsi/logos/"
 | 
				
			||||||
| 
						 | 
					@ -109,7 +110,7 @@ class DownloadNsiLogos extends Script {
 | 
				
			||||||
                        downloadCount++
 | 
					                        downloadCount++
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    return downloaded
 | 
					                    return downloaded
 | 
				
			||||||
                })
 | 
					                }),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            for (let j = 0; j < results.length; j++) {
 | 
					            for (let j = 0; j < results.length; j++) {
 | 
				
			||||||
                let didDownload = results[j]
 | 
					                let didDownload = results[j]
 | 
				
			||||||
| 
						 | 
					@ -124,6 +125,63 @@ class DownloadNsiLogos extends Script {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async generateRendering(type: string) {
 | 
				
			||||||
 | 
					        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
				
			||||||
 | 
					        const items = nsi.allPossible(type)
 | 
				
			||||||
 | 
					        const brandPrefix = [type, "name", "alt_name", "operator","brand"]
 | 
				
			||||||
 | 
					        const filterOptions: FilterConfigOptionJson[] = items.map(item => {
 | 
				
			||||||
 | 
					            let brandDetection: string[] = []
 | 
				
			||||||
 | 
					            let required: string[] = []
 | 
				
			||||||
 | 
					            const tags: Record<string, string> = item.tags
 | 
				
			||||||
 | 
					            for (const k in tags) {
 | 
				
			||||||
 | 
					                if (brandPrefix.some(br => k === br || k.startsWith(br + ":"))) {
 | 
				
			||||||
 | 
					                    brandDetection.push(k + "=" + tags[k])
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    required.push(k + "=" + tags[k])
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const osmTags = <TagConfigJson>TagUtils.optimzeJson({ and: [...required, { or: brandDetection }] })
 | 
				
			||||||
 | 
					            return ({
 | 
				
			||||||
 | 
					                question: item.displayName,
 | 
				
			||||||
 | 
					                icon: nsi.getIconUrl(item, type),
 | 
				
			||||||
 | 
					                osmTags,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const config: LayerConfigJson = {
 | 
				
			||||||
 | 
					            "#dont-translate": "*",
 | 
				
			||||||
 | 
					            id: "nsi_" + type,
 | 
				
			||||||
 | 
					            source: "special:library",
 | 
				
			||||||
 | 
					            description: {
 | 
				
			||||||
 | 
					                en: "Exposes part of the NSI to reuse in other themes, e.g. for rendering",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            pointRendering: null,
 | 
				
			||||||
 | 
					            filter: [
 | 
				
			||||||
 | 
					               <any> {
 | 
				
			||||||
 | 
					                    id: type,
 | 
				
			||||||
 | 
					                    strict: true,
 | 
				
			||||||
 | 
					                    options: [{question: type}, ...filterOptions],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            allowMove: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const path = "./assets/layers/nsi_" + type
 | 
				
			||||||
 | 
					        mkdirSync(path, { recursive: true })
 | 
				
			||||||
 | 
					        writeFileSync(path + "/nsi_" + type + ".json", JSON.stringify(config, null, "  "))
 | 
				
			||||||
 | 
					        console.log("Written", path)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async main(): Promise<void> {
 | 
				
			||||||
 | 
					        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
				
			||||||
 | 
					        const types = ["brand", "operator"]
 | 
				
			||||||
 | 
					        for (const type of types) {
 | 
				
			||||||
 | 
					            await this.generateRendering(type)
 | 
				
			||||||
 | 
					            // await this.downloadFor(type)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
new DownloadNsiLogos().run()
 | 
					new DownloadNsiLogos().run()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,39 +130,38 @@ export class And extends TagsFilter {
 | 
				
			||||||
     * t1.shadows(t2) // => false
 | 
					     * t1.shadows(t2) // => false
 | 
				
			||||||
     * t2.shadows(t0) // => false
 | 
					     * t2.shadows(t0) // => false
 | 
				
			||||||
     * t2.shadows(t1) // => false
 | 
					     * t2.shadows(t1) // => false
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * const t1 = new And([new Tag("shop","clothes"), new Or([new Tag("brand","XYZ"),new Tag("brand:wikidata","Q1234")])])
 | 
				
			||||||
 | 
					     * const t2 = new And([new RegexTag("shop","mall",true), new Or([TagUtils.Tag("shop~*"), new Tag("craft","shoemaker")])])
 | 
				
			||||||
 | 
					     * t1.shadows(t2) // => true
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    shadows(other: TagsFilter): boolean {
 | 
					    shadows(other: TagsFilter): boolean {
 | 
				
			||||||
        if (!(other instanceof And)) {
 | 
					        const phrases: TagsFilter[] = other instanceof And ? other.and : [other];
 | 
				
			||||||
            return false
 | 
					        // A phrase might be shadowed by a certain subsection. We keep track of this here
 | 
				
			||||||
        }
 | 
					        const shadowedOthers = phrases.map(() => false)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (const selfTag of this.and) {
 | 
					        for (const selfTag of this.and) {
 | 
				
			||||||
            let matchFound = false
 | 
					            let shadowsSome = false;
 | 
				
			||||||
            for (const otherTag of other.and) {
 | 
					            let shadowsAll = true;
 | 
				
			||||||
                matchFound = selfTag.shadows(otherTag)
 | 
					            for (let i = 0; i < phrases.length; i++){
 | 
				
			||||||
                if (matchFound) {
 | 
					                const otherTag = phrases[i]
 | 
				
			||||||
                    break
 | 
					                const doesShadow = selfTag.shadows(otherTag)
 | 
				
			||||||
 | 
					                if(doesShadow){
 | 
				
			||||||
 | 
					                    shadowedOthers[i] = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                shadowsSome ||= doesShadow;
 | 
				
			||||||
 | 
					                shadowsAll &&= doesShadow;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (!matchFound) {
 | 
					            // If A => X and A => Y, then
 | 
				
			||||||
                return false
 | 
					            // A&B implies X&Y. We discovered an A that implies all needed values
 | 
				
			||||||
 | 
					            if (shadowsAll) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!shadowsSome) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        return !shadowedOthers.some(v => !v);
 | 
				
			||||||
        for (const otherTag of other.and) {
 | 
					 | 
				
			||||||
            let matchFound = false
 | 
					 | 
				
			||||||
            for (const selfTag of this.and) {
 | 
					 | 
				
			||||||
                matchFound = selfTag.shadows(otherTag)
 | 
					 | 
				
			||||||
                if (matchFound) {
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (!matchFound) {
 | 
					 | 
				
			||||||
                return false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    usedKeys(): string[] {
 | 
					    usedKeys(): string[] {
 | 
				
			||||||
| 
						 | 
					@ -182,11 +181,13 @@ export class And extends TagsFilter {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * IN some contexts, some expressions can be considered true, e.g.
 | 
					     * In some contexts, some expressions can be considered true, e.g.
 | 
				
			||||||
     * (X=Y | (A=B & X=Y))
 | 
					     * (X=Y | (A=B & X=Y))
 | 
				
			||||||
     *        ^---------^
 | 
					     *        ^---------^
 | 
				
			||||||
     * When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise.
 | 
					     * When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise.
 | 
				
			||||||
     * This means that the entire 'AND' is considered FALSE
 | 
					     * This means that the entire 'AND' is considered FALSE in this case; but this is already handled by the first half.
 | 
				
			||||||
 | 
					     * In other words: this long expression is equivalent to (A=B | X=Y).
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return only phrases that should be kept.
 | 
					     * @return only phrases that should be kept.
 | 
				
			||||||
     * @param knownExpression The expression which is known in the subexpression and for which calculations can be done
 | 
					     * @param knownExpression The expression which is known in the subexpression and for which calculations can be done
 | 
				
			||||||
| 
						 | 
					@ -204,13 +205,14 @@ export class And extends TagsFilter {
 | 
				
			||||||
     * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
 | 
					     * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
 | 
				
			||||||
     * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
 | 
					     * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    removePhraseConsideredKnown(
 | 
					    public removePhraseConsideredKnown(
 | 
				
			||||||
        knownExpression: TagsFilter,
 | 
					        knownExpression: TagsFilter,
 | 
				
			||||||
        value: boolean
 | 
					        value: boolean
 | 
				
			||||||
    ): (TagsFilterClosed & OptimizedTag) | boolean {
 | 
					    ): (TagsFilterClosed & OptimizedTag) | boolean {
 | 
				
			||||||
        const newAnds: TagsFilter[] = []
 | 
					        const newAnds: TagsFilter[] = []
 | 
				
			||||||
        for (const tag of this.and) {
 | 
					        for (const tag of this.and) {
 | 
				
			||||||
            if (tag instanceof And) {
 | 
					            if (tag instanceof And) {
 | 
				
			||||||
 | 
					                console.trace("Improper optimization")
 | 
				
			||||||
                throw (
 | 
					                throw (
 | 
				
			||||||
                    "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
 | 
					                    "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
 | 
				
			||||||
                    this.asHumanString()
 | 
					                    this.asHumanString()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,6 +83,7 @@ export class Or extends TagsFilter {
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shadows(other: TagsFilter): boolean {
 | 
					    shadows(other: TagsFilter): boolean {
 | 
				
			||||||
        if (other instanceof Or) {
 | 
					        if (other instanceof Or) {
 | 
				
			||||||
            for (const selfTag of this.or) {
 | 
					            for (const selfTag of this.or) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
 | 
				
			||||||
import { ExpressionSpecification } from "maplibre-gl"
 | 
					import { ExpressionSpecification } from "maplibre-gl"
 | 
				
			||||||
import { RegexTag } from "./RegexTag"
 | 
					import { RegexTag } from "./RegexTag"
 | 
				
			||||||
import { OptimizedTag } from "./TagTypes"
 | 
					import { OptimizedTag } from "./TagTypes"
 | 
				
			||||||
 | 
					import { Or } from "./Or"
 | 
				
			||||||
 | 
					import { And } from "./And"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Tag extends TagsFilter {
 | 
					export class Tag extends TagsFilter {
 | 
				
			||||||
    public key: string
 | 
					    public key: string
 | 
				
			||||||
| 
						 | 
					@ -148,6 +150,12 @@ export class Tag extends TagsFilter {
 | 
				
			||||||
                return other.matchesProperties({ [this.key]: this.value })
 | 
					                return other.matchesProperties({ [this.key]: this.value })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if(other instanceof Or){
 | 
				
			||||||
 | 
					            return other.or.some(other => this.shadows(other))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(other instanceof And){
 | 
				
			||||||
 | 
					            return !other.and.some(other => !this.shadows(other))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -133,11 +133,11 @@ export class TagUtils {
 | 
				
			||||||
                "\n" +
 | 
					                "\n" +
 | 
				
			||||||
                "```json\n" +
 | 
					                "```json\n" +
 | 
				
			||||||
                "{\n" +
 | 
					                "{\n" +
 | 
				
			||||||
                '    "mappings": [\n' +
 | 
					                "    \"mappings\": [\n" +
 | 
				
			||||||
                "        {\n" +
 | 
					                "        {\n" +
 | 
				
			||||||
                '            "if":"key:={some_other_key}",\n' +
 | 
					                "            \"if\":\"key:={some_other_key}\",\n" +
 | 
				
			||||||
                '            "then": "...",\n' +
 | 
					                "            \"then\": \"...\",\n" +
 | 
				
			||||||
                '            "hideInAnswer": "some_other_key="\n' +
 | 
					                "            \"hideInAnswer\": \"some_other_key=\"\n" +
 | 
				
			||||||
                "        }\n" +
 | 
					                "        }\n" +
 | 
				
			||||||
                "    ]\n" +
 | 
					                "    ]\n" +
 | 
				
			||||||
                "}\n" +
 | 
					                "}\n" +
 | 
				
			||||||
| 
						 | 
					@ -175,10 +175,10 @@ export class TagUtils {
 | 
				
			||||||
        "\n" +
 | 
					        "\n" +
 | 
				
			||||||
        "```json\n" +
 | 
					        "```json\n" +
 | 
				
			||||||
        "{\n" +
 | 
					        "{\n" +
 | 
				
			||||||
        '  "osmTags": {\n' +
 | 
					        "  \"osmTags\": {\n" +
 | 
				
			||||||
        '    "or": [\n' +
 | 
					        "    \"or\": [\n" +
 | 
				
			||||||
        '      "amenity=school",\n' +
 | 
					        "      \"amenity=school\",\n" +
 | 
				
			||||||
        '      "amenity=kindergarten"\n' +
 | 
					        "      \"amenity=kindergarten\"\n" +
 | 
				
			||||||
        "    ]\n" +
 | 
					        "    ]\n" +
 | 
				
			||||||
        "  }\n" +
 | 
					        "  }\n" +
 | 
				
			||||||
        "}\n" +
 | 
					        "}\n" +
 | 
				
			||||||
| 
						 | 
					@ -194,7 +194,7 @@ export class TagUtils {
 | 
				
			||||||
        "If the schema-files note a type [`TagConfigJson`](https://github.com/pietervdvn/MapComplete/blob/develop/src/Models/ThemeConfig/Json/TagConfigJson.ts), you can use one of these values.\n" +
 | 
					        "If the schema-files note a type [`TagConfigJson`](https://github.com/pietervdvn/MapComplete/blob/develop/src/Models/ThemeConfig/Json/TagConfigJson.ts), you can use one of these values.\n" +
 | 
				
			||||||
        "\n" +
 | 
					        "\n" +
 | 
				
			||||||
        "In some cases, not every type of tags-filter can be used. For example,  _rendering_ an option with a regex is\n" +
 | 
					        "In some cases, not every type of tags-filter can be used. For example,  _rendering_ an option with a regex is\n" +
 | 
				
			||||||
        'fine (`"if": "brand~[Bb]randname", "then":" The brand is Brandname"`); but this regex can not be used to write a value\n' +
 | 
					        "fine (`\"if\": \"brand~[Bb]randname\", \"then\":\" The brand is Brandname\"`); but this regex can not be used to write a value\n" +
 | 
				
			||||||
        "into the database. The theme loader will however refuse to work with such inconsistencies and notify you of this while\n" +
 | 
					        "into the database. The theme loader will however refuse to work with such inconsistencies and notify you of this while\n" +
 | 
				
			||||||
        "you are building your theme.\n" +
 | 
					        "you are building your theme.\n" +
 | 
				
			||||||
        "\n" +
 | 
					        "\n" +
 | 
				
			||||||
| 
						 | 
					@ -205,18 +205,18 @@ export class TagUtils {
 | 
				
			||||||
        "\n" +
 | 
					        "\n" +
 | 
				
			||||||
        "```json\n" +
 | 
					        "```json\n" +
 | 
				
			||||||
        "{\n" +
 | 
					        "{\n" +
 | 
				
			||||||
        '  "and": [\n' +
 | 
					        "  \"and\": [\n" +
 | 
				
			||||||
        '    "key=value",\n' +
 | 
					        "    \"key=value\",\n" +
 | 
				
			||||||
        "    {\n" +
 | 
					        "    {\n" +
 | 
				
			||||||
        '      "or": [\n' +
 | 
					        "      \"or\": [\n" +
 | 
				
			||||||
        '        "other_key=value",\n' +
 | 
					        "        \"other_key=value\",\n" +
 | 
				
			||||||
        '        "other_key=some_other_value"\n' +
 | 
					        "        \"other_key=some_other_value\"\n" +
 | 
				
			||||||
        "      ]\n" +
 | 
					        "      ]\n" +
 | 
				
			||||||
        "    },\n" +
 | 
					        "    },\n" +
 | 
				
			||||||
        '    "key_which_should_be_missing=",\n' +
 | 
					        "    \"key_which_should_be_missing=\",\n" +
 | 
				
			||||||
        '    "key_which_should_have_a_value~*",\n' +
 | 
					        "    \"key_which_should_have_a_value~*\",\n" +
 | 
				
			||||||
        '    "key~.*some_regex_a*_b+_[a-z]?",\n' +
 | 
					        "    \"key~.*some_regex_a*_b+_[a-z]?\",\n" +
 | 
				
			||||||
        '    "height<1"\n' +
 | 
					        "    \"height<1\"\n" +
 | 
				
			||||||
        "  ]\n" +
 | 
					        "  ]\n" +
 | 
				
			||||||
        "}\n" +
 | 
					        "}\n" +
 | 
				
			||||||
        "```\n" +
 | 
					        "```\n" +
 | 
				
			||||||
| 
						 | 
					@ -246,7 +246,7 @@ export class TagUtils {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static asProperties(
 | 
					    static asProperties(
 | 
				
			||||||
        tags: TagsFilter | TagsFilter[],
 | 
					        tags: TagsFilter | TagsFilter[],
 | 
				
			||||||
        baseproperties: Record<string, string> = {}
 | 
					        baseproperties: Record<string, string> = {},
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        if (Array.isArray(tags)) {
 | 
					        if (Array.isArray(tags)) {
 | 
				
			||||||
            tags = new And(tags)
 | 
					            tags = new And(tags)
 | 
				
			||||||
| 
						 | 
					@ -274,11 +274,11 @@ export class TagUtils {
 | 
				
			||||||
    static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: false): Record<string, string[]>
 | 
					    static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: false): Record<string, string[]>
 | 
				
			||||||
    static SplitKeysRegex(
 | 
					    static SplitKeysRegex(
 | 
				
			||||||
        tagsFilters: UploadableTag[],
 | 
					        tagsFilters: UploadableTag[],
 | 
				
			||||||
        allowRegex: boolean
 | 
					        allowRegex: boolean,
 | 
				
			||||||
    ): Record<string, (string | RegexTag)[]>
 | 
					    ): Record<string, (string | RegexTag)[]>
 | 
				
			||||||
    static SplitKeysRegex(
 | 
					    static SplitKeysRegex(
 | 
				
			||||||
        tagsFilters: UploadableTag[],
 | 
					        tagsFilters: UploadableTag[],
 | 
				
			||||||
        allowRegex: boolean
 | 
					        allowRegex: boolean,
 | 
				
			||||||
    ): Record<string, (string | RegexTag)[]> {
 | 
					    ): Record<string, (string | RegexTag)[]> {
 | 
				
			||||||
        const keyValues: Record<string, (string | RegexTag)[]> = {}
 | 
					        const keyValues: Record<string, (string | RegexTag)[]> = {}
 | 
				
			||||||
        tagsFilters = [...tagsFilters] // copy all, use as queue
 | 
					        tagsFilters = [...tagsFilters] // copy all, use as queue
 | 
				
			||||||
| 
						 | 
					@ -307,7 +307,7 @@ export class TagUtils {
 | 
				
			||||||
                if (typeof key !== "string") {
 | 
					                if (typeof key !== "string") {
 | 
				
			||||||
                    console.error(
 | 
					                    console.error(
 | 
				
			||||||
                        "Invalid type to flatten the multiAnswer: key is a regex too",
 | 
					                        "Invalid type to flatten the multiAnswer: key is a regex too",
 | 
				
			||||||
                        tagsFilter
 | 
					                        tagsFilter,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    throw "Invalid type to FlattenMultiAnswer: key is a regex too"
 | 
					                    throw "Invalid type to FlattenMultiAnswer: key is a regex too"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					@ -508,7 +508,7 @@ export class TagUtils {
 | 
				
			||||||
    public static Tag(json: TagConfigJson, context?: string | ConversionContext): TagsFilterClosed
 | 
					    public static Tag(json: TagConfigJson, context?: string | ConversionContext): TagsFilterClosed
 | 
				
			||||||
    public static Tag(
 | 
					    public static Tag(
 | 
				
			||||||
        json: TagConfigJson,
 | 
					        json: TagConfigJson,
 | 
				
			||||||
        context: string | ConversionContext = ""
 | 
					        context: string | ConversionContext = "",
 | 
				
			||||||
    ): TagsFilterClosed {
 | 
					    ): TagsFilterClosed {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const ctx = typeof context === "string" ? context : context.path.join(".")
 | 
					            const ctx = typeof context === "string" ? context : context.path.join(".")
 | 
				
			||||||
| 
						 | 
					@ -540,7 +540,7 @@ export class TagUtils {
 | 
				
			||||||
            throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(
 | 
					            throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(
 | 
				
			||||||
                false,
 | 
					                false,
 | 
				
			||||||
                false,
 | 
					                false,
 | 
				
			||||||
                {}
 | 
					                {},
 | 
				
			||||||
            )}`
 | 
					            )}`
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -661,7 +661,7 @@ export class TagUtils {
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static removeShadowedElementsFrom(
 | 
					    public static removeShadowedElementsFrom(
 | 
				
			||||||
        blacklist: TagsFilter[],
 | 
					        blacklist: TagsFilter[],
 | 
				
			||||||
        listToFilter: TagsFilter[]
 | 
					        listToFilter: TagsFilter[],
 | 
				
			||||||
    ): TagsFilter[] {
 | 
					    ): TagsFilter[] {
 | 
				
			||||||
        return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf)))
 | 
					        return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf)))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -699,7 +699,7 @@ export class TagUtils {
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static containsEquivalents(
 | 
					    public static containsEquivalents(
 | 
				
			||||||
        guards: ReadonlyArray<TagsFilter>,
 | 
					        guards: ReadonlyArray<TagsFilter>,
 | 
				
			||||||
        listToFilter: ReadonlyArray<TagsFilter>
 | 
					        listToFilter: ReadonlyArray<TagsFilter>,
 | 
				
			||||||
    ): boolean {
 | 
					    ): boolean {
 | 
				
			||||||
        return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
 | 
					        return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -743,7 +743,7 @@ export class TagUtils {
 | 
				
			||||||
                    values.push(i + "")
 | 
					                    values.push(i + "")
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return values
 | 
					                return values
 | 
				
			||||||
            })
 | 
					            }),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return Utils.NoNull(spec)
 | 
					        return Utils.NoNull(spec)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -751,13 +751,13 @@ export class TagUtils {
 | 
				
			||||||
    private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
 | 
					    private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
 | 
				
			||||||
        if (json === undefined) {
 | 
					        if (json === undefined) {
 | 
				
			||||||
            throw new Error(
 | 
					            throw new Error(
 | 
				
			||||||
                `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
 | 
					                `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (typeof json != "string") {
 | 
					        if (typeof json != "string") {
 | 
				
			||||||
            if (json["and"] !== undefined && json["or"] !== undefined) {
 | 
					            if (json["and"] !== undefined && json["or"] !== undefined) {
 | 
				
			||||||
                throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of "parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
 | 
					                throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of "parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
 | 
				
			||||||
                    json
 | 
					                    json,
 | 
				
			||||||
                )}`
 | 
					                )}`
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (json["and"] !== undefined) {
 | 
					            if (json["and"] !== undefined) {
 | 
				
			||||||
| 
						 | 
					@ -839,13 +839,13 @@ export class TagUtils {
 | 
				
			||||||
                return new RegexTag(
 | 
					                return new RegexTag(
 | 
				
			||||||
                    withRegex.key,
 | 
					                    withRegex.key,
 | 
				
			||||||
                    new RegExp(".+", "si" + withRegex.modifier),
 | 
					                    new RegExp(".+", "si" + withRegex.modifier),
 | 
				
			||||||
                    withRegex.invert
 | 
					                    withRegex.invert,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return new RegexTag(
 | 
					            return new RegexTag(
 | 
				
			||||||
                withRegex.key,
 | 
					                withRegex.key,
 | 
				
			||||||
                new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
 | 
					                new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
 | 
				
			||||||
                withRegex.invert
 | 
					                withRegex.invert,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -967,10 +967,19 @@ export class TagUtils {
 | 
				
			||||||
                return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n")
 | 
					                return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n")
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            "## " +
 | 
					            "## " +
 | 
				
			||||||
                TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
 | 
					            TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
 | 
				
			||||||
                " Logical comparators",
 | 
					            " Logical comparators",
 | 
				
			||||||
            TagUtils.numberAndDateComparisonDocs,
 | 
					            TagUtils.numberAndDateComparisonDocs,
 | 
				
			||||||
            TagUtils.logicalOperator,
 | 
					            TagUtils.logicalOperator,
 | 
				
			||||||
        ].join("\n")
 | 
					        ].join("\n")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static fromProperties(tags: Record<string, string>): TagConfigJson | boolean {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const opt = new And(Object.keys(tags).map(k => new Tag(k, tags[k]))).optimize()
 | 
				
			||||||
 | 
					        if (opt === true || opt === false) {
 | 
				
			||||||
 | 
					            return opt
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return opt.asJson()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,8 @@ export abstract class TagsFilter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Indicates some form of equivalency:
 | 
					     * Indicates some form of equivalency:
 | 
				
			||||||
     * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties
 | 
					     * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties.
 | 
				
			||||||
 | 
					     * In other words: 'this' is a _stronger_ condition then 't'
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    abstract shadows(other: TagsFilter): boolean
 | 
					    abstract shadows(other: TagsFilter): boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,14 +41,14 @@ interface NSIEntry {
 | 
				
			||||||
 * Represents a single brand/operator/flagpole/...
 | 
					 * Represents a single brand/operator/flagpole/...
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export interface NSIItem {
 | 
					export interface NSIItem {
 | 
				
			||||||
    displayName: string
 | 
					    readonly displayName: string
 | 
				
			||||||
    id: string
 | 
					    readonly id: string
 | 
				
			||||||
    locationSet: {
 | 
					    locationSet: {
 | 
				
			||||||
        include: string[]
 | 
					        include: string[]
 | 
				
			||||||
        exclude: string[]
 | 
					        exclude: string[]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    tags: Record<string, string>
 | 
					    readonly  tags: Readonly<Record<string, string>>
 | 
				
			||||||
    fromTemplate?: boolean
 | 
					    readonly  fromTemplate?: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NameSuggestionIndex {
 | 
					export default class NameSuggestionIndex {
 | 
				
			||||||
| 
						 | 
					@ -77,7 +77,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
        >,
 | 
					        >,
 | 
				
			||||||
        features: Readonly<FeatureCollection>
 | 
					        features: Readonly<FeatureCollection>,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        this.nsiFile = nsiFile
 | 
					        this.nsiFile = nsiFile
 | 
				
			||||||
        this.nsiWdFile = nsiWdFile
 | 
					        this.nsiWdFile = nsiWdFile
 | 
				
			||||||
| 
						 | 
					@ -92,10 +92,10 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const [nsi, nsiWd, features] = await Promise.all(
 | 
					        const [nsi, nsiWd, features] = await Promise.all(
 | 
				
			||||||
            ["./assets/data/nsi/nsi.min.json", "./assets/data/nsi/wikidata.min.json", "./assets/data/nsi/featureCollection.min.json"].map((url) =>
 | 
					            ["./assets/data/nsi/nsi.min.json", "./assets/data/nsi/wikidata.min.json", "./assets/data/nsi/featureCollection.min.json"].map((url) =>
 | 
				
			||||||
                Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30)
 | 
					                Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"], <any> features)
 | 
					        NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"], <any>features)
 | 
				
			||||||
        return NameSuggestionIndex.inited
 | 
					        return NameSuggestionIndex.inited
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -126,13 +126,13 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    return Utils.downloadJsonCached<Record<string, number>>(
 | 
					                    return Utils.downloadJsonCached<Record<string, number>>(
 | 
				
			||||||
                        `./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
 | 
					                        `./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
 | 
				
			||||||
                        24 * 60 * 60 * 1000
 | 
					                        24 * 60 * 60 * 1000,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                } catch (e) {
 | 
					                } catch (e) {
 | 
				
			||||||
                    console.error("Could not fetch " + type + " statistics due to", e)
 | 
					                    console.error("Could not fetch " + type + " statistics due to", e)
 | 
				
			||||||
                    return undefined
 | 
					                    return undefined
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            })
 | 
					            }),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        stats = Utils.NoNull(stats)
 | 
					        stats = Utils.NoNull(stats)
 | 
				
			||||||
        if (stats.length === 1) {
 | 
					        if (stats.length === 1) {
 | 
				
			||||||
| 
						 | 
					@ -173,17 +173,17 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
    public async generateMappings(
 | 
					    public async generateMappings(
 | 
				
			||||||
        type: string,
 | 
					        type: string,
 | 
				
			||||||
        tags: Record<string, string>,
 | 
					        tags: Record<string, string>,
 | 
				
			||||||
        country: string[],
 | 
					        country?: string[],
 | 
				
			||||||
        location?: [number, number],
 | 
					        location?: [number, number],
 | 
				
			||||||
        options?: {
 | 
					        options?: {
 | 
				
			||||||
            /**
 | 
					            /**
 | 
				
			||||||
             * If set, sort by frequency instead of alphabetically
 | 
					             * If set, sort by frequency instead of alphabetically
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
            sortByFrequency: boolean
 | 
					            sortByFrequency: boolean
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
    ): Promise<Mapping[]> {
 | 
					    ): Promise<Mapping[]> {
 | 
				
			||||||
        const mappings: (Mapping & { frequency: number })[] = []
 | 
					        const mappings: (Mapping & { frequency: number })[] = []
 | 
				
			||||||
        const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country)
 | 
					        const frequencies = country !== undefined ? await NameSuggestionIndex.fetchFrequenciesFor(type, country) : {}
 | 
				
			||||||
        for (const key in tags) {
 | 
					        for (const key in tags) {
 | 
				
			||||||
            if (key.startsWith("_")) {
 | 
					            if (key.startsWith("_")) {
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
| 
						 | 
					@ -194,7 +194,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
                key,
 | 
					                key,
 | 
				
			||||||
                value,
 | 
					                value,
 | 
				
			||||||
                country.join(";"),
 | 
					                country.join(";"),
 | 
				
			||||||
                location
 | 
					                location,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if (!actualBrands) {
 | 
					            if (!actualBrands) {
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
| 
						 | 
					@ -202,8 +202,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
            for (const nsiItem of actualBrands) {
 | 
					            for (const nsiItem of actualBrands) {
 | 
				
			||||||
                const tags = nsiItem.tags
 | 
					                const tags = nsiItem.tags
 | 
				
			||||||
                const frequency = frequencies[nsiItem.displayName]
 | 
					                const frequency = frequencies[nsiItem.displayName]
 | 
				
			||||||
                const logos = this.nsiWdFile[nsiItem.tags[type + ":wikidata"]]?.logos
 | 
					                const iconUrl = this.getIconExternalUrl(nsiItem, type)
 | 
				
			||||||
                const iconUrl = logos?.facebook ?? logos?.wikidata
 | 
					 | 
				
			||||||
                const hasIcon = iconUrl !== undefined
 | 
					                const hasIcon = iconUrl !== undefined
 | 
				
			||||||
                let icon = undefined
 | 
					                let icon = undefined
 | 
				
			||||||
                if (hasIcon) {
 | 
					                if (hasIcon) {
 | 
				
			||||||
| 
						 | 
					@ -240,7 +239,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public supportedTags(
 | 
					    public supportedTags(
 | 
				
			||||||
        type: "operator" | "brand" | "flag" | "transit" | string
 | 
					        type: "operator" | "brand" | "flag" | "transit" | string,
 | 
				
			||||||
    ): Record<string, string[]> {
 | 
					    ): Record<string, string[]> {
 | 
				
			||||||
        const tags: Record<string, string[]> = {}
 | 
					        const tags: Record<string, string[]> = {}
 | 
				
			||||||
        const keys = Object.keys(this.nsiFile.nsi)
 | 
					        const keys = Object.keys(this.nsiFile.nsi)
 | 
				
			||||||
| 
						 | 
					@ -263,7 +262,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
     * Returns a list of all brands/operators
 | 
					     * Returns a list of all brands/operators
 | 
				
			||||||
     * @param type
 | 
					     * @param type
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public allPossible(type: "brand" | "operator"): NSIItem[] {
 | 
					    public allPossible(type: string): NSIItem[] {
 | 
				
			||||||
        const options: NSIItem[] = []
 | 
					        const options: NSIItem[] = []
 | 
				
			||||||
        const tags = this.supportedTags(type)
 | 
					        const tags = this.supportedTags(type)
 | 
				
			||||||
        for (const osmKey in tags) {
 | 
					        for (const osmKey in tags) {
 | 
				
			||||||
| 
						 | 
					@ -285,10 +284,10 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        type: string,
 | 
					        type: string,
 | 
				
			||||||
        tags: { key: string; value: string }[],
 | 
					        tags: { key: string; value: string }[],
 | 
				
			||||||
        country: string = undefined,
 | 
					        country: string = undefined,
 | 
				
			||||||
        location: [number, number] = undefined
 | 
					        location: [number, number] = undefined,
 | 
				
			||||||
    ): NSIItem[] {
 | 
					    ): NSIItem[] {
 | 
				
			||||||
        return tags.flatMap((tag) =>
 | 
					        return tags.flatMap((tag) =>
 | 
				
			||||||
            this.getSuggestionsForKV(type, tag.key, tag.value, country, location)
 | 
					            this.getSuggestionsForKV(type, tag.key, tag.value, country, location),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -311,7 +310,7 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        key: string,
 | 
					        key: string,
 | 
				
			||||||
        value: string,
 | 
					        value: string,
 | 
				
			||||||
        country: string = undefined,
 | 
					        country: string = undefined,
 | 
				
			||||||
        location: [number, number] = undefined
 | 
					        location: [number, number] = undefined,
 | 
				
			||||||
    ): NSIItem[] {
 | 
					    ): NSIItem[] {
 | 
				
			||||||
        const path = `${type}s/${key}/${value}`
 | 
					        const path = `${type}s/${key}/${value}`
 | 
				
			||||||
        const entry = this.nsiFile.nsi[path]
 | 
					        const entry = this.nsiFile.nsi[path]
 | 
				
			||||||
| 
						 | 
					@ -375,9 +374,29 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        center: [number, number],
 | 
					        center: [number, number],
 | 
				
			||||||
        options: {
 | 
					        options: {
 | 
				
			||||||
            sortByFrequency: boolean
 | 
					            sortByFrequency: boolean
 | 
				
			||||||
        }
 | 
					        },
 | 
				
			||||||
    ): Promise<Mapping[]> {
 | 
					    ): Promise<Mapping[]> {
 | 
				
			||||||
        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
					        const nsi = await NameSuggestionIndex.getNsiIndex()
 | 
				
			||||||
        return nsi.generateMappings(key, tags, country, center, options)
 | 
					        return nsi.generateMappings(key, tags, country, center, options)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Where can we find the URL on the world wide web?
 | 
				
			||||||
 | 
					     * Probably facebook! Don't use in the website, might expose people
 | 
				
			||||||
 | 
					     * @param nsiItem
 | 
				
			||||||
 | 
					     * @param type
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private getIconExternalUrl(nsiItem: NSIItem, type: string): string {
 | 
				
			||||||
 | 
					        const logos = this.nsiWdFile[nsiItem.tags[type + ":wikidata"]]?.logos
 | 
				
			||||||
 | 
					        return logos?.facebook ?? logos?.wikidata
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public getIconUrl(nsiItem: NSIItem, type: string) {
 | 
				
			||||||
 | 
					        let icon = "./assets/data/nsi/logos/" + nsiItem.id
 | 
				
			||||||
 | 
					        if (this.isSvg(nsiItem, type)) {
 | 
				
			||||||
 | 
					            icon = icon + ".svg"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return icon
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										220
									
								
								src/Models/ThemeConfig/Conversion/ExpandFilter.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/Models/ThemeConfig/Conversion/ExpandFilter.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,220 @@
 | 
				
			||||||
 | 
					import { DesugaringContext, DesugaringStep } from "./Conversion"
 | 
				
			||||||
 | 
					import { LayerConfigJson } from "../Json/LayerConfigJson"
 | 
				
			||||||
 | 
					import FilterConfigJson, { FilterConfigOptionJson } from "../Json/FilterConfigJson"
 | 
				
			||||||
 | 
					import predifined_filters from "../../../../assets/layers/filters/filters.json"
 | 
				
			||||||
 | 
					import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
 | 
				
			||||||
 | 
					import { ConversionContext } from "./ConversionContext"
 | 
				
			||||||
 | 
					import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
 | 
				
			||||||
 | 
					import { Utils } from "../../../Utils"
 | 
				
			||||||
 | 
					import { TagUtils } from "../../../Logic/Tags/TagUtils"
 | 
				
			||||||
 | 
					import { Tag } from "../../../Logic/Tags/Tag"
 | 
				
			||||||
 | 
					import { RegexTag } from "../../../Logic/Tags/RegexTag"
 | 
				
			||||||
 | 
					import { Or } from "../../../Logic/Tags/Or"
 | 
				
			||||||
 | 
					import Translations from "../../../UI/i18n/Translations"
 | 
				
			||||||
 | 
					import { FlatTag, OptimizedTag, TagsFilterClosed } from "../../../Logic/Tags/TagTypes"
 | 
				
			||||||
 | 
					import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
 | 
				
			||||||
 | 
					import { And } from "../../../Logic/Tags/And"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PruneFilters extends DesugaringStep<LayerConfigJson>{
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super("Removes all filters which are impossible, e.g. because they conflict with the base tags", ["filter"],"PruneFilters")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private prune(sourceTags:FlatTag, filter: FilterConfigJson, context: ConversionContext): FilterConfigJson{
 | 
				
			||||||
 | 
					        if(!filter.strict){
 | 
				
			||||||
 | 
					            return filter
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const countBefore = filter.options.length
 | 
				
			||||||
 | 
					        const newOptions: FilterConfigOptionJson[] = filter.options.filter(option => {
 | 
				
			||||||
 | 
					            if(!option.osmTags){
 | 
				
			||||||
 | 
					                return true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const condition = <OptimizedTag & TagsFilterClosed> TagUtils.Tag(option.osmTags).optimize()
 | 
				
			||||||
 | 
					            return condition.shadows(sourceTags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }).map(option => {
 | 
				
			||||||
 | 
					            if(!option.osmTags){
 | 
				
			||||||
 | 
					                return option
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let basetags: TagsFilter = <TagsFilter> And.construct([TagUtils.Tag(option.osmTags)]).optimize()
 | 
				
			||||||
 | 
					            if(basetags instanceof And){
 | 
				
			||||||
 | 
					                basetags = <TagsFilter> basetags.removePhraseConsideredKnown(sourceTags, true)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return {...option, osmTags: basetags.asJson()}
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        const countAfter = newOptions.length
 | 
				
			||||||
 | 
					        if(countAfter !== countBefore){
 | 
				
			||||||
 | 
					            context.enters("filter", filter.id   ).info("Pruned "+(countBefore-countAfter)+" options away from filter (out of "+countBefore+")")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return {...filter, options: newOptions, strict: undefined}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
 | 
				
			||||||
 | 
					        if(!Array.isArray(json.filter) || typeof json.source === "string"){
 | 
				
			||||||
 | 
					            return json
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(!json.source["osmTags"]){
 | 
				
			||||||
 | 
					            return json
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const sourceTags = TagUtils.Tag(json.source["osmTags"])
 | 
				
			||||||
 | 
					        return {...json, filter: json.filter?.map(obj => this.prune(sourceTags, <FilterConfigJson> obj, context))}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
 | 
				
			||||||
 | 
					    private static readonly predefinedFilters = ExpandFilter.load_filters()
 | 
				
			||||||
 | 
					    private _state: DesugaringContext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(state: DesugaringContext) {
 | 
				
			||||||
 | 
					        super(
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                "Expands filters: replaces a shorthand by the value found in 'filters.json'.",
 | 
				
			||||||
 | 
					                "If the string is formatted 'layername.filtername, it will be looked up into that layer instead. Note that pruning should still be done",
 | 
				
			||||||
 | 
					            ].join(" "),
 | 
				
			||||||
 | 
					            ["filter"],
 | 
				
			||||||
 | 
					            "ExpandFilter",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        this._state = state
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static load_filters(): Map<string, FilterConfigJson> {
 | 
				
			||||||
 | 
					        const filters = new Map<string, FilterConfigJson>()
 | 
				
			||||||
 | 
					        for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
 | 
				
			||||||
 | 
					            filters.set(filter.id, filter)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return filters
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static buildFilterFromTagRendering(
 | 
				
			||||||
 | 
					        tr: TagRenderingConfigJson,
 | 
				
			||||||
 | 
					        context: ConversionContext,
 | 
				
			||||||
 | 
					    ): FilterConfigJson {
 | 
				
			||||||
 | 
					        if (!(tr.mappings?.length >= 1)) {
 | 
				
			||||||
 | 
					            context.err(
 | 
				
			||||||
 | 
					                "Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const qtr = <QuestionableTagRenderingConfigJson>tr
 | 
				
			||||||
 | 
					        const options = qtr.mappings.map((mapping) => {
 | 
				
			||||||
 | 
					            let icon: string = mapping.icon?.["path"] ?? mapping.icon
 | 
				
			||||||
 | 
					            let emoji: string = undefined
 | 
				
			||||||
 | 
					            if (Utils.isEmoji(icon)) {
 | 
				
			||||||
 | 
					                emoji = icon
 | 
				
			||||||
 | 
					                icon = undefined
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let osmTags = TagUtils.Tag(mapping.if)
 | 
				
			||||||
 | 
					            if (qtr.multiAnswer && osmTags instanceof Tag) {
 | 
				
			||||||
 | 
					                osmTags = new RegexTag(
 | 
				
			||||||
 | 
					                    osmTags.key,
 | 
				
			||||||
 | 
					                    new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is"),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (mapping.alsoShowIf) {
 | 
				
			||||||
 | 
					                osmTags = new Or([osmTags, TagUtils.Tag(mapping.alsoShowIf)])
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return <FilterConfigOptionJson>{
 | 
				
			||||||
 | 
					                question: mapping.then,
 | 
				
			||||||
 | 
					                osmTags: osmTags.asJson(),
 | 
				
			||||||
 | 
					                searchTerms: mapping.searchTerms,
 | 
				
			||||||
 | 
					                icon,
 | 
				
			||||||
 | 
					                emoji,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        // Add default option
 | 
				
			||||||
 | 
					        options.unshift({
 | 
				
			||||||
 | 
					            question: tr["question"] ?? Translations.t.general.filterPanel.allTypes,
 | 
				
			||||||
 | 
					            osmTags: undefined,
 | 
				
			||||||
 | 
					            searchTerms: undefined,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            id: tr["id"],
 | 
				
			||||||
 | 
					            options,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
 | 
				
			||||||
 | 
					        if (json?.filter === undefined || json?.filter === null) {
 | 
				
			||||||
 | 
					            return json // Nothing to change here
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (json.filter["sameAs"] !== undefined) {
 | 
				
			||||||
 | 
					            return json // Nothing to change here
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const newFilters: FilterConfigJson[] = []
 | 
				
			||||||
 | 
					        const filters = <(FilterConfigJson | string)[]>json.filter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Create filters based on builtin filters or create them based on the tagRendering
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        for (let i = 0; i < filters.length; i++) {
 | 
				
			||||||
 | 
					            const filter = filters[i]
 | 
				
			||||||
 | 
					            if (filter === undefined) {
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (typeof filter !== "string") {
 | 
				
			||||||
 | 
					                newFilters.push(filter)
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const matchingTr = <TagRenderingConfigJson>(
 | 
				
			||||||
 | 
					                json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if (matchingTr) {
 | 
				
			||||||
 | 
					                const filter = ExpandFilter.buildFilterFromTagRendering(
 | 
				
			||||||
 | 
					                    matchingTr,
 | 
				
			||||||
 | 
					                    context.enters("filter", i),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                newFilters.push(filter)
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (filter.indexOf(".") > 0) {
 | 
				
			||||||
 | 
					                if (!(this._state.sharedLayers?.size > 0)) {
 | 
				
			||||||
 | 
					                    // This is a bootstrapping-run, we can safely ignore this
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const split = filter.split(".")
 | 
				
			||||||
 | 
					                if (split.length > 2) {
 | 
				
			||||||
 | 
					                    context.err(
 | 
				
			||||||
 | 
					                        "invalid filter name: " + filter + ", expected `layername.filterid`",
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const layer = this._state.sharedLayers.get(split[0])
 | 
				
			||||||
 | 
					                if (layer === undefined) {
 | 
				
			||||||
 | 
					                    context.err("Layer '" + split[0] + "' not found")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const expectedId = split[1]
 | 
				
			||||||
 | 
					                const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
 | 
				
			||||||
 | 
					                    (f) => typeof f !== "string" && f.id === expectedId,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                if (expandedFilter === undefined) {
 | 
				
			||||||
 | 
					                    context.err("Did not find filter with name " + filter)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    newFilters.push(<FilterConfigJson>expandedFilter)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Search for the filter:
 | 
				
			||||||
 | 
					            const found = ExpandFilter.predefinedFilters.get(filter)
 | 
				
			||||||
 | 
					            if (found === undefined) {
 | 
				
			||||||
 | 
					                const suggestions = Utils.sortedByLevenshteinDistance(
 | 
				
			||||||
 | 
					                    filter,
 | 
				
			||||||
 | 
					                    Array.from(ExpandFilter.predefinedFilters.keys()),
 | 
				
			||||||
 | 
					                    (t) => t,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                context
 | 
				
			||||||
 | 
					                    .enter(filter)
 | 
				
			||||||
 | 
					                    .err(
 | 
				
			||||||
 | 
					                        "While searching for predefined filter " +
 | 
				
			||||||
 | 
					                        filter +
 | 
				
			||||||
 | 
					                        ": this filter is not found. Perhaps you meant one of: " +
 | 
				
			||||||
 | 
					                        suggestions,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            newFilters.push(found)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return { ...json, filter: newFilters }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -10,10 +10,7 @@ import {
 | 
				
			||||||
    SetDefault,
 | 
					    SetDefault,
 | 
				
			||||||
} from "./Conversion"
 | 
					} from "./Conversion"
 | 
				
			||||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
 | 
					import { LayerConfigJson } from "../Json/LayerConfigJson"
 | 
				
			||||||
import {
 | 
					import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
 | 
				
			||||||
    MinimalTagRenderingConfigJson,
 | 
					 | 
				
			||||||
    TagRenderingConfigJson,
 | 
					 | 
				
			||||||
} from "../Json/TagRenderingConfigJson"
 | 
					 | 
				
			||||||
import { Utils } from "../../../Utils"
 | 
					import { Utils } from "../../../Utils"
 | 
				
			||||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
 | 
					import RewritableConfigJson from "../Json/RewritableConfigJson"
 | 
				
			||||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
 | 
					import SpecialVisualizations from "../../../UI/SpecialVisualizations"
 | 
				
			||||||
| 
						 | 
					@ -21,8 +18,7 @@ import Translations from "../../../UI/i18n/Translations"
 | 
				
			||||||
import { Translation } from "../../../UI/i18n/Translation"
 | 
					import { Translation } from "../../../UI/i18n/Translation"
 | 
				
			||||||
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
 | 
					import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
 | 
				
			||||||
import { AddContextToTranslations } from "./AddContextToTranslations"
 | 
					import { AddContextToTranslations } from "./AddContextToTranslations"
 | 
				
			||||||
import FilterConfigJson, { FilterConfigOptionJson } from "../Json/FilterConfigJson"
 | 
					import FilterConfigJson from "../Json/FilterConfigJson"
 | 
				
			||||||
import predifined_filters from "../../../../assets/layers/filters/filters.json"
 | 
					 | 
				
			||||||
import { TagConfigJson } from "../Json/TagConfigJson"
 | 
					import { TagConfigJson } from "../Json/TagConfigJson"
 | 
				
			||||||
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
 | 
					import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
 | 
				
			||||||
import ValidationUtils from "./ValidationUtils"
 | 
					import ValidationUtils from "./ValidationUtils"
 | 
				
			||||||
| 
						 | 
					@ -33,9 +29,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
 | 
				
			||||||
import { ConversionContext } from "./ConversionContext"
 | 
					import { ConversionContext } from "./ConversionContext"
 | 
				
			||||||
import { ExpandRewrite } from "./ExpandRewrite"
 | 
					import { ExpandRewrite } from "./ExpandRewrite"
 | 
				
			||||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
 | 
					import { TagUtils } from "../../../Logic/Tags/TagUtils"
 | 
				
			||||||
import { Tag } from "../../../Logic/Tags/Tag"
 | 
					import { ExpandFilter, PruneFilters } from "./ExpandFilter"
 | 
				
			||||||
import { RegexTag } from "../../../Logic/Tags/RegexTag"
 | 
					 | 
				
			||||||
import { Or } from "../../../Logic/Tags/Or"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
 | 
					class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
| 
						 | 
					@ -108,163 +102,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
 | 
				
			||||||
        return { ...json, filter: filters }
 | 
					        return { ...json, filter: filters }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
 | 
					 | 
				
			||||||
    private static readonly predefinedFilters = ExpandFilter.load_filters()
 | 
					 | 
				
			||||||
    private _state: DesugaringContext
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(state: DesugaringContext) {
 | 
					 | 
				
			||||||
        super(
 | 
					 | 
				
			||||||
            [
 | 
					 | 
				
			||||||
                "Expands filters: replaces a shorthand by the value found in 'filters.json'.",
 | 
					 | 
				
			||||||
                "If the string is formatted 'layername.filtername, it will be looked up into that layer instead.",
 | 
					 | 
				
			||||||
            ].join(" "),
 | 
					 | 
				
			||||||
            ["filter"],
 | 
					 | 
				
			||||||
            "ExpandFilter"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        this._state = state
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static load_filters(): Map<string, FilterConfigJson> {
 | 
					 | 
				
			||||||
        const filters = new Map<string, FilterConfigJson>()
 | 
					 | 
				
			||||||
        for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
 | 
					 | 
				
			||||||
            filters.set(filter.id, filter)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return filters
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static buildFilterFromTagRendering(
 | 
					 | 
				
			||||||
        tr: TagRenderingConfigJson,
 | 
					 | 
				
			||||||
        context: ConversionContext
 | 
					 | 
				
			||||||
    ): FilterConfigJson {
 | 
					 | 
				
			||||||
        if (!(tr.mappings?.length >= 1)) {
 | 
					 | 
				
			||||||
            context.err(
 | 
					 | 
				
			||||||
                "Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const qtr = <QuestionableTagRenderingConfigJson>tr
 | 
					 | 
				
			||||||
        const options = qtr.mappings.map((mapping) => {
 | 
					 | 
				
			||||||
            let icon: string = mapping.icon?.["path"] ?? mapping.icon
 | 
					 | 
				
			||||||
            let emoji: string = undefined
 | 
					 | 
				
			||||||
            if (Utils.isEmoji(icon)) {
 | 
					 | 
				
			||||||
                emoji = icon
 | 
					 | 
				
			||||||
                icon = undefined
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            let osmTags = TagUtils.Tag(mapping.if)
 | 
					 | 
				
			||||||
            if (qtr.multiAnswer && osmTags instanceof Tag) {
 | 
					 | 
				
			||||||
                osmTags = new RegexTag(
 | 
					 | 
				
			||||||
                    osmTags.key,
 | 
					 | 
				
			||||||
                    new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (mapping.alsoShowIf) {
 | 
					 | 
				
			||||||
                osmTags = new Or([osmTags, TagUtils.Tag(mapping.alsoShowIf)])
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return <FilterConfigOptionJson>{
 | 
					 | 
				
			||||||
                question: mapping.then,
 | 
					 | 
				
			||||||
                osmTags: osmTags.asJson(),
 | 
					 | 
				
			||||||
                searchTerms: mapping.searchTerms,
 | 
					 | 
				
			||||||
                icon,
 | 
					 | 
				
			||||||
                emoji,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        // Add default option
 | 
					 | 
				
			||||||
        options.unshift({
 | 
					 | 
				
			||||||
            question: tr["question"] ?? Translations.t.general.filterPanel.allTypes,
 | 
					 | 
				
			||||||
            osmTags: undefined,
 | 
					 | 
				
			||||||
            searchTerms: undefined,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            id: tr["id"],
 | 
					 | 
				
			||||||
            options,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
 | 
					 | 
				
			||||||
        if (json?.filter === undefined || json?.filter === null) {
 | 
					 | 
				
			||||||
            return json // Nothing to change here
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (json.filter["sameAs"] !== undefined) {
 | 
					 | 
				
			||||||
            return json // Nothing to change here
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const newFilters: FilterConfigJson[] = []
 | 
					 | 
				
			||||||
        const filters = <(FilterConfigJson | string)[]>json.filter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * Create filters based on builtin filters or create them based on the tagRendering
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        for (let i = 0; i < filters.length; i++) {
 | 
					 | 
				
			||||||
            const filter = filters[i]
 | 
					 | 
				
			||||||
            if (filter === undefined) {
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (typeof filter !== "string") {
 | 
					 | 
				
			||||||
                newFilters.push(filter)
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const matchingTr = <TagRenderingConfigJson>(
 | 
					 | 
				
			||||||
                json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            if (matchingTr) {
 | 
					 | 
				
			||||||
                const filter = ExpandFilter.buildFilterFromTagRendering(
 | 
					 | 
				
			||||||
                    matchingTr,
 | 
					 | 
				
			||||||
                    context.enters("filter", i)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                newFilters.push(filter)
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (filter.indexOf(".") > 0) {
 | 
					 | 
				
			||||||
                if (!(this._state.sharedLayers?.size > 0)) {
 | 
					 | 
				
			||||||
                    // This is a bootstrapping-run, we can safely ignore this
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const split = filter.split(".")
 | 
					 | 
				
			||||||
                if (split.length > 2) {
 | 
					 | 
				
			||||||
                    context.err(
 | 
					 | 
				
			||||||
                        "invalid filter name: " + filter + ", expected `layername.filterid`"
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const layer = this._state.sharedLayers.get(split[0])
 | 
					 | 
				
			||||||
                if (layer === undefined) {
 | 
					 | 
				
			||||||
                    context.err("Layer '" + split[0] + "' not found")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const expectedId = split[1]
 | 
					 | 
				
			||||||
                const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
 | 
					 | 
				
			||||||
                    (f) => typeof f !== "string" && f.id === expectedId
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                if (expandedFilter === undefined) {
 | 
					 | 
				
			||||||
                    context.err("Did not find filter with name " + filter)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    newFilters.push(<FilterConfigJson>expandedFilter)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            // Search for the filter:
 | 
					 | 
				
			||||||
            const found = ExpandFilter.predefinedFilters.get(filter)
 | 
					 | 
				
			||||||
            if (found === undefined) {
 | 
					 | 
				
			||||||
                const suggestions = Utils.sortedByLevenshteinDistance(
 | 
					 | 
				
			||||||
                    filter,
 | 
					 | 
				
			||||||
                    Array.from(ExpandFilter.predefinedFilters.keys()),
 | 
					 | 
				
			||||||
                    (t) => t
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                context
 | 
					 | 
				
			||||||
                    .enter(filter)
 | 
					 | 
				
			||||||
                    .err(
 | 
					 | 
				
			||||||
                        "While searching for predefined filter " +
 | 
					 | 
				
			||||||
                            filter +
 | 
					 | 
				
			||||||
                            ": this filter is not found. Perhaps you meant one of: " +
 | 
					 | 
				
			||||||
                            suggestions
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            newFilters.push(found)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return { ...json, filter: newFilters }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpandTagRendering extends Conversion<
 | 
					class ExpandTagRendering extends Conversion<
 | 
				
			||||||
    | string
 | 
					    | string
 | 
				
			||||||
| 
						 | 
					@ -1481,7 +1318,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
 | 
				
			||||||
                    new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
 | 
					                    new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            new AddFiltersFromTagRenderings(),
 | 
					            new AddFiltersFromTagRenderings(),
 | 
				
			||||||
            new ExpandFilter(state)
 | 
					            new ExpandFilter(state),
 | 
				
			||||||
 | 
					            new PruneFilters()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,15 @@ export default interface FilterConfigJson {
 | 
				
			||||||
     * An id/name for this filter, used to set the URL parameters
 | 
					     * An id/name for this filter, used to set the URL parameters
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    id: string
 | 
					    id: string
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * If set, the options will be pruned. Only items for which the filter match the layer source will be kept.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * For example, we import types of brands from the nsi. This contains a ton of items, e.g.
 | 
				
			||||||
 | 
					     * [{question: "Brand X", osmTags: {"and": ["shop=clothes", "brand=Brand X]}, {osmTags: {"and": "shop=convenience", ...} ...} ]
 | 
				
			||||||
 | 
					     * Of course, when making a layer about `shop=clothes`, we'll only want to keep the clothes shops.
 | 
				
			||||||
 | 
					     * If set to strict and the source is `shop=clothes`, only those options which have shop=clothes will be returned
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    strict?: boolean
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The options for a filter
 | 
					     * The options for a filter
 | 
				
			||||||
     * If there are multiple options these will be a list of radio buttons
 | 
					     * If there are multiple options these will be a list of radio buttons
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -601,4 +601,9 @@ export interface LayerConfigJson {
 | 
				
			||||||
     * group: hidden
 | 
					     * group: hidden
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    snapName?: Translatable
 | 
					    snapName?: Translatable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * group: hidden
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "#dont-translate": "*"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue