Performance: add some optimizations and tag fixes

This commit is contained in:
Pieter Vander Vennet 2024-05-07 17:04:07 +02:00
parent c46e9da511
commit fd1b6d3131
4 changed files with 88 additions and 21 deletions

View file

@ -5,6 +5,7 @@ import { Tag } from "./Tag"
import { RegexTag } from "./RegexTag" import { RegexTag } from "./RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl" import { ExpressionSpecification } from "maplibre-gl"
import ComparingTag from "./ComparingTag"
export class And extends TagsFilter { export class And extends TagsFilter {
public and: TagsFilter[] public and: TagsFilter[]
@ -242,6 +243,27 @@ export class And extends TagsFilter {
* const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}] * const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}]
* const parsed = TagUtils.Tag(raw) * const parsed = TagUtils.Tag(raw)
* parsed.optimize().asJson() // => "advertising=screen" * parsed.optimize().asJson() // => "advertising=screen"
*
* const raw = {"and": ["count=0", "count>0"]}
* const parsed = TagUtils.Tag(raw)
* parsed.optimize() // => false
*
* const raw = {"and": ["count>0", "count>10"]}
* const parsed = TagUtils.Tag(raw)
* parsed.optimize().asJson() // => "count>0"
*
* // regression test
* const orig = {
* "and": [
* "sport=climbing",
* "climbing!~route",
* "climbing!=route_top",
* "climbing!=route_bottom",
* "leisure!~sports_centre"
* ]
* }
* const parsed = TagUtils.Tag(orig)
* parsed.optimize().asJson() // => orig
*/ */
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
if (this.and.length === 0) { if (this.and.length === 0) {
@ -256,9 +278,30 @@ export class And extends TagsFilter {
} }
const optimized = <TagsFilter[]>optimizedRaw const optimized = <TagsFilter[]>optimizedRaw
for (let i = 0; i <optimized.length; i++) {
for (let j = i + 1; j < optimized.length; j++) {
const ti = optimized[i]
const tj = optimized[j]
if(ti.shadows(tj)){
// if 'ti' is true, this implies 'tj' is always true as well.
// if 'ti' is false, then 'tj' might be true or false
// (e.g. let 'ti' be 'count>0' and 'tj' be 'count>10'.
// As such, it is no use to keep 'tj' around:
// If 'ti' is true, then 'tj' will be true too and 'tj' can be ignored
// If 'ti' is false, then the entire expression will be false and it doesn't matter what 'tj' yields
optimized.splice(j, 1)
}else if (tj.shadows(ti)){
optimized.splice(i, 1)
i--
continue
}
}
}
{ {
// Conflicting keys do return false // Conflicting keys do return false
const properties: object = {} const properties: Record<string, string> = {}
for (const opt of optimized) { for (const opt of optimized) {
if (opt instanceof Tag) { if (opt instanceof Tag) {
properties[opt.key] = opt.value properties[opt.key] = opt.value
@ -277,8 +320,7 @@ export class And extends TagsFilter {
// detected an internal conflict // detected an internal conflict
return false return false
} }
} } else if (opt instanceof RegexTag) {
if (opt instanceof RegexTag) {
const k = opt.key const k = opt.key
if (typeof k !== "string") { if (typeof k !== "string") {
continue continue
@ -316,6 +358,11 @@ export class And extends TagsFilter {
i-- i--
} }
} }
}else if(opt instanceof ComparingTag) {
const ct = opt
if(properties[ct.key] !== undefined && !ct.matchesProperties(properties)){
return false
}
} }
} }
} }

View file

