Improve tag optimazations, fixes rendering of climbing map

This commit is contained in:
Pieter Vander Vennet 2022-04-14 00:53:38 +02:00
parent 01ba686270
commit 01567a4b80
16 changed files with 875 additions and 303 deletions

View file

@ -8,6 +8,13 @@ export class And extends TagsFilter {
super();
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 = [];
@ -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){
@ -173,27 +247,56 @@ export class And extends TagsFilter {
newAnds.push(tf)
}
}
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());
}
}

View file

@ -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;
}

View file

@ -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());
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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[] {

View file

@ -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)))
}
}

View file

@ -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;
@ -30,7 +34,7 @@ export abstract class TagsFilter {
* Returns an optimized version (or self) of this tagsFilter
*/
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).
*