diff --git a/src/Logic/Tags/And.ts b/src/Logic/Tags/And.ts index 73aba302f..0085198ac 100644 --- a/src/Logic/Tags/And.ts +++ b/src/Logic/Tags/And.ts @@ -6,6 +6,7 @@ import { RegexTag } from "./RegexTag" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" import ComparingTag from "./ComparingTag" +import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes" export class And extends TagsFilter { public and: TagsFilter[] @@ -15,6 +16,8 @@ export class And extends TagsFilter { this.and = and } + public static construct(and: TagsFilter[]): TagsFilter + public static construct(and: (FlatTag | (Or & OptimizedTag))[]): TagsFilterClosed & OptimizedTag public static construct(and: TagsFilter[]): TagsFilter { if (and.length === 1) { return and[0] @@ -175,6 +178,10 @@ export class And extends TagsFilter { * 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 * + * @return only phrases that should be kept. + * @param knownExpression The expression which is known in the subexpression and for which calculations can be done + * @param value the given knownExpression is considered to have this value, namely 'true' or '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") @@ -187,13 +194,14 @@ export class And extends TagsFilter { * const expr = TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr */ - removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { + removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean { const newAnds: TagsFilter[] = [] for (const tag of this.and) { if (tag instanceof And) { - throw "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: "+this.asHumanString() + throw "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " + this.asHumanString() } if (tag instanceof Or) { + // Second try const r = tag.removePhraseConsideredKnown(knownExpression, value) if (r === true) { continue @@ -232,7 +240,7 @@ export class And extends TagsFilter { if (newAnds.length === 0) { return true } - return And.construct(newAnds) + return And.construct(newAnds).optimize() } /** @@ -265,7 +273,7 @@ export class And extends TagsFilter { * const parsed = TagUtils.Tag(orig) * parsed.optimize().asJson() // => orig */ - optimize(): TagsFilter | boolean { + optimize(): (TagsFilterClosed & OptimizedTag) | boolean { if (this.and.length === 0) { return true } @@ -276,7 +284,7 @@ export class And extends TagsFilter { // We have an AND with a contained false: this is always 'false' return false } - const optimized = optimizedRaw + const optimized = <(TagsFilterClosed & OptimizedTag)[]>optimizedRaw for (let i = 0; i < optimized.length; i++) { for (let j = i + 1; j < optimized.length; j++) { @@ -299,7 +307,7 @@ export class And extends TagsFilter { } { - // Conflicting keys do return false + // Conflicting keys do return false. We build a 'known' set and check for conflicts const properties: Record = {} for (const opt of optimized) { if (opt instanceof Tag) { @@ -366,12 +374,12 @@ export class And extends TagsFilter { } } - const newAnds: TagsFilter[] = [] + const newAnds: (FlatTag | (Or & OptimizedTag))[] = [] + let containedOrs: (Or & OptimizedTag)[] = [] - let containedOrs: Or[] = [] for (const tf of optimized) { if (tf instanceof And) { - newAnds.push(...tf.and) + newAnds.push(...TagTypes.safeAnd(tf)) } else if (tf instanceof Or) { containedOrs.push(tf) } else { @@ -382,10 +390,10 @@ export class And extends TagsFilter { { let dirty = false do { - const cleanedContainedOrs: Or[] = [] + const cleanedContainedOrs: (Or & OptimizedTag)[] = [] outer: for (let containedOr of containedOrs) { for (const known of newAnds) { - // input for optimazation: (K=V & (X=Y | K=V)) + // input for optimization: (K=V & (X=Y | K=V)) // containedOr: (X=Y | K=V) // newAnds (and thus known): (K=V) --> true const cleaned = containedOr.removePhraseConsideredKnown(known, true) @@ -401,11 +409,17 @@ export class And extends TagsFilter { containedOr = cleaned continue } + if (cleaned instanceof And) { + // An optimized 'And' should not contain 'Ands', we can safely cast + newAnds.push(...TagTypes.safeAnd(cleaned)) + continue + } // the 'or' dissolved into a normal tag -> it has to be added to the newAnds newAnds.push(cleaned) dirty = true // rerun this algo later on continue outer } + cleanedContainedOrs.push(containedOr) } containedOrs = cleanedContainedOrs @@ -433,9 +447,9 @@ export class And extends TagsFilter { if (commonValues.length === 0) { newAnds.push(...containedOrs) } else { - const newOrs: TagsFilter[] = [] + const newOrs: TagsFilterClosed[] = [] for (const containedOr of containedOrs) { - const elements = containedOr.or.filter( + const elements: (FlatTag | (And & OptimizedTag))[] = TagTypes.safeOr( containedOr).filter( (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) ) if (elements.length > 0) { @@ -450,6 +464,8 @@ export class And extends TagsFilter { return false } else if (result === true) { // neutral element: skip + }else if(result instanceof And) { + newAnds.push(...TagTypes.safeAnd(result)) } else { newAnds.push(result) } diff --git a/src/Logic/Tags/ComparingTag.ts b/src/Logic/Tags/ComparingTag.ts index 88f36e09e..2dc82143b 100644 --- a/src/Logic/Tags/ComparingTag.ts +++ b/src/Logic/Tags/ComparingTag.ts @@ -1,6 +1,7 @@ import { TagsFilter } from "./TagsFilter" import { Tag } from "./Tag" import { ExpressionSpecification } from "maplibre-gl" +import { OptimizedTag } from "./TagTypes" export default class ComparingTag extends TagsFilter { public readonly key: string @@ -121,8 +122,8 @@ export default class ComparingTag extends TagsFilter { return this.key + this._representation + this._boundary } - optimize(): TagsFilter | boolean { - return this + optimize(): (ComparingTag & OptimizedTag) | boolean { + return this } isNegative(): boolean { diff --git a/src/Logic/Tags/Or.ts b/src/Logic/Tags/Or.ts index fa4b01afd..83b422005 100644 --- a/src/Logic/Tags/Or.ts +++ b/src/Logic/Tags/Or.ts @@ -3,6 +3,11 @@ import { TagUtils } from "./TagUtils" import { And } from "./And" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" +import { Tag } from "./Tag" +import { RegexTag } from "./RegexTag" +import SubstitutingTag from "./SubstitutingTag" +import ComparingTag from "./ComparingTag" +import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes" export class Or extends TagsFilter { public or: TagsFilter[] @@ -12,6 +17,9 @@ export class Or extends TagsFilter { this.or = or } + public static construct(or: TagsFilter[]): TagsFilter + public static construct(or: [T]): T + public static construct(or: ((And & OptimizedTag) | FlatTag)[]): (TagsFilterClosed & OptimizedTag) public static construct(or: TagsFilter[]): TagsFilter { if (or.length === 1) { return or[0] @@ -59,7 +67,7 @@ export class Or extends TagsFilter { return this.or .map((t) => { let e = t.asHumanString(linkToWiki, shorten, properties) - if (t["and"]) { + if (t["and"] || t["or"]) { e = "(" + e + ")" } return e @@ -115,13 +123,13 @@ export class Or extends TagsFilter { * 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")]) + * new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")]).removePhraseConsideredKnown(new Tag("foo","bar"), false) // => new Or([new RegexTag("c", "d"), new RegexTag("x", "y", true)]) */ - removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { + removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean { const newOrs: TagsFilter[] = [] for (const tag of this.or) { if (tag instanceof Or) { - throw "Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: "+this.asHumanString(false, false, {}) + throw "Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: " + this.asHumanString() } if (tag instanceof And) { const r = tag.removePhraseConsideredKnown(knownExpression, value) @@ -160,7 +168,7 @@ export class Or extends TagsFilter { if (newOrs.length === 0) { return false } - return Or.construct(newOrs) + return Or.construct(newOrs).optimize() } /** @@ -169,7 +177,7 @@ export class Or extends TagsFilter { * parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]} * */ - optimize(): TagsFilter | boolean { + optimize(): (TagsFilterClosed & OptimizedTag) | boolean { if (this.or.length === 0) { return false } @@ -181,26 +189,28 @@ export class Or extends TagsFilter { // We have an OR with a contained true: this is always 'true' return true } - const optimized = optimizedRaw + const optimized = optimizedRaw - const newOrs: TagsFilter[] = [] - let containedAnds: And[] = [] + const newOrs: ((And & OptimizedTag) | FlatTag)[] = [] + let containedAnds: (And & OptimizedTag)[] = [] for (const tf of optimized) { - if (tf["or"]) { + if (tf instanceof Or) { // expand all the nested ors... - newOrs.push(...tf["or"]) + for (const clauseOr of TagTypes.safeOr(tf)) { + newOrs.push(clauseOr) + } } else if (tf instanceof And) { // partition of all the ands containedAnds.push(tf) } else { - newOrs.push(tf) + newOrs.push(<(Tag | (And & OptimizedTag) | RegexTag | SubstitutingTag | ComparingTag)>tf) } } { let dirty = false do { - const cleanedContainedANds: And[] = [] + const cleanedContainedANds: (And & OptimizedTag)[] = [] outer: for (let containedAnd of containedAnds) { for (const known of newOrs) { // input for optimization: (K=V | (X=Y & K=V)) @@ -219,8 +229,16 @@ export class Or extends TagsFilter { containedAnd = cleaned continue // clean up with the other known values } + + if(cleaned instanceof Or){ + // An optimized 'or' should not contain 'ors', we can safely cast + newOrs.push(...TagTypes.safeOr(cleaned)) + continue + } + + const noAnd: OptimizedTag & (Tag | RegexTag | SubstitutingTag | ComparingTag) = cleaned // the 'and' dissolved into a normal tag -> it has to be added to the newOrs - newOrs.push(cleaned) + newOrs.push(noAnd) dirty = true // rerun this algo later on continue outer } @@ -243,16 +261,16 @@ export class Or extends TagsFilter { if (commonValues.length === 0) { newOrs.push(...containedAnds) } else { - const newAnds: TagsFilter[] = [] + const newAnds: TagsFilterClosed[] = [] for (const containedAnd of containedAnds) { - const elements = containedAnd.and.filter( + const elements: (FlatTag | (Or & OptimizedTag))[] = TagTypes.safeAnd(containedAnd).filter( (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) ) - if (elements.length == 0) { - continue + if (elements.length > 0) { + newAnds.push(And.construct(elements)) } - newAnds.push(And.construct(elements)) } + if (newAnds.length > 0) { commonValues.push(Or.construct(newAnds)) } @@ -262,9 +280,10 @@ export class Or extends TagsFilter { return true } else if (result === false) { // neutral element: skip - } else if (commonValues.length > 0) { - newOrs.push(And.construct(commonValues)) - } + } else if(result instanceof Or){ + newOrs.push(...TagTypes.safeOr(result)) + }else + newOrs.push(result) } } diff --git a/src/Logic/Tags/RegexTag.ts b/src/Logic/Tags/RegexTag.ts index 549266be6..4a06bd364 100644 --- a/src/Logic/Tags/RegexTag.ts +++ b/src/Logic/Tags/RegexTag.ts @@ -2,6 +2,7 @@ import { Tag } from "./Tag" import { TagsFilter } from "./TagsFilter" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" +import { OptimizedTag } from "./TagTypes" export class RegexTag extends TagsFilter { public readonly key: RegExp | string @@ -354,8 +355,8 @@ export class RegexTag extends TagsFilter { return [] } - optimize(): TagsFilter | boolean { - return this + optimize(): (RegexTag & OptimizedTag) | boolean { + return this } isNegative(): boolean { diff --git a/src/Logic/Tags/SubstitutingTag.ts b/src/Logic/Tags/SubstitutingTag.ts index 492243720..d8e126b79 100644 --- a/src/Logic/Tags/SubstitutingTag.ts +++ b/src/Logic/Tags/SubstitutingTag.ts @@ -3,6 +3,7 @@ import { Tag } from "./Tag" import { Utils } from "../../Utils" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" +import { OptimizedTag } from "./TagTypes" /** * The substituting-tag uses the tags of a feature a variables and replaces them. @@ -111,8 +112,8 @@ export default class SubstitutingTag extends TagsFilter { return [{ k: this._key, v: v }] } - optimize(): TagsFilter | boolean { - return this + optimize(): (SubstitutingTag & OptimizedTag) | boolean { + return this } isNegative(): boolean { diff --git a/src/Logic/Tags/Tag.ts b/src/Logic/Tags/Tag.ts index 92664080b..e8439b267 100644 --- a/src/Logic/Tags/Tag.ts +++ b/src/Logic/Tags/Tag.ts @@ -3,6 +3,7 @@ import { TagsFilter } from "./TagsFilter" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" import { RegexTag } from "./RegexTag" +import { OptimizedTag } from "./TagTypes" export class Tag extends TagsFilter { public key: string @@ -66,7 +67,7 @@ export class Tag extends TagsFilter { asOverpass(): string[] { if (this.value === "") { // NOT having this key - return ['[!"' + this.key + '"]'] + return ["[!\"" + this.key + "\"]"] } return [`["${this.key}"="${this.value}"]`] } @@ -164,8 +165,8 @@ export class Tag extends TagsFilter { return [{ k: this.key, v: this.value }] } - optimize(): TagsFilter | boolean { - return this + optimize(): (Tag & OptimizedTag) | boolean { + return this } isNegative(): boolean { diff --git a/src/Logic/Tags/TagTypes.ts b/src/Logic/Tags/TagTypes.ts new file mode 100644 index 000000000..a0752302a --- /dev/null +++ b/src/Logic/Tags/TagTypes.ts @@ -0,0 +1,35 @@ +import { TagsFilter } from "./TagsFilter" +import { Tag } from "./Tag" +import SubstitutingTag from "./SubstitutingTag" +import { And } from "./And" +import { RegexTag } from "./RegexTag" +import ComparingTag from "./ComparingTag" +import { Or } from "./Or" + +declare const __is_optimized: unique symbol +type Brand = { [__is_optimized]: B } +/** + * A marker class, no actual content + */ +export type OptimizedTag = Brand + + +export type UploadableTag = Tag | SubstitutingTag | And +/** + * Not nested + */ +export type FlatTag = Tag | RegexTag | SubstitutingTag | ComparingTag +export type TagsFilterClosed = FlatTag | And | Or + + +export class TagTypes { + + static safeAnd(and: And & OptimizedTag): ((FlatTag | (Or & OptimizedTag)) & OptimizedTag)[]{ + return and.and + } + + static safeOr(or: Or & OptimizedTag): ((FlatTag | (And & OptimizedTag)) & OptimizedTag)[]{ + return or.or + } + +} diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts index d175f1fb1..670a8bd3e 100644 --- a/src/Logic/Tags/TagUtils.ts +++ b/src/Logic/Tags/TagUtils.ts @@ -10,9 +10,9 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import key_counts from "../../assets/key_totals.json" import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext" +import { TagsFilterClosed, UploadableTag } from "./TagTypes" type Tags = Record -export type UploadableTag = Tag | SubstitutingTag | And export class TagUtils { public static readonly comparators: ReadonlyArray< @@ -476,7 +476,7 @@ export class TagUtils { * regex.matchesProperties({maxspeed: "50 mph"}) // => true */ - public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilter { + public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilterClosed { try { const ctx = typeof context === "string" ? context : context.path.join(".") return this.ParseTagUnsafe(json, ctx) @@ -712,7 +712,7 @@ export class TagUtils { return Utils.NoNull(spec) } - private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { + private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed { if (json === undefined) { throw new Error( `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression` diff --git a/src/Logic/Tags/TagsFilter.ts b/src/Logic/Tags/TagsFilter.ts index 5c6070755..48c88dbbb 100644 --- a/src/Logic/Tags/TagsFilter.ts +++ b/src/Logic/Tags/TagsFilter.ts @@ -1,5 +1,6 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { ExpressionSpecification } from "maplibre-gl" +import { OptimizedTag, TagsFilterClosed } from "./TagTypes" export abstract class TagsFilter { abstract asOverpass(): string[] @@ -50,7 +51,7 @@ export abstract class TagsFilter { /** * Returns an optimized version (or self) of this tagsFilter */ - abstract optimize(): TagsFilter | boolean + abstract optimize(): (OptimizedTag & TagsFilterClosed) | boolean /** * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). diff --git a/test/Logic/Tags/OptimizeTags.spec.ts b/test/Logic/Tags/OptimizeTags.spec.ts index 2dd4572ff..54c8f66d8 100644 --- a/test/Logic/Tags/OptimizeTags.spec.ts +++ b/test/Logic/Tags/OptimizeTags.spec.ts @@ -31,7 +31,7 @@ describe("Tag optimalization", () => { 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")]), + new Or([new Tag("x", "y"), new Tag("c", "d")]) ]) const opt = t.optimize() expect(TagUtils.toString(opt)).toBe("foo=bar& (x=y| (a=b&c=d) )") @@ -42,7 +42,7 @@ describe("Tag optimalization", () => { 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")]), + new Or([new RegexTag("x", "y"), new RegexTag("c", "d")]) ]) const opt = t.optimize() expect(TagUtils.toString(opt)).toBe("foo=bar& ( (a=b&c=d) |x=y)") @@ -53,7 +53,7 @@ describe("Tag optimalization", () => { 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")]), + new Or([new RegexTag("x", "y", true), new RegexTag("c", "d")]) ]) const opt = t.optimize() expect(TagUtils.toString(opt)).toBe("foo=bar& (a=b|x=y) & (c=d|x!=y)") @@ -86,12 +86,12 @@ describe("Tag optimalization", () => { { and: [ { - or: ["X=Y", "FOO=BAR"], + or: ["X=Y", "FOO=BAR"] }, - "bicycle=yes", - ], - }, - ], + "bicycle=yes" + ] + } + ] }) // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) ) // This is equivalent to (X=Y | FOO=BAR) @@ -109,11 +109,11 @@ describe("Tag optimalization", () => { "amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", - "construction:amenity=charging_station", - ], + "construction:amenity=charging_station" + ] }, - "bicycle=yes", - ], + "bicycle=yes" + ] }, { and: [ @@ -122,19 +122,19 @@ describe("Tag optimalization", () => { "amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", - "construction:amenity=charging_station", - ], - }, - ], + "construction:amenity=charging_station" + ] + } + ] }, "amenity=toilets", "amenity=bench", "leisure=picnic_table", { - and: ["tower:type=observation"], + and: ["tower:type=observation"] }, { - and: ["amenity=bicycle_repair_station"], + and: ["amenity=bicycle_repair_station"] }, { and: [ @@ -143,16 +143,16 @@ describe("Tag optimalization", () => { "amenity=bicycle_rental", "bicycle_rental~*", "service:bicycle:rental=yes", - "rental~.*bicycle.*", - ], + "rental~.*bicycle.*" + ] }, - "bicycle_rental!=docking_station", - ], + "bicycle_rental!=docking_station" + ] }, { - and: ["leisure=playground", "playground!=forest"], - }, - ], + and: ["leisure=playground", "playground!=forest"] + } + ] }) const opt = filter.optimize() const expected = [ @@ -166,7 +166,7 @@ describe("Tag optimalization", () => { "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", + "leisure=playground&playground!=forest" ] expect((opt).or.map((f) => TagUtils.toString(f))).toEqual(expected) @@ -187,7 +187,7 @@ describe("Tag optimalization", () => { 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")]), + new And([new Tag("foo", "bar"), new Tag("x", "y")]) ]) const opt = t.optimize() expect(TagUtils.toString(opt)).toBe("foo=bar") @@ -212,14 +212,14 @@ describe("Tag optimalization", () => { and: [ "sport=climbing", { - or: ["office~*", "club~*"], - }, - ], - }, - ], + or: ["office~*", "club~*"] + } + ] + } + ] }) const gym_tags = TagUtils.Tag({ - and: ["sport=climbing", "leisure=sports_centre"], + and: ["sport=climbing", "leisure=sports_centre"] }) const other_climbing = TagUtils.Tag({ and: [ @@ -227,8 +227,8 @@ describe("Tag optimalization", () => { "climbing!~route", "leisure!~sports_centre", "climbing!=route_top", - "climbing!=route_bottom", - ], + "climbing!=route_bottom" + ] }) const together = new Or([club_tags, gym_tags, other_climbing]) const opt = together.optimize() @@ -270,7 +270,7 @@ describe("Tag optimalization", () => { or: [ "club=climbing", { - and: ["sport=climbing", { or: ["club~*", "office~*"] }], + and: ["sport=climbing", { or: ["club~*", "office~*"] }] }, { and: [ @@ -283,15 +283,70 @@ describe("Tag optimalization", () => { "climbing!~route", "climbing!=route_top", "climbing!=route_bottom", - "leisure!~sports_centre", - ], - }, - ], - }, - ], - }, - ], + "leisure!~sports_centre" + ] + } + ] + } + ] + } + ] }) ) }) + + it("should optimize a complicated nested case", () => { + const spec = { + "and": + ["service:bicycle:retail=yes", + { + "or": [ + { + "and": [ + { "or": ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] }, + { + "or": ["service:bicycle:repair=yes", "shop=bicycle", + { + "and": ["shop=sports", + { "or": ["sport=bicycle", "sport=cycling", "sport="] }, + "service:bicycle:retail!=no", + "service:bicycle:repair!=no"] + }] + }] + }, { + "and": + [ + { + "or": + ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] + }, + { + "or": ["service:bicycle:repair=yes", "shop=bicycle", + { + "and": ["shop=sports", + { "or": ["sport=bicycle", "sport=cycling", "sport="] }, + "service:bicycle:retail!=no", "service:bicycle:repair!=no"] + }] + }] + }, { + "and": + [{ + "or": + ["craft=shoe_repair", "craft=key_cutter", "shop~.+"] + }, + { "or": ["shop=outdoor", "shop=sport", "shop=diy", "shop=doityourself"] }, + "shop!=mall"] + }, + "service:bicycle:retail~.+", + "service:bicycle:retail~.+"] + }] + } + + const tag = TagUtils.Tag(spec) + const opt = tag.optimize() + if (opt === false || opt === true) { + throw "Did not expect a boolean" + } + console.log(opt) + }) }) diff --git a/test/Logic/Tags/TagUtils.spec.ts b/test/Logic/Tags/TagUtils.spec.ts index a6e05e5a9..798783949 100644 --- a/test/Logic/Tags/TagUtils.spec.ts +++ b/test/Logic/Tags/TagUtils.spec.ts @@ -9,7 +9,7 @@ describe("TagUtils", () => { }) it("should handle compare tag <=5", () => { - let compare = TagUtils.Tag("key<=5") + const compare = TagUtils.Tag("key<=5") equal(compare.matchesProperties({ key: undefined }), false) equal(compare.matchesProperties({ key: "6" }), false) equal(compare.matchesProperties({ key: "5" }), true)