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 { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | import { ExpressionSpecification } from "maplibre-gl" | ||||||
| import ComparingTag from "./ComparingTag" | import ComparingTag from "./ComparingTag" | ||||||
|  | import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| export class And extends TagsFilter { | export class And extends TagsFilter { | ||||||
|     public and: TagsFilter[] |     public and: TagsFilter[] | ||||||
|  | @ -15,6 +16,8 @@ export class And extends TagsFilter { | ||||||
|         this.and = and |         this.and = and | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static construct(and: TagsFilter[]): TagsFilter | ||||||
|  |     public static construct(and: (FlatTag | (Or & OptimizedTag))[]): TagsFilterClosed & OptimizedTag | ||||||
|     public static construct(and: TagsFilter[]): TagsFilter { |     public static construct(and: TagsFilter[]): TagsFilter { | ||||||
|         if (and.length === 1) { |         if (and.length === 1) { | ||||||
|             return and[0] |             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. |      * 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 | ||||||
|      * |      * | ||||||
|  |      * @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"), 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 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")
 |      * 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~*"]}]} ) |      * 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(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { |     removePhraseConsideredKnown(knownExpression: TagsFilter, value: 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) { | ||||||
|                 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) { |             if (tag instanceof Or) { | ||||||
|  |                 // Second try
 | ||||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|                 if (r === true) { |                 if (r === true) { | ||||||
|                     continue |                     continue | ||||||
|  | @ -232,7 +240,7 @@ export class And extends TagsFilter { | ||||||
|         if (newAnds.length === 0) { |         if (newAnds.length === 0) { | ||||||
|             return true |             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) |      * const parsed = TagUtils.Tag(orig) | ||||||
|      * parsed.optimize().asJson() // => orig
 |      * parsed.optimize().asJson() // => orig
 | ||||||
|      */ |      */ | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (TagsFilterClosed & OptimizedTag) | boolean { | ||||||
|         if (this.and.length === 0) { |         if (this.and.length === 0) { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|  | @ -276,7 +284,7 @@ export class And extends TagsFilter { | ||||||
|             // We have an AND with a contained false: this is always 'false'
 |             // We have an AND with a contained false: this is always 'false'
 | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|         const optimized = <TagsFilter[]>optimizedRaw |         const optimized = <(TagsFilterClosed & OptimizedTag)[]>optimizedRaw | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < optimized.length; i++) { |         for (let i = 0; i < optimized.length; i++) { | ||||||
|             for (let j = i + 1; j < optimized.length; j++) { |             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> = {} |             const properties: Record<string, string> = {} | ||||||
|             for (const opt of optimized) { |             for (const opt of optimized) { | ||||||
|                 if (opt instanceof Tag) { |                 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) { |         for (const tf of optimized) { | ||||||
|             if (tf instanceof And) { |             if (tf instanceof And) { | ||||||
|                 newAnds.push(...tf.and) |                 newAnds.push(...TagTypes.safeAnd(tf)) | ||||||
|             } else if (tf instanceof Or) { |             } else if (tf instanceof Or) { | ||||||
|                 containedOrs.push(tf) |                 containedOrs.push(tf) | ||||||
|             } else { |             } else { | ||||||
|  | @ -382,10 +390,10 @@ export class And extends TagsFilter { | ||||||
|         { |         { | ||||||
|             let dirty = false |             let dirty = false | ||||||
|             do { |             do { | ||||||
|                 const cleanedContainedOrs: Or[] = [] |                 const cleanedContainedOrs: (Or & OptimizedTag)[] = [] | ||||||
|                 outer: for (let containedOr of containedOrs) { |                 outer: for (let containedOr of containedOrs) { | ||||||
|                     for (const known of newAnds) { |                     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)
 |                         // containedOr: (X=Y | K=V)
 | ||||||
|                         // newAnds (and thus known): (K=V) --> true
 |                         // newAnds (and thus known): (K=V) --> true
 | ||||||
|                         const cleaned = containedOr.removePhraseConsideredKnown(known, true) |                         const cleaned = containedOr.removePhraseConsideredKnown(known, true) | ||||||
|  | @ -401,11 +409,17 @@ export class And extends TagsFilter { | ||||||
|                             containedOr = cleaned |                             containedOr = cleaned | ||||||
|                             continue |                             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
 |                         // the 'or' dissolved into a normal tag -> it has to be added to the newAnds
 | ||||||
|                         newAnds.push(cleaned) |                         newAnds.push(cleaned) | ||||||
|                         dirty = true // rerun this algo later on
 |                         dirty = true // rerun this algo later on
 | ||||||
|                         continue outer |                         continue outer | ||||||
|                     } |                     } | ||||||
|  | 
 | ||||||
|                     cleanedContainedOrs.push(containedOr) |                     cleanedContainedOrs.push(containedOr) | ||||||
|                 } |                 } | ||||||
|                 containedOrs = cleanedContainedOrs |                 containedOrs = cleanedContainedOrs | ||||||
|  | @ -433,9 +447,9 @@ export class And extends TagsFilter { | ||||||
|             if (commonValues.length === 0) { |             if (commonValues.length === 0) { | ||||||
|                 newAnds.push(...containedOrs) |                 newAnds.push(...containedOrs) | ||||||
|             } else { |             } else { | ||||||
|                 const newOrs: TagsFilter[] = [] |                 const newOrs: TagsFilterClosed[] = [] | ||||||
|                 for (const containedOr of containedOrs) { |                 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)) |                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||||
|                     ) |                     ) | ||||||
|                     if (elements.length > 0) { |                     if (elements.length > 0) { | ||||||
|  | @ -450,6 +464,8 @@ export class And extends TagsFilter { | ||||||
|                     return false |                     return false | ||||||
|                 } else if (result === true) { |                 } else if (result === true) { | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|  |                 }else if(result instanceof And) { | ||||||
|  |                     newAnds.push(...TagTypes.safeAnd(result)) | ||||||
|                 } else { |                 } else { | ||||||
|                     newAnds.push(result) |                     newAnds.push(result) | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
| import { Tag } from "./Tag" | import { Tag } from "./Tag" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | import { ExpressionSpecification } from "maplibre-gl" | ||||||
|  | import { OptimizedTag } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| export default class ComparingTag extends TagsFilter { | export default class ComparingTag extends TagsFilter { | ||||||
|     public readonly key: string |     public readonly key: string | ||||||
|  | @ -121,8 +122,8 @@ export default class ComparingTag extends TagsFilter { | ||||||
|         return this.key + this._representation + this._boundary |         return this.key + this._representation + this._boundary | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (ComparingTag & OptimizedTag) | boolean { | ||||||
|         return this |         return <any> this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|  |  | ||||||
|  | @ -3,6 +3,11 @@ import { TagUtils } from "./TagUtils" | ||||||
| import { And } from "./And" | import { And } from "./And" | ||||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | 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 { | export class Or extends TagsFilter { | ||||||
|     public or: TagsFilter[] |     public or: TagsFilter[] | ||||||
|  | @ -12,6 +17,9 @@ export class Or extends TagsFilter { | ||||||
|         this.or = or |         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 { |     public static construct(or: TagsFilter[]): TagsFilter { | ||||||
|         if (or.length === 1) { |         if (or.length === 1) { | ||||||
|             return or[0] |             return or[0] | ||||||
|  | @ -59,7 +67,7 @@ export class Or extends TagsFilter { | ||||||
|         return this.or |         return this.or | ||||||
|             .map((t) => { |             .map((t) => { | ||||||
|                 let e = t.asHumanString(linkToWiki, shorten, properties) |                 let e = t.asHumanString(linkToWiki, shorten, properties) | ||||||
|                 if (t["and"]) { |                 if (t["and"] || t["or"]) { | ||||||
|                     e = "(" + e + ")" |                     e = "(" + e + ")" | ||||||
|                 } |                 } | ||||||
|                 return 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") ,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"), true) // => true
 | ||||||
|      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
 |      * 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[] = [] |         const newOrs: TagsFilter[] = [] | ||||||
|         for (const tag of this.or) { |         for (const tag of this.or) { | ||||||
|             if (tag instanceof 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) { |             if (tag instanceof And) { | ||||||
|                 const r = tag.removePhraseConsideredKnown(knownExpression, value) |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|  | @ -160,7 +168,7 @@ export class Or extends TagsFilter { | ||||||
|         if (newOrs.length === 0) { |         if (newOrs.length === 0) { | ||||||
|             return false |             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"]}
 |      * parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
 | ||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (TagsFilterClosed & OptimizedTag) | boolean { | ||||||
|         if (this.or.length === 0) { |         if (this.or.length === 0) { | ||||||
|             return false |             return false | ||||||
|         } |         } | ||||||
|  | @ -181,26 +189,28 @@ export class Or extends TagsFilter { | ||||||
|             // We have an OR with a contained true: this is always 'true'
 |             // We have an OR with a contained true: this is always 'true'
 | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|         const optimized = <TagsFilter[]>optimizedRaw |         const optimized = optimizedRaw | ||||||
| 
 | 
 | ||||||
|         const newOrs: TagsFilter[] = [] |         const newOrs: ((And & OptimizedTag) | FlatTag)[] = [] | ||||||
|         let containedAnds: And[] = [] |         let containedAnds: (And & OptimizedTag)[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if (tf["or"]) { |             if (tf instanceof Or) { | ||||||
|                 // expand all the nested ors...
 |                 // expand all the nested ors...
 | ||||||
|                 newOrs.push(...tf["or"]) |                 for (const clauseOr of TagTypes.safeOr(tf)) { | ||||||
|  |                     newOrs.push(clauseOr) | ||||||
|  |                 } | ||||||
|             } else if (tf instanceof And) { |             } else if (tf instanceof And) { | ||||||
|                 // partition of all the ands
 |                 // partition of all the ands
 | ||||||
|                 containedAnds.push(tf) |                 containedAnds.push(tf) | ||||||
|             } else { |             } else { | ||||||
|                 newOrs.push(tf) |                 newOrs.push(<(Tag | (And & OptimizedTag) | RegexTag | SubstitutingTag | ComparingTag)>tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         { |         { | ||||||
|             let dirty = false |             let dirty = false | ||||||
|             do { |             do { | ||||||
|                 const cleanedContainedANds: And[] = [] |                 const cleanedContainedANds: (And & OptimizedTag)[] = [] | ||||||
|                 outer: for (let containedAnd of containedAnds) { |                 outer: for (let containedAnd of containedAnds) { | ||||||
|                     for (const known of newOrs) { |                     for (const known of newOrs) { | ||||||
|                         // input for optimization: (K=V | (X=Y & K=V))
 |                         // input for optimization: (K=V | (X=Y & K=V))
 | ||||||
|  | @ -219,8 +229,16 @@ export class Or extends TagsFilter { | ||||||
|                             containedAnd = cleaned |                             containedAnd = cleaned | ||||||
|                             continue // clean up with the other known values
 |                             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
 |                         // 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
 |                         dirty = true // rerun this algo later on
 | ||||||
|                         continue outer |                         continue outer | ||||||
|                     } |                     } | ||||||
|  | @ -243,16 +261,16 @@ export class Or extends TagsFilter { | ||||||
|             if (commonValues.length === 0) { |             if (commonValues.length === 0) { | ||||||
|                 newOrs.push(...containedAnds) |                 newOrs.push(...containedAnds) | ||||||
|             } else { |             } else { | ||||||
|                 const newAnds: TagsFilter[] = [] |                 const newAnds: TagsFilterClosed[] = [] | ||||||
|                 for (const containedAnd of containedAnds) { |                 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)) |                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||||
|                     ) |                     ) | ||||||
|                     if (elements.length == 0) { |                     if (elements.length > 0) { | ||||||
|                         continue |                         newAnds.push(And.construct(elements)) | ||||||
|                     } |                     } | ||||||
|                     newAnds.push(And.construct(elements)) |  | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|                 if (newAnds.length > 0) { |                 if (newAnds.length > 0) { | ||||||
|                     commonValues.push(Or.construct(newAnds)) |                     commonValues.push(Or.construct(newAnds)) | ||||||
|                 } |                 } | ||||||
|  | @ -262,9 +280,10 @@ export class Or extends TagsFilter { | ||||||
|                     return true |                     return true | ||||||
|                 } else if (result === false) { |                 } else if (result === false) { | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|                 } else if (commonValues.length > 0) { |                 } else if(result instanceof Or){ | ||||||
|                     newOrs.push(And.construct(commonValues)) |                     newOrs.push(...TagTypes.safeOr(result)) | ||||||
|                 } |                 }else | ||||||
|  |                     newOrs.push(result) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { Tag } from "./Tag" | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | import { ExpressionSpecification } from "maplibre-gl" | ||||||
|  | import { OptimizedTag } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| export class RegexTag extends TagsFilter { | export class RegexTag extends TagsFilter { | ||||||
|     public readonly key: RegExp | string |     public readonly key: RegExp | string | ||||||
|  | @ -354,8 +355,8 @@ export class RegexTag extends TagsFilter { | ||||||
|         return [] |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (RegexTag & OptimizedTag) | boolean { | ||||||
|         return this |         return <any> this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { Tag } from "./Tag" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | import { ExpressionSpecification } from "maplibre-gl" | ||||||
|  | import { OptimizedTag } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The substituting-tag uses the tags of a feature a variables and replaces them. |  * 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 }] |         return [{ k: this._key, v: v }] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (SubstitutingTag & OptimizedTag) | boolean { | ||||||
|         return this |         return <any> this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { TagsFilter } from "./TagsFilter" | ||||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | 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" | ||||||
| 
 | 
 | ||||||
| export class Tag extends TagsFilter { | export class Tag extends TagsFilter { | ||||||
|     public key: string |     public key: string | ||||||
|  | @ -66,7 +67,7 @@ export class Tag extends TagsFilter { | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         if (this.value === "") { |         if (this.value === "") { | ||||||
|             // NOT having this key
 |             // NOT having this key
 | ||||||
|             return ['[!"' + this.key + '"]'] |             return ["[!\"" + this.key + "\"]"] | ||||||
|         } |         } | ||||||
|         return [`["${this.key}"="${this.value}"]`] |         return [`["${this.key}"="${this.value}"]`] | ||||||
|     } |     } | ||||||
|  | @ -164,8 +165,8 @@ export class Tag extends TagsFilter { | ||||||
|         return [{ k: this.key, v: this.value }] |         return [{ k: this.key, v: this.value }] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): (Tag & OptimizedTag) | boolean { | ||||||
|         return this |         return <any>this | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNegative(): boolean { |     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 key_counts from "../../assets/key_totals.json" | ||||||
| 
 | 
 | ||||||
| import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext" | import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext" | ||||||
|  | import { TagsFilterClosed, UploadableTag } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| type Tags = Record<string, string> | type Tags = Record<string, string> | ||||||
| export type UploadableTag = Tag | SubstitutingTag | And |  | ||||||
| 
 | 
 | ||||||
| export class TagUtils { | export class TagUtils { | ||||||
|     public static readonly comparators: ReadonlyArray< |     public static readonly comparators: ReadonlyArray< | ||||||
|  | @ -476,7 +476,7 @@ export class TagUtils { | ||||||
|      * regex.matchesProperties({maxspeed: "50 mph"}) // => true
 |      * 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 { |         try { | ||||||
|             const ctx = typeof context === "string" ? context : context.path.join(".") |             const ctx = typeof context === "string" ? context : context.path.join(".") | ||||||
|             return this.ParseTagUnsafe(json, ctx) |             return this.ParseTagUnsafe(json, ctx) | ||||||
|  | @ -712,7 +712,7 @@ export class TagUtils { | ||||||
|         return Utils.NoNull(spec) |         return Utils.NoNull(spec) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { |     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` | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| import { ExpressionSpecification } from "maplibre-gl" | import { ExpressionSpecification } from "maplibre-gl" | ||||||
|  | import { OptimizedTag, TagsFilterClosed } from "./TagTypes" | ||||||
| 
 | 
 | ||||||
| export abstract class TagsFilter { | export abstract class TagsFilter { | ||||||
|     abstract asOverpass(): string[] |     abstract asOverpass(): string[] | ||||||
|  | @ -50,7 +51,7 @@ export abstract class TagsFilter { | ||||||
|     /** |     /** | ||||||
|      * Returns an optimized version (or self) of this 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). |      * 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([ |             const t = new And([ | ||||||
|                 new Tag("foo", "bar"), |                 new Tag("foo", "bar"), | ||||||
|                 new Or([new Tag("x", "y"), new Tag("a", "b")]), |                 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() |             const opt = <TagsFilter>t.optimize() | ||||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& (x=y| (a=b&c=d) )") |             expect(TagUtils.toString(opt)).toBe("foo=bar& (x=y| (a=b&c=d) )") | ||||||
|  | @ -42,7 +42,7 @@ describe("Tag optimalization", () => { | ||||||
|             const t = new And([ |             const t = new And([ | ||||||
|                 new Tag("foo", "bar"), |                 new Tag("foo", "bar"), | ||||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), |                 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() |             const opt = <TagsFilter>t.optimize() | ||||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& ( (a=b&c=d) |x=y)") |             expect(TagUtils.toString(opt)).toBe("foo=bar& ( (a=b&c=d) |x=y)") | ||||||
|  | @ -53,7 +53,7 @@ describe("Tag optimalization", () => { | ||||||
|             const t = new And([ |             const t = new And([ | ||||||
|                 new Tag("foo", "bar"), |                 new Tag("foo", "bar"), | ||||||
|                 new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), |                 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() |             const opt = <TagsFilter>t.optimize() | ||||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar& (a=b|x=y) & (c=d|x!=y)") |             expect(TagUtils.toString(opt)).toBe("foo=bar& (a=b|x=y) & (c=d|x!=y)") | ||||||
|  | @ -86,12 +86,12 @@ describe("Tag optimalization", () => { | ||||||
|                     { |                     { | ||||||
|                         and: [ |                         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)) )
 |             // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) )
 | ||||||
|             // This is equivalent to (X=Y | FOO=BAR)
 |             // This is equivalent to (X=Y | FOO=BAR)
 | ||||||
|  | @ -109,11 +109,11 @@ describe("Tag optimalization", () => { | ||||||
|                                     "amenity=charging_station", |                                     "amenity=charging_station", | ||||||
|                                     "disused:amenity=charging_station", |                                     "disused:amenity=charging_station", | ||||||
|                                     "planned:amenity=charging_station", |                                     "planned:amenity=charging_station", | ||||||
|                                     "construction:amenity=charging_station", |                                     "construction:amenity=charging_station" | ||||||
|                                 ], |                                 ] | ||||||
|                             }, |                             }, | ||||||
|                             "bicycle=yes", |                             "bicycle=yes" | ||||||
|                         ], |                         ] | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         and: [ |                         and: [ | ||||||
|  | @ -122,19 +122,19 @@ describe("Tag optimalization", () => { | ||||||
|                                     "amenity=charging_station", |                                     "amenity=charging_station", | ||||||
|                                     "disused:amenity=charging_station", |                                     "disused:amenity=charging_station", | ||||||
|                                     "planned:amenity=charging_station", |                                     "planned:amenity=charging_station", | ||||||
|                                     "construction:amenity=charging_station", |                                     "construction:amenity=charging_station" | ||||||
|                                 ], |                                 ] | ||||||
|                             }, |                             } | ||||||
|                         ], |                         ] | ||||||
|                     }, |                     }, | ||||||
|                     "amenity=toilets", |                     "amenity=toilets", | ||||||
|                     "amenity=bench", |                     "amenity=bench", | ||||||
|                     "leisure=picnic_table", |                     "leisure=picnic_table", | ||||||
|                     { |                     { | ||||||
|                         and: ["tower:type=observation"], |                         and: ["tower:type=observation"] | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         and: ["amenity=bicycle_repair_station"], |                         and: ["amenity=bicycle_repair_station"] | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         and: [ |                         and: [ | ||||||
|  | @ -143,16 +143,16 @@ describe("Tag optimalization", () => { | ||||||
|                                     "amenity=bicycle_rental", |                                     "amenity=bicycle_rental", | ||||||
|                                     "bicycle_rental~*", |                                     "bicycle_rental~*", | ||||||
|                                     "service:bicycle:rental=yes", |                                     "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 opt = <TagsFilter>filter.optimize() | ||||||
|             const expected = [ |             const expected = [ | ||||||
|  | @ -166,7 +166,7 @@ describe("Tag optimalization", () => { | ||||||
|                 "planned:amenity=charging_station", |                 "planned:amenity=charging_station", | ||||||
|                 "tower:type=observation", |                 "tower:type=observation", | ||||||
|                 "(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~.+|rental~^(.*bicycle.*)$) &bicycle_rental!=docking_station", |                 "(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) |             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", () => { |         it("with nested And which has a common property should be dropped", () => { | ||||||
|             const t = new Or([ |             const t = new Or([ | ||||||
|                 new Tag("foo", "bar"), |                 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() |             const opt = <TagsFilter>t.optimize() | ||||||
|             expect(TagUtils.toString(opt)).toBe("foo=bar") |             expect(TagUtils.toString(opt)).toBe("foo=bar") | ||||||
|  | @ -212,14 +212,14 @@ describe("Tag optimalization", () => { | ||||||
|                     and: [ |                     and: [ | ||||||
|                         "sport=climbing", |                         "sport=climbing", | ||||||
|                         { |                         { | ||||||
|                             or: ["office~*", "club~*"], |                             or: ["office~*", "club~*"] | ||||||
|                         }, |                         } | ||||||
|                     ], |                     ] | ||||||
|                 }, |                 } | ||||||
|             ], |             ] | ||||||
|         }) |         }) | ||||||
|         const gym_tags = TagUtils.Tag({ |         const gym_tags = TagUtils.Tag({ | ||||||
|             and: ["sport=climbing", "leisure=sports_centre"], |             and: ["sport=climbing", "leisure=sports_centre"] | ||||||
|         }) |         }) | ||||||
|         const other_climbing = TagUtils.Tag({ |         const other_climbing = TagUtils.Tag({ | ||||||
|             and: [ |             and: [ | ||||||
|  | @ -227,8 +227,8 @@ describe("Tag optimalization", () => { | ||||||
|                 "climbing!~route", |                 "climbing!~route", | ||||||
|                 "leisure!~sports_centre", |                 "leisure!~sports_centre", | ||||||
|                 "climbing!=route_top", |                 "climbing!=route_top", | ||||||
|                 "climbing!=route_bottom", |                 "climbing!=route_bottom" | ||||||
|             ], |             ] | ||||||
|         }) |         }) | ||||||
|         const together = new Or([club_tags, gym_tags, other_climbing]) |         const together = new Or([club_tags, gym_tags, other_climbing]) | ||||||
|         const opt = together.optimize() |         const opt = together.optimize() | ||||||
|  | @ -270,7 +270,7 @@ describe("Tag optimalization", () => { | ||||||
|                 or: [ |                 or: [ | ||||||
|                     "club=climbing", |                     "club=climbing", | ||||||
|                     { |                     { | ||||||
|                         and: ["sport=climbing", { or: ["club~*", "office~*"] }], |                         and: ["sport=climbing", { or: ["club~*", "office~*"] }] | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         and: [ |                         and: [ | ||||||
|  | @ -283,15 +283,70 @@ describe("Tag optimalization", () => { | ||||||
|                                             "climbing!~route", |                                             "climbing!~route", | ||||||
|                                             "climbing!=route_top", |                                             "climbing!=route_top", | ||||||
|                                             "climbing!=route_bottom", |                                             "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", () => { |         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: undefined }), false) | ||||||
|             equal(compare.matchesProperties({ key: "6" }), false) |             equal(compare.matchesProperties({ key: "6" }), false) | ||||||
|             equal(compare.matchesProperties({ key: "5" }), true) |             equal(compare.matchesProperties({ key: "5" }), true) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue