diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index b8c899f08..d1dfa4b30 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -1,4 +1,3 @@ -import BaseUIElement from "../src/UI/BaseUIElement" import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts" import SimpleMetaTaggers from "../src/Logic/SimpleMetaTagger" @@ -34,6 +33,8 @@ import * as unitUsage from "../Docs/Schemas/UnitConfigJson.schema.json" import { ThemeConfigJson } from "../src/Models/ThemeConfig/Json/ThemeConfigJson" import { ServerSourceInfo, SourceOverview } from "../src/Models/SourceOverview" import { Lists } from "../src/Utils/Lists" +import { Translation, TypedTranslation } from "../src/UI/i18n/Translation" +import language_translations from "../src/assets/language_translations.json" /** * Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use @@ -177,8 +178,13 @@ export class GenerateDocs extends Script { this.generateBuiltinUnits() await this.generateSourcesOverview() + if (!existsSync("./Docs/themes_nl")) { + mkdirSync("./Docs/themes_nl") + } Array.from(AllKnownLayouts.allKnownLayouts.values()).map((theme) => { this.generateForTheme(theme) + // this.generateForTheme(theme, { path: "./Docs/themes_nl", lang: "nl" }) + ScriptUtils.erasableLog("Written docs for theme", theme.id) }) @@ -247,7 +253,7 @@ export class GenerateDocs extends Script { let md = markdown - if (options?.noTableOfContents !== false) { + if (options?.noTableOfContents !== true) { md = TableOfContents.insertTocIntoMd(md, lang, options?.tocMaxDepth) } @@ -260,15 +266,17 @@ export class GenerateDocs extends Script { const warnAutomated = "[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)\n\n" - const generatedFrom = [ - "This document is autogenerated from", - autogenSource - .map( - (s) => - `[${s}](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/${s})` - ) - .join(", "), - ].join(" ") + + const sources = autogenSource.map( + (s) => `[${s}](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/${s})`).join(", ") + + const generatedFrom = + new TypedTranslation<{ sources }>({ + en: "This document is autogenerated from {sources}", + nl: "Dit document werd gegenereerd op basis van {sources}", + }).Subs({ sources }).textFor(lang) + + writeFileSync(filename, warnAutomated + md + "\n\n" + generatedFrom + "\n") } @@ -417,7 +425,7 @@ export class GenerateDocs extends Script { if (inlineSource !== undefined) { source = `assets/themes/${inlineSource}/${inlineSource}.json` } - this.writeMarkdownFile(targetDirectory+ "/" + layer.id + ".md", element, [source]) + this.writeMarkdownFile(targetDirectory + "/" + layer.id + ".md", element, [source], { lang }) }) } @@ -509,45 +517,73 @@ export class GenerateDocs extends Script { ]) } - private generateForTheme(theme: ThemeConfig): void { + private generateForTheme(theme: ThemeConfig, options?: { path?: string, lang?: string }): void { const allLayers = AllSharedLayers.sharedLayers const layersToShow = theme.layers.filter( (l) => l.id !== "favourite" && Constants.added_by_default.indexOf(l.id) < 0 ) + const lang = options?.lang ?? "en" const layersToInline = layersToShow.filter((l) => !allLayers.has(l.id)) const el = [ [ "##", - theme.title, + theme.title.textFor(lang), "(", `[${theme.id}](https://mapcomplete.org/${theme.id})`, ")", ].join(" "), - - "_This document details some technical information about this MapComplete theme, mostly about the attributes used in the theme. Various links point toward more information about the attributes, e.g. to the OpenStreetMap-wiki, to TagInfo or tools creating statistics_", - "The theme introduction reads:\n", "> " + parse_html(theme.description.textFor("en")).textContent.replace(/\n/g, " "), "", - "This theme contains the following layers:", + new Translation({ + en: "This theme contains the following layers:", + nl: "Dit kaartthema bevat de volgende lagen", + }).textFor(lang), MarkdownUtils.list( layersToShow.map((l) => { if (allLayers.has(l.id)) { return `[${l.id}](../Layers/${l.id}.md)` } - return `[${l.id} (defined in this theme)](#${l.id.trim().replace(/ /g, "-")})` + return `[${l.id} (${l.name?.textFor(lang)})](#${l.id.trim().replace(/ /g, "-")})` }) ), - "Available languages:", - MarkdownUtils.list(theme.language.filter((ln) => ln !== "_context")), - "# Layers defined in this theme configuration file", - "These layers can not be reused in different themes.", - ...layersToInline.map((l) => l.generateDocumentation({ usedInThemes: null })), - ].join("\n") + new Translation( + { + en: "This theme is available in the following languages:", + nl: "Deze kaart is beschikbaar in de volgende talen:", + }, + ).textFor(lang), + MarkdownUtils.list(theme.language.filter((ln) => ln !== "_context").map(ln => { + if (language_translations[ln]) { + return ln + " (" + new Translation(language_translations[ln]).textFor(lang) + ")" + } else { + return ln + } + }, + )), + ] + + if (layersToInline.length > 0) { + el.push(MarkdownUtils.title(1, new Translation({ + en: "Layers defined in this theme configuration file", + nl: "Lagen gedefinieerd in dit kaartthema-bestand", + })).textFor(lang)) + el.push(MarkdownUtils.list(layersToInline.map(id => `[${id}](#${id})`))) + + el.push(new Translation({ + en: "These layers can not be reused in different themes.", + nl: "Deze lagen kunnen niet in andere kaartthemas hergebruikt worden", + }).textFor(lang)) + el.push( + ...layersToInline.map((l) => l.generateDocumentation({ usedInThemes: null, lang })), + ) + } + + const path = options?.path ?? "./Docs/Themes" this.writeMarkdownFile( - "./Docs/Themes/" + theme.id + ".md", - el, + path + "/" + theme.id + ".md", + el.join("\n"), [`assets/themes/${theme.id}/${theme.id}.json`], - { noTableOfContents: true } + { noTableOfContents: true, lang }, ) } @@ -681,7 +717,7 @@ export class GenerateDocs extends Script { * Generates the documentation for the layers overview page * @constructor */ - private generateLayerOverviewText(): BaseUIElement { + private generateLayerOverviewText(): void { for (const id of Constants.priviliged_layers) { if (!AllSharedLayers.sharedLayers.has(id)) { console.error("Priviliged layer definition not found: " + id) diff --git a/src/Logic/Tags/RegexTag.ts b/src/Logic/Tags/RegexTag.ts index 216564a4b..20c47412f 100644 --- a/src/Logic/Tags/RegexTag.ts +++ b/src/Logic/Tags/RegexTag.ts @@ -252,12 +252,24 @@ export class RegexTag extends TagsFilter { return this.invert } - asHumanString() { + asHumanString(linkToWiki: boolean) { if (typeof this.key === "string") { const oper = typeof this.value === "string" ? "=" : "~" - return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}` + if(linkToWiki){ + return `[${this.key}](https://wiki.osm.org/wiki/Key:${this.key})\`${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}\`` + } + + const v= `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}` + if(linkToWiki){ + return `\`${v}\`` + } + return v } - return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` + const v = `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` + if(linkToWiki){ + return `\`${v}\`` + } + return v } /** diff --git a/src/Models/SourceOverview.ts b/src/Models/SourceOverview.ts index 38bd6700e..f197685ae 100644 --- a/src/Models/SourceOverview.ts +++ b/src/Models/SourceOverview.ts @@ -171,9 +171,7 @@ export class SourceOverview { if (!url) { continue } - console.log(">>> SPlitting", url) const split = url.split(",https://") - console.log("Multisplit:", split) url = split[0] let urlClipped = url if (url.indexOf("?") > 0) { diff --git a/src/Models/ThemeConfig/FilterConfig.ts b/src/Models/ThemeConfig/FilterConfig.ts index 291ddd9a2..19b48a0cc 100644 --- a/src/Models/ThemeConfig/FilterConfig.ts +++ b/src/Models/ThemeConfig/FilterConfig.ts @@ -10,7 +10,6 @@ import { Utils } from "../../Utils" import { RegexTag } from "../../Logic/Tags/RegexTag" import MarkdownUtils from "../../Utils/MarkdownUtils" import Validators, { ValidatorType } from "../../UI/InputElement/Validators" -import { Lists } from "../../Utils/Lists" export type FilterConfigOption = { question: Translation @@ -223,23 +222,29 @@ export default class FilterConfig { ) } - public GenerateDocs(): string { - const hasField = this.options.some((opt) => opt.fields?.length > 0) - return MarkdownUtils.table( - Lists.noNull(["id", "question", "osmTags", hasField ? "fields" : undefined]), - this.options.map((opt, i) => { - const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i - return ( - Lists.noNull([ - this.id + "." + i, - isDefault ? `*${opt.question.txt}* (default)` : opt.question, - opt.osmTags?.asHumanString() ?? "", - opt.fields?.length > 0 - ? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ") - : undefined, - ]) - ) - }) - ) + public generateDocs(options?: { lang?: string }): string { + const lang = options?.lang ?? "en" + const header = ["id", { en: "Default", nl: "Standaard" }, { en: "Question", nl: "Vraag" }, { + en: "Attributes", + nl: "Attributen", + }, + { en: "Fields", nl: "Velden" }, + ].map(x => typeof x === "string" ? new Translation({ "*": x }) : new Translation(x)) + .map(tr => tr.textFor(lang)) + + const content: string[][] = [] + for (let i = 0; i < this.options.length; i++) { + const opt = this.options[i] + const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i + content.push([ + this.id + "." + i, + isDefault ? "✔️" : "", + opt.question.textFor(lang), + opt.osmTags?.asHumanString(true) ?? (new Translation({en: "_None - show all_", nl: "_Geen - toon alles_"})).textFor(lang), + opt.fields?.map((f) => f.name + " (" + f.type + ")").join(" "), + ]) + } + + return MarkdownUtils.table(header, content, { dropEmptyColumns: true }) } } diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index 366ccb594..c7085513d 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -1,4 +1,4 @@ -import { Translation } from "../../UI/i18n/Translation" +import { Translation, TypedTranslation } from "../../UI/i18n/Translation" import SourceConfig from "./SourceConfig" import TagRenderingConfig from "./TagRenderingConfig" import PresetConfig, { PreciseInput } from "./PresetConfig" @@ -393,52 +393,6 @@ export default class LayerConfig extends WithContextLoader { return this.mapRendering.some((r) => r.location.has("point")) } - /** - * A quick overview table of all the elements in the popup-box - * @private - */ - private generateDocumentationQuickTable(): string { - return MarkdownUtils.table( - ["id", "question", "labels", "freeform key"], - this.tagRenderings - .filter((tr) => tr.labels.indexOf("ignore_docs") < 0) - .map((tr) => { - let key = "_Multiple choice only_" - if (tr.freeform) { - const type = `[${tr.freeform.type}](../SpecialInputElements.md#${tr.freeform.type})` - - key = `*[${tr.freeform.key}](https://wiki.osm.org/wiki/Key:${tr.freeform.key})* (${type})` - } - let origDef = "" - if (tr._definedIn) { - let [layer, id] = tr._definedIn - if (layer == "questions") { - layer = "./BuiltinQuestions" - } else { - layer = "./" + layer - } - origDef = `
_(Original in [${tr._definedIn[0]}](${layer}.md#${id}))_` - } - const q = tr.question?.Subs(this.baseTags)?.txt?.trim() - let r = tr.render?.txt - if (r && r !== "") { - r = `_${r}_` - } - let options: string = undefined - if (tr.mappings?.length > 0) { - options = `${tr.mappings.length} options` - } - - return [ - `[${tr.id}](#${tr.id}) ${origDef}`, - Lists.noNull([q, r, options]).join("
"), - tr.labels.join(", "), - key, - ] - }) - ) - } - public generateDocumentation({ usedInThemes = [], layerIsNeededBy, @@ -456,234 +410,367 @@ export default class LayerConfig extends WithContextLoader { reusedTagRenderings?: Map lang?: string }): string { - const extraProps: string[] = [] - extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher") - if (canBeIncluded) { - if (addedByDefault) { - extraProps.push( - "**This layer is included automatically in every theme. This layer might contain no points**" - ) - } - if (this.shownByDefault === false) { - extraProps.push( - "This layer is not visible by default and must be enabled in the filter by the user. " - ) - } - if (this.title === undefined) { - extraProps.push( - "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable." - ) - } - if (this.name === undefined && this.shownByDefault === false) { - extraProps.push( - "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-=true" - ) - } - if (this.name === undefined) { - extraProps.push( - "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`" - ) - } - if (this.mapRendering.length === 0) { - extraProps.push( - "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`" - ) - } + const paragraphs: (string | Translation)[] = [ + "# " + this.id, + this.description, + ] - if (this.source?.geojsonSource !== undefined) { - extraProps.push( - [ - "", - "This layer is loaded from an external source, namely ", - "`" + this.source.geojsonSource + "`", - ].join("\n\n") - ) + function add(item: Translation | string | Record & { en: string, nl: string }) { + if (item instanceof Translation || typeof item === "string") { + paragraphs.push(item) + } else if (item["en"] !== undefined) { + paragraphs.push(new Translation(item)) } - } else { + } + + if (this._basedOn) { + add(new TypedTranslation<{ basedOn }>( + { + en: `This layer is based on [{basedOn}](../Layers/{basedOn}.md)`, + nl: `Deze laag is gebaseerd op [{basedOn}](../Layers/{basedOn}.md)`, + }, + ).Subs({ basedOn: this._basedOn })) + } + + // Various extra properties, added in a list below the description + { + const extraProps: Translation[] = [] extraProps.push( - "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data." - ) - } + new TypedTranslation<{ minzoom }>({ + en: "This layer is shown at zoomlevel **{minzoom}** and higher", + nl: "Deze laag wordt getoond vanaf zoomlevel **{minzoom}**", + }).Subs(this)) - let usingLayer: string[] = [] - if (!addedByDefault) { - if (usedInThemes?.length > 0) { - usingLayer = [ - "## Themes using this layer", - MarkdownUtils.list( - (usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`) - ), - ] - } else if (this.source !== null) { - usingLayer = ["No themes use this layer"] - } - } + if (canBeIncluded) { + if (addedByDefault) { + extraProps.push( + new Translation( + { + en: + "**This layer is included automatically in every theme. This layer might contain no points**", + nl: "**Deze laag wordt automatisch toegevoegd aan ieder kaartthema. Deze laag bevat mogelijks geen punten", + }, + ), + ) + } + if (this.shownByDefault === false) { + extraProps.push( + new Translation({ + en: "This layer is not visible by default and must be enabled in the filter by the user. ", + nl: "Deze laag is standaard niet zichtbaar en moet door de gebruiker aangezet worden in het 'filter'-menu ", + }), + ) + } + if (this.title === undefined) { + extraProps.push( + new Translation({ + en: + "This layers doesn't have a title set. As such, elements will appear on the map but cannot be clicked. If you import this layer in your theme, override `title` to make sure elements can be opened." + , + nl: "Deze laag heeft geen 'title' ingesteld. Elementen zullen op de kaart tonen, maar kunnen niet opengeklikt worden door de gebruiker. Hergebruik je deze laag? Voeg een `title` toe zodat element wel opengeklikt kunnen worden.", + }), + ) + } + if (this.name === undefined && this.shownByDefault === false) { + if (this.shownByDefault === false) { - for (const dep of dependencies) { - extraProps.push( - [ - "This layer will automatically load ", - `[${dep.neededLayer}](./${dep.neededLayer}.md)`, - " into the layout as it depends on it: ", - dep.reason, - "(" + dep.context + ")", - ].join(" ") - ) - } - - let presets: string[] = [] - if (this.presets.length > 0) { - presets = [ - "## Presets", - "The following options to create new points are included:", - MarkdownUtils.list( - this.presets.map((preset) => { - let snaps = "" - if (preset.preciseInput?.snapToLayers) { - snaps = - " (snaps to layers " + - preset.preciseInput.snapToLayers - .map((id) => `\`${id}\``) - .join(", ") + - ")" - } - return ( - "**" + - preset.title.textFor(lang) + - "** which has the following tags:" + - new And(preset.tags).asHumanString(true) + - snaps + extraProps.push( + new TypedTranslation<{ id }>({ + en: "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by adding the URL-parameter `layer-{id}=true`", + nl: "Deze laag is standaard niet zichtbaar én heeft geen `name`. Dit betekent dat de zichtbaarheid niet door de gebruiker ingesteld kan worden, wat in een volledig verborgen laag resulteert. Dit kan nuttig zij, bv. om metatags uit te rekenen. Wil je deze laag toch tonen (bv om te debuggen?)? Voeg de URL-parameter `layer-{id}=true` toe", + }).Subs(this), ) - }) - ), - ] - } + } else { + extraProps.push( + new Translation({ + en: "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`", + nl: "Deze laag kan niet onzichtbaar gemaakt worden in het filtermenu. Overschrijf `name` indien je dit wel wilt.", + }), + ) + } + } + if (this.mapRendering.length === 0) { + extraProps.push( + new Translation({ + en: "Not rendered on the map by default.", + nl: "Deze laag wordt standaard niet weergegeven op de kaart.", + }), + ) + } - for (const revDep of Lists.dedup(layerIsNeededBy?.get(this.id) ?? [])) { - extraProps.push( - ["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join( - " " + } else { + extraProps.push( + new Translation({ + en: "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.", + nl: "Deze laag kan **niet** aan een kaartthema toegevoegd worden. Deze laag wordt enkel en alleen gebruikt om een [speciale rendering](SpecialRenderings.md) te ondersteunen. ", + }), ) - ) + } + + + for (const dep of dependencies) { + extraProps.push( + new TypedTranslation<{ neededLayer, reason, context? }>({ + en: "This layer will automatically load [`{neededLayer}`](./{neededLayer}.md) into the theme as it depends on it: {reason} ({context??no context given})", + nl: "Deze laag laadt automatisch de laag [`{neededLayer}`](./{neededLayer}.md) in het kaartthema want deze laag steunt hierop: {reason} ({context??geen context gekend})", + + }).Subs(dep), + ) + } + for (const revDep of Lists.dedup(layerIsNeededBy?.get(this.id) ?? [])) { + extraProps.push( + new TypedTranslation<{ revDep }>({ + en: "This layer is needed as dependency for layer [`{revDep}`](#{revDep})", + }).Subs({ revDep }), + ) + } + + + add(MarkdownUtils.list(extraProps.map(tr => tr.textFor(lang)))) + } - const tableRows: string[][] = Lists.noNull( - this.tagRenderings + // Overview of what themes use this layer + { + if (!addedByDefault) { + if (usedInThemes?.length > 0) { + add( + MarkdownUtils.title(3, + new Translation({ + en: "Themes using this layer", + nl: "Kaartthemas die deze laag gebruiken", + })), + ) + add( + MarkdownUtils.list( + (usedInThemes ?? []).map((id) => `[\`${id}\`](https://mapcomplete.org/${id})`), + ), + ) + } else if (this.source !== null) { + add({ + en: "No themes use this layer", + nl: "Geen enkel kaartthema gebruikt deze laag", + }) + } + } + } + + // Data source information + { + paragraphs.push(MarkdownUtils.title(3, new Translation({ en: "Data source", nl: "Databron" }))) + + if (this.source?.geojsonSource) { + paragraphs.push( + new TypedTranslation<{ geojsonSource? }>({ + en: "⚠️ This layer is loaded from an external source, namely `{geojsonSource}`", + nl: "⚠️ Deze laag wordt van een externe bron geladen, namelijk `{geojsonSource}`", + }, + ).Subs(this.source), + ) + } else if (!this.source) { + add({ + en: "This is a special layer, probably a library layer or support layer for MapComplete", + nl: "Dit is een speciale laag, waarschijnlijk een bibliotheeklaag of een ondersteunende laag", + }) + + } else { + const neededTags = this.source.osmTags.optimize() + if (neededTags["and"]) { + add( + { + en: "Elements on this layer match **all** of the following expressions:", + nl: "Elementen in deze laag hebben **alle** van de volgende kenmerken:", + + }, + ) + const parts = neededTags["and"] + add( + parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"), + ) + } else if (neededTags["or"]) { + const parts = neededTags["or"] + add( + { + en: "Elements on this layer match **any** of the following expressions:", + nl: "Elementen in deze laag hebben **minstens één** van de volgende kenmerken:", + + }) + add( + parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"), + ) + } else { + add({ + en: "Elements on this layer match the following expression:", + nl: "Elementen in deze laag hebben de volgende kenmerken:", + + }) + add( + neededTags.asHumanString(true, false, {}), + ) + } + + + const link = Overpass.AsOverpassTurboLink(this.source.osmTags.optimize()) + .replaceAll("(", "%28") + .replaceAll(")", "%29") + const txt = new TypedTranslation<{ link }>( + { + en: "Execute on overpass-turbo.eu", + nl: "Uitvoeren op overpass-turbo.eu", + }, + ).textFor(lang) + paragraphs.push(`[🗺️ ${txt}](${link})`) + } + + } + + // Presets + { + if (this.presets.length > 0) { + add(new Translation({ + en: "## Presets", + nl: "## Nieuwe punten toevoegen", + })) + + add(new Translation({ + en: "The following options to create new points are included:", + nl: "De volgende opties bestaan om nieuwe punten toe te voegen:", + })) + + const hTags = new Translation({ + en: "Used tags", + nl: "Gebruikte tags", + }) + const hTitle = new Translation({ + en: "Title", + nl: "Titel", + }) + const hSnaps = new Translation({ + en: "Snaps to layers", + nl: "Klikt vast aan lagen", + }) + const hDescription = new Translation({ + en: "Description", + nl: "Beschrijving", + }) + + add(MarkdownUtils.table( + [ + hTags, + hTitle, + hSnaps, + hDescription, + ].map(t => t.textFor(lang)), + this.presets.map(preset => { + let category = preset.title.textFor(lang) + category = "**" + category + "**" + return [ + new And(preset.tags).asHumanString(true), + Translations.t.general.add.addNew.Subs({ category }).textFor(lang), + preset.preciseInput?.snapToLayers + ?.map((id) => `[\`${id}\`](./Layers/${id}.md)`) + ?.join(", "), + preset.description?.textFor(lang), + ] + }), + { dropEmptyColumns: true }, + )) + + } else { + add({ + en: "This layer does not allowing adding new points", + nl: "Deze laag laat niet toe om nieuwe punten toe te voegen", + }) + } + } + + // Supported attributes table + { + const keyValues = (Lists.noNull(this.tagRenderings .map((tr) => tr.FreeformValues()) .filter((values) => values !== undefined) - .filter((values) => values.key !== "id") - .map((values) => { + .filter((values) => values.key !== "id"))) + keyValues.sort((a, b) => a.key < b.key ? -1 : 1) + if (keyValues.length > 0) { + add(MarkdownUtils.title(2, new Translation({ + en: "Attribute overview", + nl: "Attributenoverzicht", + }))) + add({ + en: "This table gives an overview of most OpenStreetMap [keys](https://wiki.openstreetmap.org/wiki/Tags) that this layer shows and/or edits", + nl: "Deze tabel geeft een overzicht van de meeste OpenStreetMap [sleutels](https://wiki.openstreetmap.org/wiki/Tags) die deze laag toont en/of aanpast", + }) + const tableRows: string[][] = keyValues.map((values) => { const embedded: string[] = values.values?.map((v) => - OsmWiki.constructLinkMd(values.key, v) + OsmWiki.constructLinkMd(values.key, v), ) ?? ["_no preset options defined, or no values in them_"] const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent( - values.key + values.key, )}/` const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values` return [ [ - ``, - ``, + `[🔎](${tagInfo})`, + `[📈](${statistics})`, OsmWiki.constructLinkMd(values.key), ].join(" "), values.type === undefined - ? "Multiple choice" + ? "" : `[${values.type}](../SpecialInputElements.md#${values.type})`, embedded.join(" "), ] }) - ) - let quickOverview: string[] = [] - if (tableRows.length > 0) { - quickOverview = [ - "**Warning:**: this quick overview is incomplete", + const header: { en: string, nl: string }[] = [ + { en: "Key", nl: "OSM-sleutel" }, { + en: "Type (for freeform input)", + nl: "Inputtype", + }, { en: "Predefined, supported options", nl: "Voorgedefinieerde, ondersteunde opties" }, + ] + + add( MarkdownUtils.table( - ["attribute", "type", "values which are supported by this layer"], + header.map(tr => new Translation(tr).textFor(lang)), tableRows - ), - ] + ) + ) } - let overpassLink: string = undefined - if (this.source !== undefined) { - try { - overpassLink = - "[Execute on overpass](" + - Overpass.AsOverpassTurboLink(this.source.osmTags.optimize()) - .replaceAll("(", "%28") - .replaceAll(")", "%29") + - ")" - } catch (e) { - console.error("Could not generate overpasslink for " + this.id) + } + + + // Elements in the popup + { + add(MarkdownUtils.title(2, new Translation({ + en: "Overview of questions (and other elements) in the popup", + nl: "Overzicht van vragen (en andere elementen) in de popup", + }))) + for (const tagRendering of this.tagRenderings) { + if (tagRendering.labels.indexOf("ignore_docs") >= 0) { + continue + } + add(tagRendering.generateDocumentation( + this.id, + lang, + reusedTagRenderings?.get(tagRendering.id)?.map((l) => l.layer), + )) } } - const filterDocs: string[] = [] + // Filters if (this.filters.length > 0) { - filterDocs.push("## Filters") - filterDocs.push(...this.filters.map((filter) => filter.GenerateDocs())) - } - - const tagsDescription: string[] = [] - if (this.source !== null) { - tagsDescription.push("## Basic tags for this layer") - - const neededTags = this.source.osmTags.optimize() - if (neededTags["and"]) { - const parts = neededTags["and"] - tagsDescription.push( - "Elements must match **all** of the following expressions:", - parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n") - ) - } else if (neededTags["or"]) { - const parts = neededTags["or"] - tagsDescription.push( - "Elements must match **any** of the following expressions:", - parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n") - ) - } else { - tagsDescription.push( - "Elements must match the expression **" + - neededTags.asHumanString(true, false, {}) + - "**" - ) + add(MarkdownUtils.title(2, new Translation({ + en: "Filters", + nl: "Filters" + }))) + for (const filter of this.filters) { + add(filter.generateDocs({lang})) } - - tagsDescription.push(overpassLink) - } else { - tagsDescription.push("This is a special layer - data is not sourced from OpenStreetMap") } - return [ - [ - "# " + this.id + "\n", - this._basedOn - ? `This layer is based on [${this._basedOn}](../Layers/${this._basedOn}.md)` - : "", - this.description.textFor(lang), - "\n", - ].join("\n\n"), - MarkdownUtils.list(extraProps), - ...usingLayer, - ...presets, - ...tagsDescription, - "## Supported attributes", - ...quickOverview, - "## Featureview elements and TagRenderings", - this.generateDocumentationQuickTable(), - ...this.tagRenderings - .filter((tr) => tr.labels.indexOf("ignore_docs") < 0) - .map((tr) => - tr.generateDocumentation( - this.id, - lang, - reusedTagRenderings?.get(tr.id)?.map((l) => l.layer) - ) - ), - ...filterDocs, - ].join("\n\n") + + return (Lists.noEmpty(Lists.noNull(paragraphs).map(p => typeof p === "string" ? p : p.textFor(lang)))).join("\n\n") } public CustomCodeSnippets(): string[] { diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index 5888cebc0..6c70badf0 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -24,6 +24,7 @@ import { Unit } from "../Unit" import { Lists } from "../../Utils/Lists" import { IsOnline } from "../../Logic/Web/IsOnline" import SubstitutingTag from "../../Logic/Tags/SubstitutingTag" +import { Strings } from "../../Utils/Strings" export interface Mapping { readonly if: UploadableTag @@ -753,6 +754,8 @@ export default class TagRenderingConfig { * Given a value for the freeform key and an overview of the selected mappings, construct the correct tagsFilter to apply. * Result should be interpreted as "and" * + * If this matches 'invalidValues', will return undefined + * * const config = new TagRenderingConfig({"id":"bookcase-booktypes","render":{"en":"This place mostly serves {books}" }, * "question":{"en":"What kind of books can be found in this public bookcase?"}, * "freeform":{"key":"books","addExtraTags":["fixme=Freeform tag `books` used, to be doublechecked"], @@ -959,17 +962,12 @@ export default class TagRenderingConfig { this.description] if (this.question === undefined) { - paragraphs.push(new Translation({ + paragraphs.push(MarkdownUtils.quote(new Translation({ en: "_This tagrendering has no question and is thus read-only_", nl: "_Deze tagRendering heeft geen vraag en wordt dus enkel weergegeven_", - })) + }))) } else { - paragraphs.push(new TypedTranslation<{ question }>( - { - en: "If no attribute matches, the question *{question}* will be asked", - nl: "Indien geen attribuut overeenkomt, wordt *{question}* gevraagd", - }, - ).Subs({ question: this.question.textFor(lang) })) + paragraphs.push(MarkdownUtils.quote(this.question)) } if (this.render) { @@ -1017,10 +1015,13 @@ export default class TagRenderingConfig { this.mappings.map((m) => { let icon = "" if (m.icon?.indexOf(";") < 0) { - icon = - "" + } } const msgs: Translation[] = [ new TypedTranslation<{ icon, then, cond }>({ @@ -1064,6 +1065,18 @@ export default class TagRenderingConfig { ).Subs({ conditionAsLink })) } + if (this.invalidValues) { + paragraphs.push( + new Translation({ + en: "This tagRendering has some values that are not valid and cannot be entered. The following values are _not_ accepted:", + nl: "Deze tagRendering heeft waardes die als ongeldig beschouwd worden en _niet_ ingevoerd kunnen worden:", + }), + ) + paragraphs.push( + "❌ " + this.invalidValues.asHumanString(true), + ) + } + if (this.labels?.length > 0) { paragraphs.push(new Translation({ en: "This tagRendering has the following labels:", diff --git a/src/Utils/MarkdownUtils.ts b/src/Utils/MarkdownUtils.ts index 7b483b541..5fbadee3b 100644 --- a/src/Utils/MarkdownUtils.ts +++ b/src/Utils/MarkdownUtils.ts @@ -1,3 +1,6 @@ +import { Translation } from "../UI/i18n/Translation" +import { Utils } from "../Utils" + export default class MarkdownUtils { public static table( header: string[], @@ -41,4 +44,13 @@ export default class MarkdownUtils { } return "\n\n" + strings.map((item) => " - " + item).join("\n") + "\n\n" } + + static quote(translation: Translation) { + return translation.OnEveryLanguage(s => "> "+s) + } + + static title(number: number, translation: Translation) { + const t = Utils.times(() => "#", number)+" " + return translation.OnEveryLanguage(l => t+ l) + } }