mapcomplete/scripts/generateLayerOverview.ts

509 lines
19 KiB
TypeScript
Raw Normal View History

2022-09-08 21:40:48 +02:00
import ScriptUtils from "./ScriptUtils"
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"
import licenses from "../assets/generated/license_info.json"
2022-09-08 21:40:48 +02:00
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import Constants from "../Models/Constants"
import * as fakedom from "fake-dom"
2022-06-20 01:41:34 +02:00
import {
DetectDuplicateFilters,
2022-07-06 11:14:19 +02:00
DoesImageExist,
2022-06-20 01:41:34 +02:00
PrevalidateTheme,
ValidateLayer,
ValidateTagRenderings,
2022-09-08 21:40:48 +02:00
ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../UI/i18n/Translation"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import questions from "../assets/tagRenderings/questions.json"
2022-09-08 21:40:48 +02:00
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"
import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../Utils"
import { AllSharedLayers } from "../Customizations/AllSharedLayers"
2021-04-10 03:18:32 +02:00
// This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them
class LayerOverviewUtils {
public static readonly layerPath = "./assets/generated/layers/"
public static readonly themePath = "./assets/generated/themes/"
private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set<string> {
2022-09-08 21:40:48 +02:00
const publicThemes = [].concat(...themefiles.filter((th) => !th.hideFromOverview))
2022-09-08 21:40:48 +02:00
return new Set([].concat(...publicThemes.map((th) => this.extractLayerIdsFrom(th))))
}
2022-09-08 21:40:48 +02:00
private static extractLayerIdsFrom(
themeFile: LayoutConfigJson,
includeInlineLayers = true
): string[] {
const publicLayerIds = []
for (const publicLayer of themeFile.layers) {
if (typeof publicLayer === "string") {
publicLayerIds.push(publicLayer)
continue
}
if (publicLayer["builtin"] !== undefined) {
const bi = publicLayer["builtin"]
if (typeof bi === "string") {
publicLayerIds.push(bi)
continue
}
2022-09-08 21:40:48 +02:00
bi.forEach((id) => publicLayerIds.push(id))
continue
}
if (includeInlineLayers) {
publicLayerIds.push(publicLayer["id"])
}
}
return publicLayerIds
}
shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean {
if (!existsSync(targetfile)) {
2022-09-08 21:40:48 +02:00
return true
}
const targetModified = statSync(targetfile).mtime
if (typeof sourcefile === "string") {
sourcefile = [sourcefile]
}
2022-09-08 21:40:48 +02:00
return sourcefile.some((sourcefile) => statSync(sourcefile).mtime > targetModified)
}
2022-09-08 21:40:48 +02:00
writeSmallOverview(
themes: {
id: string
title: any
shortDescription: any
icon: string
hideFromOverview: boolean
mustHaveLanguage: boolean
layers: (LayerConfigJson | string | { builtin })[]
}[]
) {
const perId = new Map<string, any>()
2021-12-21 18:35:31 +01:00
for (const theme of themes) {
2022-06-20 01:41:34 +02:00
const keywords: {}[] = []
2022-09-08 21:40:48 +02:00
for (const layer of theme.layers ?? []) {
const l = <LayerConfigJson>layer
keywords.push({ "*": l.id })
keywords.push(l.title)
keywords.push(l.description)
}
2022-06-20 01:41:34 +02:00
2021-12-21 18:35:31 +01:00
const data = {
id: theme.id,
title: theme.title,
shortDescription: theme.shortDescription,
icon: theme.icon,
2022-04-01 12:51:55 +02:00
hideFromOverview: theme.hideFromOverview,
mustHaveLanguage: theme.mustHaveLanguage,
2022-09-08 21:40:48 +02:00
keywords: Utils.NoNull(keywords),
2021-12-21 18:35:31 +01:00
}
2022-09-08 21:40:48 +02:00
perId.set(theme.id, data)
2021-12-21 18:35:31 +01:00
}
2022-09-08 21:40:48 +02:00
const sorted = Constants.themeOrder.map((id) => {
2021-12-21 18:35:31 +01:00
if (!perId.has(id)) {
throw "Ordered theme id " + id + " not found"
}
2022-09-08 21:40:48 +02:00
return perId.get(id)
})
2021-12-21 18:35:31 +01:00
perId.forEach((value) => {
if (Constants.themeOrder.indexOf(value.id) >= 0) {
2022-09-08 21:40:48 +02:00
return // actually a continue
2021-12-21 18:35:31 +01:00
}
sorted.push(value)
})
2022-09-08 21:40:48 +02:00
writeFileSync(
"./assets/generated/theme_overview.json",
JSON.stringify(sorted, null, " "),
2023-01-15 23:28:02 +01:00
{ encoding: "utf8" }
2022-09-08 21:40:48 +02:00
)
2021-04-10 03:18:32 +02:00
}
2021-04-10 03:50:44 +02:00
2021-12-21 18:35:31 +01:00
writeTheme(theme: LayoutConfigJson) {
if (!existsSync(LayerOverviewUtils.themePath)) {
2022-09-08 21:40:48 +02:00
mkdirSync(LayerOverviewUtils.themePath)
2021-05-19 20:47:41 +02:00
}
2022-09-08 21:40:48 +02:00
writeFileSync(
`${LayerOverviewUtils.themePath}${theme.id}.json`,
JSON.stringify(theme, null, " "),
2023-01-15 23:28:02 +01:00
{ encoding: "utf8" }
2022-09-08 21:40:48 +02:00
)
2021-12-21 18:35:31 +01:00
}
writeLayer(layer: LayerConfigJson) {
if (!existsSync(LayerOverviewUtils.layerPath)) {
2022-09-08 21:40:48 +02:00
mkdirSync(LayerOverviewUtils.layerPath)
}
2022-09-08 21:40:48 +02:00
writeFileSync(
`${LayerOverviewUtils.layerPath}${layer.id}.json`,
JSON.stringify(layer, null, " "),
2023-01-15 23:28:02 +01:00
{ encoding: "utf8" }
2022-09-08 21:40:48 +02:00
)
2021-12-21 18:35:31 +01:00
}
getSharedTagRenderings(doesImageExist: DoesImageExist): Map<string, TagRenderingConfigJson> {
2022-09-08 21:40:48 +02:00
const dict = new Map<string, TagRenderingConfigJson>()
2022-09-08 21:40:48 +02:00
const validator = new ValidateTagRenderings(undefined, doesImageExist)
for (const key in questions) {
2022-01-26 21:40:38 +01:00
if (key === "id") {
continue
}
2022-09-08 21:40:48 +02:00
questions[key].id = key
2022-04-01 12:51:55 +02:00
questions[key]["source"] = "shared-questions"
2022-06-20 01:41:34 +02:00
const config = <TagRenderingConfigJson>questions[key]
2022-09-08 21:40:48 +02:00
validator.convertStrict(
config,
"generate-layer-overview:tagRenderings/questions.json:" + key
)
2022-06-20 01:41:34 +02:00
dict.set(key, config)
2021-12-21 18:35:31 +01:00
}
2021-09-04 18:59:51 +02:00
2021-12-21 18:35:31 +01:00
dict.forEach((value, key) => {
2022-01-26 21:40:38 +01:00
if (key === "id") {
2022-01-24 00:24:51 +01:00
return
}
2022-09-08 21:40:48 +02:00
value.id = value.id ?? key
2021-12-21 18:35:31 +01:00
})
2021-05-19 20:47:41 +02:00
2022-09-08 21:40:48 +02:00
return dict
2021-12-21 18:35:31 +01:00
}
2021-05-19 20:47:41 +02:00
checkAllSvgs() {
const allSvgs = ScriptUtils.readDirRecSync("./assets")
2022-09-08 21:40:48 +02:00
.filter((path) => path.endsWith(".svg"))
.filter((path) => !path.startsWith("./assets/generated"))
let errCount = 0
const exempt = [
"assets/SocialImageTemplate.svg",
"assets/SocialImageTemplateWide.svg",
"assets/SocialImageBanner.svg",
2022-10-24 01:07:31 +02:00
"assets/SocialImageRepo.svg",
2022-09-08 21:40:48 +02:00
"assets/svg/osm-logo.svg",
2022-10-27 01:50:01 +02:00
"assets/templates/*",
2022-09-08 21:40:48 +02:00
]
for (const path of allSvgs) {
2022-10-27 01:50:01 +02:00
if (
exempt.some((p) => {
if (p.endsWith("*") && path.startsWith("./" + p.substring(0, p.length - 1))) {
return true
}
return "./" + p === path
})
) {
2022-03-08 04:09:03 +01:00
continue
}
2023-01-15 23:28:02 +01:00
const contents = readFileSync(path, { encoding: "utf8" })
2022-05-07 21:41:58 +02:00
if (contents.indexOf("data:image/png;") >= 0) {
console.warn("The SVG at " + path + " is a fake SVG: it contains PNG data!")
2022-09-08 21:40:48 +02:00
errCount++
2022-05-07 21:41:58 +02:00
if (path.startsWith("./assets/svg")) {
throw "A core SVG is actually a PNG. Don't do this!"
}
}
2022-06-20 01:41:34 +02:00
if (contents.indexOf("<text") > 0) {
2022-09-08 21:40:48 +02:00
console.warn(
"The SVG at " +
path +
" contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path"
)
errCount++
}
}
if (errCount > 0) {
2022-05-07 21:41:58 +02:00
throw `There are ${errCount} invalid svgs`
}
}
main(args: string[]) {
if (fakedom === undefined) {
throw "Fakedom not initialized"
}
2022-09-08 21:40:48 +02:00
const forceReload = args.some((a) => a == "--force")
2022-01-16 01:59:06 +01:00
const licensePaths = new Set<string>()
for (const i in licenses) {
licensePaths.add(licenses[i].path)
}
const doesImageExist = new DoesImageExist(licensePaths, existsSync)
2022-09-08 21:40:48 +02:00
const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload)
2022-07-08 15:37:31 +02:00
const recompiledThemes: string[] = []
2022-09-08 21:40:48 +02:00
const sharedThemes = this.buildThemeIndex(
licensePaths,
2022-09-08 21:40:48 +02:00
sharedLayers,
recompiledThemes,
forceReload
)
writeFileSync(
"./assets/generated/known_themes.json",
2022-09-08 21:40:48 +02:00
JSON.stringify({
themes: Array.from(sharedThemes.values()),
})
)
writeFileSync(
"./assets/generated/known_layers.json",
JSON.stringify({ layers: Array.from(sharedLayers.values()) })
)
if (
recompiledThemes.length > 0 &&
!(recompiledThemes.length === 1 && recompiledThemes[0] === "mapcomplete-changes")
) {
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
2022-09-08 21:40:48 +02:00
const iconsPerTheme = Array.from(sharedThemes.values()).map((th) => ({
if: "theme=" + th.id,
then: th.icon,
}))
const proto: LayoutConfigJson = JSON.parse(
readFileSync("./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", {
encoding: "utf8",
})
2022-09-08 21:40:48 +02:00
)
const protolayer = <LayerConfigJson>(
proto.layers.filter((l) => l["id"] === "mapcomplete-changes")[0]
)
const rendering = <PointRenderingConfigJson>protolayer.mapRendering[0]
rendering.icon["mappings"] = iconsPerTheme
2022-09-08 21:40:48 +02:00
writeFileSync(
"./assets/themes/mapcomplete-changes/mapcomplete-changes.json",
JSON.stringify(proto, null, " ")
)
}
this.checkAllSvgs()
2022-09-08 21:40:48 +02:00
2022-10-27 01:50:01 +02:00
new DetectDuplicateFilters().convertStrict(
{
layers: ScriptUtils.getLayerFiles().map((f) => f.parsed),
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed),
},
"GenerateLayerOverview:"
)
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
2022-09-08 21:40:48 +02:00
console.error("This was a bootstrapping-run. Run generate layeroverview again!")
} else {
const green = (s) => "\x1b[92m" + s + "\x1b[0m"
console.log(green("All done!"))
2022-07-11 09:14:26 +02:00
}
2022-01-16 01:59:06 +01:00
}
2021-12-21 18:35:31 +01:00
2022-09-08 21:40:48 +02:00
private buildLayerIndex(
doesImageExist: DoesImageExist,
forceReload: boolean
): Map<string, LayerConfigJson> {
2021-12-21 18:35:31 +01:00
// First, we expand and validate all builtin layers. These are written to assets/generated/layers
// At the same time, an index of available layers is built.
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------")
2022-09-08 21:40:48 +02:00
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist)
2021-12-21 18:35:31 +01:00
const state: DesugaringContext = {
tagRenderings: sharedTagRenderings,
sharedLayers: AllSharedLayers.getSharedLayersConfigs(),
2021-12-21 18:35:31 +01:00
}
2022-07-11 09:14:26 +02:00
const sharedLayers = new Map<string, LayerConfigJson>()
2022-09-08 21:40:48 +02:00
const prepLayer = new PrepareLayer(state)
const skippedLayers: string[] = []
const recompiledLayers: string[] = []
for (const sharedLayerPath of ScriptUtils.getLayerPaths()) {
{
2022-09-08 21:40:48 +02:00
const targetPath =
LayerOverviewUtils.layerPath +
sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/"))
if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) {
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
sharedLayers.set(sharedLayer.id, sharedLayer)
skippedLayers.push(sharedLayer.id)
2022-09-08 21:40:48 +02:00
console.log("Loaded " + sharedLayer.id)
continue
}
2022-07-08 15:37:31 +02:00
}
2022-09-08 21:40:48 +02:00
let parsed
2022-07-08 15:37:31 +02:00
try {
parsed = JSON.parse(readFileSync(sharedLayerPath, "utf8"))
} catch (e) {
2022-09-08 21:40:48 +02:00
throw "Could not parse or read file " + sharedLayerPath
2022-07-08 15:37:31 +02:00
}
const context = "While building builtin layer " + sharedLayerPath
const fixed = prepLayer.convertStrict(parsed, context)
if (fixed.source.osmTags["and"] === undefined) {
2022-09-08 21:40:48 +02:00
fixed.source.osmTags = { and: [fixed.source.osmTags] }
2022-06-20 02:13:04 +02:00
}
2022-09-08 21:40:48 +02:00
const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist)
2022-02-04 01:05:35 +01:00
validator.convertStrict(fixed, context)
2021-12-21 18:35:31 +01:00
if (sharedLayers.has(fixed.id)) {
throw "There are multiple layers with the id " + fixed.id
2021-05-19 20:47:41 +02:00
}
2021-12-21 18:35:31 +01:00
sharedLayers.set(fixed.id, fixed)
recompiledLayers.push(fixed.id)
2021-12-21 18:35:31 +01:00
this.writeLayer(fixed)
}
2022-06-20 01:41:34 +02:00
2022-09-08 21:40:48 +02:00
console.log(
"Recompiled layers " +
recompiledLayers.join(", ") +
" and skipped " +
skippedLayers.length +
" layers"
)
2022-09-08 21:40:48 +02:00
return sharedLayers
}
2021-04-10 03:18:32 +02:00
2022-09-08 21:40:48 +02:00
private buildThemeIndex(
licensePaths: Set<string>,
2022-09-08 21:40:48 +02:00
sharedLayers: Map<string, LayerConfigJson>,
recompiledThemes: string[],
forceReload: boolean
): Map<string, LayoutConfigJson> {
2021-12-21 18:35:31 +01:00
console.log(" ---------- VALIDATING BUILTIN THEMES ---------")
2022-09-08 21:40:48 +02:00
const themeFiles = ScriptUtils.getThemeFiles()
const fixed = new Map<string, LayoutConfigJson>()
2022-09-08 21:40:48 +02:00
const publicLayers = LayerOverviewUtils.publicLayerIdsFrom(
themeFiles.map((th) => th.parsed)
)
2021-12-21 18:35:31 +01:00
const convertState: DesugaringContext = {
sharedLayers,
tagRenderings: this.getSharedTagRenderings(
new DoesImageExist(licensePaths, existsSync)
),
2022-09-08 21:40:48 +02:00
publicLayers,
2021-05-19 20:47:41 +02:00
}
const knownTagRenderings = new Set<string>()
convertState.tagRenderings.forEach((_, key) => knownTagRenderings.add(key))
sharedLayers.forEach((layer) => {
for (const tagRendering of layer.tagRenderings ?? []) {
if (tagRendering["id"]) {
knownTagRenderings.add(layer.id + "." + tagRendering["id"])
}
if (tagRendering["labels"]) {
for (const label of tagRendering["labels"]) {
knownTagRenderings.add(layer.id + "." + label)
}
}
}
})
const skippedThemes: string[] = []
2021-12-21 18:35:31 +01:00
for (const themeInfo of themeFiles) {
2022-09-08 21:40:48 +02:00
const themePath = themeInfo.path
2021-12-21 18:35:31 +01:00
let themeFile = themeInfo.parsed
{
2022-09-08 21:40:48 +02:00
const targetPath =
LayerOverviewUtils.themePath +
"/" +
themePath.substring(themePath.lastIndexOf("/"))
const usedLayers = Array.from(
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
2022-09-08 21:40:48 +02:00
fixed.set(
themeFile.id,
JSON.parse(
readFileSync(
LayerOverviewUtils.themePath + themeFile.id + ".json",
"utf8"
)
)
)
skippedThemes.push(themeFile.id)
2022-09-08 21:40:48 +02:00
continue
}
recompiledThemes.push(themeFile.id)
}
2022-01-16 01:59:06 +01:00
2022-02-04 01:05:35 +01:00
new PrevalidateTheme().convertStrict(themeFile, themePath)
2022-06-20 01:41:34 +02:00
try {
2022-04-22 03:17:40 +02:00
themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath)
2022-06-20 01:41:34 +02:00
2022-09-08 21:40:48 +02:00
new ValidateThemeAndLayers(
new DoesImageExist(licensePaths, existsSync, knownTagRenderings),
2022-09-08 21:40:48 +02:00
themePath,
true,
knownTagRenderings
2022-09-08 21:40:48 +02:00
).convertStrict(themeFile, themePath)
2022-06-20 01:41:34 +02:00
if (themeFile.icon.endsWith(".svg")) {
try {
2023-01-17 01:53:50 +01:00
ScriptUtils.ReadSvgSync(themeFile.icon, (svg) => {
const width: string = svg.$.width
const height: string = svg.$.height
2023-01-17 01:53:50 +01:00
const err = themeFile.hideFromOverview ? console.warn : console.error
if (width !== height) {
const e =
2023-01-17 01:53:50 +01:00
`the icon for theme ${themeFile.id} is not square. Please square the icon at ${themeFile.icon}` +
` Width = ${width} height = ${height}`
2023-01-17 01:53:50 +01:00
err(e)
}
const w = parseInt(width)
const h = parseInt(height)
if (w < 370 || h < 370) {
const e: string = [
2023-01-17 01:53:50 +01:00
`the icon for theme ${themeFile.id} is too small. Please rescale the icon at ${themeFile.icon}`,
`Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`,
` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`,
].join("\n")
2023-01-17 01:53:50 +01:00
err(e)
}
})
} catch (e) {
2023-01-17 01:53:50 +01:00
console.error("Could not read " + themeFile.icon + " due to " + e)
}
}
2022-04-22 03:17:40 +02:00
this.writeTheme(themeFile)
fixed.set(themeFile.id, themeFile)
2022-06-20 01:41:34 +02:00
} catch (e) {
console.error("ERROR: could not prepare theme " + themePath + " due to " + e)
2022-09-08 21:40:48 +02:00
throw e
}
}
2022-09-08 21:40:48 +02:00
this.writeSmallOverview(
Array.from(fixed.values()).map((t) => {
return {
...t,
hideFromOverview: t.hideFromOverview ?? false,
shortDescription:
t.shortDescription ??
new Translation(t.description).FirstSentence().translations,
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
}
})
)
console.log(
"Recompiled themes " +
recompiledThemes.join(", ") +
" and skipped " +
skippedThemes.length +
" themes"
)
return fixed
2021-12-21 18:35:31 +01:00
}
2021-04-10 14:25:06 +02:00
}
new LayerOverviewUtils().main(process.argv)