diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts
index b019ec3..402a91a 100644
--- a/Customizations/JSON/FromJSON.ts
+++ b/Customizations/JSON/FromJSON.ts
@@ -1,7 +1,11 @@
import {AndOrTagConfigJson} from "./TagConfigJson";
-import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
+import {Or} from "../../Logic/Or";
import {Utils} from "../../Utils";
+import {TagsFilter} from "../../Logic/TagsFilter";
+import {RegexTag} from "../../Logic/RegexTag";
+import {Tag} from "../../Logic/Tag";
+import {And} from "../../Logic/And";
export class FromJSON {
diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts
index 0a4b444..c6841db 100644
--- a/Customizations/JSON/LayerConfig.ts
+++ b/Customizations/JSON/LayerConfig.ts
@@ -1,6 +1,5 @@
import Translations from "../../UI/i18n/Translations";
import TagRenderingConfig from "./TagRenderingConfig";
-import {Tag, TagsFilter} from "../../Logic/Tags";
import {LayerConfigJson} from "./LayerConfigJson";
import {FromJSON} from "./FromJSON";
import SharedTagRenderings from "../SharedTagRenderings";
@@ -16,6 +15,8 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {UIElement} from "../../UI/UIElement";
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
import SourceConfig from "./SourceConfig";
+import {TagsFilter} from "../../Logic/TagsFilter";
+import {Tag} from "../../Logic/Tag";
export default class LayerConfig {
diff --git a/Customizations/JSON/SourceConfig.ts b/Customizations/JSON/SourceConfig.ts
index 22b5fab..80f4cb7 100644
--- a/Customizations/JSON/SourceConfig.ts
+++ b/Customizations/JSON/SourceConfig.ts
@@ -1,4 +1,4 @@
-import {TagsFilter} from "../../Logic/Tags";
+import {TagsFilter} from "../../Logic/TagsFilter";
export default class SourceConfig {
diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts
index 7455769..6340307 100644
--- a/Customizations/JSON/TagRenderingConfig.ts
+++ b/Customizations/JSON/TagRenderingConfig.ts
@@ -1,10 +1,12 @@
-import {And, TagsFilter, TagUtils} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {Translation} from "../../UI/i18n/Translation";
import {Utils} from "../../Utils";
+import {TagsFilter} from "../../Logic/TagsFilter";
+import {And} from "../../Logic/And";
+import {TagUtils} from "../../Logic/TagUtils";
/***
* The parsed version of TagRenderingConfigJSON
diff --git a/Logic/Actors/UpdateFromOverpass.ts b/Logic/Actors/UpdateFromOverpass.ts
index 8f2809b..c2ac6e8 100644
--- a/Logic/Actors/UpdateFromOverpass.ts
+++ b/Logic/Actors/UpdateFromOverpass.ts
@@ -1,11 +1,12 @@
import {UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc";
-import {Or, TagsFilter} from "../Tags";
+import {Or} from "../Or";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import {Overpass} from "../Osm/Overpass";
import Bounds from "../../Models/Bounds";
import FeatureSource from "../FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
+import {TagsFilter} from "../TagsFilter";
export default class UpdateFromOverpass implements FeatureSource {
diff --git a/Logic/And.ts b/Logic/And.ts
new file mode 100644
index 0000000..9ca6e27
--- /dev/null
+++ b/Logic/And.ts
@@ -0,0 +1,119 @@
+import {TagsFilter} from "./TagsFilter";
+
+export class And extends TagsFilter {
+ public and: TagsFilter[]
+
+ constructor(and: TagsFilter[]) {
+ super();
+ this.and = and;
+ }
+
+ private static combine(filter: string, choices: string[]): string[] {
+ const values = [];
+ for (const or of choices) {
+ values.push(filter + or);
+ }
+ return values;
+ }
+
+ matchesProperties(tags: any): boolean {
+ for (const tagsFilter of this.and) {
+ if (!tagsFilter.matchesProperties(tags)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ asOverpass(): string[] {
+ let allChoices: string[] = null;
+ for (const andElement of this.and) {
+ const andElementFilter = andElement.asOverpass();
+ if (allChoices === null) {
+ allChoices = andElementFilter;
+ continue;
+ }
+
+ const newChoices: string[] = [];
+ for (const choice of allChoices) {
+ newChoices.push(
+ ...And.combine(choice, andElementFilter)
+ )
+ }
+ allChoices = newChoices;
+ }
+ return allChoices;
+ }
+
+ substituteValues(tags: any): TagsFilter {
+ const newChoices = [];
+ for (const c of this.and) {
+ newChoices.push(c.substituteValues(tags));
+ }
+ return new And(newChoices);
+ }
+
+ asHumanString(linkToWiki: boolean, shorten: boolean) {
+ return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&");
+ }
+
+ isUsableAsAnswer(): boolean {
+ for (const t of this.and) {
+ if (!t.isUsableAsAnswer()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ isEquivalent(other: TagsFilter): boolean {
+ if (!(other instanceof And)) {
+ return false;
+ }
+
+ for (const selfTag of this.and) {
+ let matchFound = false;
+ for (let i = 0; i < other.and.length && !matchFound; i++) {
+ let otherTag = other.and[i];
+ matchFound = selfTag.isEquivalent(otherTag);
+ }
+ if (!matchFound) {
+ return false;
+ }
+ }
+
+ for (const selfTag of this.and) {
+ let matchFound = false;
+ for (const otherTag of other.and) {
+ matchFound = selfTag.isEquivalent(otherTag);
+ if (matchFound) {
+ break;
+ }
+ }
+ if (!matchFound) {
+ return false;
+ }
+ }
+
+ for (const otherTag of other.and) {
+ let matchFound = false;
+ for (const selfTag of this.and) {
+ matchFound = selfTag.isEquivalent(otherTag);
+ if (matchFound) {
+ break;
+ }
+ }
+ if (!matchFound) {
+ return false;
+ }
+ }
+
+
+ return true;
+ }
+
+ usedKeys(): string[] {
+ return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
+ }
+}
\ No newline at end of file
diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts
index c95c2ff..0b57f33 100644
--- a/Logic/MetaTagging.ts
+++ b/Logic/MetaTagging.ts
@@ -1,10 +1,12 @@
import {GeoOperations} from "./GeoOperations";
import State from "../State";
import opening_hours from "opening_hours";
-import {And, Or, Tag} from "./Tags";
+import {Or} from "./Or";
import {Utils} from "../Utils";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
+import {Tag} from "./Tag";
+import {And} from "./And";
class SimpleMetaTagger {
public readonly keys: string[];
diff --git a/Logic/Or.ts b/Logic/Or.ts
new file mode 100644
index 0000000..c05e2d1
--- /dev/null
+++ b/Logic/Or.ts
@@ -0,0 +1,72 @@
+import {TagsFilter} from "./TagsFilter";
+
+
+export class Or extends TagsFilter {
+ public or: TagsFilter[]
+
+ constructor(or: TagsFilter[]) {
+ super();
+ this.or = or;
+ }
+
+ matchesProperties(properties: any): boolean {
+ for (const tagsFilter of this.or) {
+ if (tagsFilter.matchesProperties(properties)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ asOverpass(): string[] {
+ const choices = [];
+ for (const tagsFilter of this.or) {
+ const subChoices = tagsFilter.asOverpass();
+ for (const subChoice of subChoices) {
+ choices.push(subChoice)
+ }
+ }
+ return choices;
+ }
+
+ substituteValues(tags: any): TagsFilter {
+ const newChoices = [];
+ for (const c of this.or) {
+ newChoices.push(c.substituteValues(tags));
+ }
+ return new Or(newChoices);
+ }
+
+ asHumanString(linkToWiki: boolean, shorten: boolean) {
+ return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|");
+ }
+
+ isUsableAsAnswer(): boolean {
+ return false;
+ }
+
+ isEquivalent(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);
+ }
+ if (!matchFound) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ usedKeys(): string[] {
+ return [].concat(...this.or.map(subkeys => subkeys.usedKeys()));
+ }
+}
+
+
diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts
index ebe2e85..b661629 100644
--- a/Logic/Osm/Changes.ts
+++ b/Logic/Osm/Changes.ts
@@ -1,10 +1,12 @@
import {OsmNode, OsmObject} from "./OsmObject";
-import {And, Tag, TagsFilter} from "../Tags";
import State from "../../State";
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
import Constants from "../../Models/Constants";
import FeatureSource from "../FeatureSource/FeatureSource";
+import {TagsFilter} from "../TagsFilter";
+import {Tag} from "../Tag";
+import {And} from "../And";
/**
* Handles all changes made to OSM.
diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts
index 7035329..c597d84 100644
--- a/Logic/Osm/Overpass.ts
+++ b/Logic/Osm/Overpass.ts
@@ -1,7 +1,7 @@
-import {TagsFilter} from "../Tags";
import * as $ from "jquery"
import * as OsmToGeoJson from "osmtogeojson";
import Bounds from "../../Models/Bounds";
+import {TagsFilter} from "../TagsFilter";
/**
* Interfaces overpass to get all the latest data
diff --git a/Logic/RegexTag.ts b/Logic/RegexTag.ts
new file mode 100644
index 0000000..35fdaa4
--- /dev/null
+++ b/Logic/RegexTag.ts
@@ -0,0 +1,85 @@
+import {Tag} from "./Tag";
+import {TagsFilter} from "./TagsFilter";
+
+export class RegexTag extends TagsFilter {
+ private readonly key: RegExp | string;
+ private readonly value: RegExp | string;
+ private readonly invert: boolean;
+ private readonly matchesEmpty: boolean
+
+ constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
+ super();
+ this.key = key;
+ this.value = value;
+ this.invert = invert;
+ this.matchesEmpty = RegexTag.doesMatch("", this.value);
+ }
+
+ private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
+ if (typeof possibleRegex === "string") {
+ return fromTag === possibleRegex;
+ }
+ return fromTag.match(possibleRegex) !== null;
+ }
+
+ private static source(r: string | RegExp) {
+ if (typeof (r) === "string") {
+ return r;
+ }
+ return r.source;
+ }
+
+ asOverpass(): string[] {
+ if (typeof this.key === "string") {
+ return [`['${this.key}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`];
+ }
+ return [`[~'${this.key.source}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`];
+ }
+
+ isUsableAsAnswer(): boolean {
+ return false;
+ }
+
+ matchesProperties(tags: any): boolean {
+ for (const key in tags) {
+ if (RegexTag.doesMatch(key, this.key)) {
+ const value = tags[key]
+ return RegexTag.doesMatch(value, this.value) != this.invert;
+ }
+ }
+ if (this.matchesEmpty) {
+ // The value is 'empty'
+ return !this.invert;
+ }
+ // The matching key was not found
+ return this.invert;
+ }
+
+ substituteValues(tags: any): TagsFilter {
+ return this;
+ }
+
+ asHumanString() {
+ if (typeof this.key === "string") {
+ return `${this.key}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}`;
+ }
+ return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}`
+ }
+
+ isEquivalent(other: TagsFilter): boolean {
+ if (other instanceof RegexTag) {
+ return other.asHumanString() == this.asHumanString();
+ }
+ if (other instanceof Tag) {
+ return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value);
+ }
+ return false;
+ }
+
+ usedKeys(): string[] {
+ if (typeof this.key === "string") {
+ return [this.key];
+ }
+ throw "Key cannot be determined as it is a regex"
+ }
+}
\ No newline at end of file
diff --git a/Logic/Tag.ts b/Logic/Tag.ts
new file mode 100644
index 0000000..3519d7e
--- /dev/null
+++ b/Logic/Tag.ts
@@ -0,0 +1,84 @@
+import {Utils} from "../Utils";
+import {RegexTag} from "./RegexTag";
+import {TagsFilter} from "./TagsFilter";
+import {TagUtils} from "./TagUtils";
+
+export class Tag extends TagsFilter {
+ public key: string
+ public value: string
+
+ constructor(key: string, value: string) {
+ super()
+ this.key = key
+ this.value = value
+ if (key === undefined || key === "") {
+ throw "Invalid key: undefined or empty";
+ }
+ if (value === undefined) {
+ throw "Invalid value: value is undefined";
+ }
+ if (value === "*") {
+ console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`)
+ }
+ }
+
+
+ matchesProperties(properties: any): boolean {
+ for (const propertiesKey in properties) {
+ if (this.key === propertiesKey) {
+ const value = properties[propertiesKey];
+ return value === this.value;
+ }
+ }
+ // The tag was not found
+ if (this.value === "") {
+ // and it shouldn't be found!
+ return true;
+ }
+
+ return false;
+ }
+
+ asOverpass(): string[] {
+ if (this.value === "") {
+ // NOT having this key
+ return ['[!"' + this.key + '"]'];
+ }
+ return [`["${this.key}"="${this.value}"]`];
+ }
+
+ substituteValues(tags: any) {
+ return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
+ }
+
+ asHumanString(linkToWiki: boolean, shorten: boolean) {
+ let v = this.value;
+ if (shorten) {
+ v = Utils.EllipsesAfter(v, 25);
+ }
+ if (linkToWiki) {
+ return `${this.key}` +
+ `=` +
+ `${v}`
+ }
+ return this.key + "=" + v;
+ }
+
+ isUsableAsAnswer(): boolean {
+ return true;
+ }
+
+ isEquivalent(other: TagsFilter): boolean {
+ if (other instanceof Tag) {
+ return this.key === other.key && this.value === other.value;
+ }
+ if (other instanceof RegexTag) {
+ other.isEquivalent(this);
+ }
+ return false;
+ }
+
+ usedKeys(): string[] {
+ return [this.key];
+ }
+}
\ No newline at end of file
diff --git a/Logic/TagUtils.ts b/Logic/TagUtils.ts
new file mode 100644
index 0000000..15c8901
--- /dev/null
+++ b/Logic/TagUtils.ts
@@ -0,0 +1,121 @@
+import {Tag} from "./Tag";
+import {TagsFilter} from "./TagsFilter";
+import {And} from "./And";
+import {Utils} from "../Utils";
+
+export class TagUtils {
+ static ApplyTemplate(template: string, tags: any): string {
+ for (const k in tags) {
+ while (template.indexOf("{" + k + "}") >= 0) {
+ const escaped = tags[k].replace(//g, '>');
+ template = template.replace("{" + k + "}", escaped);
+ }
+ }
+ return template;
+ }
+
+ static KVtoProperties(tags: Tag[]): any {
+ const properties = {};
+ for (const tag of tags) {
+ properties[tag.key] = tag.value
+ }
+ return properties;
+ }
+
+ /**
+ * Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags
+ */
+ static AllKeysAreContained(availableTags: any, neededTags: any) {
+ for (const neededKey in neededTags) {
+ const availableValues: string[] = availableTags[neededKey]
+ if (availableValues === undefined) {
+ return false;
+ }
+ const neededValues: string[] = neededTags[neededKey];
+ for (const neededValue of neededValues) {
+ if (availableValues.indexOf(neededValue) < 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /***
+ * Creates a hash {key --> [values]}, with all the values present in the tagsfilter
+ *
+ * @param tagsFilters
+ * @constructor
+ */
+ static SplitKeys(tagsFilters: TagsFilter[]) {
+ const keyValues = {} // Map string -> string[]
+ tagsFilters = [...tagsFilters] // copy all
+ while (tagsFilters.length > 0) {
+ // Queue
+ const tagsFilter = tagsFilters.shift();
+
+ if (tagsFilter === undefined) {
+ continue;
+ }
+
+ if (tagsFilter instanceof And) {
+ tagsFilters.push(...tagsFilter.and);
+ continue;
+ }
+
+ if (tagsFilter instanceof Tag) {
+ if (keyValues[tagsFilter.key] === undefined) {
+ keyValues[tagsFilter.key] = [];
+ }
+ keyValues[tagsFilter.key].push(...tagsFilter.value.split(";"));
+ continue;
+ }
+
+ console.error("Invalid type to flatten the multiAnswer", tagsFilter);
+ throw "Invalid type to FlattenMultiAnswer"
+ }
+ return keyValues;
+ }
+
+ /**
+ * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
+ * E.g:
+ *
+ * FlattenMultiAnswer([and: [ "x=a", "y=0;1"], and: ["x=b", "y=2"], and: ["x=", "y=3"]])
+ * will result in
+ * ["x=a;b", "y=0;1;2;3"]
+ *
+ * @param tagsFilters
+ * @constructor
+ */
+ static FlattenMultiAnswer(tagsFilters: TagsFilter[]): And {
+ if (tagsFilters === undefined) {
+ return new And([]);
+ }
+
+ let keyValues = TagUtils.SplitKeys(tagsFilters);
+ const and: TagsFilter[] = []
+ for (const key in keyValues) {
+ and.push(new Tag(key, Utils.Dedup(keyValues[key]).join(";")));
+ }
+ return new And(and);
+ }
+
+ static MatchesMultiAnswer(tag: TagsFilter, tags: any): boolean {
+ const splitted = TagUtils.SplitKeys([tag]);
+ for (const splitKey in splitted) {
+ const neededValues = splitted[splitKey];
+ if (tags[splitKey] === undefined) {
+ return false;
+ }
+
+ const actualValue = tags[splitKey].split(";");
+ for (const neededValue of neededValues) {
+ if (actualValue.indexOf(neededValue) < 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Logic/Tags.ts b/Logic/Tags.ts
deleted file mode 100644
index 7a36612..0000000
--- a/Logic/Tags.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-import {Utils} from "../Utils";
-
-export abstract class TagsFilter {
-
- abstract asOverpass(): string[]
-
- abstract substituteValues(tags: any): TagsFilter;
-
- abstract isUsableAsAnswer(): boolean;
-
- abstract isEquivalent(other: TagsFilter): boolean;
-
- abstract matchesProperties(properties: any): boolean;
-
- abstract asHumanString(linkToWiki: boolean, shorten: boolean);
-
- abstract usedKeys(): string[];
-
- public matches(tags: {k: string, v: string}[]){
- const properties = {};
- for (const kv of tags) {
- properties[kv.k] = kv.v;
- }
- return this.matchesProperties(properties);
- }
-}
-
-
-export class RegexTag extends TagsFilter {
- private readonly key: RegExp | string;
- private readonly value: RegExp | string;
- private readonly invert: boolean;
- private readonly matchesEmpty: boolean
-
- constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
- super();
- this.key = key;
- this.value = value;
- this.invert = invert;
- this.matchesEmpty = RegexTag.doesMatch("", this.value);
- }
-
- private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
- if (typeof possibleRegex === "string") {
- return fromTag === possibleRegex;
- }
- return fromTag.match(possibleRegex) !== null;
- }
-
- private static source(r: string | RegExp) {
- if (typeof (r) === "string") {
- return r;
- }
- return r.source;
- }
-
- asOverpass(): string[] {
- if (typeof this.key === "string") {
- return [`['${this.key}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`];
- }
- return [`[~'${this.key.source}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`];
- }
-
- isUsableAsAnswer(): boolean {
- return false;
- }
-
- matchesProperties(tags: any): boolean {
- for (const key in tags) {
- if (RegexTag.doesMatch(key, this.key)) {
- const value = tags[key]
- return RegexTag.doesMatch(value, this.value) != this.invert;
- }
- }
- if (this.matchesEmpty) {
- // The value is 'empty'
- return !this.invert;
- }
- // The matching key was not found
- return this.invert;
- }
-
- substituteValues(tags: any): TagsFilter {
- return this;
- }
-
- asHumanString() {
- if (typeof this.key === "string") {
- return `${this.key}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}`;
- }
- return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}`
- }
-
- isEquivalent(other: TagsFilter): boolean {
- if (other instanceof RegexTag) {
- return other.asHumanString() == this.asHumanString();
- }
- if (other instanceof Tag) {
- return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value);
- }
- return false;
- }
-
- usedKeys(): string[] {
- if (typeof this.key === "string") {
- return [this.key];
- }
- throw "Key cannot be determined as it is a regex"
- }
-}
-
-
-export class Tag extends TagsFilter {
- public key: string
- public value: string
-
- constructor(key: string, value: string) {
- super()
- this.key = key
- this.value = value
- if (key === undefined || key === "") {
- throw "Invalid key: undefined or empty";
- }
- if (value === undefined) {
- throw "Invalid value: value is undefined";
- }
- if (value === "*") {
- console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`)
- }
- }
-
-
- matchesProperties(properties: any): boolean {
- for (const propertiesKey in properties) {
- if(this.key === propertiesKey){
- const value = properties[propertiesKey];
- return value === this.value;
- }
- }
- // The tag was not found
- if (this.value === "") {
- // and it shouldn't be found!
- return true;
- }
-
- return false;
- }
-
- asOverpass(): string[] {
- if (this.value === "") {
- // NOT having this key
- return ['[!"' + this.key + '"]'];
- }
- return [`["${this.key}"="${this.value}"]`];
- }
-
- substituteValues(tags: any) {
- return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
- }
-
- asHumanString(linkToWiki: boolean, shorten: boolean) {
- let v = this.value;
- if (shorten) {
- v = Utils.EllipsesAfter(v, 25);
- }
- if (linkToWiki) {
- return `${this.key}` +
- `=` +
- `${v}`
- }
- return this.key + "=" + v;
- }
-
- isUsableAsAnswer(): boolean {
- return true;
- }
-
- isEquivalent(other: TagsFilter): boolean {
- if (other instanceof Tag) {
- return this.key === other.key && this.value === other.value;
- }
- if (other instanceof RegexTag) {
- other.isEquivalent(this);
- }
- return false;
- }
-
- usedKeys(): string[] {
- return [this.key];
- }
-}
-
-
-export class Or extends TagsFilter {
- public or: TagsFilter[]
-
- constructor(or: TagsFilter[]) {
- super();
- this.or = or;
- }
-
- matchesProperties(properties: any): boolean {
- for (const tagsFilter of this.or) {
- if (tagsFilter.matchesProperties(properties)) {
- return true;
- }
- }
-
- return false;
- }
-
- asOverpass(): string[] {
- const choices = [];
- for (const tagsFilter of this.or) {
- const subChoices = tagsFilter.asOverpass();
- for (const subChoice of subChoices) {
- choices.push(subChoice)
- }
- }
- return choices;
- }
-
- substituteValues(tags: any): TagsFilter {
- const newChoices = [];
- for (const c of this.or) {
- newChoices.push(c.substituteValues(tags));
- }
- return new Or(newChoices);
- }
-
- asHumanString(linkToWiki: boolean, shorten: boolean) {
- return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|");
- }
-
- isUsableAsAnswer(): boolean {
- return false;
- }
-
- isEquivalent(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);
- }
- if (!matchFound) {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- usedKeys(): string[] {
- return [].concat(...this.or.map(subkeys => subkeys.usedKeys()));
- }
-}
-
-
-export class And extends TagsFilter {
- public and: TagsFilter[]
-
- constructor(and: TagsFilter[]) {
- super();
- this.and = and;
- }
-
- private static combine(filter: string, choices: string[]): string[] {
- const values = [];
- for (const or of choices) {
- values.push(filter + or);
- }
- return values;
- }
-
- matchesProperties(tags: any): boolean {
- for (const tagsFilter of this.and) {
- if (!tagsFilter.matchesProperties(tags)) {
- return false;
- }
- }
-
- return true;
- }
-
- asOverpass(): string[] {
- let allChoices: string[] = null;
- for (const andElement of this.and) {
- const andElementFilter = andElement.asOverpass();
- if (allChoices === null) {
- allChoices = andElementFilter;
- continue;
- }
-
- const newChoices: string[] = [];
- for (const choice of allChoices) {
- newChoices.push(
- ...And.combine(choice, andElementFilter)
- )
- }
- allChoices = newChoices;
- }
- return allChoices;
- }
-
- substituteValues(tags: any): TagsFilter {
- const newChoices = [];
- for (const c of this.and) {
- newChoices.push(c.substituteValues(tags));
- }
- return new And(newChoices);
- }
-
- asHumanString(linkToWiki: boolean, shorten: boolean) {
- return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&");
- }
-
- isUsableAsAnswer(): boolean {
- for (const t of this.and) {
- if (!t.isUsableAsAnswer()) {
- return false;
- }
- }
- return true;
- }
-
- isEquivalent(other: TagsFilter): boolean {
- if (!(other instanceof And)) {
- return false;
- }
-
- for (const selfTag of this.and) {
- let matchFound = false;
- for (let i = 0; i < other.and.length && !matchFound; i++) {
- let otherTag = other.and[i];
- matchFound = selfTag.isEquivalent(otherTag);
- }
- if (!matchFound) {
- return false;
- }
- }
-
- for (const selfTag of this.and) {
- let matchFound = false;
- for (const otherTag of other.and) {
- matchFound = selfTag.isEquivalent(otherTag);
- if (matchFound) {
- break;
- }
- }
- if (!matchFound) {
- return false;
- }
- }
-
- for (const otherTag of other.and) {
- let matchFound = false;
- for (const selfTag of this.and) {
- matchFound = selfTag.isEquivalent(otherTag);
- if (matchFound) {
- break;
- }
- }
- if (!matchFound) {
- return false;
- }
- }
-
-
- return true;
- }
-
- usedKeys(): string[] {
- return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
- }
-}
-
-
-export class TagUtils {
- static ApplyTemplate(template: string, tags: any): string {
- for (const k in tags) {
- while (template.indexOf("{" + k + "}") >= 0) {
- const escaped = tags[k].replace(//g, '>');
- template = template.replace("{" + k + "}", escaped);
- }
- }
- return template;
- }
-
- static KVtoProperties(tags: Tag[]): any {
- const properties = {};
- for (const tag of tags) {
- properties[tag.key] = tag.value
- }
- return properties;
- }
-
- /**
- * Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags
- */
- static AllKeysAreContained(availableTags: any, neededTags: any) {
- for (const neededKey in neededTags) {
- const availableValues: string[] = availableTags[neededKey]
- if (availableValues === undefined) {
- return false;
- }
- const neededValues: string[] = neededTags[neededKey];
- for (const neededValue of neededValues) {
- if (availableValues.indexOf(neededValue) < 0) {
- return false;
- }
- }
- }
- return true;
- }
-
- /***
- * Creates a hash {key --> [values]}, with all the values present in the tagsfilter
- *
- * @param tagsFilters
- * @constructor
- */
- static SplitKeys(tagsFilters: TagsFilter[]) {
- const keyValues = {} // Map string -> string[]
- tagsFilters = [...tagsFilters] // copy all
- while (tagsFilters.length > 0) {
- // Queue
- const tagsFilter = tagsFilters.shift();
-
- if (tagsFilter === undefined) {
- continue;
- }
-
- if (tagsFilter instanceof And) {
- tagsFilters.push(...tagsFilter.and);
- continue;
- }
-
- if (tagsFilter instanceof Tag) {
- if (keyValues[tagsFilter.key] === undefined) {
- keyValues[tagsFilter.key] = [];
- }
- keyValues[tagsFilter.key].push(...tagsFilter.value.split(";"));
- continue;
- }
-
- console.error("Invalid type to flatten the multiAnswer", tagsFilter);
- throw "Invalid type to FlattenMultiAnswer"
- }
- return keyValues;
- }
-
- /**
- * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
- * E.g:
- *
- * FlattenMultiAnswer([and: [ "x=a", "y=0;1"], and: ["x=b", "y=2"], and: ["x=", "y=3"]])
- * will result in
- * ["x=a;b", "y=0;1;2;3"]
- *
- * @param tagsFilters
- * @constructor
- */
- static FlattenMultiAnswer(tagsFilters: TagsFilter[]): And {
- if (tagsFilters === undefined) {
- return new And([]);
- }
-
- let keyValues = TagUtils.SplitKeys(tagsFilters);
- const and: TagsFilter[] = []
- for (const key in keyValues) {
- and.push(new Tag(key, Utils.Dedup(keyValues[key]).join(";")));
- }
- return new And(and);
- }
-
- static MatchesMultiAnswer(tag: TagsFilter, tags: any): boolean {
- const splitted = TagUtils.SplitKeys([tag]);
- for (const splitKey in splitted) {
- const neededValues = splitted[splitKey];
- if (tags[splitKey] === undefined) {
- return false;
- }
-
- const actualValue = tags[splitKey].split(";");
- for (const neededValue of neededValues) {
- if (actualValue.indexOf(neededValue) < 0) {
- return false;
- }
- }
- }
- return true;
- }
-}
\ No newline at end of file
diff --git a/Logic/TagsFilter.ts b/Logic/TagsFilter.ts
new file mode 100644
index 0000000..3283986
--- /dev/null
+++ b/Logic/TagsFilter.ts
@@ -0,0 +1,24 @@
+export abstract class TagsFilter {
+
+ abstract asOverpass(): string[]
+
+ abstract substituteValues(tags: any): TagsFilter;
+
+ abstract isUsableAsAnswer(): boolean;
+
+ abstract isEquivalent(other: TagsFilter): boolean;
+
+ abstract matchesProperties(properties: any): boolean;
+
+ abstract asHumanString(linkToWiki: boolean, shorten: boolean);
+
+ abstract usedKeys(): string[];
+
+ public matches(tags: { k: string, v: string }[]) {
+ const properties = {};
+ for (const kv of tags) {
+ properties[kv.k] = kv.v;
+ }
+ return this.matchesProperties(properties);
+ }
+}
\ No newline at end of file
diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts
index f9556ab..716f8fe 100644
--- a/UI/BigComponents/SimpleAddUI.ts
+++ b/UI/BigComponents/SimpleAddUI.ts
@@ -3,7 +3,6 @@
*/
import Locale from "../i18n/Locale";
import {UIEventSource} from "../../Logic/UIEventSource";
-import {Tag, TagUtils} from "../../Logic/Tags";
import {UIElement} from "../UIElement";
import Svg from "../../Svg";
import {SubtleButton} from "../Base/SubtleButton";
@@ -13,6 +12,8 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants";
import LayerConfig from "../../Customizations/JSON/LayerConfig";
+import {Tag} from "../../Logic/Tag";
+import {TagUtils} from "../../Logic/TagUtils";
export default class SimpleAddUI extends UIElement {
private readonly _loginButton: UIElement;
diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts
index 2556205..0c86c4e 100644
--- a/UI/Image/DeleteImage.ts
+++ b/UI/Image/DeleteImage.ts
@@ -4,8 +4,8 @@ import Translations from "../i18n/Translations";
import CheckBox from "../Input/CheckBox";
import Combine from "../Base/Combine";
import State from "../../State";
-import {Tag} from "../../Logic/Tags";
import Svg from "../../Svg";
+import {Tag} from "../../Logic/Tag";
export default class DeleteImage extends UIElement {
diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts
index 4ae250e..9c43010 100644
--- a/UI/Image/ImageUploadFlow.ts
+++ b/UI/Image/ImageUploadFlow.ts
@@ -6,9 +6,9 @@ import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Imgur} from "../../Logic/Web/Imgur";
import {DropDown} from "../Input/DropDown";
-import {Tag} from "../../Logic/Tags";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
+import {Tag} from "../../Logic/Tag";
export class ImageUploadFlow extends UIElement {
private readonly _licensePicker: UIElement;
diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts
index c665d0b..cd8411d 100644
--- a/UI/Popup/EditableTagRendering.ts
+++ b/UI/Popup/EditableTagRendering.ts
@@ -7,7 +7,7 @@ import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State";
import Svg from "../../Svg";
-import {TagUtils} from "../../Logic/Tags";
+import {TagUtils} from "../../Logic/TagUtils";
export default class EditableTagRendering extends UIElement {
private readonly _tags: UIEventSource MapComplete is an OpenStreetMap editor that is meant to help everyone to easily add information on a single theme. Only features relevant to a single theme are shown with a few predefined questions, in order to keep things simple and extremly user-friendly.The theme maintainer can also choose a language for the interface, choose to disable elements or even to embed it into a different website without any UI-element at all. However, another important part of MapComplete is to always offer the next step to learn more about OpenStreetMap:Kundenspezifische Themen
Dies sind zuvor besuchte benutzergenerierte Themen"
},
"aboutMapcomplete": {
- "en": "About MapComplete
Do you notice an issue with MapComplete? Do you have a feature request? Do you want to help translating? Head over to the source code or issue tracker. Follow the edit count on OsmCha
", - "nl": "MapComplete is een OpenStreetMap-editor om eenvoudig informatie toe te voegen over één enkel onderwerp.
Om de editor zo simpel en gebruiksvriendelijk mogelijk te houden, worden enkel objecten relevant voor het thema getoond.Voor deze objecten kunnen dan vragen beantwoord worden, of men kan een nieuw punt van dit thema toevoegen.De maker van het thema kan er ook voor opteren om een aantal elementen van de gebruikersinterface uit te schakelen of de taal ervan in te stellen.
Een ander belangrijk aspect is om bezoekers stap voor stap meer te leren over OpenStreetMap:
Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker. Volg de edits op OsmCha
", + "en": "MapComplete is an OpenStreetMap editor that is meant to help everyone to easily add information on a single theme.
Only features relevant to a single theme are shown with a few predefined questions, in order to keep things simple and extremly user-friendly.The theme maintainer can also choose a language for the interface, choose to disable elements or even to embed it into a different website without any UI-element at all.
However, another important part of MapComplete is to always offer the next step to learn more about OpenStreetMap:
Do you notice an issue with MapComplete? Do you have a feature request? Do you want to help translating? Head over to the source code or issue tracker. Follow the edit count on OsmCha
", + "nl": "MapComplete is een OpenStreetMap-editor om eenvoudig informatie toe te voegen over één enkel onderwerp.
Om de editor zo simpel en gebruiksvriendelijk mogelijk te houden, worden enkel objecten relevant voor het thema getoond.Voor deze objecten kunnen dan vragen beantwoord worden, of men kan een nieuw punt van dit thema toevoegen.De maker van het thema kan er ook voor opteren om een aantal elementen van de gebruikersinterface uit te schakelen of de taal ervan in te stellen.
Een ander belangrijk aspect is om bezoekers stap voor stap meer te leren over OpenStreetMap:
Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker. Volg de edits op OsmCha
", "de": "MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem Einzelthema hinzuzufügen.
Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge einfach und extrem benutzerfreundlich zu halten.Der Themen-Betreuer kann auch eine Sprache für die Schnittstelle wählen, Elemente deaktivieren oder sogar in eine andere Website ohne jegliches UI-Element einbetten.
Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer den nächsten Schritt anzubietenum mehr über OpenStreetMap zu erfahren:
Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie zum Quellcode oder zur Problemverfolgung.
" }, "backgroundMap": { diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 2d9dc49..3171328 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -3,7 +3,6 @@ Utils.runningFromConsole = true; import {equal} from "assert"; import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; -import {And, Tag} from "../Logic/Tags"; import Locale from "../UI/i18n/Locale"; import Translations from "../UI/i18n/Translations"; import {UIEventSource} from "../Logic/UIEventSource"; @@ -13,6 +12,8 @@ import {Translation} from "../UI/i18n/Translation"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; +import {Tag} from "../Logic/Tag"; +import {And} from "../Logic/And"; new T("Tags", [ diff --git a/test/TagQuestion.spec.ts b/test/TagQuestion.spec.ts index a8dd6d7..6d44a0b 100644 --- a/test/TagQuestion.spec.ts +++ b/test/TagQuestion.spec.ts @@ -5,8 +5,6 @@ Utils.runningFromConsole = true; import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; -import {equal} from "assert"; -import * as assert from "assert"; new T("TagQuestionElement",