forked from MapComplete/MapComplete
Optimize queries to overpass
This commit is contained in:
parent
fbcb72df7a
commit
9008e333ac
15 changed files with 787 additions and 18 deletions
|
@ -25,7 +25,12 @@ export class Overpass {
|
|||
includeMeta = true) {
|
||||
this._timeout = timeout;
|
||||
this._interpreterUrl = interpreterUrl;
|
||||
this._filter = filter
|
||||
const optimized = filter.optimize()
|
||||
if(optimized === true || optimized === false){
|
||||
throw "Invalid filter: optimizes to true of false"
|
||||
}
|
||||
this._filter = optimized
|
||||
console.log("Overpass filter is",this._filter)
|
||||
this._extraScripts = extraScripts;
|
||||
this._includeMeta = includeMeta;
|
||||
this._relationTracker = relationTracker
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import {TagsFilter} from "./TagsFilter";
|
||||
import {Or} from "./Or";
|
||||
import {TagUtils} from "./TagUtils";
|
||||
|
||||
export class And extends TagsFilter {
|
||||
public and: TagsFilter[]
|
||||
|
@ -109,6 +111,10 @@ export class And extends TagsFilter {
|
|||
usedKeys(): string[] {
|
||||
return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [].concat(...this.and.map(subkeys => subkeys.usedTags()));
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
const result = []
|
||||
|
@ -123,4 +129,89 @@ export class And extends TagsFilter {
|
|||
and: this.and.map(a => a.AsJson())
|
||||
}
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
if(this.and.length === 0){
|
||||
return true
|
||||
}
|
||||
const optimized = this.and.map(t => t.optimize())
|
||||
|
||||
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){
|
||||
containedOrs.push(tf)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
// Extract common keys from the OR
|
||||
if(containedOrs.length === 1){
|
||||
newAnds.push(containedOrs[0])
|
||||
}
|
||||
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)))
|
||||
}
|
||||
if(commonValues.length === 0){
|
||||
newAnds.push(...containedOrs)
|
||||
}else{
|
||||
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)
|
||||
}
|
||||
|
||||
commonValues.push(new And(newOrs))
|
||||
const result = new Or(commonValues).optimize()
|
||||
if(result === false){
|
||||
return false
|
||||
}else if(result === true){
|
||||
// neutral element: skip
|
||||
}else{
|
||||
newAnds.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newAnds.length === 1){
|
||||
return newAnds[0]
|
||||
}
|
||||
TagUtils.sortFilters(newAnds, true)
|
||||
|
||||
return new And(newAnds)
|
||||
}
|
||||
}
|
|
@ -38,9 +38,16 @@ export default class ComparingTag implements TagsFilter {
|
|||
usedKeys(): string[] {
|
||||
return [this._key];
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
AsJson() {
|
||||
return this.asHumanString(false, false, {})
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import {TagsFilter} from "./TagsFilter";
|
||||
import {TagUtils} from "./TagUtils";
|
||||
import {And} from "./And";
|
||||
|
||||
|
||||
export class Or extends TagsFilter {
|
||||
|
@ -58,6 +60,10 @@ export class Or extends TagsFilter {
|
|||
return [].concat(...this.or.map(subkeys => subkeys.usedKeys()));
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [].concat(...this.or.map(subkeys => subkeys.usedTags()));
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
const result = []
|
||||
for (const tagsFilter of this.or) {
|
||||
|
@ -71,6 +77,83 @@ export class Or extends TagsFilter {
|
|||
or: this.or.map(o => o.AsJson())
|
||||
}
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
|
||||
if(this.or.length === 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
const optimized = this.or.map(t => t.optimize())
|
||||
|
||||
const newOrs : TagsFilter[] = []
|
||||
|
||||
let containedAnds : And[] = []
|
||||
for (const tf of optimized) {
|
||||
if(tf === true){
|
||||
return true
|
||||
}
|
||||
if(tf === false){
|
||||
continue
|
||||
}
|
||||
|
||||
if(tf instanceof Or){
|
||||
newOrs.push(...tf.or)
|
||||
}else if(tf instanceof And){
|
||||
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
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
// Extract common keys from the ANDS
|
||||
if(containedAnds.length === 1){
|
||||
newOrs.push(containedAnds[0])
|
||||
} else if(containedAnds.length > 1){
|
||||
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)))
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
commonValues.push(new Or(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newOrs.length === 1){
|
||||
return newOrs[0]
|
||||
}
|
||||
TagUtils.sortFilters(newOrs, false)
|
||||
|
||||
return new Or(newOrs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -43,10 +43,24 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
|
||||
asOverpass(): string[] {
|
||||
if (typeof this.key === "string") {
|
||||
return [`["${this.key}"${this.invert ? "!" : ""}~"${RegexTag.source(this.value)}"]`];
|
||||
const inv =this.invert ? "!" : ""
|
||||
if (typeof this.key !== "string") {
|
||||
// The key is a regex too
|
||||
return [`[~"${this.key.source}"${inv}~"${RegexTag.source(this.value)}"]`];
|
||||
}
|
||||
return [`[~"${this.key.source}"${this.invert ? "!" : ""}~"${RegexTag.source(this.value)}"]`];
|
||||
|
||||
if(this.value instanceof RegExp){
|
||||
const src =this.value.source
|
||||
if(src === "^..*$"){
|
||||
// anything goes
|
||||
return [`[${inv}"${this.key}"]`]
|
||||
}
|
||||
return [`["${this.key}"${inv}~"${src}"]`]
|
||||
}else{
|
||||
// Normal key and normal value
|
||||
return [`["${this.key}"${inv}="${this.value}"]`];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isUsableAsAnswer(): boolean {
|
||||
|
@ -99,6 +113,10 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
throw "Key cannot be determined as it is a regex"
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
if (this.invert) {
|
||||
|
@ -120,4 +138,8 @@ export class RegexTag extends TagsFilter {
|
|||
AsJson() {
|
||||
return this.asHumanString()
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -59,6 +59,10 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
return [this._key];
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return []
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
if (this._invert) {
|
||||
throw "An inverted substituting tag can not be used to create a change"
|
||||
|
@ -73,4 +77,8 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
AsJson() {
|
||||
return this._key + (this._invert ? '!' : '') + "=" + this._value
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -80,6 +80,13 @@ export class Tag extends TagsFilter {
|
|||
return [this.key];
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
if(this.value == ""){
|
||||
return []
|
||||
}
|
||||
return [this]
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
return [{k: this.key, v: this.value}];
|
||||
}
|
||||
|
@ -87,4 +94,8 @@ export class Tag extends TagsFilter {
|
|||
AsJson() {
|
||||
return this.asHumanString(false, false)
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -8,8 +8,10 @@ import SubstitutingTag from "./SubstitutingTag";
|
|||
import {Or} from "./Or";
|
||||
import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import {isRegExp} from "util";
|
||||
import * as key_counts from "../../assets/key_totals.json"
|
||||
|
||||
export class TagUtils {
|
||||
private static keyCounts : {keys: any, tags: any} = key_counts["default"] ?? key_counts
|
||||
private static comparators
|
||||
: [string, (a: number, b: number) => boolean][]
|
||||
= [
|
||||
|
@ -174,6 +176,29 @@ export class TagUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INLINE sort of the given list
|
||||
*/
|
||||
public static sortFilters(filters: TagsFilter [], usePopularity: boolean): void {
|
||||
filters.sort((a,b) => TagUtils.order(a, b, usePopularity))
|
||||
}
|
||||
|
||||
public static toString(f: TagsFilter, toplevel = true): string {
|
||||
let r: string
|
||||
if (f instanceof Or) {
|
||||
r = TagUtils.joinL(f.or, "|", toplevel)
|
||||
} else if (f instanceof And) {
|
||||
r = TagUtils.joinL(f.and, "&", toplevel)
|
||||
} else {
|
||||
r = f.asHumanString(false, false, {})
|
||||
}
|
||||
if(toplevel){
|
||||
r = r.trim()
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
|
||||
|
||||
if (json === undefined) {
|
||||
|
@ -285,10 +310,11 @@ export class TagUtils {
|
|||
throw `Error while parsing tag '${tag}' in ${context}: no key part and value part were found`
|
||||
|
||||
}
|
||||
|
||||
if(json.and !== undefined && json.or !== undefined){
|
||||
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined`}
|
||||
|
||||
|
||||
if (json.and !== undefined && json.or !== undefined) {
|
||||
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined`
|
||||
}
|
||||
|
||||
if (json.and !== undefined) {
|
||||
return new And(json.and.map(t => TagUtils.Tag(t, context)));
|
||||
}
|
||||
|
@ -296,4 +322,56 @@ export class TagUtils {
|
|||
return new Or(json.or.map(t => TagUtils.Tag(t, context)));
|
||||
}
|
||||
}
|
||||
|
||||
private static GetCount(key: string, value?: string) {
|
||||
if(key === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const tag = TagUtils.keyCounts.tags[key]
|
||||
if(tag !== undefined && tag[value] !== undefined) {
|
||||
return tag[value]
|
||||
}
|
||||
return TagUtils.keyCounts.keys[key]
|
||||
}
|
||||
|
||||
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
|
||||
const rta = a instanceof RegexTag
|
||||
const rtb = b instanceof RegexTag
|
||||
if(rta !== rtb) {
|
||||
// Regex tags should always go at the end: these use a lot of computation at the overpass side, avoiding it is better
|
||||
if(rta) {
|
||||
return 1 // b < a
|
||||
}else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
if (a["key"] !== undefined && b["key"] !== undefined) {
|
||||
if(usePopularity) {
|
||||
const countA = TagUtils.GetCount(a["key"], a["value"])
|
||||
const countB = TagUtils.GetCount(b["key"], b["value"])
|
||||
if(countA !== undefined && countB !== undefined) {
|
||||
return countA - countB
|
||||
}
|
||||
}
|
||||
|
||||
if (a["key"] === b["key"]) {
|
||||
return 0
|
||||
}
|
||||
if (a["key"] < b["key"]) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) {
|
||||
const joined = tfs.map(e => TagUtils.toString(e, false)).join(seperator)
|
||||
if (toplevel) {
|
||||
return joined
|
||||
}
|
||||
return " (" + joined + ") "
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,12 @@ export abstract class TagsFilter {
|
|||
|
||||
abstract usedKeys(): string[];
|
||||
|
||||
/**
|
||||
* Returns all normal key/value pairs
|
||||
* Regex tags, substitutions, comparisons, ... are exempt
|
||||
*/
|
||||
abstract usedTags(): {key: string, value: string}[];
|
||||
|
||||
/**
|
||||
* Converts the tagsFilter into a list of key-values that should be uploaded to OSM.
|
||||
* Throws an error if not applicable.
|
||||
|
@ -21,4 +27,11 @@ export abstract class TagsFilter {
|
|||
abstract asChange(properties: any): { k: string, v: string }[]
|
||||
|
||||
abstract AsJson() ;
|
||||
|
||||
/**
|
||||
* Returns an optimized version (or self) of this tagsFilter
|
||||
*/
|
||||
abstract optimize(): TagsFilter | boolean;
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue