forked from MapComplete/MapComplete
961 lines
39 KiB
TypeScript
961 lines
39 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
|
import { AllKnownLayouts, AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts"
|
|
import QueryParameterDocumentation from "../src/UI/QueryParameterDocumentation"
|
|
import ScriptUtils from "./ScriptUtils"
|
|
import Translations from "../src/UI/i18n/Translations"
|
|
import themeOverview from "../src/assets/generated/theme_overview.json"
|
|
import ThemeConfig from "../src/Models/ThemeConfig/ThemeConfig"
|
|
import fakedom from "fake-dom"
|
|
import unit from "../public/assets/generated/layers/unit.json"
|
|
import { QueryParameters } from "../src/Logic/Web/QueryParameters"
|
|
import Constants from "../src/Models/Constants"
|
|
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
|
|
import DependencyCalculator from "../src/Models/ThemeConfig/DependencyCalculator"
|
|
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
|
|
import questions from "../public/assets/generated/layers/questions.json"
|
|
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
|
|
import Script from "./Script"
|
|
import TableOfContents from "../src/UI/Base/TableOfContents"
|
|
import MarkdownUtils from "../src/Utils/MarkdownUtils"
|
|
import { parse as parse_html } from "node-html-parser"
|
|
import { AvailableRasterLayers } from "../src/Models/RasterLayers"
|
|
import * as unitUsage from "../Docs/Schemas/UnitConfigJson.schema.json"
|
|
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"
|
|
import { Changes } from "../src/Logic/Osm/Changes"
|
|
import SimpleMetaTaggers from "../src/Logic/SimpleMetaTagger"
|
|
import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
|
|
import Validators from "../src/UI/InputElement/Validators"
|
|
import { TagUtils } from "../src/Logic/Tags/TagUtils"
|
|
import SpecialVisualizations from "../src/UI/SpecialVisualizations"
|
|
|
|
/**
|
|
* Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use
|
|
*
|
|
* These are used in the studio
|
|
*/
|
|
class ToSlideshowJson {
|
|
private readonly _source: string
|
|
private readonly _target: string
|
|
|
|
constructor(source: string, target: string) {
|
|
this._source = source
|
|
this._target = target
|
|
}
|
|
|
|
public convert() {
|
|
const lines = readFileSync(this._source, "utf8")
|
|
.split("\n")
|
|
|
|
const sections: string[][] = []
|
|
let currentSection: string[] = []
|
|
for (let line of lines) {
|
|
if (line.trim().startsWith("# ")) {
|
|
sections.push(currentSection)
|
|
currentSection = []
|
|
}
|
|
line = line.replaceAll('src="https://mapcomplete.org/', 'src="./')
|
|
line = line.replaceAll('src="../../public/', 'src="./')
|
|
line = line.replaceAll('src="../../', 'src="./')
|
|
currentSection.push(line)
|
|
}
|
|
sections.push(currentSection)
|
|
writeFileSync(
|
|
this._target,
|
|
JSON.stringify({
|
|
sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0),
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a wiki page with the theme overview
|
|
* The wikitable should be updated regularly as some tools show an overview of apps based on the wiki.
|
|
*/
|
|
class WikiPageGenerator {
|
|
private readonly _target: string
|
|
|
|
constructor(target: string = "Docs/wikiIndex.txt") {
|
|
this._target = target
|
|
}
|
|
|
|
generate() {
|
|
let wikiPage =
|
|
'{|class="wikitable sortable"\n' +
|
|
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
|
|
"|-"
|
|
|
|
for (const layout of themeOverview.themes) {
|
|
if (layout.hideFromOverview) {
|
|
continue
|
|
}
|
|
wikiPage += "\n" + this.generateWikiEntryFor(layout)
|
|
}
|
|
|
|
wikiPage += "\n|}"
|
|
|
|
writeFileSync(this._target, wikiPage)
|
|
}
|
|
|
|
private generateWikiEntryFor(layout: {
|
|
hideFromOverview: boolean
|
|
id: string
|
|
shortDescription: any
|
|
}): string {
|
|
if (layout.hideFromOverview) {
|
|
return ""
|
|
}
|
|
|
|
const languagesInDescr = Array.from(Object.keys(layout.shortDescription)).filter(
|
|
(k) => k !== "_context"
|
|
)
|
|
const languages = languagesInDescr.map((ln) => `{{#language:${ln}|en}}`).join(", ")
|
|
const auth = "Yes"
|
|
return `{{service_item
|
|
|name= [https://mapcomplete.org/${layout.id} ${layout.id}]
|
|
|region= Worldwide
|
|
|lang= ${languages}
|
|
|descr= A MapComplete theme: ${Translations.T(layout.shortDescription)
|
|
.textFor("en")
|
|
.replace("<a href='", "[[")
|
|
.replace(/'>.*<\/a>/, "]]")}
|
|
|material= {{yes|[https://mapcomplete.org/ ${auth}]}}
|
|
|image= MapComplete_Screenshot.png
|
|
|genre= POI, editor, ${layout.id}
|
|
}}`
|
|
}
|
|
}
|
|
|
|
export class GenerateDocs extends Script {
|
|
private generatedPaths: string[] = []
|
|
|
|
constructor() {
|
|
super("Generates various documentation files")
|
|
}
|
|
|
|
async main(args: string[]) {
|
|
console.log("Starting documentation generation...")
|
|
ScriptUtils.fixUtils()
|
|
{
|
|
// For studio: prepare slideshow
|
|
new ToSlideshowJson(
|
|
"./Docs/Studio/Introduction.md",
|
|
"./src/assets/studio_introduction.json"
|
|
).convert()
|
|
new ToSlideshowJson(
|
|
"./Docs/Studio/TagRendering_How_to_work_with_TagRenderings.md",
|
|
"./src/assets/studio_tagrenderings_intro.json"
|
|
).convert()
|
|
}
|
|
|
|
// Files for studio/for theme creators
|
|
{
|
|
this.generateSpecialLayerOverviewText()
|
|
this.generateBuiltinIndex()
|
|
this.generateBuiltinQuestions()
|
|
this.generateEliDocs()
|
|
this.generateBuiltinUnits()
|
|
this.writeMarkdownFile("./Docs/Studio/SpecialInputElements.md", Validators.HelpText(), [
|
|
"src/UI/InputElement/Validators.ts"
|
|
])
|
|
this.writeMarkdownFile("./Docs/Studio/Tags_format.md", TagUtils.generateDocs(), [
|
|
"src/Logic/Tags/TagUtils.ts"
|
|
])
|
|
|
|
this.writeMarkdownFile(
|
|
"./Docs/Studio/SpecialRenderings.md",
|
|
SpecialVisualizations.HelpMessage(),
|
|
["src/UI/SpecialVisualizations.ts"],
|
|
{
|
|
tocMaxDepth: 3
|
|
}
|
|
)
|
|
this.writeMarkdownFile(
|
|
"./Docs/Studio/CalculatedTags.md",
|
|
["# Metatags", SimpleMetaTaggers.HelpText(), ExtraFunctions.HelpText()].join("\n"),
|
|
["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"],
|
|
{ noTableOfContents: false }
|
|
)
|
|
}
|
|
|
|
|
|
// For dev
|
|
{
|
|
this.generateQueryParameterDocs()
|
|
await this.generateSourcesOverview()
|
|
}
|
|
|
|
// For layer+theme overview
|
|
{
|
|
if (!existsSync("./Docs/Themes")) {
|
|
mkdirSync("./Docs/Themes")
|
|
}
|
|
this.generateOverviewsForAllSingleLayer()
|
|
this.generateNormalLayerOverview("Layers")
|
|
this.generateNormalLayerOverview("Layers", "nl")
|
|
this.generateNormalLayerOverview("Themes")
|
|
this.generateNormalLayerOverview("Themes", "nl")
|
|
|
|
if (!existsSync("./Docs/nl")) {
|
|
mkdirSync("./Docs/nl")
|
|
}
|
|
Array.from(AllKnownLayouts.allKnownLayouts.values()).map((theme) => {
|
|
this.generateForTheme(theme)
|
|
this.generateForTheme(theme, { path: "./Docs/nl/Themes", lang: "nl" })
|
|
ScriptUtils.erasableLog("Written docs for theme", theme.id)
|
|
})
|
|
|
|
|
|
this.generateOverviewsForAllSingleLayer("./Docs/nl/Layers", "nl")
|
|
}
|
|
|
|
this.writeMarkdownFile("./Docs/ChangesetMeta.md", Changes.getDocs(), [
|
|
"src/Logic/Osm/Changes.ts",
|
|
"src/Logic/Osm/ChangesetHandler.ts"
|
|
])
|
|
|
|
new WikiPageGenerator().generate()
|
|
this.generatedPaths.push("wikiIndex.txt")
|
|
|
|
this.generateSidebar() // Must be last as it inspects the generated markdown files
|
|
this.generateSidebar("nl")
|
|
|
|
this.generatedPaths.push(".gitignore")
|
|
writeFileSync("./Docs/.gitignore", this.generatedPaths
|
|
.map(p => p.replace("./Docs/", ""))
|
|
.join("\n"), "utf-8")
|
|
|
|
console.log("Generated docs")
|
|
}
|
|
|
|
private writeMarkdownFile(
|
|
filename: string,
|
|
markdown: string,
|
|
autogenSource: string[],
|
|
options?: {
|
|
noTableOfContents?: boolean
|
|
tocMaxDepth?: number
|
|
lang?: string,
|
|
noWarn?: boolean
|
|
}
|
|
): void {
|
|
const lang = options?.lang ?? "en"
|
|
for (const source of autogenSource) {
|
|
if (source.indexOf("*") > 0) {
|
|
continue
|
|
}
|
|
if (!existsSync(source)) {
|
|
throw (
|
|
"While creating a documentation file and checking that the generation sources are properly linked: source file " +
|
|
source +
|
|
" was not found. Typo?"
|
|
)
|
|
}
|
|
}
|
|
|
|
let md = markdown
|
|
|
|
if (options?.noTableOfContents !== true) {
|
|
md = TableOfContents.insertTocIntoMd(md, lang, options?.tocMaxDepth)
|
|
}
|
|
|
|
md = md.replace(/\n\n\n+/g, "\n\n")
|
|
md = md.replace("<script", "<script")
|
|
|
|
if (!md.endsWith("\n")) {
|
|
md += "\n"
|
|
}
|
|
|
|
const warnAutomated = options?.noWarn ? "" :
|
|
"[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)\n\n"
|
|
|
|
|
|
const sources = autogenSource.map(
|
|
(s) => `[${s}](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/${s})`).join(", ")
|
|
|
|
const generatedFrom =
|
|
new TypedTranslation<{ sources, date }>({
|
|
en: "This document is autogenerated from {sources} on {date}",
|
|
nl: "Dit document werd gegenereerd op basis van {sources} op {date}"
|
|
}).Subs({ sources, date: new Date().toDateString() }).textFor(lang)
|
|
|
|
|
|
writeFileSync(filename, warnAutomated + md + (options?.noWarn ? "" : "\n\n" + generatedFrom + "\n"))
|
|
this.generatedPaths.push(filename)
|
|
}
|
|
|
|
|
|
private generateEliDocs() {
|
|
const eli = AvailableRasterLayers.editorLayerIndex()
|
|
this.writeMarkdownFile(
|
|
"./Docs/Studio/ELI-overview.md",
|
|
[
|
|
"# Layers in the Editor Layer Index",
|
|
"This table gives a summary of ids, names and other metainformation of background imagery that is available in MapComplete and that can be used as (default) map background." +
|
|
"These are sourced from [the Editor Layer Index](https://github.com/osmlab/editor-layer-index)", +
|
|
"\n[See the online, interactive map here](https://osmlab.github.io/editor-layer-index/)",
|
|
MarkdownUtils.table(
|
|
["id", "name", "category", "Best", "attribution"],
|
|
eli.map((f) => [
|
|
f.properties.id,
|
|
f.properties.name,
|
|
f.properties.category,
|
|
f.properties.best ? "⭐" : "",
|
|
f.properties.attribution?.html ?? f.properties.attribution?.text,
|
|
]),
|
|
),
|
|
].join("\n\n"),
|
|
["./public/assets/data/editor-layer-index.json"],
|
|
)
|
|
}
|
|
|
|
private generateBuiltinUnits() {
|
|
const layer = new LayerConfig(<LayerConfigJson>(<unknown>unit), "units", true)
|
|
const els: string[] = [
|
|
"# Units",
|
|
"## How to use",
|
|
unitUsage.description,
|
|
"Units ",
|
|
"## " + layer.id,
|
|
]
|
|
for (const unit of layer.units) {
|
|
els.push("### " + unit.quantity)
|
|
const defaultUnit = unit.getDefaultDenomination(() => undefined)
|
|
for (const denomination of unit.denominations) {
|
|
els.push("#### " + denomination.canonical)
|
|
if (denomination.validator) {
|
|
els.push(`Validator is *${denomination.validator.name}*`)
|
|
}
|
|
|
|
if (denomination.factorToCanonical) {
|
|
els.push(
|
|
`1${denomination.canonical} = ${denomination.factorToCanonical}${defaultUnit.canonical}`
|
|
)
|
|
}
|
|
|
|
if (denomination.useIfNoUnitGiven === true) {
|
|
els.push("*Default denomination*")
|
|
} else if (
|
|
denomination.useIfNoUnitGiven &&
|
|
denomination.useIfNoUnitGiven.length > 0
|
|
) {
|
|
els.push("Default denomination in the following countries:")
|
|
els.push(MarkdownUtils.list(denomination.useIfNoUnitGiven))
|
|
}
|
|
if (denomination.prefix) {
|
|
els.push("Prefixed")
|
|
}
|
|
if (denomination.alternativeDenominations.length > 0) {
|
|
els.push(
|
|
"Alternative denominations:",
|
|
MarkdownUtils.list(denomination.alternativeDenominations)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.writeMarkdownFile("./Docs/Studio/builtin_units.md", els.join("\n\n"), [
|
|
`assets/layers/unit/unit.json`,
|
|
`src/Models/ThemeConfig/Json/UnitConfigJson.ts`,
|
|
])
|
|
}
|
|
|
|
/**
|
|
* Generates documentation for the all the individual layers.
|
|
* Inline layers are included (if the theme is public)
|
|
*/
|
|
private generateOverviewsForAllSingleLayer(targetDirectory: string = "./Docs/Layers", lang: string = "en"): void {
|
|
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
|
|
(layer) => layer["source"] !== null
|
|
)
|
|
const qLayer = new LayerConfig(
|
|
<LayerConfigJson>(<unknown>questions),
|
|
"questions.json",
|
|
true
|
|
)
|
|
allLayers.push(qLayer)
|
|
const builtinLayerIds: Set<string> = new Set<string>()
|
|
allLayers.forEach((l) => builtinLayerIds.add(l.id))
|
|
const inlineLayers = new Map<string, string>()
|
|
|
|
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
|
|
if (layout.hideFromOverview) {
|
|
continue
|
|
}
|
|
|
|
for (const layer of layout.layers) {
|
|
if (layer.source === null) {
|
|
continue
|
|
}
|
|
if (builtinLayerIds.has(layer.id)) {
|
|
continue
|
|
}
|
|
if (layer.source.geojsonSource !== undefined) {
|
|
// Not an OSM-source
|
|
continue
|
|
}
|
|
allLayers.push(layer)
|
|
builtinLayerIds.add(layer.id)
|
|
inlineLayers.set(layer.id, layout.id)
|
|
}
|
|
}
|
|
|
|
const themesPerLayer = new Map<string, string[]>()
|
|
|
|
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
|
|
for (const layer of layout.layers) {
|
|
if (!builtinLayerIds.has(layer.id)) {
|
|
// This is an inline layer
|
|
continue
|
|
}
|
|
if (!themesPerLayer.has(layer.id)) {
|
|
themesPerLayer.set(layer.id, [])
|
|
}
|
|
themesPerLayer.get(layer.id).push(layout.id)
|
|
}
|
|
}
|
|
|
|
// Determine the cross-dependencies
|
|
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
|
|
|
|
for (const layer of allLayers) {
|
|
for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
|
|
const dependency = dep.neededLayer
|
|
if (!layerIsNeededBy.has(dependency)) {
|
|
layerIsNeededBy.set(dependency, [])
|
|
}
|
|
layerIsNeededBy.get(dependency).push(layer.id)
|
|
}
|
|
}
|
|
if (!existsSync(targetDirectory)) {
|
|
mkdirSync(targetDirectory)
|
|
}
|
|
allLayers.forEach((layer) => {
|
|
const element = layer.generateDocumentation({
|
|
usedInThemes: themesPerLayer.get(layer.id),
|
|
layerIsNeededBy: layerIsNeededBy,
|
|
dependencies: DependencyCalculator.getLayerDependencies(layer),
|
|
lang
|
|
}).replaceAll("./Docs/Layers", targetDirectory)
|
|
const inlineSource = inlineLayers.get(layer.id)
|
|
ScriptUtils.erasableLog("Exporting layer documentation for", layer.id)
|
|
let source: string = `assets/layers/${layer.id}/${layer.id}.json`
|
|
if (inlineSource !== undefined) {
|
|
source = `assets/themes/${inlineSource}/${inlineSource}.json`
|
|
}
|
|
this.writeMarkdownFile(targetDirectory + "/" + layer.id + ".md", element, [source], { lang })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Generate the builtinIndex which shows interlayer dependencies
|
|
* @private
|
|
*/
|
|
|
|
private generateBuiltinIndex() {
|
|
const layers = ScriptUtils.getLayerFiles().map((f) => f.parsed)
|
|
const builtinsPerLayer = new Map<string, string[]>()
|
|
const layersUsingBuiltin = new Map<string /* Builtin */, string[]>()
|
|
for (const layer of layers) {
|
|
if (layer.tagRenderings === undefined) {
|
|
continue
|
|
}
|
|
const usedBuiltins: string[] = []
|
|
for (const tagRendering of layer.tagRenderings) {
|
|
if (typeof tagRendering === "string") {
|
|
usedBuiltins.push(tagRendering)
|
|
continue
|
|
}
|
|
if (tagRendering["builtin"] !== undefined) {
|
|
const builtins = tagRendering["builtin"]
|
|
if (typeof builtins === "string") {
|
|
usedBuiltins.push(builtins)
|
|
} else {
|
|
usedBuiltins.push(...builtins)
|
|
}
|
|
}
|
|
}
|
|
for (const usedBuiltin of usedBuiltins) {
|
|
const usingLayers = layersUsingBuiltin.get(usedBuiltin)
|
|
if (usingLayers === undefined) {
|
|
layersUsingBuiltin.set(usedBuiltin, [layer.id])
|
|
} else {
|
|
usingLayers.push(layer.id)
|
|
}
|
|
}
|
|
|
|
builtinsPerLayer.set(layer.id, usedBuiltins)
|
|
}
|
|
|
|
const docs: string[] = [
|
|
"# Which tagrendering is used where?", "",
|
|
"This document details where a tagRendering from one layer is reused in another layer, either by directly using it or by using a `{\"builtin\": id, \"override\": ...}` syntax",
|
|
"Having this overview supports e.g. refactoring efforts",
|
|
"## Existing builtin tagrenderings", ""]
|
|
|
|
for (const [builtin, usedByLayers] of Array.from(layersUsingBuiltin.entries())) {
|
|
docs.push(`### ${builtin}\n`)
|
|
docs.push(usedByLayers.length + " usages")
|
|
docs.push(`${usedByLayers.map((item) => ` - [${item}](./Docs/Layers/${item}.md)`).join("\n")}`)
|
|
}
|
|
this.writeMarkdownFile("./Docs/Studio/TagRendering_reuse_overview.md", docs.join("\n"), ["assets/layers/*.json"])
|
|
}
|
|
|
|
private generateQueryParameterDocs() {
|
|
if (fakedom === undefined) {
|
|
throw "FakeDom not initialized"
|
|
}
|
|
QueryParameters.GetQueryParameter(
|
|
"mode",
|
|
"map",
|
|
"The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
|
|
)
|
|
|
|
this.writeMarkdownFile(
|
|
"./Docs/Dev/URL_Parameters.md",
|
|
QueryParameterDocumentation.GenerateQueryParameterDocs(),
|
|
["src/Logic/Web/QueryParameters.ts", "src/UI/QueryParameterDocumentation.ts"]
|
|
)
|
|
}
|
|
|
|
private generateBuiltinQuestions() {
|
|
const qLayer = new LayerConfig(
|
|
<LayerConfigJson>(<unknown>questions),
|
|
"questions.json",
|
|
true
|
|
)
|
|
const reusedTagRenderings = DependencyCalculator.tagRenderingImportedBy(
|
|
qLayer,
|
|
Array.from(AllSharedLayers.sharedLayers.values())
|
|
)
|
|
const docs = qLayer.generateDocumentation({ reusedTagRenderings })
|
|
this.writeMarkdownFile("./Docs/Studio/BuiltinQuestions.md", docs, [
|
|
"assets/layers/questions/questions.json",
|
|
])
|
|
}
|
|
|
|
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(<any>l.id) < 0
|
|
)
|
|
const lang = options?.lang ?? "en"
|
|
const layersToInline = layersToShow.filter((l) => !allLayers.has(l.id))
|
|
const el = [
|
|
[
|
|
"##",
|
|
theme.title.textFor(lang),
|
|
"(",
|
|
`[${theme.id}](https://mapcomplete.org/${theme.id})`,
|
|
")",
|
|
].join(" "),
|
|
"> " + parse_html(theme.description.textFor("en")).textContent.replace(/\n/g, " "),
|
|
"",
|
|
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} (${l.name?.textFor(lang)})](#${l.id.trim().replace(/ /g, "-")})`
|
|
})
|
|
),
|
|
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(l => `[${l.name?.textFor(lang) ?? ""} (\`${l.id}\`)](#${l.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"
|
|
if (!existsSync(path)) {
|
|
mkdirSync(path)
|
|
}
|
|
this.writeMarkdownFile(
|
|
path + "/" + theme.id + ".md",
|
|
el.join("\n"),
|
|
[`assets/themes/${theme.id}/${theme.id}.json`],
|
|
{ noTableOfContents: true, lang },
|
|
)
|
|
}
|
|
|
|
private async generateSourcesOverview() {
|
|
console.log("Generating the sources overview - this might take a bit")
|
|
const sources = await new SourceOverview().getOverview()
|
|
const md = [
|
|
"# Overview of used online services",
|
|
"This document list all the hosts that MapComplete might contact via 'fetch'-requests for various API services. We do our best to use FLOSS- and/or self-hostable services as much as possible.",
|
|
"Some of the core services (especially Panoramax and MapLibre) could be selfhosted, but MapComplete has no support for a user to configure a different host. Right now, the userbase is relatively small and there is little demand to make this configurable, but the technical cost for this is quite high. Someone who wishes to use a different service, is able to build a fork.",
|
|
"One service that is hard to replace, is [*Mapillary*](https://wiki.openstreetmap.org/wiki/Mapillary). It contains a vast trove of streetview data, but cannot be selfhosted and is currently owned by Meta Inc. We use this to find nearby images of features, but promote the use of [Panoramax](https://wiki.openstreetmap.org/wiki/Panoramax) instead (both by uploading contributions to our panoramax-instance and by lobbying in the community for this)",
|
|
]
|
|
|
|
const serverInfosDupl = <ServerSourceInfo[]>(
|
|
sources
|
|
.filter((s) => typeof s !== "string")
|
|
.filter(
|
|
(item) =>
|
|
typeof item === "string" ||
|
|
item.url.startsWith("https://") ||
|
|
item.url.startsWith("pmtiles://")
|
|
)
|
|
)
|
|
const serverInfos = Lists.dedupOnId(serverInfosDupl, (item) => item.url)
|
|
const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category)))
|
|
titles.sort()
|
|
|
|
function getHost(item: ServerSourceInfo) {
|
|
let url = item.url
|
|
if (url.startsWith("pmtiles://")) {
|
|
url = url.slice("pmtiles://".length)
|
|
}
|
|
const host = new URL(url).host.split(".")
|
|
|
|
return (host.at(-2) + "." + host.at(-1)).toLowerCase()
|
|
}
|
|
|
|
const categoryExplanation: Record<ServerSourceInfo["category"], string> = {
|
|
core: [
|
|
"Core features are always active and will be contacted as soon as you visit any map",
|
|
"### About displayed images",
|
|
"MapComplete will read the 'image' attribute from OpenStreetMap-data when a POI is opened and will attempt to display this image (and thus download it). Those 'images' can be spread all over the internet and thus leak the IP address of the visitor",
|
|
].join("\n\n"),
|
|
feature: "These are only enabled for certain maps or certain features",
|
|
maplayer: [
|
|
"MapLayers are integrated from the [Editor Layer Index](https://github.com/osmlab/editor-layer-index). A map layer listed here will only be contacted if the user decides to use this map background. This list changes over time, as new background layers are published and old background layers go offline. For all map layers, we have permission to use them to improve OpenStreetMap",
|
|
"A full listing can be found in [ELI-overview](ELI-overview.md)",
|
|
].join("\n\n"),
|
|
}
|
|
|
|
const seenUrls = new Set<string>()
|
|
for (const title of titles) {
|
|
md.push("## " + title)
|
|
const items = serverInfos.filter(
|
|
(info) => info.category.toLowerCase() === title.toLowerCase()
|
|
)
|
|
md.push(items.length + " items")
|
|
md.push(categoryExplanation[title])
|
|
|
|
const hosts = Lists.dedup(items.map(getHost))
|
|
hosts.sort()
|
|
if (title === "maplayer") {
|
|
md.push(MarkdownUtils.list(hosts))
|
|
continue
|
|
}
|
|
|
|
for (const host of hosts) {
|
|
md.push("### " + host)
|
|
const itemsForHost = items.filter((info) => getHost(info) === host)
|
|
const identicalDescription = itemsForHost.every(
|
|
(item) => item.description === itemsForHost[0].description
|
|
)
|
|
if (identicalDescription) {
|
|
md.push(itemsForHost[0].description)
|
|
}
|
|
const table = MarkdownUtils.table(
|
|
["source", "description", "license;selfhosting;more info"],
|
|
itemsForHost.map((item) => {
|
|
let selfHostable = ""
|
|
if (item.selfhostable) {
|
|
if (typeof item.selfhostable === "string") {
|
|
selfHostable = item.selfhostable
|
|
} else {
|
|
selfHostable = "self hostable"
|
|
}
|
|
}
|
|
let sourceAvailable = ""
|
|
if (item.sourceAvailable) {
|
|
if (typeof item.sourceAvailable === "string") {
|
|
sourceAvailable = item.sourceAvailable
|
|
} else {
|
|
sourceAvailable = "source available"
|
|
}
|
|
}
|
|
seenUrls.add(item.url)
|
|
return [
|
|
item.url,
|
|
identicalDescription ? "" : item.description,
|
|
Lists.noEmpty([
|
|
item.openData ? "OpenData" : "",
|
|
sourceAvailable,
|
|
selfHostable,
|
|
item.moreInfo?.join(" , "),
|
|
]).join(", "),
|
|
]
|
|
}),
|
|
{
|
|
dropEmptyColumns: true,
|
|
}
|
|
)
|
|
md.push(table)
|
|
}
|
|
}
|
|
|
|
md.push("## No category")
|
|
const urls: string[] = <string[]>(
|
|
sources.filter((s) => typeof s === "string" && !seenUrls.has(s))
|
|
)
|
|
md.push(urls.length + " items")
|
|
md.push(MarkdownUtils.list(urls))
|
|
|
|
this.writeMarkdownFile(
|
|
"./Docs/Dev/OnlineServicesOverview.md",
|
|
md.join("\n\n"),
|
|
["src/Models/SourceOverview.ts"],
|
|
{ tocMaxDepth: 2 }
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Generates the '_sidebar.md' file that is used by docsify
|
|
* Returns _all_ the filepaths (including ''hidden'' ones)
|
|
*/
|
|
private generateSidebar(subdirectory = ""): string[] {
|
|
const tr = Translations.t.app.back.textFor(subdirectory)
|
|
const sidebar: string[] = [
|
|
`<a href='https://mapcomplete.org' class='back-to-mc'>${tr}</a>`
|
|
]
|
|
const allFiles = ScriptUtils.readDirRecSync("./Docs/" + subdirectory)
|
|
.filter(path => path.endsWith(".md"))
|
|
.filter(path => !path.startsWith("_"))
|
|
.filter(path => !path.startsWith("./Docs/nl/") || (path.startsWith("./Docs/" + subdirectory) && subdirectory !== ""))
|
|
.map(path => path.substring("./Docs/".length + subdirectory.length + 1))
|
|
const perDirectory = new Map<string, string[]>()
|
|
|
|
function addFile(dir: string, path: string) {
|
|
const list = perDirectory.get(dir)
|
|
if (!list) {
|
|
perDirectory.set(dir, [path])
|
|
} else {
|
|
list.push(path)
|
|
}
|
|
}
|
|
|
|
for (const file of allFiles) {
|
|
const perDir = file.split("/")
|
|
if (perDir.length === 1) {
|
|
addFile("", perDir[0])
|
|
} else if (perDir.length === 2) {
|
|
const [dir, path] = perDir
|
|
addFile(dir, path)
|
|
} else {
|
|
const dir = perDir.shift()
|
|
const path = perDir.join("/")
|
|
addFile(dir, path)
|
|
}
|
|
}
|
|
|
|
// The directories to run over:
|
|
const directories: [string, Translation | string][] = [
|
|
["", ""],
|
|
["Layers", new Translation({ en: "Overview of layers", nl: "Overzicht van de lagen" })],
|
|
["Themes", new Translation({ en: "Overview of map themes", nl: "Overzicht van de themas" })],
|
|
["UserTests", "Usability tests with users"],
|
|
["Studio", "For theme builders"],
|
|
["Dev", "For developers"],
|
|
]
|
|
|
|
|
|
for (const [dir, title] of directories) {
|
|
if (title === null) {
|
|
continue
|
|
}
|
|
const files = perDirectory.get(dir)
|
|
if (!files) {
|
|
console.error("No directory for " + dir)
|
|
continue
|
|
}
|
|
if (dir !== "") {
|
|
let titleStr = title
|
|
if (typeof titleStr !== "string") {
|
|
titleStr = titleStr.textFor(subdirectory)
|
|
}
|
|
sidebar.push(`\n\n [**${titleStr}**](${dir}/README.md)`)
|
|
}
|
|
if (dir === "Layers" || dir == "Themes") {
|
|
continue
|
|
}
|
|
for (const path of files) {
|
|
if (path.startsWith("_") || path.endsWith("README.md")) {
|
|
continue
|
|
}
|
|
const shown = path.substring(0, path.length - 3).replaceAll("_", " ")
|
|
if (dir === "") {
|
|
sidebar.push(`- [${shown}](${encodeURIComponent(path)})`)
|
|
} else {
|
|
sidebar.push(` + [${shown}](${encodeURIComponent(dir + "/" + path)})`)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.writeMarkdownFile("./Docs/" + subdirectory + "/_sidebar.md", sidebar.join("\n"), [], {
|
|
noTableOfContents: true,
|
|
noWarn: true,
|
|
})
|
|
const scriptPath = `./Docs/${subdirectory}/_paths.js`
|
|
writeFileSync(scriptPath, "var docsify_paths = " + JSON.stringify(allFiles))
|
|
this.generatedPaths.push(scriptPath)
|
|
|
|
return allFiles
|
|
}
|
|
|
|
private generateNormalLayerOverview(type: "Layers" | "Themes", subdir = "") {
|
|
|
|
const layerinfo: [string, string, string][] = []
|
|
const source: ReadonlyMap<string, LayerConfig> | AllKnownLayoutsLazy
|
|
= type === "Layers" ? AllSharedLayers.sharedLayers : AllKnownLayouts.allKnownLayouts
|
|
const keys = Array.from(source.keys())
|
|
keys.sort()
|
|
|
|
for (const id of keys) {
|
|
const layer = source.get(id)
|
|
let name: Translation
|
|
if (type === "Layers") {
|
|
const layer_ = (<LayerConfig>layer)
|
|
if (!layer_.isNormal()) {
|
|
continue
|
|
}
|
|
name = layer_.name
|
|
} else {
|
|
name = (<ThemeConfig>layer).title
|
|
}
|
|
layerinfo.push([`[${id}](./${type}/${id})`, name.textFor(subdir), (layer["shortDescription"] ?? layer.description)?.textFor(subdir)])
|
|
}
|
|
|
|
const titles = {
|
|
"Layers": new Translation({ en: "Layers", nl: "Lagen" }),
|
|
"Themes": new Translation({ en: "Themes", nl: "Kaartthema's" })
|
|
}
|
|
|
|
const intro: Record<string, TypedTranslation<{ version }>> = {
|
|
"Layers": new TypedTranslation<{ version }>({
|
|
en: "The following layers are available in MapComplete {version}:",
|
|
nl: "De volgende lagen zijn beschikbaar in MapComplete {version}:"
|
|
}),
|
|
"Themes": new TypedTranslation<{ version }>({
|
|
en: "The following themes are available in MapComplete {version}:",
|
|
nl: "De volgende kaartthemas zijn beschikbaar in MapComplete {version}:"
|
|
})
|
|
}
|
|
|
|
const doc = ["# " + titles[type].textFor(subdir),
|
|
intro[type].Subs({ version: Constants.vNumber }).textFor(subdir)
|
|
, MarkdownUtils.table(
|
|
["id", "name", "description"],
|
|
layerinfo)
|
|
]
|
|
const path = `./Docs/${subdir}/${type}`
|
|
if (!existsSync(path)) {
|
|
mkdirSync(path)
|
|
}
|
|
this.writeMarkdownFile(`${path}/README.md`, doc.join("\n\n"), [`./assets/${type.toLowerCase()}/*.json`])
|
|
}
|
|
|
|
/**
|
|
* Generates the documentation for the layers overview page
|
|
* @constructor
|
|
*/
|
|
private generateSpecialLayerOverviewText(): void {
|
|
console.log("Generating the special layers overview")
|
|
|
|
for (const id of Constants.priviliged_layers) {
|
|
if (id === "favourite") {
|
|
continue
|
|
}
|
|
if (!AllSharedLayers.sharedLayers.has(id)) {
|
|
throw ("Privileged layer definition not found: " + id)
|
|
|
|
}
|
|
}
|
|
|
|
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
|
|
(layer) => layer["source"] === null
|
|
)
|
|
|
|
const builtinLayerIds: Set<string> = new Set<string>()
|
|
allLayers.forEach((l) => builtinLayerIds.add(l.id))
|
|
|
|
const themesPerLayer = new Map<string, string[]>()
|
|
|
|
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
|
|
for (const layer of layout.layers) {
|
|
if (!builtinLayerIds.has(layer.id)) {
|
|
continue
|
|
}
|
|
if (!themesPerLayer.has(layer.id)) {
|
|
themesPerLayer.set(layer.id, [])
|
|
}
|
|
themesPerLayer.get(layer.id).push(layout.id)
|
|
}
|
|
}
|
|
|
|
// Determine the cross-dependencies
|
|
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
|
|
|
|
for (const layer of allLayers) {
|
|
for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
|
|
const dependency = dep.neededLayer
|
|
if (!layerIsNeededBy.has(dependency)) {
|
|
layerIsNeededBy.set(dependency, [])
|
|
}
|
|
layerIsNeededBy.get(dependency).push(layer.id)
|
|
}
|
|
}
|
|
|
|
const el = [
|
|
"# Special and priviliged layers", "",
|
|
"MapComplete has a few data layers available which have special properties through builtin-hooks.",
|
|
"They perform various tasks, such as showing the GPS-location and track on the screen or help in special elements such as the 'cut way'-element.",
|
|
"As a theme builder, you can influence the behaviour of those layers by overriding them in your .json file",
|
|
MarkdownUtils.list(
|
|
Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")
|
|
),
|
|
...Lists.noNull(
|
|
Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id))
|
|
).map((l) =>
|
|
l.generateDocumentation({
|
|
usedInThemes: themesPerLayer.get(l.id),
|
|
layerIsNeededBy: layerIsNeededBy,
|
|
dependencies: DependencyCalculator.getLayerDependencies(l),
|
|
addedByDefault: Constants.added_by_default.indexOf(<any>l.id) >= 0,
|
|
canBeIncluded: Constants.no_include.indexOf(<any>l.id) < 0,
|
|
}) + "\n"
|
|
),
|
|
].join("\n")
|
|
this.writeMarkdownFile("./Docs/Studio/SpecialLayers.md", el, [
|
|
"src/Customizations/AllKnownLayouts.ts",
|
|
])
|
|
}
|
|
}
|
|
|
|
new GenerateDocs().run()
|