Split TagsFilter into multiple files

This commit is contained in:
pietervdvn 2021-03-22 03:05:08 +01:00
parent 830f34b183
commit d8c1f72857
26 changed files with 541 additions and 518 deletions

View file

@ -1,7 +1,11 @@
import {AndOrTagConfigJson} from "./TagConfigJson"; import {AndOrTagConfigJson} from "./TagConfigJson";
import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags"; import {Or} from "../../Logic/Or";
import {Utils} from "../../Utils"; 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 { export class FromJSON {

View file

@ -1,6 +1,5 @@
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import TagRenderingConfig from "./TagRenderingConfig"; import TagRenderingConfig from "./TagRenderingConfig";
import {Tag, TagsFilter} from "../../Logic/Tags";
import {LayerConfigJson} from "./LayerConfigJson"; import {LayerConfigJson} from "./LayerConfigJson";
import {FromJSON} from "./FromJSON"; import {FromJSON} from "./FromJSON";
import SharedTagRenderings from "../SharedTagRenderings"; import SharedTagRenderings from "../SharedTagRenderings";
@ -16,6 +15,8 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {UIElement} from "../../UI/UIElement"; import {UIElement} from "../../UI/UIElement";
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
import SourceConfig from "./SourceConfig"; import SourceConfig from "./SourceConfig";
import {TagsFilter} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tag";
export default class LayerConfig { export default class LayerConfig {

View file

@ -1,4 +1,4 @@
import {TagsFilter} from "../../Logic/Tags"; import {TagsFilter} from "../../Logic/TagsFilter";
export default class SourceConfig { export default class SourceConfig {

View file

@ -1,10 +1,12 @@
import {And, TagsFilter, TagUtils} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON"; import {FromJSON} from "./FromJSON";
import ValidatedTextField from "../../UI/Input/ValidatedTextField"; import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {Translation} from "../../UI/i18n/Translation"; import {Translation} from "../../UI/i18n/Translation";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {TagsFilter} from "../../Logic/TagsFilter";
import {And} from "../../Logic/And";
import {TagUtils} from "../../Logic/TagUtils";
/*** /***
* The parsed version of TagRenderingConfigJSON * The parsed version of TagRenderingConfigJSON

View file

@ -1,11 +1,12 @@
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {Or, TagsFilter} from "../Tags"; import {Or} from "../Or";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import {Overpass} from "../Osm/Overpass"; import {Overpass} from "../Osm/Overpass";
import Bounds from "../../Models/Bounds"; import Bounds from "../../Models/Bounds";
import FeatureSource from "../FeatureSource/FeatureSource"; import FeatureSource from "../FeatureSource/FeatureSource";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {TagsFilter} from "../TagsFilter";
export default class UpdateFromOverpass implements FeatureSource { export default class UpdateFromOverpass implements FeatureSource {

119
Logic/And.ts Normal file
View file

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

View file

@ -1,10 +1,12 @@
import {GeoOperations} from "./GeoOperations"; import {GeoOperations} from "./GeoOperations";
import State from "../State"; import State from "../State";
import opening_hours from "opening_hours"; import opening_hours from "opening_hours";
import {And, Or, Tag} from "./Tags"; import {Or} from "./Or";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {UIElement} from "../UI/UIElement"; import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine"; import Combine from "../UI/Base/Combine";
import {Tag} from "./Tag";
import {And} from "./And";
class SimpleMetaTagger { class SimpleMetaTagger {
public readonly keys: string[]; public readonly keys: string[];

72
Logic/Or.ts Normal file
View file

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

View file

@ -1,10 +1,12 @@
import {OsmNode, OsmObject} from "./OsmObject"; import {OsmNode, OsmObject} from "./OsmObject";
import {And, Tag, TagsFilter} from "../Tags";
import State from "../../State"; import State from "../../State";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import FeatureSource from "../FeatureSource/FeatureSource"; import FeatureSource from "../FeatureSource/FeatureSource";
import {TagsFilter} from "../TagsFilter";
import {Tag} from "../Tag";
import {And} from "../And";
/** /**
* Handles all changes made to OSM. * Handles all changes made to OSM.

View file

@ -1,7 +1,7 @@
import {TagsFilter} from "../Tags";
import * as $ from "jquery" import * as $ from "jquery"
import * as OsmToGeoJson from "osmtogeojson"; import * as OsmToGeoJson from "osmtogeojson";
import Bounds from "../../Models/Bounds"; import Bounds from "../../Models/Bounds";
import {TagsFilter} from "../TagsFilter";
/** /**
* Interfaces overpass to get all the latest data * Interfaces overpass to get all the latest data

85
Logic/RegexTag.ts Normal file
View file

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

84
Logic/Tag.ts Normal file
View file

@ -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 `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` +
`=` +
`<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${v}</a>`
}
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];
}
}

121
Logic/TagUtils.ts Normal file
View file

@ -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, '&lt;').replace(/>/g, '&gt;');
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;
}
}

View file

@ -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 `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` +
`=` +
`<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${v}</a>`
}
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, '&lt;').replace(/>/g, '&gt;');
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;
}
}

24
Logic/TagsFilter.ts Normal file
View file

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

View file

@ -3,7 +3,6 @@
*/ */
import Locale from "../i18n/Locale"; import Locale from "../i18n/Locale";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {Tag, TagUtils} from "../../Logic/Tags";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
@ -13,6 +12,8 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import LayerConfig from "../../Customizations/JSON/LayerConfig"; import LayerConfig from "../../Customizations/JSON/LayerConfig";
import {Tag} from "../../Logic/Tag";
import {TagUtils} from "../../Logic/TagUtils";
export default class SimpleAddUI extends UIElement { export default class SimpleAddUI extends UIElement {
private readonly _loginButton: UIElement; private readonly _loginButton: UIElement;

View file

@ -4,8 +4,8 @@ import Translations from "../i18n/Translations";
import CheckBox from "../Input/CheckBox"; import CheckBox from "../Input/CheckBox";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import State from "../../State"; import State from "../../State";
import {Tag} from "../../Logic/Tags";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {Tag} from "../../Logic/Tag";
export default class DeleteImage extends UIElement { export default class DeleteImage extends UIElement {

View file

@ -6,9 +6,9 @@ import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {Imgur} from "../../Logic/Web/Imgur"; import {Imgur} from "../../Logic/Web/Imgur";
import {DropDown} from "../Input/DropDown"; import {DropDown} from "../Input/DropDown";
import {Tag} from "../../Logic/Tags";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {Tag} from "../../Logic/Tag";
export class ImageUploadFlow extends UIElement { export class ImageUploadFlow extends UIElement {
private readonly _licensePicker: UIElement; private readonly _licensePicker: UIElement;

View file

@ -7,7 +7,7 @@ import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer"; import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State"; import State from "../../State";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {TagUtils} from "../../Logic/Tags"; import {TagUtils} from "../../Logic/TagUtils";
export default class EditableTagRendering extends UIElement { export default class EditableTagRendering extends UIElement {
private readonly _tags: UIEventSource<any>; private readonly _tags: UIEventSource<any>;

View file

@ -9,7 +9,7 @@ import State from "../../State";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {Tag} from "../../Logic/Tags"; import {Tag} from "../../Logic/Tag";
export default class FeatureInfoBox extends ScrollableFullScreen { export default class FeatureInfoBox extends ScrollableFullScreen {

View file

@ -3,7 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import TagRenderingQuestion from "./TagRenderingQuestion"; import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {TagUtils} from "../../Logic/Tags"; import {TagUtils} from "../../Logic/TagUtils";
/** /**

View file

@ -3,9 +3,9 @@ import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {TagUtils} from "../../Logic/Tags";
import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import {TagUtils} from "../../Logic/TagUtils";
/*** /***
* Displays the correct value for a known tagrendering * Displays the correct value for a known tagrendering

View file

@ -3,7 +3,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {InputElement} from "../Input/InputElement"; import {InputElement} from "../Input/InputElement";
import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags";
import ValidatedTextField from "../Input/ValidatedTextField"; import ValidatedTextField from "../Input/ValidatedTextField";
import {FixedInputElement} from "../Input/FixedInputElement"; import {FixedInputElement} from "../Input/FixedInputElement";
import {RadioButton} from "../Input/RadioButton"; import {RadioButton} from "../Input/RadioButton";
@ -19,6 +18,10 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {TagsFilter} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tag";
import {And} from "../../Logic/And";
import {TagUtils} from "../../Logic/TagUtils";
/** /**
* Shows the question element. * Shows the question element.

View file

@ -774,8 +774,8 @@
"de": "<h3>Kundenspezifische Themen</h3>Dies sind zuvor besuchte benutzergenerierte Themen" "de": "<h3>Kundenspezifische Themen</h3>Dies sind zuvor besuchte benutzergenerierte Themen"
}, },
"aboutMapcomplete": { "aboutMapcomplete": {
"en": "<h3>About MapComplete</h3><p>MapComplete is an OpenStreetMap editor that is meant to help everyone to easily add information on a <b>single theme.</b></p><p>Only features relevant to a single theme are shown with a few predefined questions, in order to keep things <b>simple and extremly user-friendly</b>.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.</p><p>However, another important part of MapComplete is to always <b>offer the next step</b> to learn more about OpenStreetMap:<ul><li>An iframe without UI-elements will link to a full-screen version</li><li>The fullscreen version offers information about OpenStreetMap</li><li>If you're not logged in, you're asked to log in</li><li>If you answered a single question, you are allowed to add points</li><li>At a certain point, the actual added tags appear which later get linked to the wiki...</li></ul></p><p>Do you notice an issue with MapComplete? Do you have a feature request? Do you want to help translating? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a></p>", "en": "<h3>About MapComplete</h3><p>MapComplete is an OpenStreetMap editor that is meant to help everyone to easily add information on a <b>single theme.</b></p><p>Only features relevant to a single theme are shown with a few predefined questions, in order to keep things <b>simple and extremly user-friendly</b>.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.</p><p>However, another important part of MapComplete is to always <b>offer the next step</b> to learn more about OpenStreetMap:<ul><li>An iframe without UI-elements will link to a full-screen version</li><li>The fullscreen version offers information about OpenStreetMap</li><li>If you're not logged in, you're asked to log in</li><li>If you answered a single question, you are allowed to add points</li><li>At a certain point, the actual added tags appear which later get linked to the wiki...</li></ul></p><p>Do you notice an issue with MapComplete? Do you have a feature request? Do you want to help translating? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a></p>",
"nl": "<h3>Over MapComplete</h3><p>MapComplete is een OpenStreetMap-editor om eenvoudig informatie toe te voegen over <b>één enkel onderwerp</b>.</p><p>Om de editor zo <b>simpel en gebruiksvriendelijk mogelijk</b> 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.</p><p>Een ander belangrijk aspect is om bezoekers stap voor stap meer te leren over OpenStreetMap:<ul><li>Een iframe zonder verdere uitleg linkt naar de volledige versie van MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Als je niet aangemeld bent, wordt er je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je punten gaan toevoegen.</li><li>Heb je genoeg changesets, dan verschijnen de tags die wat later doorlinken naar de wiki</li></ul></p><p>Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a></p>", "nl": "<h3>Over MapComplete</h3><p>MapComplete is een OpenStreetMap-editor om eenvoudig informatie toe te voegen over <b>één enkel onderwerp</b>.</p><p>Om de editor zo <b>simpel en gebruiksvriendelijk mogelijk</b> 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.</p><p>Een ander belangrijk aspect is om bezoekers stap voor stap meer te leren over OpenStreetMap:<ul><li>Een iframe zonder verdere uitleg linkt naar de volledige versie van MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Als je niet aangemeld bent, wordt er je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je punten gaan toevoegen.</li><li>Heb je genoeg changesets, dan verschijnen de tags die wat later doorlinken naar de wiki</li></ul></p><p>Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a></p>",
"de": "<h3>Über MapComplete</h3><p>MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem <b>Einzelthema hinzuzufügen.</b></p><p>Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge <b>einfach und extrem benutzerfreundlich</b> 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.</p><p>Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer <b>den nächsten Schritt anzubieten</b>um mehr über OpenStreetMap zu erfahren:<ul><li>Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion</li><li>Die Vollbildversion bietet Informationen über OpenStreetMap</li><li>Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen</li><li>Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen</li><li>An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...</li></ul></p><p>Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>zum Quellcode</a> oder <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>zur Problemverfolgung</a>.</p>" "de": "<h3>Über MapComplete</h3><p>MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem <b>Einzelthema hinzuzufügen.</b></p><p>Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge <b>einfach und extrem benutzerfreundlich</b> 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.</p><p>Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer <b>den nächsten Schritt anzubieten</b>um mehr über OpenStreetMap zu erfahren:<ul><li>Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion</li><li>Die Vollbildversion bietet Informationen über OpenStreetMap</li><li>Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen</li><li>Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen</li><li>An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...</li></ul></p><p>Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>zum Quellcode</a> oder <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>zur Problemverfolgung</a>.</p>"
}, },
"backgroundMap": { "backgroundMap": {

View file

@ -3,7 +3,6 @@ Utils.runningFromConsole = true;
import {equal} from "assert"; import {equal} from "assert";
import T from "./TestHelper"; import T from "./TestHelper";
import {FromJSON} from "../Customizations/JSON/FromJSON"; import {FromJSON} from "../Customizations/JSON/FromJSON";
import {And, Tag} from "../Logic/Tags";
import Locale from "../UI/i18n/Locale"; import Locale from "../UI/i18n/Locale";
import Translations from "../UI/i18n/Translations"; import Translations from "../UI/i18n/Translations";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
@ -13,6 +12,8 @@ import {Translation} from "../UI/i18n/Translation";
import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours";
import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput";
import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; import {SubstitutedTranslation} from "../UI/SubstitutedTranslation";
import {Tag} from "../Logic/Tag";
import {And} from "../Logic/And";
new T("Tags", [ new T("Tags", [

View file

@ -5,8 +5,6 @@ Utils.runningFromConsole = true;
import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig";
import {equal} from "assert";
import * as assert from "assert";
new T("TagQuestionElement", new T("TagQuestionElement",