forked from MapComplete/MapComplete
		
	Fix naughty bug in tag optimization by adding better typing
This commit is contained in:
		
							parent
							
								
									b98245fafb
								
							
						
					
					
						commit
						fd16e165c4
					
				
					 11 changed files with 223 additions and 93 deletions
				
			
		|  | @ -6,6 +6,7 @@ import { RegexTag } from "./RegexTag" | |||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import ComparingTag from "./ComparingTag" | ||||
| import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes" | ||||
| 
 | ||||
| export class And extends TagsFilter { | ||||
|     public and: TagsFilter[] | ||||
|  | @ -15,6 +16,8 @@ export class And extends TagsFilter { | |||
|         this.and = and | ||||
|     } | ||||
| 
 | ||||
|     public static construct(and: TagsFilter[]): TagsFilter | ||||
|     public static construct(and: (FlatTag | (Or & OptimizedTag))[]): TagsFilterClosed & OptimizedTag | ||||
|     public static construct(and: TagsFilter[]): TagsFilter { | ||||
|         if (and.length === 1) { | ||||
|             return and[0] | ||||
|  | @ -175,6 +178,10 @@ export class And extends TagsFilter { | |||
|      * 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 | ||||
|      * | ||||
|      * @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 value the given knownExpression is considered to have this value, namely 'true' or 'false' | ||||
|      * | ||||
|      * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
 | ||||
|      * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
 | ||||
|      * new And([ new RegexTag("key",/^..*$/) ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
 | ||||
|  | @ -187,13 +194,14 @@ export class And extends TagsFilter { | |||
|      * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) | ||||
|      * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
 | ||||
|      */ | ||||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean { | ||||
|         const newAnds: TagsFilter[] = [] | ||||
|         for (const tag of this.and) { | ||||
|             if (tag instanceof And) { | ||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: "+this.asHumanString() | ||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " + this.asHumanString() | ||||
|             } | ||||
|             if (tag instanceof Or) { | ||||
|                 // Second try
 | ||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||
|                 if (r === true) { | ||||
|                     continue | ||||
|  | @ -232,7 +240,7 @@ export class And extends TagsFilter { | |||
|         if (newAnds.length === 0) { | ||||
|             return true | ||||
|         } | ||||
|         return And.construct(newAnds) | ||||
|         return And.construct(newAnds).optimize() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -265,7 +273,7 @@ export class And extends TagsFilter { | |||
|      * const parsed = TagUtils.Tag(orig) | ||||
|      * parsed.optimize().asJson() // => orig
 | ||||
|      */ | ||||
|     optimize(): TagsFilter | boolean { | ||||
|     optimize(): (TagsFilterClosed & OptimizedTag) | boolean { | ||||
|         if (this.and.length === 0) { | ||||
|             return true | ||||
|         } | ||||
|  | @ -276,7 +284,7 @@ export class And extends TagsFilter { | |||
|             // We have an AND with a contained false: this is always 'false'
 | ||||
|             return false | ||||
|         } | ||||
|         const optimized = <TagsFilter[]>optimizedRaw | ||||
|         const optimized = <(TagsFilterClosed & OptimizedTag)[]>optimizedRaw | ||||
| 
 | ||||
|         for (let i = 0; i < optimized.length; i++) { | ||||
|             for (let j = i + 1; j < optimized.length; j++) { | ||||
|  | @ -299,7 +307,7 @@ export class And extends TagsFilter { | |||
|         } | ||||
| 
 | ||||
|         { | ||||
|             // Conflicting keys do return false
 | ||||
|             // Conflicting keys do return false. We build a 'known' set and check for conflicts
 | ||||
|             const properties: Record<string, string> = {} | ||||
|             for (const opt of optimized) { | ||||
|                 if (opt instanceof Tag) { | ||||
|  | @ -366,12 +374,12 @@ export class And extends TagsFilter { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const newAnds: TagsFilter[] = [] | ||||
|         const newAnds: (FlatTag | (Or & OptimizedTag))[] = [] | ||||
|         let containedOrs: (Or & OptimizedTag)[] = [] | ||||
| 
 | ||||
|         let containedOrs: Or[] = [] | ||||
|         for (const tf of optimized) { | ||||
|             if (tf instanceof And) { | ||||
|                 newAnds.push(...tf.and) | ||||
|                 newAnds.push(...TagTypes.safeAnd(tf)) | ||||
|             } else if (tf instanceof Or) { | ||||
|                 containedOrs.push(tf) | ||||
|             } else { | ||||
|  | @ -382,10 +390,10 @@ export class And extends TagsFilter { | |||
|         { | ||||
|             let dirty = false | ||||
|             do { | ||||
|                 const cleanedContainedOrs: Or[] = [] | ||||
|                 const cleanedContainedOrs: (Or & OptimizedTag)[] = [] | ||||
|                 outer: for (let containedOr of containedOrs) { | ||||
|                     for (const known of newAnds) { | ||||
|                         // input for optimazation: (K=V & (X=Y | K=V))
 | ||||
|                         // input for optimization: (K=V & (X=Y | K=V))
 | ||||
|                         // containedOr: (X=Y | K=V)
 | ||||
|                         // newAnds (and thus known): (K=V) --> true
 | ||||
|                         const cleaned = containedOr.removePhraseConsideredKnown(known, true) | ||||
|  | @ -401,11 +409,17 @@ export class And extends TagsFilter { | |||
|                             containedOr = cleaned | ||||
|                             continue | ||||
|                         } | ||||
|                         if (cleaned instanceof And) { | ||||
|                             // An optimized 'And' should not contain 'Ands', we can safely cast
 | ||||
|                             newAnds.push(...TagTypes.safeAnd(cleaned)) | ||||
|                             continue | ||||
|                         } | ||||
|                         // the 'or' dissolved into a normal tag -> it has to be added to the newAnds
 | ||||
|                         newAnds.push(cleaned) | ||||
|                         dirty = true // rerun this algo later on
 | ||||
|                         continue outer | ||||
|                     } | ||||
| 
 | ||||
|                     cleanedContainedOrs.push(containedOr) | ||||
|                 } | ||||
|                 containedOrs = cleanedContainedOrs | ||||
|  | @ -433,9 +447,9 @@ export class And extends TagsFilter { | |||
|             if (commonValues.length === 0) { | ||||
|                 newAnds.push(...containedOrs) | ||||
|             } else { | ||||
|                 const newOrs: TagsFilter[] = [] | ||||
|                 const newOrs: TagsFilterClosed[] = [] | ||||
|                 for (const containedOr of containedOrs) { | ||||
|                     const elements = containedOr.or.filter( | ||||
|                     const elements: (FlatTag | (And & OptimizedTag))[] = TagTypes.safeOr( containedOr).filter( | ||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||
|                     ) | ||||
|                     if (elements.length > 0) { | ||||
|  | @ -450,6 +464,8 @@ export class And extends TagsFilter { | |||
|                     return false | ||||
|                 } else if (result === true) { | ||||
|                     // neutral element: skip
 | ||||
|                 }else if(result instanceof And) { | ||||
|                     newAnds.push(...TagTypes.safeAnd(result)) | ||||
|                 } else { | ||||
|                     newAnds.push(result) | ||||
|                 } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { Tag } from "./Tag" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { OptimizedTag } from "./TagTypes" | ||||
| 
 | ||||
| export default class ComparingTag extends TagsFilter { | ||||
|     public readonly key: string | ||||
|  | @ -121,8 +122,8 @@ export default class ComparingTag extends TagsFilter { | |||
|         return this.key + this._representation + this._boundary | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this | ||||
|     optimize(): (ComparingTag & OptimizedTag) | boolean { | ||||
|         return <any> this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|  |  | |||
|  | @ -3,6 +3,11 @@ import { TagUtils } from "./TagUtils" | |||
| import { And } from "./And" | ||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { Tag } from "./Tag" | ||||
| import { RegexTag } from "./RegexTag" | ||||
| import SubstitutingTag from "./SubstitutingTag" | ||||
| import ComparingTag from "./ComparingTag" | ||||
| import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes" | ||||
| 
 | ||||
| export class Or extends TagsFilter { | ||||
|     public or: TagsFilter[] | ||||
|  | @ -12,6 +17,9 @@ export class Or extends TagsFilter { | |||
|         this.or = or | ||||
|     } | ||||
| 
 | ||||
|     public static construct(or: TagsFilter[]): TagsFilter | ||||
|     public static construct<T extends TagsFilter>(or: [T]): T | ||||
|     public static construct(or: ((And & OptimizedTag) | FlatTag)[]): (TagsFilterClosed & OptimizedTag) | ||||
|     public static construct(or: TagsFilter[]): TagsFilter { | ||||
|         if (or.length === 1) { | ||||
|             return or[0] | ||||
|  | @ -59,7 +67,7 @@ export class Or extends TagsFilter { | |||
|         return this.or | ||||
|             .map((t) => { | ||||
|                 let e = t.asHumanString(linkToWiki, shorten, properties) | ||||
|                 if (t["and"]) { | ||||
|                 if (t["and"] || t["or"]) { | ||||
|                     e = "(" + e + ")" | ||||
|                 } | ||||
|                 return e | ||||
|  | @ -115,13 +123,13 @@ export class Or extends TagsFilter { | |||
|      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => new Tag("other_key","value")
 | ||||
|      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 | ||||
|      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
 | ||||
|      * new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")]).removePhraseConsideredKnown(new Tag("foo","bar"), false) // => new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")])
 | ||||
|      * new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")]).removePhraseConsideredKnown(new Tag("foo","bar"), false) // => new Or([new RegexTag("c", "d"), new RegexTag("x", "y", true)])
 | ||||
|      */ | ||||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||
|     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean { | ||||
|         const newOrs: TagsFilter[] = [] | ||||
|         for (const tag of this.or) { | ||||
|             if (tag instanceof Or) { | ||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: "+this.asHumanString(false, false, {}) | ||||
|                 throw "Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: " + this.asHumanString() | ||||
|             } | ||||
|             if (tag instanceof And) { | ||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||
|  | @ -160,7 +168,7 @@ export class Or extends TagsFilter { | |||
|         if (newOrs.length === 0) { | ||||
|             return false | ||||
|         } | ||||
|         return Or.construct(newOrs) | ||||
|         return Or.construct(newOrs).optimize() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -169,7 +177,7 @@ export class Or extends TagsFilter { | |||
|      * parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
 | ||||
|      * | ||||
|      */ | ||||
|     optimize(): TagsFilter | boolean { | ||||
|     optimize(): (TagsFilterClosed & OptimizedTag) | boolean { | ||||
|         if (this.or.length === 0) { | ||||
|             return false | ||||
|         } | ||||
|  | @ -181,26 +189,28 @@ export class Or extends TagsFilter { | |||
|             // We have an OR with a contained true: this is always 'true'
 | ||||
|             return true | ||||
|         } | ||||
|         const optimized = <TagsFilter[]>optimizedRaw | ||||
|         const optimized = optimizedRaw | ||||
| 
 | ||||
|         const newOrs: TagsFilter[] = [] | ||||
|         let containedAnds: And[] = [] | ||||
|         const newOrs: ((And & OptimizedTag) | FlatTag)[] = [] | ||||
|         let containedAnds: (And & OptimizedTag)[] = [] | ||||
|         for (const tf of optimized) { | ||||
|             if (tf["or"]) { | ||||
|             if (tf instanceof Or) { | ||||
|                 // expand all the nested ors...
 | ||||
|                 newOrs.push(...tf["or"]) | ||||
|                 for (const clauseOr of TagTypes.safeOr(tf)) { | ||||
|                     newOrs.push(clauseOr) | ||||
|                 } | ||||
|             } else if (tf instanceof And) { | ||||
|                 // partition of all the ands
 | ||||
|                 containedAnds.push(tf) | ||||
|             } else { | ||||
|                 newOrs.push(tf) | ||||
|                 newOrs.push(<(Tag | (And & OptimizedTag) | RegexTag | SubstitutingTag | ComparingTag)>tf) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         { | ||||
|             let dirty = false | ||||
|             do { | ||||
|                 const cleanedContainedANds: And[] = [] | ||||
|                 const cleanedContainedANds: (And & OptimizedTag)[] = [] | ||||
|                 outer: for (let containedAnd of containedAnds) { | ||||
|                     for (const known of newOrs) { | ||||
|                         // input for optimization: (K=V | (X=Y & K=V))
 | ||||
|  | @ -219,8 +229,16 @@ export class Or extends TagsFilter { | |||
|                             containedAnd = cleaned | ||||
|                             continue // clean up with the other known values
 | ||||
|                         } | ||||
| 
 | ||||
|                         if(cleaned instanceof Or){ | ||||
|                             // An optimized 'or' should not contain 'ors', we can safely cast
 | ||||
|                             newOrs.push(...TagTypes.safeOr(cleaned)) | ||||
|                             continue | ||||
|                         } | ||||
| 
 | ||||
|                         const noAnd: OptimizedTag & (Tag | RegexTag | SubstitutingTag | ComparingTag) = cleaned | ||||
|                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 | ||||
|                         newOrs.push(cleaned) | ||||
|                         newOrs.push(noAnd) | ||||
|                         dirty = true // rerun this algo later on
 | ||||
|                         continue outer | ||||
|                     } | ||||
|  | @ -243,16 +261,16 @@ export class Or extends TagsFilter { | |||
|             if (commonValues.length === 0) { | ||||
|                 newOrs.push(...containedAnds) | ||||
|             } else { | ||||
|                 const newAnds: TagsFilter[] = [] | ||||
|                 const newAnds: TagsFilterClosed[] = [] | ||||
|                 for (const containedAnd of containedAnds) { | ||||
|                     const elements = containedAnd.and.filter( | ||||
|                     const elements: (FlatTag | (Or & OptimizedTag))[] = TagTypes.safeAnd(containedAnd).filter( | ||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||
|                     ) | ||||
|                     if (elements.length == 0) { | ||||
|                         continue | ||||
|                     } | ||||
|                     if (elements.length > 0) { | ||||
|                         newAnds.push(And.construct(elements)) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (newAnds.length > 0) { | ||||
|                     commonValues.push(Or.construct(newAnds)) | ||||
|                 } | ||||
|  | @ -262,9 +280,10 @@ export class Or extends TagsFilter { | |||
|                     return true | ||||
|                 } else if (result === false) { | ||||
|                     // neutral element: skip
 | ||||
|                 } else if (commonValues.length > 0) { | ||||
|                     newOrs.push(And.construct(commonValues)) | ||||
|                 } | ||||
|                 } else if(result instanceof Or){ | ||||
|                     newOrs.push(...TagTypes.safeOr(result)) | ||||
|                 }else | ||||
|                     newOrs.push(result) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { Tag } from "./Tag" | |||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { OptimizedTag } from "./TagTypes" | ||||
| 
 | ||||
| export class RegexTag extends TagsFilter { | ||||
|     public readonly key: RegExp | string | ||||
|  | @ -354,8 +355,8 @@ export class RegexTag extends TagsFilter { | |||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this | ||||
|     optimize(): (RegexTag & OptimizedTag) | boolean { | ||||
|         return <any> this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { Tag } from "./Tag" | |||
| import { Utils } from "../../Utils" | ||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { OptimizedTag } from "./TagTypes" | ||||
| 
 | ||||
| /** | ||||
|  * The substituting-tag uses the tags of a feature a variables and replaces them. | ||||
|  | @ -111,8 +112,8 @@ export default class SubstitutingTag extends TagsFilter { | |||
|         return [{ k: this._key, v: v }] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this | ||||
|     optimize(): (SubstitutingTag & OptimizedTag) | boolean { | ||||
|         return <any> this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { TagsFilter } from "./TagsFilter" | |||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { RegexTag } from "./RegexTag" | ||||
| import { OptimizedTag } from "./TagTypes" | ||||
| 
 | ||||
| export class Tag extends TagsFilter { | ||||
|     public key: string | ||||
|  | @ -66,7 +67,7 @@ export class Tag extends TagsFilter { | |||
|     asOverpass(): string[] { | ||||
|         if (this.value === "") { | ||||
|             // NOT having this key
 | ||||
|             return ['[!"' + this.key + '"]'] | ||||
|             return ["[!\"" + this.key + "\"]"] | ||||
|         } | ||||
|         return [`["${this.key}"="${this.value}"]`] | ||||
|     } | ||||
|  | @ -164,8 +165,8 @@ export class Tag extends TagsFilter { | |||
|         return [{ k: this.key, v: this.value }] | ||||
|     } | ||||
| 
 | ||||
|     optimize(): TagsFilter | boolean { | ||||
|         return this | ||||
|     optimize(): (Tag & OptimizedTag) | boolean { | ||||
|         return <any>this | ||||
|     } | ||||
| 
 | ||||
|     isNegative(): boolean { | ||||
|  |  | |||
							
								
								
									
										35
									
								
								src/Logic/Tags/TagTypes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/Logic/Tags/TagTypes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| import { TagsFilter } from "./TagsFilter" | ||||
| import { Tag } from "./Tag" | ||||
| import SubstitutingTag from "./SubstitutingTag" | ||||
| import { And } from "./And" | ||||
| import { RegexTag } from "./RegexTag" | ||||
| import ComparingTag from "./ComparingTag" | ||||
| import { Or } from "./Or" | ||||
| 
 | ||||
| declare const __is_optimized: unique symbol | ||||
| type Brand<B> = { [__is_optimized]: B } | ||||
| /** | ||||
|  * A marker class, no actual content | ||||
|  */ | ||||
| export type OptimizedTag = Brand<TagsFilter> | ||||
| 
 | ||||
| 
 | ||||
| export type UploadableTag = Tag | SubstitutingTag | And | ||||
| /** | ||||
|  * Not nested | ||||
|  */ | ||||
| export type FlatTag = Tag | RegexTag | SubstitutingTag | ComparingTag | ||||
| export type TagsFilterClosed = FlatTag | And | Or | ||||
| 
 | ||||
| 
 | ||||
| export class TagTypes { | ||||
| 
 | ||||
|     static safeAnd(and: And & OptimizedTag): ((FlatTag | (Or & OptimizedTag)) & OptimizedTag)[]{ | ||||
|         return <any> and.and | ||||
|     } | ||||
| 
 | ||||
|     static safeOr(or: Or & OptimizedTag): ((FlatTag | (And & OptimizedTag)) & OptimizedTag)[]{ | ||||
|         return <any> or.or | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -10,9 +10,9 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | |||
| import key_counts from "../../assets/key_totals.json" | ||||
| 
 | ||||
| import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext" | ||||
| import { TagsFilterClosed, UploadableTag } from "./TagTypes" | ||||
| 
 | ||||
| type Tags = Record<string, string> | ||||
| export type UploadableTag = Tag | SubstitutingTag | And | ||||
| 
 | ||||
| export class TagUtils { | ||||
|     public static readonly comparators: ReadonlyArray< | ||||
|  | @ -476,7 +476,7 @@ export class TagUtils { | |||
|      * regex.matchesProperties({maxspeed: "50 mph"}) // => true
 | ||||
|      */ | ||||
| 
 | ||||
|     public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilter { | ||||
|     public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilterClosed { | ||||
|         try { | ||||
|             const ctx = typeof context === "string" ? context : context.path.join(".") | ||||
|             return this.ParseTagUnsafe(json, ctx) | ||||
|  | @ -712,7 +712,7 @@ export class TagUtils { | |||
|         return Utils.NoNull(spec) | ||||
|     } | ||||
| 
 | ||||
|     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { | ||||
|     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed { | ||||
|         if (json === undefined) { | ||||
|             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` | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
| import { ExpressionSpecification } from "maplibre-gl" | ||||
| import { OptimizedTag, TagsFilterClosed } from "./TagTypes" | ||||
| 
 | ||||
| export abstract class TagsFilter { | ||||
|     abstract asOverpass(): string[] | ||||
|  | @ -50,7 +51,7 @@ export abstract class TagsFilter { | |||
|     /** | ||||
|      * Returns an optimized version (or self) of this tagsFilter | ||||
|      */ | ||||
|     abstract optimize(): TagsFilter | boolean | ||||
|     abstract optimize(): (OptimizedTag & TagsFilterClosed) | boolean | ||||
| 
 | ||||
|     /** | ||||
|      * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ describe("Tag optimalization", () => { | |||
|             const t = new And([ | ||||
|                 new Tag("foo", "bar"), | ||||
|                 new Or([new Tag("x", "y"), new Tag("a", "b")]), | ||||
|                 new Or([new Tag("x", "y"), new Tag("c", "d")]), | ||||
|                 new Or([new Tag("x", "y"), new Tag("c", "d")]) | ||||
|             ]) | ||||
|             const opt = <TagsFilter>t.optimize() | ||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& (x=y| (a=b&c=d) )") | ||||
|  | @ -42,7 +42,7 @@ describe("Tag optimalization", () => { | |||
|             const t = new And([ | ||||
|                 new Tag("foo", "bar"), | ||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), | ||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("c", "d")]), | ||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("c", "d")]) | ||||
|             ]) | ||||
|             const opt = <TagsFilter>t.optimize() | ||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& ( (a=b&c=d) |x=y)") | ||||
|  | @ -53,7 +53,7 @@ describe("Tag optimalization", () => { | |||
|             const t = new And([ | ||||
|                 new Tag("foo", "bar"), | ||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), | ||||
|                 new Or([new RegexTag("x", "y", true), new RegexTag("c", "d")]), | ||||
|                 new Or([new RegexTag("x", "y", true), new RegexTag("c", "d")]) | ||||
|             ]) | ||||
|             const opt = <TagsFilter>t.optimize() | ||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& (a=b|x=y) & (c=d|x!=y)") | ||||
|  | @ -86,12 +86,12 @@ describe("Tag optimalization", () => { | |||
|                     { | ||||
|                         and: [ | ||||
|                             { | ||||
|                                 or: ["X=Y", "FOO=BAR"], | ||||
|                                 or: ["X=Y", "FOO=BAR"] | ||||
|                             }, | ||||
|                             "bicycle=yes", | ||||
|                         ], | ||||
|                     }, | ||||
|                 ], | ||||
|                             "bicycle=yes" | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             }) | ||||
|             // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) )
 | ||||
|             // This is equivalent to (X=Y | FOO=BAR)
 | ||||
|  | @ -109,11 +109,11 @@ describe("Tag optimalization", () => { | |||
|                                     "amenity=charging_station", | ||||
|                                     "disused:amenity=charging_station", | ||||
|                                     "planned:amenity=charging_station", | ||||
|                                     "construction:amenity=charging_station", | ||||
|                                 ], | ||||
|                                     "construction:amenity=charging_station" | ||||
|                                 ] | ||||
|                             }, | ||||
|                             "bicycle=yes", | ||||
|                         ], | ||||
|                             "bicycle=yes" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         and: [ | ||||
|  | @ -122,19 +122,19 @@ describe("Tag optimalization", () => { | |||
|                                     "amenity=charging_station", | ||||
|                                     "disused:amenity=charging_station", | ||||
|                                     "planned:amenity=charging_station", | ||||
|                                     "construction:amenity=charging_station", | ||||
|                                 ], | ||||
|                             }, | ||||
|                         ], | ||||
|                                     "construction:amenity=charging_station" | ||||
|                                 ] | ||||
|                             } | ||||
|                         ] | ||||
|                     }, | ||||
|                     "amenity=toilets", | ||||
|                     "amenity=bench", | ||||
|                     "leisure=picnic_table", | ||||
|                     { | ||||
|                         and: ["tower:type=observation"], | ||||
|                         and: ["tower:type=observation"] | ||||
|                     }, | ||||
|                     { | ||||
|                         and: ["amenity=bicycle_repair_station"], | ||||
|                         and: ["amenity=bicycle_repair_station"] | ||||
|                     }, | ||||
|                     { | ||||
|                         and: [ | ||||
|  | @ -143,16 +143,16 @@ describe("Tag optimalization", () => { | |||
|                                     "amenity=bicycle_rental", | ||||
|                                     "bicycle_rental~*", | ||||
|                                     "service:bicycle:rental=yes", | ||||
|                                     "rental~.*bicycle.*", | ||||
|                                 ], | ||||
|                                     "rental~.*bicycle.*" | ||||
|                                 ] | ||||
|                             }, | ||||
|                             "bicycle_rental!=docking_station", | ||||
|                         ], | ||||
|                             "bicycle_rental!=docking_station" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         and: ["leisure=playground", "playground!=forest"], | ||||
|                     }, | ||||
|                 ], | ||||
|                         and: ["leisure=playground", "playground!=forest"] | ||||
|                     } | ||||
|                 ] | ||||
|             }) | ||||
|             const opt = <TagsFilter>filter.optimize() | ||||
|             const expected = [ | ||||
|  | @ -166,7 +166,7 @@ describe("Tag optimalization", () => { | |||
|                 "planned:amenity=charging_station", | ||||
|                 "tower:type=observation", | ||||
|                 "(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~.+|rental~^(.*bicycle.*)$) &bicycle_rental!=docking_station", | ||||
|                 "leisure=playground&playground!=forest", | ||||
|                 "leisure=playground&playground!=forest" | ||||
|             ] | ||||
| 
 | ||||
|             expect((<Or>opt).or.map((f) => TagUtils.toString(f))).toEqual(expected) | ||||
|  | @ -187,7 +187,7 @@ describe("Tag optimalization", () => { | |||
|         it("with nested And which has a common property should be dropped", () => { | ||||
|             const t = new Or([ | ||||
|                 new Tag("foo", "bar"), | ||||
|                 new And([new Tag("foo", "bar"), new Tag("x", "y")]), | ||||
|                 new And([new Tag("foo", "bar"), new Tag("x", "y")]) | ||||
|             ]) | ||||
|             const opt = <TagsFilter>t.optimize() | ||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar") | ||||
|  | @ -212,14 +212,14 @@ describe("Tag optimalization", () => { | |||
|                     and: [ | ||||
|                         "sport=climbing", | ||||
|                         { | ||||
|                             or: ["office~*", "club~*"], | ||||
|                         }, | ||||
|                     ], | ||||
|                 }, | ||||
|             ], | ||||
|                             or: ["office~*", "club~*"] | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         const gym_tags = TagUtils.Tag({ | ||||
|             and: ["sport=climbing", "leisure=sports_centre"], | ||||
|             and: ["sport=climbing", "leisure=sports_centre"] | ||||
|         }) | ||||
|         const other_climbing = TagUtils.Tag({ | ||||
|             and: [ | ||||
|  | @ -227,8 +227,8 @@ describe("Tag optimalization", () => { | |||
|                 "climbing!~route", | ||||
|                 "leisure!~sports_centre", | ||||
|                 "climbing!=route_top", | ||||
|                 "climbing!=route_bottom", | ||||
|             ], | ||||
|                 "climbing!=route_bottom" | ||||
|             ] | ||||
|         }) | ||||
|         const together = new Or([club_tags, gym_tags, other_climbing]) | ||||
|         const opt = together.optimize() | ||||
|  | @ -270,7 +270,7 @@ describe("Tag optimalization", () => { | |||
|                 or: [ | ||||
|                     "club=climbing", | ||||
|                     { | ||||
|                         and: ["sport=climbing", { or: ["club~*", "office~*"] }], | ||||
|                         and: ["sport=climbing", { or: ["club~*", "office~*"] }] | ||||
|                     }, | ||||
|                     { | ||||
|                         and: [ | ||||
|  | @ -283,15 +283,70 @@ describe("Tag optimalization", () => { | |||
|                                             "climbing!~route", | ||||
|                                             "climbing!=route_top", | ||||
|                                             "climbing!=route_bottom", | ||||
|                                             "leisure!~sports_centre", | ||||
|                                         ], | ||||
|                                     }, | ||||
|                                 ], | ||||
|                             }, | ||||
|                         ], | ||||
|                     }, | ||||
|                 ], | ||||
|                                             "leisure!~sports_centre" | ||||
|                                         ] | ||||
|                                     } | ||||
|                                 ] | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             }) | ||||
|         ) | ||||
|     }) | ||||
| 
 | ||||
|     it("should optimize a complicated nested case", () => { | ||||
|         const spec = { | ||||
|             "and": | ||||
|                 ["service:bicycle:retail=yes", | ||||
|                     { | ||||
|                         "or": [ | ||||
|                             { | ||||
|                                 "and": [ | ||||
|                                     { "or": ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] }, | ||||
|                                     { | ||||
|                                         "or": ["service:bicycle:repair=yes", "shop=bicycle", | ||||
|                                             { | ||||
|                                                 "and": ["shop=sports", | ||||
|                                                     { "or": ["sport=bicycle", "sport=cycling", "sport="] }, | ||||
|                                                     "service:bicycle:retail!=no", | ||||
|                                                     "service:bicycle:repair!=no"] | ||||
|                                             }] | ||||
|                                     }] | ||||
|                             }, { | ||||
|                                 "and": | ||||
|                                     [ | ||||
|                                         { | ||||
|                                             "or": | ||||
|                                                 ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] | ||||
|                                         }, | ||||
|                                         { | ||||
|                                             "or": ["service:bicycle:repair=yes", "shop=bicycle", | ||||
|                                                 { | ||||
|                                                     "and": ["shop=sports", | ||||
|                                                         { "or": ["sport=bicycle", "sport=cycling", "sport="] }, | ||||
|                                                         "service:bicycle:retail!=no", "service:bicycle:repair!=no"] | ||||
|                                                 }] | ||||
|                                         }] | ||||
|                             }, { | ||||
|                                 "and": | ||||
|                                     [{ | ||||
|                                         "or": | ||||
|                                             ["craft=shoe_repair", "craft=key_cutter", "shop~.+"] | ||||
|                                     }, | ||||
|                                         { "or": ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] }, | ||||
|                                         "shop!=mall"] | ||||
|                             }, | ||||
|                             "service:bicycle:retail~.+", | ||||
|                             "service:bicycle:retail~.+"] | ||||
|                     }] | ||||
|         } | ||||
| 
 | ||||
|         const tag = TagUtils.Tag(spec) | ||||
|         const opt = tag.optimize() | ||||
|         if (opt === false || opt === true) { | ||||
|             throw "Did not expect a boolean" | ||||
|         } | ||||
|         console.log(opt) | ||||
|     }) | ||||
| }) | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ describe("TagUtils", () => { | |||
|         }) | ||||
| 
 | ||||
|         it("should handle compare tag <=5", () => { | ||||
|             let compare = TagUtils.Tag("key<=5") | ||||
|             const compare = TagUtils.Tag("key<=5") | ||||
|             equal(compare.matchesProperties({ key: undefined }), false) | ||||
|             equal(compare.matchesProperties({ key: "6" }), false) | ||||
|             equal(compare.matchesProperties({ key: "5" }), true) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue