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
|
@ -9,6 +9,13 @@ export class And extends TagsFilter {
|
|||
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[] {
|
||||
const values = [];
|
||||
for (const or of choices) {
|
||||
|
@ -45,7 +52,7 @@ export class And extends TagsFilter {
|
|||
* import {RegexTag} from "./RegexTag";
|
||||
*
|
||||
* 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[] {
|
||||
let allChoices: string[] = null;
|
||||
|
@ -87,17 +94,17 @@ export class And extends TagsFilter {
|
|||
* ])
|
||||
* const t1 = new And([new Tag("valves", "A")])
|
||||
* const t2 = new And([new Tag("valves", "B")])
|
||||
* t0.isEquivalent(t0) // => true
|
||||
* t1.isEquivalent(t1) // => true
|
||||
* t2.isEquivalent(t2) // => true
|
||||
* t0.isEquivalent(t1) // => false
|
||||
* t0.isEquivalent(t2) // => false
|
||||
* t1.isEquivalent(t0) // => false
|
||||
* t1.isEquivalent(t2) // => false
|
||||
* t2.isEquivalent(t0) // => false
|
||||
* t2.isEquivalent(t1) // => false
|
||||
* t0.shadows(t0) // => true
|
||||
* t1.shadows(t1) // => true
|
||||
* t2.shadows(t2) // => true
|
||||
* t0.shadows(t1) // => false
|
||||
* t0.shadows(t2) // => false
|
||||
* t1.shadows(t0) // => false
|
||||
* t1.shadows(t2) // => false
|
||||
* t2.shadows(t0) // => false
|
||||
* t2.shadows(t1) // => false
|
||||
*/
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (!(other instanceof And)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -105,7 +112,7 @@ export class And extends TagsFilter {
|
|||
for (const selfTag of this.and) {
|
||||
let matchFound = false;
|
||||
for (const otherTag of other.and) {
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
if (matchFound) {
|
||||
break;
|
||||
}
|
||||
|
@ -118,7 +125,7 @@ export class And extends TagsFilter {
|
|||
for (const otherTag of other.and) {
|
||||
let matchFound = false;
|
||||
for (const selfTag of this.and) {
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
if (matchFound) {
|
||||
break;
|
||||
}
|
||||
|
@ -148,23 +155,90 @@ export class And extends TagsFilter {
|
|||
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 {
|
||||
if(this.and.length === 0){
|
||||
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[] = []
|
||||
|
||||
let containedOrs : Or[] = []
|
||||
for (const tf of optimized) {
|
||||
if(tf === false){
|
||||
return false
|
||||
}
|
||||
if(tf === true){
|
||||
continue
|
||||
}
|
||||
|
||||
if(tf instanceof And){
|
||||
newAnds.push(...tf.and)
|
||||
}else if(tf instanceof Or){
|
||||
|
@ -174,26 +248,55 @@ export class And extends TagsFilter {
|
|||
}
|
||||
}
|
||||
|
||||
containedOrs = containedOrs.filter(ca => {
|
||||
for (const element of ca.or) {
|
||||
if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){
|
||||
// At least one part of the 'OR' is matched by the outer or, so this means that this OR isn't needed at all
|
||||
// XY & (XY | AB) === XY
|
||||
return false
|
||||
{
|
||||
let dirty = false;
|
||||
do {
|
||||
const cleanedContainedOrs : Or[] = []
|
||||
outer: for (let containedOr of containedOrs) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return true;
|
||||
containedOrs = cleanedContainedOrs
|
||||
} 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
|
||||
if(containedOrs.length === 1){
|
||||
newAnds.push(containedOrs[0])
|
||||
}
|
||||
if(containedOrs.length > 1){
|
||||
}else if(containedOrs.length > 1){
|
||||
let commonValues : TagsFilter [] = containedOrs[0].or
|
||||
for (let i = 1; i < containedOrs.length && commonValues.length > 0; 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){
|
||||
newAnds.push(...containedOrs)
|
||||
|
@ -201,19 +304,11 @@ export class And extends TagsFilter {
|
|||
const newOrs: TagsFilter[] = []
|
||||
for (const containedOr of containedOrs) {
|
||||
const elements = containedOr.or
|
||||
.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate)))
|
||||
const or = new Or(elements).optimize()
|
||||
if(or === true){
|
||||
// neutral element
|
||||
continue
|
||||
}
|
||||
if(or === false){
|
||||
return false
|
||||
}
|
||||
newOrs.push(or)
|
||||
.filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
|
||||
newOrs.push(Or.construct(elements))
|
||||
}
|
||||
|
||||
commonValues.push(new And(newOrs))
|
||||
commonValues.push(And.construct(newOrs))
|
||||
const result = new Or(commonValues).optimize()
|
||||
if(result === false){
|
||||
return false
|
||||
|
@ -224,16 +319,22 @@ export class And extends TagsFilter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newAnds.length === 1){
|
||||
return newAnds[0]
|
||||
if(newAnds.length === 0){
|
||||
return true
|
||||
}
|
||||
|
||||
if(TagUtils.ContainsOppositeTags(newAnds)){
|
||||
return false
|
||||
}
|
||||
|
||||
TagUtils.sortFilters(newAnds, true)
|
||||
|
||||
return new And(newAnds)
|
||||
return And.construct(newAnds)
|
||||
}
|
||||
|
||||
isNegative(): boolean {
|
||||
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"
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
public static construct(or: TagsFilter[]): TagsFilter{
|
||||
if(or.length === 1){
|
||||
return or[0]
|
||||
}
|
||||
return new Or(or)
|
||||
}
|
||||
|
||||
|
||||
matchesProperties(properties: any): boolean {
|
||||
for (const tagsFilter of this.or) {
|
||||
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 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
|
||||
* 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;
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (other instanceof Or) {
|
||||
|
||||
for (const selfTag of this.or) {
|
||||
let matchFound = false;
|
||||
for (let i = 0; i < other.or.length && !matchFound; i++) {
|
||||
let otherTag = other.or[i];
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
}
|
||||
if (!matchFound) {
|
||||
return false;
|
||||
|
@ -85,45 +93,127 @@ export class Or extends TagsFilter {
|
|||
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 {
|
||||
|
||||
if(this.or.length === 0){
|
||||
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[] = []
|
||||
|
||||
let containedAnds : And[] = []
|
||||
for (const tf of optimized) {
|
||||
if(tf === true){
|
||||
return true
|
||||
}
|
||||
if(tf === false){
|
||||
continue
|
||||
}
|
||||
|
||||
if(tf instanceof Or){
|
||||
// expand all the nested ors...
|
||||
newOrs.push(...tf.or)
|
||||
}else if(tf instanceof And){
|
||||
// partition of all the ands
|
||||
containedAnds.push(tf)
|
||||
} else {
|
||||
newOrs.push(tf)
|
||||
}
|
||||
}
|
||||
|
||||
containedAnds = containedAnds.filter(ca => {
|
||||
for (const element of ca.and) {
|
||||
if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){
|
||||
// At least one part of the 'AND' is matched by the outer or, so this means that this OR isn't needed at all
|
||||
// XY | (XY & AB) === XY
|
||||
return false
|
||||
{
|
||||
let dirty = false;
|
||||
do {
|
||||
const cleanedContainedANds : And[] = []
|
||||
outer: for (let containedAnd of containedAnds) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
containedAnds = cleanedContainedANds
|
||||
} while(dirty)
|
||||
}
|
||||
// Extract common keys from the ANDS
|
||||
if(containedAnds.length === 1){
|
||||
newOrs.push(containedAnds[0])
|
||||
|
@ -131,40 +221,46 @@ export class Or extends TagsFilter {
|
|||
let commonValues : TagsFilter [] = containedAnds[0].and
|
||||
for (let i = 1; i < containedAnds.length && commonValues.length > 0; 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){
|
||||
newOrs.push(...containedAnds)
|
||||
}else{
|
||||
const newAnds: TagsFilter[] = []
|
||||
for (const containedAnd of containedAnds) {
|
||||
const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate)))
|
||||
newAnds.push(new And(elements))
|
||||
const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
|
||||
newAnds.push(And.construct(elements))
|
||||
}
|
||||
|
||||
commonValues.push(new Or(newAnds))
|
||||
commonValues.push(Or.construct(newAnds))
|
||||
const result = new And(commonValues).optimize()
|
||||
if(result === true){
|
||||
return true
|
||||
}else if(result === false){
|
||||
// neutral element: skip
|
||||
}else{
|
||||
newOrs.push(new And(commonValues))
|
||||
newOrs.push(And.construct(commonValues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newOrs.length === 1){
|
||||
return newOrs[0]
|
||||
if(newOrs.length === 0){
|
||||
return false
|
||||
}
|
||||
|
||||
if(TagUtils.ContainsOppositeTags(newOrs)){
|
||||
return true
|
||||
}
|
||||
|
||||
TagUtils.sortFilters(newOrs, false)
|
||||
|
||||
return new Or(newOrs)
|
||||
return Or.construct(newOrs)
|
||||
}
|
||||
|
||||
isNegative(): boolean {
|
||||
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) {
|
||||
super();
|
||||
this.key = key;
|
||||
if (typeof value === "string") {
|
||||
if (value.indexOf("^") < 0 && value.indexOf("$") < 0) {
|
||||
value = "^" + value + "$"
|
||||
}
|
||||
value = new RegExp(value)
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.invert = invert;
|
||||
this.matchesEmpty = RegexTag.doesMatch("", this.value);
|
||||
|
@ -79,14 +72,14 @@ export class RegexTag extends TagsFilter {
|
|||
/**
|
||||
* 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": "other_value"}) // => true
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => false
|
||||
* isNotEmpty.matchesProperties({"other_key": ""}) // => 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": "other_value"}) // => false
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => true
|
||||
|
@ -121,6 +114,9 @@ export class RegexTag extends TagsFilter {
|
|||
* importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
|
||||
* importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
|
||||
* 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 {
|
||||
if (typeof this.key === "string") {
|
||||
|
@ -147,17 +143,87 @@ export class RegexTag extends TagsFilter {
|
|||
|
||||
asHumanString() {
|
||||
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)}`
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
throw "A variable with substitution can not be used to query overpass"
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (!(other instanceof SubstitutingTag)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -88,14 +88,23 @@ export class Tag extends TagsFilter {
|
|||
return true;
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
if (other instanceof Tag) {
|
||||
return this.key === other.key && this.value === other.value;
|
||||
/**
|
||||
* // should handle advanced regexes
|
||||
* 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) {
|
||||
other.isEquivalent(this);
|
||||
}
|
||||
return false;
|
||||
return other.matchesProperties({[this.key]: this.value});
|
||||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
|
|
|
@ -200,15 +200,16 @@ export class TagUtils {
|
|||
*
|
||||
* TagUtils.Tag("key=value") // => new Tag("key", "value")
|
||||
* TagUtils.Tag("key=") // => new Tag("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~*") // => new RegexTag("key", /^..*$/)
|
||||
* TagUtils.Tag("key!=value") // => new RegexTag("key", "value", true)
|
||||
* TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
|
||||
* 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("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
|
||||
* TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
|
||||
* 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("xyz<5").matchesProperties({xyz: 4}) // => true
|
||||
|
@ -306,7 +307,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
split[1],
|
||||
new RegExp("^"+ split[1]+"$"),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
@ -338,17 +339,6 @@ export class TagUtils {
|
|||
split[1] = "..*"
|
||||
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(
|
||||
split[0],
|
||||
split[1],
|
||||
|
@ -357,15 +347,18 @@ export class TagUtils {
|
|||
}
|
||||
if (tag.indexOf("~") >= 0) {
|
||||
const split = Utils.SplitFirst(tag, "~");
|
||||
let value : string | RegExp = split[1]
|
||||
if (split[1] === "") {
|
||||
throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")"
|
||||
}
|
||||
if (split[1] === "*") {
|
||||
split[1] = "..*"
|
||||
if (value === "*") {
|
||||
value = /^..*$/
|
||||
}else {
|
||||
value = new RegExp("^"+value+"$")
|
||||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
split[1]
|
||||
value
|
||||
);
|
||||
}
|
||||
if (tag.indexOf("=") >= 0) {
|
||||
|
@ -431,4 +424,94 @@ export class TagUtils {
|
|||
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 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;
|
||||
|
||||
|
|
|
@ -127,6 +127,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
idKey: json.source["idKey"]
|
||||
|
||||
},
|
||||
Constants.priviliged_layers.indexOf(this.id) > 0,
|
||||
json.id
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||
import {param} from "jquery";
|
||||
|
||||
export default class SourceConfig {
|
||||
|
||||
|
@ -19,7 +20,7 @@ export default class SourceConfig {
|
|||
isOsmCache?: boolean,
|
||||
geojsonSourceLevel?: number,
|
||||
idKey?: string
|
||||
}, context?: string) {
|
||||
}, isSpecialLayer: boolean, context?: string) {
|
||||
|
||||
let defined = 0;
|
||||
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})`
|
||||
}
|
||||
}
|
||||
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.overpassScript = params.overpassScript;
|
||||
this.geojsonSource = params.geojsonSource;
|
||||
|
|
|
@ -248,7 +248,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
const inputEl = new InputElementMap<number[], TagsFilter>(
|
||||
checkBoxes,
|
||||
(t0, t1) => {
|
||||
return t0?.isEquivalent(t1) ?? false
|
||||
return t0?.shadows(t1) ?? false
|
||||
},
|
||||
(indices) => {
|
||||
if (indices.length === 0) {
|
||||
|
@ -370,7 +370,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
return new FixedInputElement(
|
||||
TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state),
|
||||
tagging,
|
||||
(t0, t1) => t1.isEquivalent(t0));
|
||||
(t0, t1) => t1.shadows(t0));
|
||||
}
|
||||
|
||||
private static GenerateMappingContent(mapping: {
|
||||
|
@ -450,7 +450,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
})
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"title": {
|
||||
"en": "Changes made with MapComplete",
|
||||
"de": "Änderungen mit MapComplete"
|
||||
"en": "Changes made with MapComplete"
|
||||
},
|
||||
"shortDescription": {
|
||||
"en": "Shows changes made by MapComplete",
|
||||
"de": "Zeigt Änderungen von MapComplete"
|
||||
"en": "Shows changes made by MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete",
|
||||
"de": "Diese Karte zeigt alle Änderungen die mit MapComplete gemacht wurden"
|
||||
"en": "This maps shows all the changes made with MapComplete"
|
||||
},
|
||||
"maintainer": "",
|
||||
"icon": "./assets/svg/logo.svg",
|
||||
|
@ -25,8 +22,7 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"name": {
|
||||
"en": "Changeset centers",
|
||||
"de": "Schwerpunkte von Änderungssätzen"
|
||||
"en": "Changeset centers"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
|
@ -40,41 +36,35 @@
|
|||
],
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Changeset for {theme}",
|
||||
"de": "Änderungen für {theme}"
|
||||
"en": "Changeset for {theme}"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"en": "Shows all MapComplete changes",
|
||||
"de": "Zeigt alle MapComplete Änderungen"
|
||||
"en": "Shows all MapComplete changes"
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "render_id",
|
||||
"render": {
|
||||
"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>"
|
||||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contributor",
|
||||
"render": {
|
||||
"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"
|
||||
"en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "theme",
|
||||
"render": {
|
||||
"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>"
|
||||
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "theme~http.*",
|
||||
"then": {
|
||||
"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>"
|
||||
"en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -338,8 +328,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Themename contains {search}",
|
||||
"de": "Themenname enthält {search}"
|
||||
"en": "Themename contains {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -355,8 +344,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made by contributor {search}",
|
||||
"de": "Erstellt von {search}"
|
||||
"en": "Made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -372,8 +360,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "<b>Not</b> made by contributor {search}",
|
||||
"de": "<b>Nicht</b> erstellt von {search}"
|
||||
"en": "<b>Not</b> made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -388,8 +375,7 @@
|
|||
{
|
||||
"id": "link_to_more",
|
||||
"render": {
|
||||
"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>"
|
||||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</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")
|
||||
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([
|
||||
"natuurpunt",
|
||||
|
|
Loading…
Reference in a new issue