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