@ -1,10 +1,9 @@
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { Tag } from "./Tag" import { Tag } from "./Tag"
import { ExpressionSpecification } from "maplibre-gl" import { ExpressionSpecification } from "maplibre-gl"
export default class ComparingTag extends TagsFilter { export default class ComparingTag extends TagsFilter {
private readonly _key: string public readonly key: string
private readonly _predicate: (value: string) => boolean private readonly _predicate: (value: string) => boolean
private readonly _representation: "<" | ">" | "<=" | ">=" private readonly _representation: "<" | ">" | "<=" | ">="
private readonly _boundary: string private readonly _boundary: string
@ -16,7 +15,7 @@ export default class ComparingTag extends TagsFilter {
boundary: string boundary: string
) { ) {
super() super()
this._key = key this.key = key
this._predicate = predicate this._predicate = predicate
this._representation = representation this._representation = representation
this._boundary = boundary this._boundary = boundary
@ -27,7 +26,7 @@ export default class ComparingTag extends TagsFilter {
} }
asHumanString() { asHumanString() {
return this._key + this._representation + this._boundary return this.asJson()
} }
asOverpass(): string[] { asOverpass(): string[] {
@ -55,7 +54,7 @@ export default class ComparingTag extends TagsFilter {
return true return true
} }
if (other instanceof ComparingTag) { if (other instanceof ComparingTag) {
if (other._key !== this._key) { if (other.key !== this.key) {
return false return false
} }
const selfDesc = this._representation === "<" || this._representation === "<=" const selfDesc = this._representation === "<" || this._representation === "<="
@ -76,7 +75,7 @@ export default class ComparingTag extends TagsFilter {
} }
if (other instanceof Tag) { if (other instanceof Tag) {
if (other.key !== this._key) { if (other.key !== this.key) {
return false return false
} }
if (this.matchesProperties({ [other.key]: other.value })) { if (this.matchesProperties({ [other.key]: other.value })) {
@ -101,19 +100,25 @@ export default class ComparingTag extends TagsFilter {
* t.matchesProperties({differentKey: 42}) // => false * t.matchesProperties({differentKey: 42}) // => false
*/ */
matchesProperties(properties: Record<string, string>): boolean { matchesProperties(properties: Record<string, string>): boolean {
return this._predicate(properties[this._key]) return this._predicate(properties[this.key])
} }
usedKeys(): string[] { usedKeys(): string[] {
return [this._key] return [this.key]
} }
usedTags(): { key: string; value: string }[] { usedTags(): { key: string; value: string }[] {
return [] return []
} }
asJson(): TagConfigJson { /**
return this._key + this._representation * import { TagUtils } from "../../../src/Logic/Tags/TagUtils"
*
* TagUtils.Tag("count>42").asJson() // => "count>42"
* TagUtils.Tag("count<0").asJson() // => "count<0"
*/
asJson(): string {
return this.key + this._representation + this._boundary
} }
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
@ -124,11 +129,11 @@ export default class ComparingTag extends TagsFilter {
return true return true
} }
visit(f: (TagsFilter) => void) { visit(f: (tf: TagsFilter) => void) {
f(this) f(this)
} }
asMapboxExpression(): ExpressionSpecification { asMapboxExpression(): ExpressionSpecification {
return [this._representation, ["get", this._key], this._boundary] return [this._representation, ["get", this.key], this._boundary]
} }
} }

View file

@ -259,6 +259,13 @@ export class RegexTag extends TagsFilter {
* new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true * 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","value")) // => false
* new RegexTag("key","value", true).shadows(new Tag("key","some_other_value")) // => false * new RegexTag("key","value", true).shadows(new Tag("key","some_other_value")) // => false
* new RegexTag("key","value", true).shadows(new Tag("key","some_other_value", true)) // => false
*
* const route = TagUtils.Tag("climbing!~route")
* const routeBottom = TagUtils.Tag("climbing!~route_bottom")
* route.shadows(routeBottom) // => false
* routeBottom.shadows(route) // => false
*
*/ */
shadows(other: TagsFilter): boolean { shadows(other: TagsFilter): boolean {
if (other instanceof RegexTag) { if (other instanceof RegexTag) {
@ -267,7 +274,7 @@ export class RegexTag extends TagsFilter {
return false return false
} }
if ( if (
(other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && (other.value["source"] ?? other.value) === (this.value["source"] ?? this.value) &&
this.invert == other.invert this.invert == other.invert
) { ) {
// Values (and inverts) match // Values (and inverts) match

View file

@ -2,6 +2,7 @@ import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl" import { ExpressionSpecification } from "maplibre-gl"
import { RegexTag } from "./RegexTag"
export class Tag extends TagsFilter { export class Tag extends TagsFilter {
public key: string public key: string
@ -122,6 +123,7 @@ export class Tag extends TagsFilter {
/** /**
* *
* import {RegexTag} from "./RegexTag"; * import {RegexTag} from "./RegexTag";
* import {And} from "./And";
* *
* // should handle advanced regexes * // should handle advanced regexes
* new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
@ -131,14 +133,20 @@ export class Tag extends TagsFilter {
* new Tag("key","value").shadows(new RegexTag("key", "value", true)) // => false * 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", true)) // => false
* new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
* new Tag("key","value").shadows(new And([new Tag("x","y"), new RegexTag("a","b", true)]) // => false
*/ */
shadows(other: TagsFilter): boolean { shadows(other: TagsFilter): boolean {
if (other["key"] !== undefined) { if ((other["key"] !== this.key)) {
if (other["key"] !== this.key) { return false
return false
}
} }
return other.matchesProperties({ [this.key]: this.value }) if(other instanceof Tag){
// Other.key === this.key
return other.value === this.value
}
if(other instanceof RegexTag){
return other.matchesProperties({[this.key]: this.value})
}
return false
} }
usedKeys(): string[] { usedKeys(): string[] {