forked from MapComplete/MapComplete
		
	Improve tag optimazations, fixes rendering of climbing map
This commit is contained in:
		
							parent
							
								
									01ba686270
								
							
						
					
					
						commit
						01567a4b80
					
				
					 16 changed files with 875 additions and 303 deletions
				
			
		|  | @ -8,6 +8,13 @@ export class And extends TagsFilter { | ||||||
|         super(); |         super(); | ||||||
|         this.and = and |         this.and = and | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     public static construct(and: TagsFilter[]): TagsFilter{ | ||||||
|  |         if(and.length === 1){ | ||||||
|  |             return and[0] | ||||||
|  |         } | ||||||
|  |         return new And(and) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private static combine(filter: string, choices: string[]): string[] { |     private static combine(filter: string, choices: string[]): string[] { | ||||||
|         const values = []; |         const values = []; | ||||||
|  | @ -45,7 +52,7 @@ export class And extends TagsFilter { | ||||||
|      * import {RegexTag} from "./RegexTag"; |      * import {RegexTag} from "./RegexTag"; | ||||||
|      *  |      *  | ||||||
|      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) |      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) | ||||||
|      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]" ]
 |      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
 | ||||||
|      */ |      */ | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         let allChoices: string[] = null; |         let allChoices: string[] = null; | ||||||
|  | @ -87,17 +94,17 @@ export class And extends TagsFilter { | ||||||
|      * ]) |      * ]) | ||||||
|      * const t1 = new And([new Tag("valves", "A")]) |      * const t1 = new And([new Tag("valves", "A")]) | ||||||
|      * const t2 = new And([new Tag("valves", "B")]) |      * const t2 = new And([new Tag("valves", "B")]) | ||||||
|      * t0.isEquivalent(t0) // => true
 |      * t0.shadows(t0) // => true
 | ||||||
|      * t1.isEquivalent(t1) // => true
 |      * t1.shadows(t1) // => true
 | ||||||
|      * t2.isEquivalent(t2) // => true
 |      * t2.shadows(t2) // => true
 | ||||||
|      * t0.isEquivalent(t1) // => false
 |      * t0.shadows(t1) // => false
 | ||||||
|      * t0.isEquivalent(t2) // => false
 |      * t0.shadows(t2) // => false
 | ||||||
|      * t1.isEquivalent(t0) // => false
 |      * t1.shadows(t0) // => false
 | ||||||
|      * t1.isEquivalent(t2) // => false
 |      * t1.shadows(t2) // => false
 | ||||||
|      * t2.isEquivalent(t0) // => false
 |      * t2.shadows(t0) // => false
 | ||||||
|      * t2.isEquivalent(t1) // => false
 |      * t2.shadows(t1) // => false
 | ||||||
|      */ |      */ | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof And)) { |         if (!(other instanceof And)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -105,7 +112,7 @@ export class And extends TagsFilter { | ||||||
|         for (const selfTag of this.and) { |         for (const selfTag of this.and) { | ||||||
|             let matchFound = false; |             let matchFound = false; | ||||||
|             for (const otherTag of other.and) { |             for (const otherTag of other.and) { | ||||||
|                 matchFound = selfTag.isEquivalent(otherTag); |                 matchFound = selfTag.shadows(otherTag); | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|  | @ -118,7 +125,7 @@ export class And extends TagsFilter { | ||||||
|         for (const otherTag of other.and) { |         for (const otherTag of other.and) { | ||||||
|             let matchFound = false; |             let matchFound = false; | ||||||
|             for (const selfTag of this.and) { |             for (const selfTag of this.and) { | ||||||
|                 matchFound = selfTag.isEquivalent(otherTag); |                 matchFound = selfTag.shadows(otherTag); | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|  | @ -148,23 +155,90 @@ export class And extends TagsFilter { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * IN some contexts, some expressions can be considered true, e.g. | ||||||
|  |      * (X=Y | (A=B & X=Y)) | ||||||
|  |      *        ^---------^ | ||||||
|  |      * When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise. | ||||||
|  |      * This means that the entire 'AND' is considered 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")
 | ||||||
|  |      * new And([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 | ||||||
|  |      *  | ||||||
|  |      * // should remove 'club~*' if we know that 'club=climbing'
 | ||||||
|  |      * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) | ||||||
|  |      * expr.removePhraseConsideredKnown(new Tag("club","climbing"), true) // => new Tag("sport","climbing")
 | ||||||
|  |      *  | ||||||
|  |      * 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 { | ||||||
|  |         const newAnds: TagsFilter[] = [] | ||||||
|  |         for (const tag of this.and) { | ||||||
|  |             if(tag instanceof And){ | ||||||
|  |                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||||
|  |             } | ||||||
|  |             if(tag instanceof Or){ | ||||||
|  |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|  |                 if(r === true){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(r === false){ | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 newAnds.push(r) | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(value && knownExpression.shadows(tag)){ | ||||||
|  |                 /** | ||||||
|  |                  * At this point, we do know that 'knownExpression' is true in every case | ||||||
|  |                  * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, | ||||||
|  |                  * we can be sure that 'tag' is true as well. | ||||||
|  |                  *  | ||||||
|  |                  * "True" is the neutral element in an AND, so we can skip the tag | ||||||
|  |                  */ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(!value && tag.shadows(knownExpression)){ | ||||||
|  | 
 | ||||||
|  |                 /** | ||||||
|  |                  * We know that knownExpression is unmet. | ||||||
|  |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  |                  * then tag CANNOT be met too, as known expression is not met. | ||||||
|  |                  *  | ||||||
|  |                  * This implies that 'tag' must be false too! | ||||||
|  |                  */ | ||||||
|  | 
 | ||||||
|  |                 // false is the element which absorbs all
 | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             newAnds.push(tag) | ||||||
|  |         } | ||||||
|  |         if(newAnds.length === 0){ | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         return And.construct(newAnds) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         if(this.and.length === 0){ |         if(this.and.length === 0){ | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|         const optimized = this.and.map(t => t.optimize()) |         const optimizedRaw = this.and.map(t => t.optimize()) | ||||||
|  |             .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/ ) | ||||||
|  |         if(optimizedRaw.some(t => t === false)){ | ||||||
|  |             // We have an AND with a contained false: this is always 'false'
 | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         const optimized = <TagsFilter[]> optimizedRaw; | ||||||
|          |          | ||||||
|         const newAnds : TagsFilter[] = [] |         const newAnds : TagsFilter[] = [] | ||||||
|          |          | ||||||
|         let containedOrs : Or[] = [] |         let containedOrs : Or[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if(tf === false){ |  | ||||||
|                 return false |  | ||||||
|             } |  | ||||||
|             if(tf === true){ |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             if(tf instanceof And){ |             if(tf instanceof And){ | ||||||
|                 newAnds.push(...tf.and) |                 newAnds.push(...tf.and) | ||||||
|             }else if(tf instanceof Or){ |             }else if(tf instanceof Or){ | ||||||
|  | @ -173,27 +247,56 @@ export class And extends TagsFilter { | ||||||
|                 newAnds.push(tf) |                 newAnds.push(tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |          | ||||||
|         containedOrs = containedOrs.filter(ca => { |         { | ||||||
|             for (const element of ca.or) { |             let dirty = false; | ||||||
|                 if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){ |             do { | ||||||
|                     // At least one part of the 'OR' is matched by the outer or, so this means that this OR isn't needed at all
 |                 const cleanedContainedOrs : Or[] = [] | ||||||
|                     // XY & (XY | AB) === XY
 |                 outer: for (let containedOr of containedOrs) { | ||||||
|                     return false |                     for (const known of newAnds) { | ||||||
|  |                         // input for optimazation: (K=V & (X=Y | K=V))
 | ||||||
|  |                         // containedOr: (X=Y | K=V)
 | ||||||
|  |                         // newAnds (and thus known): (K=V) --> true
 | ||||||
|  |                         const cleaned = containedOr.removePhraseConsideredKnown(known, true) | ||||||
|  |                         if (cleaned === true) { | ||||||
|  |                             // The neutral element within an AND
 | ||||||
|  |                             continue outer // skip addition too
 | ||||||
|  |                         } | ||||||
|  |                         if (cleaned === false) { | ||||||
|  |                             // zero element
 | ||||||
|  |                             return false | ||||||
|  |                         } | ||||||
|  |                         if (cleaned instanceof Or) { | ||||||
|  |                             containedOr = 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 | ||||||
|             return true; |             } while(dirty) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |          | ||||||
|  |         containedOrs = containedOrs.filter(ca => { | ||||||
|  |             const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) | ||||||
|  |             // If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all
 | ||||||
|  |             // XY & (XY | AB) === XY
 | ||||||
|  |             return !isShadowed; | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         // Extract common keys from the OR
 |         // Extract common keys from the OR
 | ||||||
|         if(containedOrs.length === 1){ |         if(containedOrs.length === 1){ | ||||||
|             newAnds.push(containedOrs[0]) |             newAnds.push(containedOrs[0]) | ||||||
|         } |         }else if(containedOrs.length > 1){ | ||||||
|         if(containedOrs.length > 1){ |  | ||||||
|             let commonValues : TagsFilter [] = containedOrs[0].or |             let commonValues : TagsFilter [] = containedOrs[0].or | ||||||
|             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){ |             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){ | ||||||
|                 const containedOr = containedOrs[i]; |                 const containedOr = containedOrs[i]; | ||||||
|                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.isEquivalent(cv))) |                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) | ||||||
|             } |             } | ||||||
|             if(commonValues.length === 0){ |             if(commonValues.length === 0){ | ||||||
|                 newAnds.push(...containedOrs) |                 newAnds.push(...containedOrs) | ||||||
|  | @ -201,19 +304,11 @@ export class And extends TagsFilter { | ||||||
|                 const newOrs: TagsFilter[] = [] |                 const newOrs: TagsFilter[] = [] | ||||||
|                 for (const containedOr of containedOrs) { |                 for (const containedOr of containedOrs) { | ||||||
|                     const elements = containedOr.or |                     const elements = containedOr.or | ||||||
|                         .filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate))) |                         .filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||||
|                     const or = new Or(elements).optimize() |                     newOrs.push(Or.construct(elements)) | ||||||
|                     if(or === true){ |  | ||||||
|                         // neutral element
 |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     if(or === false){ |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
|                     newOrs.push(or) |  | ||||||
|                 } |                 } | ||||||
|                  |                  | ||||||
|                 commonValues.push(new And(newOrs)) |                 commonValues.push(And.construct(newOrs)) | ||||||
|                 const result = new Or(commonValues).optimize() |                 const result = new Or(commonValues).optimize() | ||||||
|                 if(result === false){ |                 if(result === false){ | ||||||
|                     return false |                     return false | ||||||
|  | @ -224,16 +319,22 @@ export class And extends TagsFilter { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |         if(newAnds.length === 0){ | ||||||
|         if(newAnds.length === 1){ |             return true | ||||||
|             return newAnds[0] |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if(TagUtils.ContainsOppositeTags(newAnds)){ | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |          | ||||||
|         TagUtils.sortFilters(newAnds, true) |         TagUtils.sortFilters(newAnds, true) | ||||||
|          |          | ||||||
|         return new And(newAnds) |         return And.construct(newAnds) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return !this.and.some(t => !t.isNegative()); |         return !this.and.some(t => !t.isNegative()); | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |      | ||||||
| } | } | ||||||
|  | @ -23,7 +23,7 @@ export default class ComparingTag implements TagsFilter { | ||||||
|         throw "A comparable tag can not be used as overpass filter" |         throw "A comparable tag can not be used as overpass filter" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         return other === this; |         return other === this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										156
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							
							
						
						
									
										156
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							|  | @ -11,6 +11,14 @@ export class Or extends TagsFilter { | ||||||
|         this.or = or; |         this.or = or; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static construct(or: TagsFilter[]): TagsFilter{ | ||||||
|  |         if(or.length === 1){ | ||||||
|  |             return or[0] | ||||||
|  |         } | ||||||
|  |         return new Or(or) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         for (const tagsFilter of this.or) { |         for (const tagsFilter of this.or) { | ||||||
|             if (tagsFilter.matchesProperties(properties)) { |             if (tagsFilter.matchesProperties(properties)) { | ||||||
|  | @ -28,7 +36,7 @@ export class Or extends TagsFilter { | ||||||
|      *  |      *  | ||||||
|      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) |      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) | ||||||
|      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) |      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) | ||||||
|      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 |      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 | ||||||
|      *  |      *  | ||||||
|      * // should fuse nested ors into a single list
 |      * // should fuse nested ors into a single list
 | ||||||
|      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) |      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) | ||||||
|  | @ -51,14 +59,14 @@ export class Or extends TagsFilter { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof Or) { |         if (other instanceof Or) { | ||||||
| 
 | 
 | ||||||
|             for (const selfTag of this.or) { |             for (const selfTag of this.or) { | ||||||
|                 let matchFound = false; |                 let matchFound = false; | ||||||
|                 for (let i = 0; i < other.or.length && !matchFound; i++) { |                 for (let i = 0; i < other.or.length && !matchFound; i++) { | ||||||
|                     let otherTag = other.or[i]; |                     let otherTag = other.or[i]; | ||||||
|                     matchFound = selfTag.isEquivalent(otherTag); |                     matchFound = selfTag.shadows(otherTag); | ||||||
|                 } |                 } | ||||||
|                 if (!matchFound) { |                 if (!matchFound) { | ||||||
|                     return false; |                     return false; | ||||||
|  | @ -85,45 +93,127 @@ export class Or extends TagsFilter { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * IN some contexts, some expressions can be considered true, e.g. | ||||||
|  |      * (X=Y & (A=B | X=Y)) | ||||||
|  |      *        ^---------^ | ||||||
|  |      * When the evaluation hits (A=B | X=Y), we know _for sure_ that X=Y _does match, as it would have failed the first clause otherwise. | ||||||
|  |      * This means we can safely ignore this in the OR | ||||||
|  |      *  | ||||||
|  |      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // =>true
 | ||||||
|  |      * 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")])
 | ||||||
|  |      */ | ||||||
|  |     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||||
|  |         const newOrs: TagsFilter[] = [] | ||||||
|  |         for (const tag of this.or) { | ||||||
|  |             if(tag instanceof Or){ | ||||||
|  |                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||||
|  |             } | ||||||
|  |             if(tag instanceof And){ | ||||||
|  |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|  |                 if(r === false){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(r === true){ | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |                 newOrs.push(r) | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(value && knownExpression.shadows(tag)){ | ||||||
|  |                 /** | ||||||
|  |                  * At this point, we do know that 'knownExpression' is true in every case | ||||||
|  |                  * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, | ||||||
|  |                  * we can be sure that 'tag' is true as well. | ||||||
|  |                  * | ||||||
|  |                  * "True" is the absorbing element in an OR, so we can return true | ||||||
|  |                  */ | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |             if(!value && tag.shadows(knownExpression)){ | ||||||
|  | 
 | ||||||
|  |                 /** | ||||||
|  |                  * We know that knownExpression is unmet. | ||||||
|  |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  |                  * then tag CANNOT be met too, as known expression is not met. | ||||||
|  |                  * | ||||||
|  |                  * This implies that 'tag' must be false too! | ||||||
|  |                  * false is the neutral element in an OR | ||||||
|  |                  */ | ||||||
|  |                continue | ||||||
|  |             } | ||||||
|  |             newOrs.push(tag) | ||||||
|  |         } | ||||||
|  |         if(newOrs.length === 0){ | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         return Or.construct(newOrs) | ||||||
|  |     } | ||||||
|  |      | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|          |          | ||||||
|         if(this.or.length === 0){ |         if(this.or.length === 0){ | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         const optimized = this.or.map(t => t.optimize()) |         const optimizedRaw = this.or.map(t => t.optimize()) | ||||||
|  |             .filter(t => t !== false /* false is the neutral element in an OR, we drop them*/ ) | ||||||
|  |         if(optimizedRaw.some(t => t === true)){ | ||||||
|  |             // We have an OR with a contained true: this is always 'true'
 | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         const optimized = <TagsFilter[]> optimizedRaw; | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
|         const newOrs : TagsFilter[] = [] |         const newOrs : TagsFilter[] = [] | ||||||
| 
 |  | ||||||
|         let containedAnds : And[] = [] |         let containedAnds : And[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if(tf === true){ |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|             if(tf === false){ |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if(tf instanceof Or){ |             if(tf instanceof Or){ | ||||||
|  |                 // expand all the nested ors...
 | ||||||
|                 newOrs.push(...tf.or) |                 newOrs.push(...tf.or) | ||||||
|             }else if(tf instanceof And){ |             }else if(tf instanceof And){ | ||||||
|  |                 // partition of all the ands
 | ||||||
|                 containedAnds.push(tf) |                 containedAnds.push(tf) | ||||||
|             } else { |             } else { | ||||||
|                 newOrs.push(tf) |                 newOrs.push(tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         containedAnds = containedAnds.filter(ca => { |         { | ||||||
|             for (const element of ca.and) { |             let dirty = false; | ||||||
|                 if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){ |             do { | ||||||
|                     // At least one part of the 'AND' is matched by the outer or, so this means that this OR isn't needed at all
 |                 const cleanedContainedANds : And[] = [] | ||||||
|                     // XY | (XY & AB) === XY
 |                 outer: for (let containedAnd of containedAnds) { | ||||||
|                     return false |                     for (const known of newOrs) { | ||||||
|  |                         // input for optimazation: (K=V | (X=Y & K=V))
 | ||||||
|  |                         // containedAnd: (X=Y & K=V)
 | ||||||
|  |                         // newOrs (and thus known): (K=V) --> false
 | ||||||
|  |                         const cleaned = containedAnd.removePhraseConsideredKnown(known, false) | ||||||
|  |                         if (cleaned === false) { | ||||||
|  |                             // The neutral element within an OR
 | ||||||
|  |                             continue outer // skip addition too
 | ||||||
|  |                         } | ||||||
|  |                         if (cleaned === true) { | ||||||
|  |                             // zero element
 | ||||||
|  |                             return true | ||||||
|  |                         } | ||||||
|  |                         if (cleaned instanceof And) { | ||||||
|  |                             containedAnd = cleaned | ||||||
|  |                             continue // clean up with the other known values
 | ||||||
|  |                         } | ||||||
|  |                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 | ||||||
|  |                         newOrs.push(cleaned) | ||||||
|  |                         dirty = true; // rerun this algo later on
 | ||||||
|  |                         continue outer; | ||||||
|  |                     } | ||||||
|  |                     cleanedContainedANds.push(containedAnd) | ||||||
|                 } |                 } | ||||||
|             } |                 containedAnds = cleanedContainedANds | ||||||
|             return true; |             } while(dirty) | ||||||
|         }) |         } | ||||||
| 
 |  | ||||||
|         // Extract common keys from the ANDS
 |         // Extract common keys from the ANDS
 | ||||||
|         if(containedAnds.length === 1){ |         if(containedAnds.length === 1){ | ||||||
|             newOrs.push(containedAnds[0]) |             newOrs.push(containedAnds[0]) | ||||||
|  | @ -131,40 +221,46 @@ export class Or extends TagsFilter { | ||||||
|             let commonValues : TagsFilter [] = containedAnds[0].and |             let commonValues : TagsFilter [] = containedAnds[0].and | ||||||
|             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ |             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ | ||||||
|                 const containedAnd = containedAnds[i]; |                 const containedAnd = containedAnds[i]; | ||||||
|                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.isEquivalent(cv))) |                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.shadows(cv))) | ||||||
|             } |             } | ||||||
|             if(commonValues.length === 0){ |             if(commonValues.length === 0){ | ||||||
|                 newOrs.push(...containedAnds) |                 newOrs.push(...containedAnds) | ||||||
|             }else{ |             }else{ | ||||||
|                 const newAnds: TagsFilter[] = [] |                 const newAnds: TagsFilter[] = [] | ||||||
|                 for (const containedAnd of containedAnds) { |                 for (const containedAnd of containedAnds) { | ||||||
|                     const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate))) |                     const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||||
|                     newAnds.push(new And(elements)) |                     newAnds.push(And.construct(elements)) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 commonValues.push(new Or(newAnds)) |                 commonValues.push(Or.construct(newAnds)) | ||||||
|                 const result = new And(commonValues).optimize() |                 const result = new And(commonValues).optimize() | ||||||
|                 if(result === true){ |                 if(result === true){ | ||||||
|                     return true |                     return true | ||||||
|                 }else if(result === false){ |                 }else if(result === false){ | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|                 }else{ |                 }else{ | ||||||
|                     newOrs.push(new And(commonValues)) |                     newOrs.push(And.construct(commonValues)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(newOrs.length === 1){ |         if(newOrs.length === 0){ | ||||||
|             return newOrs[0] |             return false | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if(TagUtils.ContainsOppositeTags(newOrs)){ | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |          | ||||||
|         TagUtils.sortFilters(newOrs, false) |         TagUtils.sortFilters(newOrs, false) | ||||||
| 
 | 
 | ||||||
|         return new Or(newOrs) |         return Or.construct(newOrs) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return this.or.some(t => t.isNegative()); |         return this.or.some(t => t.isNegative()); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,13 +10,6 @@ export class RegexTag extends TagsFilter { | ||||||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { |     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||||
|         super(); |         super(); | ||||||
|         this.key = key; |         this.key = key; | ||||||
|         if (typeof value === "string") { |  | ||||||
|             if (value.indexOf("^") < 0 && value.indexOf("$") < 0) { |  | ||||||
|                 value = "^" + value + "$" |  | ||||||
|             } |  | ||||||
|             value = new RegExp(value) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.value = value; |         this.value = value; | ||||||
|         this.invert = invert; |         this.invert = invert; | ||||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); |         this.matchesEmpty = RegexTag.doesMatch("", this.value); | ||||||
|  | @ -79,14 +72,14 @@ export class RegexTag extends TagsFilter { | ||||||
|     /**  |     /**  | ||||||
|      * Checks if this tag matches the given properties |      * Checks if this tag matches the given properties | ||||||
|      *  |      *  | ||||||
|      * const isNotEmpty = new RegexTag("key","^$", true); |      * const isNotEmpty = new RegexTag("key",/^$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 | ||||||
|      *  |      *  | ||||||
|      * const isNotEmpty = new RegexTag("key","^..*$", true); |      * const isNotEmpty = new RegexTag("key",/^..*$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"key": ""}) // => true
 |      * isNotEmpty.matchesProperties({"key": ""}) // => true
 | ||||||
|  | @ -121,6 +114,9 @@ export class RegexTag extends TagsFilter { | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 |      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 | ||||||
|  |      *  | ||||||
|  |      * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false
 | ||||||
|  |      * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true
 | ||||||
|      */ |      */ | ||||||
|     matchesProperties(tags: any): boolean { |     matchesProperties(tags: any): boolean { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|  | @ -147,17 +143,87 @@ export class RegexTag extends TagsFilter { | ||||||
| 
 | 
 | ||||||
|     asHumanString() { |     asHumanString() { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             return `${this.key}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}`; |             const oper = typeof this.value === "string" ? "=" : "~" | ||||||
|  |             return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}`; | ||||||
|         } |         } | ||||||
|         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` |         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     /** | ||||||
|  |      *  | ||||||
|  |      * new RegexTag("key","value").shadows(new Tag("key","value")) // => true
 | ||||||
|  |      * new RegexTag("key",/value/).shadows(new RegexTag("key","value")) // => true
 | ||||||
|  |      * new RegexTag("key",/^..*$/).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key",/^..*$/).shadows(new Tag("other_key","value")) // => false
 | ||||||
|  |      * new RegexTag("key", /^a+$/).shadows(new Tag("key", "a")) // => false
 | ||||||
|  |      *  | ||||||
|  |      *  | ||||||
|  |      * // should not shadow too eagerly: the first tag might match 'key=abc', the second won't
 | ||||||
|  |      *  new RegexTag("key", /^..*$/).shadows(new Tag("key", "some_value")) // => false
 | ||||||
|  |      *  | ||||||
|  |      * // should handle 'invert'
 | ||||||
|  |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true
 | ||||||
|  |      * new RegexTag("key","value", true).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key","value", true).shadows(new Tag("key","some_other_value")) // => false
 | ||||||
|  |      */ | ||||||
|  |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof RegexTag) { |         if (other instanceof RegexTag) { | ||||||
|             return other.asHumanString() == this.asHumanString(); |             if((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key) ){ | ||||||
|  |                 // Keys don't match, never shadowing
 | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             if((other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && this.invert  == other.invert ){ | ||||||
|  |                 // Values (and inverts) match
 | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             if(typeof other.value ==="string"){ | ||||||
|  |                 const valuesMatch = RegexTag.doesMatch(other.value, this.value) | ||||||
|  |                 if(!this.invert && !other.invert){ | ||||||
|  |                     // this: key~value, other: key=value
 | ||||||
|  |                     return valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(this.invert && !other.invert){ | ||||||
|  |                     // this: key!~value, other: key=value
 | ||||||
|  |                     return !valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(!this.invert && other.invert){ | ||||||
|  |                     // this: key~value, other: key!=value
 | ||||||
|  |                     return !valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(!this.invert && !other.invert){ | ||||||
|  |                     // this: key!~value, other: key!=value
 | ||||||
|  |                     return valuesMatch | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|         if (other instanceof Tag) { |         if (other instanceof Tag) { | ||||||
|             return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value); |             if(!RegexTag.doesMatch(other.key, this.key)){ | ||||||
|  |                 // Keys don't match
 | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |              | ||||||
|  |             if(this.value["source"] === "^..*$") { | ||||||
|  |                 if(this.invert){ | ||||||
|  |                     return other.value === "" | ||||||
|  |                 } | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if (this.invert) { | ||||||
|  |                 /* | ||||||
|  |                  * this: "a!=b" | ||||||
|  |                  * other: "a=c" | ||||||
|  |                  * actual property: a=x | ||||||
|  |                  * In other words: shadowing will never occur here | ||||||
|  |                  */ | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             // Unless the values are the same, it is pretty hard to figure out if they are shadowing. This is future work
 | ||||||
|  |             return (this.value["source"] ?? this.value) === other.value; | ||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ export default class SubstitutingTag implements TagsFilter { | ||||||
|         throw "A variable with substitution can not be used to query overpass" |         throw "A variable with substitution can not be used to query overpass" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof SubstitutingTag)) { |         if (!(other instanceof SubstitutingTag)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -88,14 +88,23 @@ export class Tag extends TagsFilter { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     /** | ||||||
|         if (other instanceof Tag) { |      * // should handle advanced regexes
 | ||||||
|             return this.key === other.key && this.value === other.value; |      * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new Tag("key","value")) // => true
 | ||||||
|  |      * new Tag("key","some_other_value").shadows(new RegexTag("key", "value", true)) // => true
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("key", "value", true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
 | ||||||
|  |      */ | ||||||
|  |     shadows(other: TagsFilter): boolean { | ||||||
|  |         if(other["key"] !== undefined){ | ||||||
|  |             if(other["key"] !== this.key){ | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         if (other instanceof RegexTag) { |         return other.matchesProperties({[this.key]: this.value}); | ||||||
|             other.isEquivalent(this); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|  |  | ||||||
|  | @ -200,15 +200,16 @@ export class TagUtils { | ||||||
|      * |      * | ||||||
|      * TagUtils.Tag("key=value") // => new Tag("key", "value")
 |      * TagUtils.Tag("key=value") // => new Tag("key", "value")
 | ||||||
|      * TagUtils.Tag("key=") // => new Tag("key", "")
 |      * TagUtils.Tag("key=") // => new Tag("key", "")
 | ||||||
|      * TagUtils.Tag("key!=") // => new RegexTag("key", "^..*$")
 |      * TagUtils.Tag("key!=") // => new RegexTag("key", /^..*$/)
 | ||||||
|      * TagUtils.Tag("key!=value") // => new RegexTag("key", /^value$/, true)
 |      * TagUtils.Tag("key~*") // => new RegexTag("key", /^..*$/)
 | ||||||
|  |      * TagUtils.Tag("key!=value") // => new RegexTag("key", "value", true)
 | ||||||
|      * TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
 |      * TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
 | ||||||
|      * TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/, true)
 |      * TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/, true)
 | ||||||
|      * TagUtils.Tag({"and": ["key=value", "x=y"]}) // => new And([new Tag("key","value"), new Tag("x","y")])
 |      * TagUtils.Tag({"and": ["key=value", "x=y"]}) // => new And([new Tag("key","value"), new Tag("x","y")])
 | ||||||
|      * TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
 |      * TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
 | ||||||
|      * TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
 |      * TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
 | ||||||
|      * TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true)
 |      * TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true)
 | ||||||
|      * TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)") // => new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/)
 |      * TagUtils.Tag("tags~(.*;)?amenity=public_bookcase(;.*)?") // => new RegexTag("tags", /^(.*;)?amenity=public_bookcase(;.*)?$/)
 | ||||||
|      * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/)
 |      * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/)
 | ||||||
|      * |      * | ||||||
|      * TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
 |      * TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
 | ||||||
|  | @ -306,7 +307,7 @@ export class TagUtils { | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1], |                 new RegExp("^"+ split[1]+"$"), | ||||||
|                 true |                 true | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  | @ -338,17 +339,6 @@ export class TagUtils { | ||||||
|                 split[1] = "..*" |                 split[1] = "..*" | ||||||
|                 return new RegexTag(split[0], /^..*$/) |                 return new RegexTag(split[0], /^..*$/) | ||||||
|             } |             } | ||||||
|             return new RegexTag( |  | ||||||
|                 split[0], |  | ||||||
|                 new RegExp("^" + split[1] + "$"), |  | ||||||
|                 true |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         if (tag.indexOf("!~") >= 0) { |  | ||||||
|             const split = Utils.SplitFirst(tag, "!~"); |  | ||||||
|             if (split[1] === "*") { |  | ||||||
|                 split[1] = "..*" |  | ||||||
|             } |  | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1], |                 split[1], | ||||||
|  | @ -357,15 +347,18 @@ export class TagUtils { | ||||||
|         } |         } | ||||||
|         if (tag.indexOf("~") >= 0) { |         if (tag.indexOf("~") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, "~"); |             const split = Utils.SplitFirst(tag, "~"); | ||||||
|  |             let value : string | RegExp = split[1] | ||||||
|             if (split[1] === "") { |             if (split[1] === "") { | ||||||
|                 throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" |                 throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" | ||||||
|             } |             } | ||||||
|             if (split[1] === "*") { |             if (value === "*") { | ||||||
|                 split[1] = "..*" |                 value = /^..*$/ | ||||||
|  |             }else { | ||||||
|  |                 value = new RegExp("^"+value+"$") | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1] |                 value | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         if (tag.indexOf("=") >= 0) { |         if (tag.indexOf("=") >= 0) { | ||||||
|  | @ -431,4 +424,94 @@ export class TagUtils { | ||||||
|         return " (" + joined + ") " |         return " (" + joined + ") " | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns 'true' is opposite tags are detected. | ||||||
|  |      * Note that this method will never work perfectly | ||||||
|  |      *  | ||||||
|  |      * // should be false for some simple cases
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new Tag("key0", "value")]) // => false
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new Tag("key", "value0")]) // => false
 | ||||||
|  |      *  | ||||||
|  |      * // should detect simple cases
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", "value", true)]) // => true
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", /value/, true)]) // => true
 | ||||||
|  |      */ | ||||||
|  |     public static ContainsOppositeTags(tags: (TagsFilter)[]) : boolean{ | ||||||
|  |         for (let i = 0; i < tags.length; i++){ | ||||||
|  |             const tag = tags[i]; | ||||||
|  |             if(!(tag instanceof Tag || tag instanceof RegexTag)){ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             for (let j = i + 1; j < tags.length; j++){ | ||||||
|  |                 const guard = tags[j]; | ||||||
|  |                 if(!(guard instanceof Tag || guard instanceof RegexTag)){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(guard.key !== tag.key) { | ||||||
|  |                     // Different keys: they can _never_ be opposites
 | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if((guard.value["source"] ?? guard.value) !== (tag.value["source"] ?? tag.value)){ | ||||||
|  |                     // different values: the can _never_ be opposites
 | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if( (guard["invert"] ?? false) !== (tag["invert"] ?? false) ) { | ||||||
|  |                     // The 'invert' flags are opposite, the key and value is the same for both
 | ||||||
|  |                     // This means we have found opposite tags!
 | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns a filtered version of 'listToFilter'. | ||||||
|  |      * For a list [t0, t1, t2], If `blackList` contains an equivalent (or broader) match of any `t`, this respective `t` is dropped from the returned list | ||||||
|  |      * Ignores nested ORS and ANDS | ||||||
|  |      *  | ||||||
|  |      * TagUtils.removeShadowedElementsFrom([new Tag("key","value")],  [new Tag("key","value"), new Tag("other_key","value")]) // => [new Tag("other_key","value")]
 | ||||||
|  |      */ | ||||||
|  |     public static removeShadowedElementsFrom(blacklist: TagsFilter[], listToFilter: TagsFilter[] ) : TagsFilter[] { | ||||||
|  |         return listToFilter.filter(tf => !blacklist.some(guard => guard.shadows(tf))) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns a filtered version of 'listToFilter', where no duplicates and no equivalents exists. | ||||||
|  |      *  | ||||||
|  |      * TagUtils.removeEquivalents([new RegexTag("key", /^..*$/), new Tag("key","value")]) // => [new Tag("key", "value")]
 | ||||||
|  |      */ | ||||||
|  |     public static removeEquivalents( listToFilter: (Tag | RegexTag)[]) : TagsFilter[] { | ||||||
|  |         const result: TagsFilter[] = [] | ||||||
|  |         outer: for (let i = 0; i < listToFilter.length; i++){ | ||||||
|  |             const tag = listToFilter[i]; | ||||||
|  |             for (let j = 0; j < listToFilter.length; j++){ | ||||||
|  |                 if(i === j){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const guard = listToFilter[j]; | ||||||
|  |                 if(guard.shadows(tag)) { | ||||||
|  |                     // the guard 'kills' the tag: we continue the outer loop without adding the tag
 | ||||||
|  |                     continue outer; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             result.push(tag) | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Returns `true` if at least one element of the 'guards' shadows one element of the 'listToFilter'. | ||||||
|  |      * | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [new Tag("key","value"), new Tag("other_key","value")]) // => true
 | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("other_key","value")]) // => false
 | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("key","other_value")]) // => false
 | ||||||
|  |      */ | ||||||
|  |     public static containsEquivalents( guards: TagsFilter[], listToFilter: TagsFilter[] ) : boolean { | ||||||
|  |         return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |    | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -4,7 +4,11 @@ export abstract class TagsFilter { | ||||||
| 
 | 
 | ||||||
|     abstract isUsableAsAnswer(): boolean; |     abstract isUsableAsAnswer(): boolean; | ||||||
| 
 | 
 | ||||||
|     abstract isEquivalent(other: TagsFilter): boolean; |     /** | ||||||
|  |      * Indicates some form of equivalency: | ||||||
|  |      * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties | ||||||
|  |      */ | ||||||
|  |     abstract shadows(other: TagsFilter): boolean; | ||||||
| 
 | 
 | ||||||
|     abstract matchesProperties(properties: any): boolean; |     abstract matchesProperties(properties: any): boolean; | ||||||
| 
 | 
 | ||||||
|  | @ -30,7 +34,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(): TagsFilter | 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). | ||||||
|      *  |      *  | ||||||
|  |  | ||||||
|  | @ -127,6 +127,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|                 idKey: json.source["idKey"] |                 idKey: json.source["idKey"] | ||||||
| 
 | 
 | ||||||
|             }, |             }, | ||||||
|  |             Constants.priviliged_layers.indexOf(this.id) > 0, | ||||||
|             json.id |             json.id | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
| import {RegexTag} from "../../Logic/Tags/RegexTag"; | import {RegexTag} from "../../Logic/Tags/RegexTag"; | ||||||
|  | import {param} from "jquery"; | ||||||
| 
 | 
 | ||||||
| export default class SourceConfig { | export default class SourceConfig { | ||||||
| 
 | 
 | ||||||
|  | @ -19,7 +20,7 @@ export default class SourceConfig { | ||||||
|         isOsmCache?: boolean, |         isOsmCache?: boolean, | ||||||
|         geojsonSourceLevel?: number, |         geojsonSourceLevel?: number, | ||||||
|         idKey?: string |         idKey?: string | ||||||
|     }, context?: string) { |     }, isSpecialLayer: boolean, context?: string) { | ||||||
| 
 | 
 | ||||||
|         let defined = 0; |         let defined = 0; | ||||||
|         if (params.osmTags) { |         if (params.osmTags) { | ||||||
|  | @ -43,6 +44,15 @@ export default class SourceConfig { | ||||||
|                 throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` |                 throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         if(params.osmTags !== undefined && !isSpecialLayer){ | ||||||
|  |             const optimized = params.osmTags.optimize() | ||||||
|  |             if(optimized === false){ | ||||||
|  |                 throw "Error at "+context+": the specified tags are conflicting with each other: they will never match anything at all" | ||||||
|  |             } | ||||||
|  |             if(optimized === true){ | ||||||
|  |                        throw "Error at "+context+": the specified tags are very wide: they will always match everything" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|         this.osmTags = params.osmTags ?? new RegexTag("id", /.*/); |         this.osmTags = params.osmTags ?? new RegexTag("id", /.*/); | ||||||
|         this.overpassScript = params.overpassScript; |         this.overpassScript = params.overpassScript; | ||||||
|         this.geojsonSource = params.geojsonSource; |         this.geojsonSource = params.geojsonSource; | ||||||
|  |  | ||||||
|  | @ -248,7 +248,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         const inputEl = new InputElementMap<number[], TagsFilter>( |         const inputEl = new InputElementMap<number[], TagsFilter>( | ||||||
|             checkBoxes, |             checkBoxes, | ||||||
|             (t0, t1) => { |             (t0, t1) => { | ||||||
|                 return t0?.isEquivalent(t1) ?? false |                 return t0?.shadows(t1) ?? false | ||||||
|             }, |             }, | ||||||
|             (indices) => { |             (indices) => { | ||||||
|                 if (indices.length === 0) { |                 if (indices.length === 0) { | ||||||
|  | @ -370,7 +370,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         return new FixedInputElement( |         return new FixedInputElement( | ||||||
|             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), |             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), | ||||||
|             tagging, |             tagging, | ||||||
|             (t0, t1) => t1.isEquivalent(t0)); |             (t0, t1) => t1.shadows(t0)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateMappingContent(mapping: { |     private static GenerateMappingContent(mapping: { | ||||||
|  | @ -450,7 +450,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( |         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( | ||||||
|             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), |             input, (a, b) => a === b || (a?.shadows(b) ?? false), | ||||||
|             pickString, toString |             pickString, toString | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,16 +1,13 @@ | ||||||
| { | { | ||||||
|   "id": "mapcomplete-changes", |   "id": "mapcomplete-changes", | ||||||
|   "title": { |   "title": { | ||||||
|     "en": "Changes made with MapComplete", |     "en": "Changes made with MapComplete" | ||||||
|     "de": "Änderungen mit MapComplete" |  | ||||||
|   }, |   }, | ||||||
|   "shortDescription": { |   "shortDescription": { | ||||||
|     "en": "Shows changes made by MapComplete", |     "en": "Shows changes made by MapComplete" | ||||||
|     "de": "Zeigt Änderungen von MapComplete" |  | ||||||
|   }, |   }, | ||||||
|   "description": { |   "description": { | ||||||
|     "en": "This maps shows all the changes made with MapComplete", |     "en": "This maps shows all the changes made with MapComplete" | ||||||
|     "de": "Diese Karte zeigt alle Änderungen die mit MapComplete gemacht wurden" |  | ||||||
|   }, |   }, | ||||||
|   "maintainer": "", |   "maintainer": "", | ||||||
|   "icon": "./assets/svg/logo.svg", |   "icon": "./assets/svg/logo.svg", | ||||||
|  | @ -25,8 +22,7 @@ | ||||||
|     { |     { | ||||||
|       "id": "mapcomplete-changes", |       "id": "mapcomplete-changes", | ||||||
|       "name": { |       "name": { | ||||||
|         "en": "Changeset centers", |         "en": "Changeset centers" | ||||||
|         "de": "Schwerpunkte von Änderungssätzen" |  | ||||||
|       }, |       }, | ||||||
|       "minzoom": 0, |       "minzoom": 0, | ||||||
|       "source": { |       "source": { | ||||||
|  | @ -40,41 +36,35 @@ | ||||||
|       ], |       ], | ||||||
|       "title": { |       "title": { | ||||||
|         "render": { |         "render": { | ||||||
|           "en": "Changeset for {theme}", |           "en": "Changeset for {theme}" | ||||||
|           "de": "Änderungen für {theme}" |  | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "description": { |       "description": { | ||||||
|         "en": "Shows all MapComplete changes", |         "en": "Shows all MapComplete changes" | ||||||
|         "de": "Zeigt alle MapComplete Änderungen" |  | ||||||
|       }, |       }, | ||||||
|       "tagRenderings": [ |       "tagRenderings": [ | ||||||
|         { |         { | ||||||
|           "id": "render_id", |           "id": "render_id", | ||||||
|           "render": { |           "render": { | ||||||
|             "en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>", |             "en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>" | ||||||
|             "de": "Änderung <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>" |  | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "id": "contributor", |           "id": "contributor", | ||||||
|           "render": { |           "render": { | ||||||
|             "en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>", |             "en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>" | ||||||
|             "de": "Änderung wurde von <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a> gemacht" |  | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "id": "theme", |           "id": "theme", | ||||||
|           "render": { |           "render": { | ||||||
|             "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>", |             "en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>" | ||||||
|             "de": "Änderung mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>" |  | ||||||
|           }, |           }, | ||||||
|           "mappings": [ |           "mappings": [ | ||||||
|             { |             { | ||||||
|               "if": "theme~http.*", |               "if": "theme~http.*", | ||||||
|               "then": { |               "then": { | ||||||
|                 "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>", |                 "en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>" | ||||||
|                 "de": "Änderung mit <b>inoffiziellem</b> Thema <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>" |  | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
|  | @ -338,8 +328,7 @@ | ||||||
|                 } |                 } | ||||||
|               ], |               ], | ||||||
|               "question": { |               "question": { | ||||||
|                 "en": "Themename contains {search}", |                 "en": "Themename contains {search}" | ||||||
|                 "de": "Themenname enthält {search}" |  | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
|  | @ -355,8 +344,7 @@ | ||||||
|                 } |                 } | ||||||
|               ], |               ], | ||||||
|               "question": { |               "question": { | ||||||
|                 "en": "Made by contributor {search}", |                 "en": "Made by contributor {search}" | ||||||
|                 "de": "Erstellt von {search}" |  | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
|  | @ -372,8 +360,7 @@ | ||||||
|                 } |                 } | ||||||
|               ], |               ], | ||||||
|               "question": { |               "question": { | ||||||
|                 "en": "<b>Not</b> made by contributor {search}", |                 "en": "<b>Not</b> made by contributor {search}" | ||||||
|                 "de": "<b>Nicht</b> erstellt von {search}" |  | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
|  | @ -388,8 +375,7 @@ | ||||||
|           { |           { | ||||||
|             "id": "link_to_more", |             "id": "link_to_more", | ||||||
|             "render": { |             "render": { | ||||||
|               "en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>", |               "en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>" | ||||||
|               "de": "Weitere Statistiken finden Sie <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>" |  | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|  |  | ||||||
							
								
								
									
										347
									
								
								test/Logic/Tags/OptimizeTags.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								test/Logic/Tags/OptimizeTags.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,347 @@ | ||||||
|  | import {describe} from 'mocha' | ||||||
|  | import {expect} from 'chai' | ||||||
|  | import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; | ||||||
|  | import {And} from "../../../Logic/Tags/And"; | ||||||
|  | import {Tag} from "../../../Logic/Tags/Tag"; | ||||||
|  | import {TagUtils} from "../../../Logic/Tags/TagUtils"; | ||||||
|  | import {Or} from "../../../Logic/Tags/Or"; | ||||||
|  | import {RegexTag} from "../../../Logic/Tags/RegexTag"; | ||||||
|  | 
 | ||||||
|  | describe("Tag optimalization", () => { | ||||||
|  | 
 | ||||||
|  |     describe("And", () => { | ||||||
|  |         it("with condition and nested and should be flattened", () => { | ||||||
|  |             const t = new And( | ||||||
|  |                 [ | ||||||
|  |                     new And([ | ||||||
|  |                         new Tag("x", "y") | ||||||
|  |                     ]), | ||||||
|  |                     new Tag("a", "b") | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq(`a=b&x=y`) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should be 'true' if no conditions are given", () => { | ||||||
|  |             const t = new And( | ||||||
|  |                 [] | ||||||
|  |             ) | ||||||
|  |             const opt = t.optimize() | ||||||
|  |             expect(opt).eq(true) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and common property should be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             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") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and common regextag should be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             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") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& ( (a=b&c=d) |x=y)") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and inverted regextags should _not_ be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             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") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& (a=b|x=y) & (c=d|x!=y)") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should move regextag to the end", () => { | ||||||
|  |             const t = new And([ | ||||||
|  |                 new RegexTag("x", "y"), | ||||||
|  |                 new Tag("a", "b") | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("a=b&x=y") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should sort tags by their popularity (least popular first)", () => { | ||||||
|  |             const t = new And([ | ||||||
|  |                 new Tag("bicycle", "yes"), | ||||||
|  |                 new Tag("amenity", "binoculars") | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should optimize nested ORs", () => { | ||||||
|  |             const filter = TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     "X=Y", "FOO=BAR", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["X=Y", "FOO=BAR"] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle=yes" | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }) | ||||||
|  |             // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) )
 | ||||||
|  |             // This is equivalent to (X=Y | FOO=BAR)
 | ||||||
|  |             const opt = filter.optimize() | ||||||
|  |             console.log(opt) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should optimize an advanced, real world case", () => { | ||||||
|  |             const filter = TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle=yes" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] | ||||||
|  |                             }, | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     "amenity=toilets", | ||||||
|  |                     "amenity=bench", | ||||||
|  |                     "leisure=picnic_table", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "tower:type=observation" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "amenity=bicycle_repair_station" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": [ | ||||||
|  |                                     "amenity=bicycle_rental", | ||||||
|  |                                     "bicycle_rental~*", | ||||||
|  |                                     "service:bicycle:rental=yes", | ||||||
|  |                                     "rental~.*bicycle.*" | ||||||
|  |                                 ] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle_rental!=docking_station" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "leisure=playground", | ||||||
|  |                             "playground!=forest" | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }); | ||||||
|  |             const opt = <TagsFilter>filter.optimize() | ||||||
|  |             const expected = ["amenity=charging_station", | ||||||
|  |                 "amenity=toilets", | ||||||
|  |                 "amenity=bench", | ||||||
|  |                 "amenity=bicycle_repair_station", | ||||||
|  |                 "construction:amenity=charging_station", | ||||||
|  |                 "disused:amenity=charging_station", | ||||||
|  |                 "leisure=picnic_table", | ||||||
|  |                 "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"] | ||||||
|  | 
 | ||||||
|  |             expect((<Or>opt).or.map(f => TagUtils.toString(f))).deep.eq( | ||||||
|  |                 expected | ||||||
|  |             ) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should detect conflicting tags", () => { | ||||||
|  |             const q = new And([new Tag("key", "value"), new RegexTag("key", "value", true)]) | ||||||
|  |             expect(q.optimize()).eq(false) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should detect conflicting tags with a regex", () => { | ||||||
|  |             const q = new And([new Tag("key", "value"), new RegexTag("key", /value/, true)]) | ||||||
|  |             expect(q.optimize()).eq(false) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     describe("Or", () => { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         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"), | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should flatten nested ors", () => { | ||||||
|  |             const t = new Or([ | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y") | ||||||
|  |                 ]) | ||||||
|  |             ]).optimize() | ||||||
|  |             expect(t).deep.eq(new Tag("x", "y")) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should flatten nested ors", () => { | ||||||
|  |             const t = new Or([ | ||||||
|  |                 new Tag("a", "b"), | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y") | ||||||
|  |                 ]) | ||||||
|  |             ]).optimize() | ||||||
|  |             expect(t).deep.eq(new Or([new Tag("a", "b"), new Tag("x", "y")])) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it("should not generate a conflict for climbing tags", () => { | ||||||
|  |         const club_tags = TagUtils.Tag( | ||||||
|  |             { | ||||||
|  |                 "or": [ | ||||||
|  |                     "club=climbing", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "sport=climbing", | ||||||
|  |                             { | ||||||
|  |                                 "or": [ | ||||||
|  |                                     "office~*", | ||||||
|  |                                     "club~*" | ||||||
|  |                                 ] | ||||||
|  |                             } | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }) | ||||||
|  |         const gym_tags = TagUtils.Tag({ | ||||||
|  |             "and": [ | ||||||
|  |                 "sport=climbing", | ||||||
|  |                 "leisure=sports_centre" | ||||||
|  |             ] | ||||||
|  |         }) | ||||||
|  |         const other_climbing = TagUtils.Tag({ | ||||||
|  |             "and": [ | ||||||
|  |                 "sport=climbing", | ||||||
|  |                 "climbing!~route", | ||||||
|  |                 "leisure!~sports_centre", | ||||||
|  |                 "climbing!=route_top", | ||||||
|  |                 "climbing!=route_bottom" | ||||||
|  |             ] | ||||||
|  |         }) | ||||||
|  |         const together = new Or([club_tags, gym_tags, other_climbing]) | ||||||
|  |         const opt = together.optimize() | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          club=climbing | (sport=climbing&(office~* | club~*)) | ||||||
|  |          OR | ||||||
|  |          sport=climbing & leisure=sports_centre | ||||||
|  |          OR | ||||||
|  |          sport=climbing & climbing!~route & leisure!~sports_centre | ||||||
|  |         */ | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          > When the first OR is written out, this becomes | ||||||
|  |          club=climbing  | ||||||
|  |          OR  | ||||||
|  |          (sport=climbing&(office~* | club~*)) | ||||||
|  |          OR | ||||||
|  |          (sport=climbing & leisure=sports_centre) | ||||||
|  |          OR | ||||||
|  |          (sport=climbing & climbing!~route & leisure!~sports_centre & ...) | ||||||
|  |          */ | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          > We can join the 'sport=climbing' in the last 3 phrases | ||||||
|  |          club=climbing  | ||||||
|  |          OR  | ||||||
|  |          (sport=climbing AND  | ||||||
|  |              (office~* | club~*)) | ||||||
|  |              OR | ||||||
|  |              (leisure=sports_centre) | ||||||
|  |              OR | ||||||
|  |              (climbing!~route & leisure!~sports_centre & ...) | ||||||
|  |          ) | ||||||
|  |          */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         expect(opt).deep.eq( | ||||||
|  |             TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     "club=climbing", | ||||||
|  |                     { | ||||||
|  |                         and: ["sport=climbing", | ||||||
|  |                             {or: ["club~*", "office~*"]}] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         and: ["sport=climbing", | ||||||
|  |                             { | ||||||
|  |                                 or: [ | ||||||
|  |                                     "leisure=sports_centre", | ||||||
|  |                                     { | ||||||
|  |                                         and: [ | ||||||
|  |                                             "climbing!~route", | ||||||
|  |                                             "climbing!=route_top", | ||||||
|  |                                             "climbing!=route_bottom", | ||||||
|  |                                             "leisure!~sports_centre" | ||||||
|  |                                         ] | ||||||
|  |                                     } | ||||||
|  |                                 ] | ||||||
|  |                             }] | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 ], | ||||||
|  | 
 | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -1,150 +0,0 @@ | ||||||
| import {describe} from 'mocha' |  | ||||||
| import {expect} from 'chai' |  | ||||||
| import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; |  | ||||||
| import {And} from "../../../Logic/Tags/And"; |  | ||||||
| import {Tag} from "../../../Logic/Tags/Tag"; |  | ||||||
| import {TagUtils} from "../../../Logic/Tags/TagUtils"; |  | ||||||
| import {Or} from "../../../Logic/Tags/Or"; |  | ||||||
| import {RegexTag} from "../../../Logic/Tags/RegexTag"; |  | ||||||
| 
 |  | ||||||
| describe("Tag optimalization", () => { |  | ||||||
|      |  | ||||||
|     describe("And", () => { |  | ||||||
|         it("with condition and nested and should be flattened", () => { |  | ||||||
|             const t = new And( |  | ||||||
|                 [ |  | ||||||
|                     new And([ |  | ||||||
|                         new Tag("x", "y") |  | ||||||
|                     ]), |  | ||||||
|                     new Tag("a", "b") |  | ||||||
|                 ] |  | ||||||
|             ) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq(`a=b&x=y`) |  | ||||||
|         }) |  | ||||||
|          |  | ||||||
|         it("with nested ors and commons property should be extracted", () => { |  | ||||||
| 
 |  | ||||||
|             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 |  | ||||||
|             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") |  | ||||||
|                 ]) |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )") |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|         it("should move regextag to the end", () => { |  | ||||||
|             const t = new And([ |  | ||||||
|                 new RegexTag("x","y"), |  | ||||||
|                 new Tag("a","b") |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("a=b&x~^y$") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|          |  | ||||||
|         it("should sort tags by their popularity (least popular first)", () => { |  | ||||||
|             const t = new And([ |  | ||||||
|                 new Tag("bicycle","yes"), |  | ||||||
|                 new Tag("amenity","binoculars") |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|         it("should optimize an advanced, real world case", () => { |  | ||||||
|             const filter = TagUtils.Tag(  {or:   [ |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"] |  | ||||||
|                             }, |  | ||||||
|                             "bicycle=yes" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"] |  | ||||||
|                             }, |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     "amenity=toilets", |  | ||||||
|                     "amenity=bench", |  | ||||||
|                     "leisure=picnic_table", |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "tower:type=observation" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "amenity=bicycle_repair_station" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": [ |  | ||||||
|                                     "amenity=bicycle_rental", |  | ||||||
|                                     "bicycle_rental~*", |  | ||||||
|                                     "service:bicycle:rental=yes", |  | ||||||
|                                     "rental~.*bicycle.*" |  | ||||||
|                                 ] |  | ||||||
|                             }, |  | ||||||
|                             "bicycle_rental!=docking_station" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "leisure=playground", |  | ||||||
|                             "playground!=forest" |  | ||||||
|                         ] |  | ||||||
|                     } |  | ||||||
|                 ]}); |  | ||||||
|             const opt = <TagsFilter> filter.optimize() |  | ||||||
|             const expected = "amenity=charging_station|" + |  | ||||||
|                 "amenity=toilets|" + |  | ||||||
|                 "amenity=bench|" + |  | ||||||
|                 "amenity=bicycle_repair_station" + |  | ||||||
|                 "|construction:amenity=charging_station|" + |  | ||||||
|                 "disused:amenity=charging_station|" + |  | ||||||
|                 "leisure=picnic_table|" + |  | ||||||
|                 "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$)" |  | ||||||
|              |  | ||||||
|             expect(TagUtils.toString(opt).replace(/ /g, "")) |  | ||||||
|                 .eq(expected.replace(/ /g, "")) |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|     }) |  | ||||||
|      |  | ||||||
|     describe("Or", () => { |  | ||||||
|         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"), |  | ||||||
|                 ]) |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("foo=bar") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
| }) |  | ||||||
							
								
								
									
										19
									
								
								test/Models/ThemeConfig/SourceConfig.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								test/Models/ThemeConfig/SourceConfig.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import {describe} from 'mocha' | ||||||
|  | import {expect} from 'chai' | ||||||
|  | import SourceConfig from "../../../Models/ThemeConfig/SourceConfig"; | ||||||
|  | import {TagUtils} from "../../../Logic/Tags/TagUtils"; | ||||||
|  | 
 | ||||||
|  | describe("SourceConfig", () => { | ||||||
|  | 
 | ||||||
|  |     it("should throw an error on conflicting tags", () => { | ||||||
|  |         expect(() => { | ||||||
|  |             new SourceConfig( | ||||||
|  |                 { | ||||||
|  |                     osmTags: TagUtils.Tag({ | ||||||
|  |                         and: ["x=y", "a=b", "x!=y"] | ||||||
|  |                     }) | ||||||
|  |                 }, false | ||||||
|  |             ) | ||||||
|  |         }).to.throw(/tags are conflicting/) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -34,7 +34,7 @@ describe("GenerateCache", () => { | ||||||
|             } |             } | ||||||
|             mkdirSync("/tmp/np-cache") |             mkdirSync("/tmp/np-cache") | ||||||
|             initDownloads( |             initDownloads( | ||||||
|                 "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!~%22%5E98%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22access%22!~%22%5Epermissive%24%22%5D%5B%22access%22!~%22%5Eprivate%24%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" |                 "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" | ||||||
|             ); |             ); | ||||||
|             await main([ |             await main([ | ||||||
|                 "natuurpunt", |                 "natuurpunt", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue