diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index ff47a8776..3a87d00a8 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -13,13 +13,201 @@ type Tags = Record export type UploadableTag = Tag | SubstitutingTag | And export class TagUtils { + public static readonly comparators: ReadonlyArray<[string, (a: number, b: number) => boolean]> = + [ + ["<=", (a, b) => a <= b], + [">=", (a, b) => a >= b], + ["<", (a, b) => a < b], + [">", (a, b) => a > b], + ] + public static modeDocumentation: Record< + string, + { name: string; docs: string; uploadable?: boolean; overpassSupport: boolean } + > = { + "=": { + name: "strict equality", + uploadable: true, + overpassSupport: true, + docs: + "Strict equality is denoted by `key=value`. This key matches __only if__ the keypair is present exactly as stated.\n" + + "\n" + + "**Only normal tags (eventually in an `and`) can be used in places where they are uploaded**. Normal tags are used in " + + "the `mappings` of a [TagRendering] (unless `hideInAnswer` is specified), they are used in `addExtraTags` of [Freeform] " + + "and are used in the `tags`-list of a preset.\n" + + "\n" + + "If a different kind of tag specification is given, your theme will fail to parse.\n" + + "\n" + + "### If key is not present\n" + + "\n" + + "If you want to check if a key is not present, use `key=` (pronounce as *key is empty*). A tag collection will match this\n" + + "if `key` is missing or if `key` is a literal empty value.\n" + + "\n" + + "### Removing a key\n" + + "\n" + + "If a key should be deleted in the OpenStreetMap-database, specify `key=` as well. This can be used e.g. to remove a\n" + + "fixme or value from another mapping if another field is filled out.", + }, + "!=": { + name: "strict not equals", + overpassSupport: true, + docs: + "To check if a key does _not_ equal a certain value, use `key!=value`. This is converted behind the scenes\n" + + "to `key!~^value$`\n" + + "\n" + + "If `key` is not present or empty, this will match too.\n" + + "\n" + + "### If key is present\n" + + "\n" + + "This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not\n" + + "empty.", + }, + "~": { + name: "Value matches regex", + overpassSupport: true, + docs: + "A tag can also be tested against a regex with `key~regex`. Note that this regex __must match__ the entire value. If the\n" + + "value is allowed to appear anywhere as substring, use `key~.*regex.*`.\n" + + "The regex is put within braces as to prevent runaway values.\n" + + "\nUse `key~*` to indicate that any value is allowed. This is effectively the check that the attribute is present (defined _and_ not empty)." + + "\n" + + "Regexes will match the newline character with `.` too - the `s`-flag is enabled by default.", + }, + "~i~": { + name: "Value matches case-invariant regex", + overpassSupport: true, + docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value", + }, + "!~": { + name: "Value should _not_ match regex", + overpassSupport: true, + docs: + "A tag can also be tested against a regex with `key!~regex`. This filter will match if the value does *not* match the regex. " + + "\n If the\n" + + "value is allowed to appear anywhere as substring, use `key~.*regex.*`.\n" + + "The regex is put within braces as to prevent runaway values.\n", + }, + "!~i~": { + name: "Value does *not* match case-invariant regex", + overpassSupport: true, + docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value. This filter returns true if the value does *not* match", + }, + "~~": { + name: "Key and value should match given regex", + overpassSupport: true, + docs: "Both the `key` and `value` part of this specification are interpreted as regexes, both the key and value musth completely match their respective regexes", + }, + ":=": { + name: "Substitute `{some_key}` should match `key`", + overpassSupport: false, + docs: + "**This is an advanced feature - use with caution**\n" + + "\n" + + "Some tags are automatically set or calculated - see [CalculatedTags](CalculatedTags.md) for an entire overview. If one\n" + + "wants to apply such a value as tag, use a substituting-tag such, for example`survey:date:={_date:now}`. Note that the\n" + + "separator between key and value here is `:=`. The text between `{` and `}` is interpreted as a key, and the respective\n" + + "value is substituted into the string.\n" + + "\n" + + "One can also append, e.g. `key:={some_key} fixed text {some_other_key}`.\n" + + "\n" + + "An assigning tag _cannot_ be used to query OpenStreetMap/Overpass.\n" + + "\n" + + "If using a key or variable which might not be defined, add a condition in the mapping to hide the option. This is\n" + + "because, if `some_other_key` is not defined, one might actually upload the literal text `key={some_other_key}` to OSM -\n" + + "which we do not want.\n" + + "\n" + + "To mitigate this, use:\n" + + "\n" + + "```json\n" + + "{\n" + + ' "mappings": [\n' + + " {\n" + + ' "if":"key:={some_other_key}",\n' + + ' "then": "...",\n' + + ' "hideInAnswer": "some_other_key="\n' + + " }\n" + + " ]\n" + + "}\n" + + "```\n" + + "\n" + + "One can use `key!:=prefix-{other_key}-postfix` as well, to match if `key` is _not_ the same\n" + + "as `prefix-{other_key}-postfix` (with `other_key` substituted by the value)", + }, + "!:=": { + name: "Substitute `{some_key}` should not match `key`", + overpassSupport: false, + docs: "See `:=`, except that this filter is inverted", + }, + } private static keyCounts: { keys: any; tags: any } = key_counts - private static comparators: [string, (a: number, b: number) => boolean][] = [ - ["<=", (a, b) => a <= b], - [">=", (a, b) => a >= b], - ["<", (a, b) => a < b], - [">", (a, b) => a > b], - ] + public static readonly numberAndDateComparisonDocs = + "If the value of a tag is a number (e.g. `key=42`), one can use a filter `key<=42`, `key>=35`, `key>40` or `key<50` to\n" + + "match this, e.g. in conditions for renderings. These tags cannot be used to generate an answer nor can they be used to\n" + + "request data upstream from overpass.\n" + + "\n" + + "Note that the value coming from OSM will first be stripped by removing all non-numeric characters. For\n" + + "example, `length=42 meter` will be interpreted as `length=42` and will thus match `length<=42` and `length>=42`. In\n" + + "special circumstances (e.g. `surface_area=42 m2` or `length=100 feet`), this will result in erronous\n" + + "values (`surface=422` or if a length in meters is compared to). However, this can be partially alleviated by using '\n" + + "Units' to rewrite to a default format.\n" + + "\n" + + "Dates can be compared with the same expression: `somekey<2022-06-22` will match if `somekey` is a date and is smaller\n" + + "then 22nd june '22." + + public static readonly logicalOperator = + "\n" + + "## Logical operators\n" + + "\n" + + "One can combine multiple tags by using `and` or `or`, e.g.:\n" + + "\n" + + "```json\n" + + "{\n" + + ' "osmTags": {\n' + + ' "or": [\n' + + ' "amenity=school",\n' + + ' "amenity=kindergarten"\n' + + " ]\n" + + " }\n" + + "}\n" + + "```\n" + + public static readonly intro = + "Tags format\n" + + "=============\n" + + "\n" + + "When creating the `json` file describing your layer or theme, you'll have to add a few tags to describe what you want.\n" + + "This document gives an overview of what every expression means and how it behaves in edge cases.\n" + + "\n" + + "If the schema-files note a type [`TagConfigJson`](https://github.com/pietervdvn/MapComplete/blob/develop/Models/ThemeConfig/Json/TagConfigJson.ts), you can use one of these values.\n" + + "\n" + + "In some cases, not every type of tags-filter can be used. For example, _rendering_ an option with a regex is\n" + + 'fine (`"if": "brand~[Bb]randname", "then":" The brand is Brandname"`); but this regex can not be used to write a value\n' + + "into the database. The theme loader will however refuse to work with such inconsistencies and notify you of this while\n" + + "you are building your theme.\n" + + "\n" + + "Example\n" + + "-------\n" + + "\n" + + "This example shows the most common options on how to specify tags:\n" + + "\n" + + "```json\n" + + "{\n" + + ' "and": [\n' + + ' "key=value",\n' + + " {\n" + + ' "or": [\n' + + ' "other_key=value",\n' + + ' "other_key=some_other_value"\n' + + " ]\n" + + " },\n" + + ' "key_which_should_be_missing=",\n' + + ' "key_which_should_have_a_value~*",\n' + + ' "key~.*some_regex_a*_b+_[a-z]?",\n' + + ' "height<1"\n' + + " ]\n" + + "}\n" + + "```\n" + + "\n" + + "\n" static KVtoProperties(tags: Tag[]): Record { const properties: Record = {} @@ -688,4 +876,19 @@ export class TagUtils { } return " (" + joined + ") " } + + public static generateDocs(): string { + return [ + TagUtils.intro, + ...Object.keys(TagUtils.modeDocumentation).map((mode) => { + const doc = TagUtils.modeDocumentation[mode] + return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n") + }), + "## " + + TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") + + " Logical comparators", + TagUtils.numberAndDateComparisonDocs, + TagUtils.logicalOperator, + ].join("\n") + } }