forked from MapComplete/MapComplete
Performance: split validation into multiple files, avoid using 'fixImages' and 'exractImages' into well-known themes as it takes a big chunk of data
This commit is contained in:
parent
ac853ab021
commit
8c779fe09b
16 changed files with 1009 additions and 949 deletions
|
@ -9,7 +9,6 @@ import {
|
||||||
DoesImageExist,
|
DoesImageExist,
|
||||||
PrevalidateTheme,
|
PrevalidateTheme,
|
||||||
ValidateLayer,
|
ValidateLayer,
|
||||||
ValidateThemeAndLayers,
|
|
||||||
ValidateThemeEnsemble,
|
ValidateThemeEnsemble,
|
||||||
} from "../src/Models/ThemeConfig/Conversion/Validation"
|
} from "../src/Models/ThemeConfig/Conversion/Validation"
|
||||||
import { Translation } from "../src/UI/i18n/Translation"
|
import { Translation } from "../src/UI/i18n/Translation"
|
||||||
|
@ -33,6 +32,8 @@ import { GenerateFavouritesLayer } from "./generateFavouritesLayer"
|
||||||
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
|
||||||
import Translations from "../src/UI/i18n/Translations"
|
import Translations from "../src/UI/i18n/Translations"
|
||||||
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
|
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
|
||||||
|
import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
|
||||||
|
import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
|
||||||
|
|
||||||
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
|
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
|
||||||
// It spits out an overview of those to be used to load them
|
// It spits out an overview of those to be used to load them
|
||||||
|
@ -272,6 +273,7 @@ class LayerOverviewUtils extends Script {
|
||||||
JSON.stringify(theme, null, " "),
|
JSON.stringify(theme, null, " "),
|
||||||
{ encoding: "utf8" }
|
{ encoding: "utf8" }
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeLayer(layer: LayerConfigJson) {
|
writeLayer(layer: LayerConfigJson) {
|
||||||
|
@ -850,6 +852,11 @@ class LayerOverviewUtils extends Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usedImages = Utils.Dedup(new ExtractImages(true, knownTagRenderings).convertStrict(themeFile).map(x => x.path))
|
||||||
|
usedImages.sort()
|
||||||
|
|
||||||
|
themeFile["_usedImages"] = usedImages
|
||||||
|
|
||||||
this.writeTheme(themeFile)
|
this.writeTheme(themeFile)
|
||||||
fixed.set(themeFile.id, themeFile)
|
fixed.set(themeFile.id, themeFile)
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,13 @@ import licenses from "../assets/generated/license_info.json"
|
||||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
|
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
|
||||||
import questions from "../assets/generated/layers/questions.json"
|
import questions from "../assets/generated/layers/questions.json"
|
||||||
import { DoesImageExist, PrevalidateTheme, ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/Validation"
|
import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation"
|
||||||
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
|
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
|
||||||
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||||
import Hash from "./Web/Hash"
|
import Hash from "./Web/Hash"
|
||||||
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||||
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
|
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
|
||||||
|
import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
|
||||||
|
|
||||||
export default class DetermineLayout {
|
export default class DetermineLayout {
|
||||||
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
|
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
|
||||||
|
|
|
@ -163,11 +163,11 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
||||||
render: "{add_image_to_note()}",
|
render: "{add_image_to_note()}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "nearby_images",
|
id: "nearby_images_note",
|
||||||
render: tr(t.nearbyImagesIntro),
|
render: tr(t.nearbyImagesIntro),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "all_tags",
|
id: "all_tags_note",
|
||||||
render: "{all_tags()}",
|
render: "{all_tags()}",
|
||||||
metacondition: {
|
metacondition: {
|
||||||
or: [
|
or: [
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { DesugaringStep } from "./Conversion"
|
||||||
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
|
import { ConversionContext } from "./ConversionContext"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
import Translations from "../../../UI/i18n/Translations"
|
||||||
|
import { DoesImageExist } from "./Validation"
|
||||||
|
|
||||||
|
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
|
private readonly _doesImageExist: DoesImageExist
|
||||||
|
|
||||||
|
constructor(doesImageExist: DoesImageExist) {
|
||||||
|
super(
|
||||||
|
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
|
||||||
|
[],
|
||||||
|
"DetectMappingsWithImages",
|
||||||
|
)
|
||||||
|
this._doesImageExist = doesImageExist
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* const context = ConversionContext.test()
|
||||||
|
* const r = new DetectMappingsWithImages(new DoesImageExist(new Set<string>())).convert({
|
||||||
|
* "mappings": [
|
||||||
|
* {
|
||||||
|
* "if": "bicycle_parking=stands",
|
||||||
|
* "then": {
|
||||||
|
* "en": "Staple racks <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "nl": "Nietjes <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "fr": "Arceaux <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "gl": "De roda (Stands) <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "de": "Fahrradbügel <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "hu": "Korlát <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "it": "Archetti <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
||||||
|
* "zh_Hant": "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
|
||||||
|
* }
|
||||||
|
* }]
|
||||||
|
* }, context);
|
||||||
|
* context.hasErrors() // => true
|
||||||
|
* context.getAll("error").some(msg => msg.message.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
|
||||||
|
*/
|
||||||
|
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
|
||||||
|
if (json.mappings === undefined || json.mappings.length === 0) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
const ignoreToken = "ignore-image-in-then"
|
||||||
|
for (let i = 0; i < json.mappings.length; i++) {
|
||||||
|
const mapping = json.mappings[i]
|
||||||
|
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
|
||||||
|
const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? [])
|
||||||
|
const ctx = context.enters("mappings", i)
|
||||||
|
if (images.length > 0) {
|
||||||
|
if (!ignore) {
|
||||||
|
ctx.err(
|
||||||
|
`A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
|
||||||
|
", ",
|
||||||
|
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ctx.info(
|
||||||
|
`Ignored image ${images.join(
|
||||||
|
", ",
|
||||||
|
)} in 'then'-clause of a mapping as this check has been disabled`,
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
this._doesImageExist.convert(image, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ignore) {
|
||||||
|
ctx.warn(`Unused '${ignoreToken}' - please remove this`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
256
src/Models/ThemeConfig/Conversion/MiscTagRenderingChecks.ts
Normal file
256
src/Models/ThemeConfig/Conversion/MiscTagRenderingChecks.ts
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import { DesugaringStep } from "./Conversion"
|
||||||
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
|
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||||
|
import { ConversionContext } from "./ConversionContext"
|
||||||
|
import { Translation } from "../../../UI/i18n/Translation"
|
||||||
|
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
|
||||||
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
|
import { Tag } from "../../../Logic/Tags/Tag"
|
||||||
|
import Validators from "../../../UI/InputElement/Validators"
|
||||||
|
import { CheckTranslation } from "./Validation"
|
||||||
|
|
||||||
|
export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
|
private readonly _layerConfig: LayerConfigJson
|
||||||
|
|
||||||
|
constructor(layerConfig?: LayerConfigJson) {
|
||||||
|
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
|
||||||
|
this._layerConfig = layerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(
|
||||||
|
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
|
||||||
|
context: ConversionContext,
|
||||||
|
): TagRenderingConfigJson {
|
||||||
|
if (json["special"] !== undefined) {
|
||||||
|
context.err(
|
||||||
|
"Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(json).length === 1 && typeof json["render"] === "string") {
|
||||||
|
context.warn(
|
||||||
|
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
for (const key of ["question", "questionHint", "render"]) {
|
||||||
|
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
|
||||||
|
}
|
||||||
|
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
|
||||||
|
const mapping: MappingConfigJson = json.mappings[i]
|
||||||
|
CheckTranslation.noUndefined.convert(
|
||||||
|
mapping.then,
|
||||||
|
context.enters("mappings", i, "then"),
|
||||||
|
)
|
||||||
|
if (!mapping.if) {
|
||||||
|
console.log(
|
||||||
|
"Checking mappings",
|
||||||
|
i,
|
||||||
|
"if",
|
||||||
|
mapping.if,
|
||||||
|
context.path.join("."),
|
||||||
|
mapping.then,
|
||||||
|
)
|
||||||
|
context.enters("mappings", i, "if").err("No `if` is defined")
|
||||||
|
}
|
||||||
|
if (mapping.addExtraTags) {
|
||||||
|
for (let j = 0; j < mapping.addExtraTags.length; j++) {
|
||||||
|
if (!mapping.addExtraTags[j]) {
|
||||||
|
context
|
||||||
|
.enters("mappings", i, "addExtraTags", j)
|
||||||
|
.err(
|
||||||
|
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const en = mapping?.then?.["en"]
|
||||||
|
if (en && this.detectYesOrNo(en)) {
|
||||||
|
console.log("Found a match with yes or no: ", { en })
|
||||||
|
context
|
||||||
|
.enters("mappings", i, "then")
|
||||||
|
.warn(
|
||||||
|
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (json["group"]) {
|
||||||
|
context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
|
||||||
|
context.err(
|
||||||
|
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
|
||||||
|
context.err("A question is defined, but there is only one option to choose from.")
|
||||||
|
}
|
||||||
|
if (json["questionHint"] && !json["question"]) {
|
||||||
|
context
|
||||||
|
.enter("questionHint")
|
||||||
|
.err(
|
||||||
|
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.icon?.["size"]) {
|
||||||
|
context
|
||||||
|
.enters("icon", "size")
|
||||||
|
.err(
|
||||||
|
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.freeform) {
|
||||||
|
if (json.render === undefined) {
|
||||||
|
context
|
||||||
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
"This tagRendering allows to set a value to key " +
|
||||||
|
json.freeform.key +
|
||||||
|
", but does not define a `render`. Please, add a value here which contains `{" +
|
||||||
|
json.freeform.key +
|
||||||
|
"}`",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const render = new Translation(<any>json.render)
|
||||||
|
for (const ln in render.translations) {
|
||||||
|
if (ln.startsWith("_")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const txt: string = render.translations[ln]
|
||||||
|
if (txt === "") {
|
||||||
|
context.enter("render").err(" Rendering for language " + ln + " is empty")
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
|
||||||
|
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (txt.indexOf("{" + json.freeform.key + ":") >= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.freeform["type"] === "opening_hours" &&
|
||||||
|
txt.indexOf("{opening_hours_table(") >= 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
||||||
|
if (
|
||||||
|
keyFirstArg.some(
|
||||||
|
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
json.freeform["type"] === "wikidata" &&
|
||||||
|
txt.indexOf("{wikipedia(" + json.freeform.key) >= 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (json.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
json.freeform["type"] === "wikidata" &&
|
||||||
|
txt.indexOf(`{wikidata_label(${json.freeform.key})`) >= 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (json.freeform.key.indexOf("wikidata") >= 0) {
|
||||||
|
context
|
||||||
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
txt.indexOf(json.freeform.key) >= 0 &&
|
||||||
|
txt.indexOf("{" + json.freeform.key + "}") < 0
|
||||||
|
) {
|
||||||
|
context
|
||||||
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
|
.enter("render")
|
||||||
|
.err(
|
||||||
|
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._layerConfig?.source?.osmTags &&
|
||||||
|
NameSuggestionIndex.supportedTypes().indexOf(json.freeform.key) >= 0
|
||||||
|
) {
|
||||||
|
const tags = TagUtils.TagD(this._layerConfig?.source?.osmTags)?.usedTags()
|
||||||
|
const suggestions = NameSuggestionIndex.getSuggestionsFor(json.freeform.key, tags)
|
||||||
|
if (suggestions === undefined) {
|
||||||
|
context
|
||||||
|
.enters("freeform", "type")
|
||||||
|
.err(
|
||||||
|
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
|
||||||
|
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (json.freeform.type === "nsi") {
|
||||||
|
context
|
||||||
|
.enters("freeform", "type")
|
||||||
|
.warn(
|
||||||
|
"No need to explicitly set type to 'NSI', autodetected based on freeform type",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (json.render && json["question"] && json.freeform === undefined) {
|
||||||
|
context.err(
|
||||||
|
`Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation(
|
||||||
|
json["question"],
|
||||||
|
).textFor("en")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const freeformType = json["freeform"]?.["type"]
|
||||||
|
if (freeformType) {
|
||||||
|
if (Validators.availableTypes.indexOf(freeformType) < 0) {
|
||||||
|
context
|
||||||
|
.enters("freeform", "type")
|
||||||
|
.err(
|
||||||
|
"Unknown type: " +
|
||||||
|
freeformType +
|
||||||
|
"; try one of " +
|
||||||
|
Validators.availableTypes.join(", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.hasErrors()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* const obj = new MiscTagRenderingChecks()
|
||||||
|
* obj.detectYesOrNo("Yes, this place has") // => true
|
||||||
|
* obj.detectYesOrNo("Yes") // => true
|
||||||
|
* obj.detectYesOrNo("No, this place does not have...") // => true
|
||||||
|
* obj.detectYesOrNo("This place does not have...") // => false
|
||||||
|
*/
|
||||||
|
private detectYesOrNo(en: string): boolean {
|
||||||
|
return en.toLowerCase().match(/^(yes|no)([,:;.?]|$)/) !== null
|
||||||
|
}
|
||||||
|
}
|
|
@ -669,6 +669,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
||||||
new PreparePersonalTheme(state),
|
new PreparePersonalTheme(state),
|
||||||
new WarnForUnsubstitutedLayersInTheme(),
|
new WarnForUnsubstitutedLayersInTheme(),
|
||||||
new On("layers", new Concat(new SubstituteLayer(state))),
|
new On("layers", new Concat(new SubstituteLayer(state))),
|
||||||
|
|
||||||
new SetDefault("socialImage", "assets/SocialImage.png", true),
|
new SetDefault("socialImage", "assets/SocialImage.png", true),
|
||||||
// We expand all tagrenderings first...
|
// We expand all tagrenderings first...
|
||||||
new On("layers", new Each(new PrepareLayer(state))),
|
new On("layers", new Each(new PrepareLayer(state))),
|
||||||
|
|
400
src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts
Normal file
400
src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
import { DesugaringStep, Each, On } from "./Conversion"
|
||||||
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
|
import { ConversionContext } from "./ConversionContext"
|
||||||
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
|
import LayerConfig from "../LayerConfig"
|
||||||
|
import Constants from "../../Constants"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
import DeleteConfig from "../DeleteConfig"
|
||||||
|
import { And } from "../../../Logic/Tags/And"
|
||||||
|
import { DoesImageExist, ValidateFilter, ValidatePointRendering } from "./Validation"
|
||||||
|
import { ValidateTagRenderings } from "./ValidateTagRenderings"
|
||||||
|
|
||||||
|
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
|
private readonly _isBuiltin: boolean
|
||||||
|
private readonly _doesImageExist: DoesImageExist
|
||||||
|
/**
|
||||||
|
* The paths where this layer is originally saved. Triggers some extra checks
|
||||||
|
*/
|
||||||
|
private readonly _path: string
|
||||||
|
private readonly _studioValidations: boolean
|
||||||
|
private readonly _validatePointRendering = new ValidatePointRendering()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
path: string,
|
||||||
|
isBuiltin: boolean,
|
||||||
|
doesImageExist: DoesImageExist,
|
||||||
|
studioValidations: boolean,
|
||||||
|
) {
|
||||||
|
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
|
||||||
|
this._path = path
|
||||||
|
this._isBuiltin = isBuiltin
|
||||||
|
this._doesImageExist = doesImageExist
|
||||||
|
this._studioValidations = studioValidations
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||||
|
if (json.id === undefined) {
|
||||||
|
context.enter("id").err(`Not a valid layer: id is undefined`)
|
||||||
|
} else {
|
||||||
|
if (json.id?.toLowerCase() !== json.id) {
|
||||||
|
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||||
|
}
|
||||||
|
const layerRegex = /[a-zA-Z][a-zA-Z_0-9]+/
|
||||||
|
if (json.id.match(layerRegex) === null) {
|
||||||
|
context.enter("id").err("Invalid ID. A layer ID should match " + layerRegex.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.source === undefined) {
|
||||||
|
context
|
||||||
|
.enter("source")
|
||||||
|
.err(
|
||||||
|
"No source section is defined; please define one as data is not loaded otherwise",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (json.source === "special" || json.source === "special:library") {
|
||||||
|
} else if (json.source && json.source["osmTags"] === undefined) {
|
||||||
|
context
|
||||||
|
.enters("source", "osmTags")
|
||||||
|
.err(
|
||||||
|
"No osmTags defined in the source section - these should always be present, even for geojson layer",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
|
||||||
|
if (osmTags.isNegative()) {
|
||||||
|
context
|
||||||
|
.enters("source", "osmTags")
|
||||||
|
.err(
|
||||||
|
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
||||||
|
osmTags.asHumanString(false, false, {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.source["geoJsonSource"] !== undefined) {
|
||||||
|
context
|
||||||
|
.enters("source", "geoJsonSource")
|
||||||
|
.err("Use 'geoJson' instead of 'geoJsonSource'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.source["geojson"] !== undefined) {
|
||||||
|
context
|
||||||
|
.enters("source", "geojson")
|
||||||
|
.err("Use 'geoJson' instead of 'geojson' (the J is a capital letter)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.syncSelection !== undefined &&
|
||||||
|
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||||
|
) {
|
||||||
|
context
|
||||||
|
.enter("syncSelection")
|
||||||
|
.err(
|
||||||
|
"Invalid sync-selection: must be one of " +
|
||||||
|
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
||||||
|
" but got '" +
|
||||||
|
json.syncSelection +
|
||||||
|
"'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (json["pointRenderings"]?.length > 0) {
|
||||||
|
context
|
||||||
|
.enter("pointRenderings")
|
||||||
|
.err("Detected a 'pointRenderingS', it is written singular")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(json.pointRendering?.length > 0) &&
|
||||||
|
json.pointRendering !== null &&
|
||||||
|
json.source !== "special" &&
|
||||||
|
json.source !== "special:library"
|
||||||
|
) {
|
||||||
|
context.enter("pointRendering").err("There are no pointRenderings at all...")
|
||||||
|
}
|
||||||
|
|
||||||
|
json.pointRendering?.forEach((pr, i) =>
|
||||||
|
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (json["mapRendering"]) {
|
||||||
|
context.enter("mapRendering").err("This layer has a legacy 'mapRendering'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.presets?.length > 0) {
|
||||||
|
if (!(json.pointRendering?.length > 0)) {
|
||||||
|
context.enter("presets").warn("A preset is defined, but there is no pointRendering")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.source === "special") {
|
||||||
|
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||||
|
context.err(
|
||||||
|
"Layer " +
|
||||||
|
json.id +
|
||||||
|
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.hasErrors()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
|
||||||
|
new On("tagRenderings", new Each(new ValidateTagRenderings(json)))
|
||||||
|
if (json.title === undefined && json.source !== "special:library") {
|
||||||
|
context
|
||||||
|
.enter("title")
|
||||||
|
.err(
|
||||||
|
"This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (json.title === null) {
|
||||||
|
context.info(
|
||||||
|
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check for multiple, identical builtin questions - usability for studio users
|
||||||
|
const duplicates = Utils.Duplicates(
|
||||||
|
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"),
|
||||||
|
)
|
||||||
|
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||||
|
const tagRendering = json.tagRenderings[i]
|
||||||
|
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
|
||||||
|
context
|
||||||
|
.enters("tagRenderings", i)
|
||||||
|
.err(`This builtin question is used multiple times (${tagRendering})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json["builtin"] !== undefined) {
|
||||||
|
context.err("This layer hasn't been expanded: " + json)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.minzoom > Constants.minZoomLevelToAddNewPoint) {
|
||||||
|
const c = context.enter("minzoom")
|
||||||
|
const msg = `Minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`
|
||||||
|
if (json.presets?.length > 0) {
|
||||||
|
c.err(msg)
|
||||||
|
} else {
|
||||||
|
c.warn(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// duplicate ids in tagrenderings check
|
||||||
|
const duplicates = Utils.NoNull(
|
||||||
|
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))),
|
||||||
|
)
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
|
||||||
|
context
|
||||||
|
.enter("tagRenderings")
|
||||||
|
.err(
|
||||||
|
"Some tagrenderings have a duplicate id: " +
|
||||||
|
duplicates.join(", ") +
|
||||||
|
"\n" +
|
||||||
|
JSON.stringify(
|
||||||
|
json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) {
|
||||||
|
if (json.deletion.softDeletionTags === undefined) {
|
||||||
|
context
|
||||||
|
.enter("deletion")
|
||||||
|
.warn("No soft-deletion tags in deletion block for layer " + json.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
} catch (e) {
|
||||||
|
context.err("Could not validate layer due to: " + e + e.stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._studioValidations) {
|
||||||
|
if (!json.description) {
|
||||||
|
context.enter("description").err("A description is required")
|
||||||
|
}
|
||||||
|
if (!json.name) {
|
||||||
|
context.enter("name").err("A name is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isBuiltin) {
|
||||||
|
// Some checks for legacy elements
|
||||||
|
|
||||||
|
if (json["overpassTags"] !== undefined) {
|
||||||
|
context.err(
|
||||||
|
"Layer " +
|
||||||
|
json.id +
|
||||||
|
"still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const forbiddenTopLevel = [
|
||||||
|
"icon",
|
||||||
|
"wayHandling",
|
||||||
|
"roamingRenderings",
|
||||||
|
"roamingRendering",
|
||||||
|
"label",
|
||||||
|
"width",
|
||||||
|
"color",
|
||||||
|
"colour",
|
||||||
|
"iconOverlays",
|
||||||
|
]
|
||||||
|
for (const forbiddenKey of forbiddenTopLevel) {
|
||||||
|
if (json[forbiddenKey] !== undefined)
|
||||||
|
context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
|
||||||
|
}
|
||||||
|
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||||
|
context.err(
|
||||||
|
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.isShown !== undefined &&
|
||||||
|
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||||
|
) {
|
||||||
|
context.warn("Has a tagRendering as `isShown`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._isBuiltin) {
|
||||||
|
// Check location of layer file
|
||||||
|
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||||
|
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||||
|
context.err(
|
||||||
|
"Layer is in an incorrect place. The path is " +
|
||||||
|
this._path +
|
||||||
|
", but expected " +
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._isBuiltin) {
|
||||||
|
// Check for correct IDs
|
||||||
|
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||||
|
const emptyIndexes: number[] = []
|
||||||
|
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||||
|
const tagRendering = json.tagRenderings[i]
|
||||||
|
if (tagRendering["id"] === "") {
|
||||||
|
emptyIndexes.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context
|
||||||
|
.enter(["tagRenderings", ...emptyIndexes])
|
||||||
|
.err(
|
||||||
|
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
|
||||||
|
",",
|
||||||
|
)}])`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateIds = Utils.Duplicates(
|
||||||
|
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"),
|
||||||
|
)
|
||||||
|
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||||
|
context
|
||||||
|
.enter("tagRenderings")
|
||||||
|
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.description === undefined) {
|
||||||
|
if (typeof json.source === null) {
|
||||||
|
context.err("A priviliged layer must have a description")
|
||||||
|
} else {
|
||||||
|
context.warn("A builtin layer should have a description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.filter) {
|
||||||
|
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.tagRenderings !== undefined) {
|
||||||
|
new On(
|
||||||
|
"tagRenderings",
|
||||||
|
new Each(new ValidateTagRenderings(json, this._doesImageExist)),
|
||||||
|
).convert(json, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||||
|
if (!Array.isArray(json.pointRendering)) {
|
||||||
|
throw (
|
||||||
|
"pointRendering in " +
|
||||||
|
json.id +
|
||||||
|
" is not iterable, it is: " +
|
||||||
|
typeof json.pointRendering
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||||
|
const pointRendering = json.pointRendering[i]
|
||||||
|
if (pointRendering.marker === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const icon of pointRendering?.marker) {
|
||||||
|
const indexM = pointRendering?.marker.indexOf(icon)
|
||||||
|
if (!icon.icon) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (icon.icon["condition"]) {
|
||||||
|
context
|
||||||
|
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
||||||
|
.err(
|
||||||
|
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.presets !== undefined) {
|
||||||
|
if (typeof json.source === "string") {
|
||||||
|
context.enter("presets").err("A special layer cannot have presets")
|
||||||
|
}
|
||||||
|
// Check that a preset will be picked up by the layer itself
|
||||||
|
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||||
|
for (let i = 0; i < json.presets.length; i++) {
|
||||||
|
const preset = json.presets[i]
|
||||||
|
if (!preset) {
|
||||||
|
context.enters("presets", i).err("This preset is undefined")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!preset.tags) {
|
||||||
|
context.enters("presets", i, "tags").err("No tags defined for this preset")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!preset.tags) {
|
||||||
|
context.enters("presets", i, "title").err("No title defined for this preset")
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = new And(preset.tags.map((t) => TagUtils.Tag(t)))
|
||||||
|
const properties = {}
|
||||||
|
for (const tag of tags.asChange({ id: "node/-1" })) {
|
||||||
|
properties[tag.k] = tag.v
|
||||||
|
}
|
||||||
|
const doMatch = baseTags.matchesProperties(properties)
|
||||||
|
if (!doMatch) {
|
||||||
|
context
|
||||||
|
.enters("presets", i, "tags")
|
||||||
|
.err(
|
||||||
|
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||||
|
tags.asHumanString(false, false, {}) +
|
||||||
|
"\n The required tags are: " +
|
||||||
|
baseTags.asHumanString(false, false, {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
32
src/Models/ThemeConfig/Conversion/ValidateTagRenderings.ts
Normal file
32
src/Models/ThemeConfig/Conversion/ValidateTagRenderings.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Each, Fuse, On } from "./Conversion"
|
||||||
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
|
import { DetectMappingsWithImages } from "./DetectMappingsWithImages"
|
||||||
|
import {
|
||||||
|
DetectConflictingAddExtraTags,
|
||||||
|
DetectMappingsShadowedByCondition,
|
||||||
|
DetectShadowedMappings,
|
||||||
|
DoesImageExist,
|
||||||
|
ValidatePossibleLinks,
|
||||||
|
} from "./Validation"
|
||||||
|
import { MiscTagRenderingChecks } from "./MiscTagRenderingChecks"
|
||||||
|
|
||||||
|
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
||||||
|
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
|
||||||
|
super(
|
||||||
|
"Various validation on tagRenderingConfigs",
|
||||||
|
new MiscTagRenderingChecks(layerConfig),
|
||||||
|
new DetectShadowedMappings(layerConfig),
|
||||||
|
|
||||||
|
new DetectMappingsShadowedByCondition(),
|
||||||
|
new DetectConflictingAddExtraTags(),
|
||||||
|
// TODO enable new DetectNonErasedKeysInMappings(),
|
||||||
|
new DetectMappingsWithImages(doesImageExist),
|
||||||
|
new On("render", new ValidatePossibleLinks()),
|
||||||
|
new On("question", new ValidatePossibleLinks()),
|
||||||
|
new On("questionHint", new ValidatePossibleLinks()),
|
||||||
|
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
|
||||||
|
new MiscTagRenderingChecks(layerConfig),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
182
src/Models/ThemeConfig/Conversion/ValidateTheme.ts
Normal file
182
src/Models/ThemeConfig/Conversion/ValidateTheme.ts
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import { DesugaringStep } from "./Conversion"
|
||||||
|
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||||
|
import { AvailableRasterLayers } from "../../RasterLayers"
|
||||||
|
import { ExtractImages } from "./FixImages"
|
||||||
|
import { ConversionContext } from "./ConversionContext"
|
||||||
|
import LayoutConfig from "../LayoutConfig"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation"
|
||||||
|
|
||||||
|
export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
||||||
|
private static readonly _availableLayers = AvailableRasterLayers.allIds()
|
||||||
|
/**
|
||||||
|
* The paths where this layer is originally saved. Triggers some extra checks
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private readonly _path?: string
|
||||||
|
private readonly _isBuiltin: boolean
|
||||||
|
//private readonly _sharedTagRenderings: Map<string, any>
|
||||||
|
private readonly _validateImage: DesugaringStep<string>
|
||||||
|
private readonly _extractImages: ExtractImages = undefined
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
doesImageExist: DoesImageExist,
|
||||||
|
path: string,
|
||||||
|
isBuiltin: boolean,
|
||||||
|
sharedTagRenderings?: Set<string>,
|
||||||
|
) {
|
||||||
|
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
|
||||||
|
this._validateImage = doesImageExist
|
||||||
|
this._path = path
|
||||||
|
this._isBuiltin = isBuiltin
|
||||||
|
if (sharedTagRenderings) {
|
||||||
|
this._extractImages = new ExtractImages(this._isBuiltin, sharedTagRenderings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
|
||||||
|
const theme = new LayoutConfig(json, this._isBuiltin)
|
||||||
|
{
|
||||||
|
// Legacy format checks
|
||||||
|
if (this._isBuiltin) {
|
||||||
|
if (json["units"] !== undefined) {
|
||||||
|
context.err(
|
||||||
|
"The theme " +
|
||||||
|
json.id +
|
||||||
|
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (json["roamingRenderings"] !== undefined) {
|
||||||
|
context.err(
|
||||||
|
"Theme " +
|
||||||
|
json.id +
|
||||||
|
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!json.title) {
|
||||||
|
context.enter("title").err(`The theme ${json.id} does not have a title defined.`)
|
||||||
|
}
|
||||||
|
if (!json.icon) {
|
||||||
|
context.enter("icon").err("A theme should have an icon")
|
||||||
|
}
|
||||||
|
if (this._isBuiltin && this._extractImages !== undefined) {
|
||||||
|
// Check images: are they local, are the licenses there, is the theme icon square, ...
|
||||||
|
const images = this._extractImages.convert(json, context.inOperation("ValidateTheme"))
|
||||||
|
const remoteImages = images.filter((img) => img.path.indexOf("http") == 0)
|
||||||
|
for (const remoteImage of remoteImages) {
|
||||||
|
context.err(
|
||||||
|
"Found a remote image: " +
|
||||||
|
remoteImage.path +
|
||||||
|
" in theme " +
|
||||||
|
json.id +
|
||||||
|
", please download it.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (const image of images) {
|
||||||
|
this._validateImage.convert(image.path, context.enters(image.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this._isBuiltin) {
|
||||||
|
if (theme.id !== theme.id.toLowerCase()) {
|
||||||
|
context.err("Theme ids should be in lowercase, but it is " + theme.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = this._path.substring(
|
||||||
|
this._path.lastIndexOf("/") + 1,
|
||||||
|
this._path.length - 5,
|
||||||
|
)
|
||||||
|
if (theme.id !== filename) {
|
||||||
|
context.err(
|
||||||
|
"Theme ids should be the same as the name.json, but we got id: " +
|
||||||
|
theme.id +
|
||||||
|
" and filename " +
|
||||||
|
filename +
|
||||||
|
" (" +
|
||||||
|
this._path +
|
||||||
|
")",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this._validateImage.convert(theme.icon, context.enter("icon"))
|
||||||
|
}
|
||||||
|
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
|
||||||
|
if (dups.length > 0) {
|
||||||
|
context.err(
|
||||||
|
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (json["mustHaveLanguage"] !== undefined) {
|
||||||
|
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
|
||||||
|
theme,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
|
||||||
|
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
|
||||||
|
const targetLanguage = theme.title.SupportedLanguages()[0]
|
||||||
|
if (targetLanguage !== "en") {
|
||||||
|
context.err(
|
||||||
|
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Official, public themes must have a full english translation
|
||||||
|
new ValidateLanguageCompleteness("en").convert(theme, context)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
context.err("Could not validate the theme due to: " + e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme.id !== "personal") {
|
||||||
|
new DetectDuplicatePresets().convert(theme, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theme.title) {
|
||||||
|
context.enter("title").err("A theme must have a title")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theme.description) {
|
||||||
|
context.enter("description").err("A theme must have a description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme.overpassUrl && typeof theme.overpassUrl === "string") {
|
||||||
|
context
|
||||||
|
.enter("overpassUrl")
|
||||||
|
.err("The overpassURL is a string, use a list of strings instead. Wrap it with [ ]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.defaultBackgroundId) {
|
||||||
|
const backgroundId = json.defaultBackgroundId
|
||||||
|
|
||||||
|
const isCategory =
|
||||||
|
backgroundId === "photo" || backgroundId === "map" || backgroundId === "osmbasedmap"
|
||||||
|
|
||||||
|
if (!isCategory && !ValidateTheme._availableLayers.has(backgroundId)) {
|
||||||
|
const options = Array.from(ValidateTheme._availableLayers)
|
||||||
|
const nearby = Utils.sortedByLevenshteinDistance(backgroundId, options, (t) => t)
|
||||||
|
context
|
||||||
|
.enter("defaultBackgroundId")
|
||||||
|
.err(
|
||||||
|
`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby
|
||||||
|
.slice(0, 5)
|
||||||
|
.join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < theme.layers.length; i++) {
|
||||||
|
const layer = theme.layers[i]
|
||||||
|
if (!layer.id.match("[a-z][a-z0-9_]*")) {
|
||||||
|
context
|
||||||
|
.enters("layers", i, "id")
|
||||||
|
.err("Invalid ID:" + layer.id + "should match [a-z][a-z0-9_]*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
28
src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers.ts
Normal file
28
src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Bypass, Each, Fuse, On } from "./Conversion"
|
||||||
|
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||||
|
import Constants from "../../Constants"
|
||||||
|
import { DoesImageExist, ValidateLayerConfig } from "./Validation"
|
||||||
|
import { ValidateTheme } from "./ValidateTheme"
|
||||||
|
|
||||||
|
export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
||||||
|
constructor(
|
||||||
|
doesImageExist: DoesImageExist,
|
||||||
|
path: string,
|
||||||
|
isBuiltin: boolean,
|
||||||
|
sharedTagRenderings?: Set<string>,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
"Validates a theme and the contained layers",
|
||||||
|
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
|
||||||
|
new On(
|
||||||
|
"layers",
|
||||||
|
new Each(
|
||||||
|
new Bypass(
|
||||||
|
(layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0,
|
||||||
|
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Bypass, Conversion, DesugaringStep, Each, Fuse, On } from "./Conversion"
|
import { Conversion, DesugaringStep, Fuse } from "./Conversion"
|
||||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
import LayerConfig from "../LayerConfig"
|
import LayerConfig from "../LayerConfig"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
|
@ -8,15 +8,9 @@ import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||||
import LayoutConfig from "../LayoutConfig"
|
import LayoutConfig from "../LayoutConfig"
|
||||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
import { ExtractImages } from "./FixImages"
|
|
||||||
import { And } from "../../../Logic/Tags/And"
|
import { And } from "../../../Logic/Tags/And"
|
||||||
import Translations from "../../../UI/i18n/Translations"
|
|
||||||
import FilterConfigJson from "../Json/FilterConfigJson"
|
import FilterConfigJson from "../Json/FilterConfigJson"
|
||||||
import DeleteConfig from "../DeleteConfig"
|
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||||
import {
|
|
||||||
MappingConfigJson,
|
|
||||||
QuestionableTagRenderingConfigJson,
|
|
||||||
} from "../Json/QuestionableTagRenderingConfigJson"
|
|
||||||
import Validators from "../../../UI/InputElement/Validators"
|
import Validators from "../../../UI/InputElement/Validators"
|
||||||
import TagRenderingConfig from "../TagRenderingConfig"
|
import TagRenderingConfig from "../TagRenderingConfig"
|
||||||
import { parse as parse_html } from "node-html-parser"
|
import { parse as parse_html } from "node-html-parser"
|
||||||
|
@ -24,12 +18,10 @@ import PresetConfig from "../PresetConfig"
|
||||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||||
import { Translatable } from "../Json/Translatable"
|
import { Translatable } from "../Json/Translatable"
|
||||||
import { ConversionContext } from "./ConversionContext"
|
import { ConversionContext } from "./ConversionContext"
|
||||||
import { AvailableRasterLayers } from "../../RasterLayers"
|
|
||||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||||
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
|
import { PrevalidateLayer } from "./PrevalidateLayer"
|
||||||
import { Tag } from "../../../Logic/Tags/Tag"
|
|
||||||
|
|
||||||
class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
export class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
||||||
private readonly _languages: string[]
|
private readonly _languages: string[]
|
||||||
|
|
||||||
constructor(...languages: string[]) {
|
constructor(...languages: string[]) {
|
||||||
|
@ -130,203 +122,6 @@ export class DoesImageExist extends DesugaringStep<string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|
||||||
private static readonly _availableLayers = AvailableRasterLayers.allIds()
|
|
||||||
/**
|
|
||||||
* The paths where this layer is originally saved. Triggers some extra checks
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _path?: string
|
|
||||||
private readonly _isBuiltin: boolean
|
|
||||||
//private readonly _sharedTagRenderings: Map<string, any>
|
|
||||||
private readonly _validateImage: DesugaringStep<string>
|
|
||||||
private readonly _extractImages: ExtractImages = undefined
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
doesImageExist: DoesImageExist,
|
|
||||||
path: string,
|
|
||||||
isBuiltin: boolean,
|
|
||||||
sharedTagRenderings?: Set<string>,
|
|
||||||
) {
|
|
||||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
|
|
||||||
this._validateImage = doesImageExist
|
|
||||||
this._path = path
|
|
||||||
this._isBuiltin = isBuiltin
|
|
||||||
if (sharedTagRenderings) {
|
|
||||||
this._extractImages = new ExtractImages(this._isBuiltin, sharedTagRenderings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
|
|
||||||
const theme = new LayoutConfig(json, this._isBuiltin)
|
|
||||||
{
|
|
||||||
// Legacy format checks
|
|
||||||
if (this._isBuiltin) {
|
|
||||||
if (json["units"] !== undefined) {
|
|
||||||
context.err(
|
|
||||||
"The theme " +
|
|
||||||
json.id +
|
|
||||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (json["roamingRenderings"] !== undefined) {
|
|
||||||
context.err(
|
|
||||||
"Theme " +
|
|
||||||
json.id +
|
|
||||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!json.title) {
|
|
||||||
context.enter("title").err(`The theme ${json.id} does not have a title defined.`)
|
|
||||||
}
|
|
||||||
if (!json.icon) {
|
|
||||||
context.enter("icon").err("A theme should have an icon")
|
|
||||||
}
|
|
||||||
if (this._isBuiltin && this._extractImages !== undefined) {
|
|
||||||
// Check images: are they local, are the licenses there, is the theme icon square, ...
|
|
||||||
const images = this._extractImages.convert(json, context.inOperation("ValidateTheme"))
|
|
||||||
const remoteImages = images.filter((img) => img.path.indexOf("http") == 0)
|
|
||||||
for (const remoteImage of remoteImages) {
|
|
||||||
context.err(
|
|
||||||
"Found a remote image: " +
|
|
||||||
remoteImage.path +
|
|
||||||
" in theme " +
|
|
||||||
json.id +
|
|
||||||
", please download it.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (const image of images) {
|
|
||||||
this._validateImage.convert(image.path, context.enters(image.context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this._isBuiltin) {
|
|
||||||
if (theme.id !== theme.id.toLowerCase()) {
|
|
||||||
context.err("Theme ids should be in lowercase, but it is " + theme.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = this._path.substring(
|
|
||||||
this._path.lastIndexOf("/") + 1,
|
|
||||||
this._path.length - 5,
|
|
||||||
)
|
|
||||||
if (theme.id !== filename) {
|
|
||||||
context.err(
|
|
||||||
"Theme ids should be the same as the name.json, but we got id: " +
|
|
||||||
theme.id +
|
|
||||||
" and filename " +
|
|
||||||
filename +
|
|
||||||
" (" +
|
|
||||||
this._path +
|
|
||||||
")",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this._validateImage.convert(theme.icon, context.enter("icon"))
|
|
||||||
}
|
|
||||||
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
|
|
||||||
if (dups.length > 0) {
|
|
||||||
context.err(
|
|
||||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (json["mustHaveLanguage"] !== undefined) {
|
|
||||||
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
|
|
||||||
theme,
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
|
|
||||||
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
|
|
||||||
const targetLanguage = theme.title.SupportedLanguages()[0]
|
|
||||||
if (targetLanguage !== "en") {
|
|
||||||
context.err(
|
|
||||||
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Official, public themes must have a full english translation
|
|
||||||
new ValidateLanguageCompleteness("en").convert(theme, context)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
context.err("Could not validate the theme due to: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theme.id !== "personal") {
|
|
||||||
new DetectDuplicatePresets().convert(theme, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!theme.title) {
|
|
||||||
context.enter("title").err("A theme must have a title")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!theme.description) {
|
|
||||||
context.enter("description").err("A theme must have a description")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theme.overpassUrl && typeof theme.overpassUrl === "string") {
|
|
||||||
context
|
|
||||||
.enter("overpassUrl")
|
|
||||||
.err("The overpassURL is a string, use a list of strings instead. Wrap it with [ ]")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.defaultBackgroundId) {
|
|
||||||
const backgroundId = json.defaultBackgroundId
|
|
||||||
|
|
||||||
const isCategory =
|
|
||||||
backgroundId === "photo" || backgroundId === "map" || backgroundId === "osmbasedmap"
|
|
||||||
|
|
||||||
if (!isCategory && !ValidateTheme._availableLayers.has(backgroundId)) {
|
|
||||||
const options = Array.from(ValidateTheme._availableLayers)
|
|
||||||
const nearby = Utils.sortedByLevenshteinDistance(backgroundId, options, (t) => t)
|
|
||||||
context
|
|
||||||
.enter("defaultBackgroundId")
|
|
||||||
.err(
|
|
||||||
`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby
|
|
||||||
.slice(0, 5)
|
|
||||||
.join(", ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < theme.layers.length; i++) {
|
|
||||||
const layer = theme.layers[i]
|
|
||||||
if (!layer.id.match("[a-z][a-z0-9_]*")) {
|
|
||||||
context
|
|
||||||
.enters("layers", i, "id")
|
|
||||||
.err("Invalid ID:" + layer.id + "should match [a-z][a-z0-9_]*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|
||||||
constructor(
|
|
||||||
doesImageExist: DoesImageExist,
|
|
||||||
path: string,
|
|
||||||
isBuiltin: boolean,
|
|
||||||
sharedTagRenderings?: Set<string>,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
"Validates a theme and the contained layers",
|
|
||||||
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
|
|
||||||
new On(
|
|
||||||
"layers",
|
|
||||||
new Each(
|
|
||||||
new Bypass(
|
|
||||||
(layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0,
|
|
||||||
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
|
@ -763,77 +558,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
|
export class ValidatePossibleLinks extends DesugaringStep<string | Record<string, string>> {
|
||||||
private readonly _doesImageExist: DoesImageExist
|
|
||||||
|
|
||||||
constructor(doesImageExist: DoesImageExist) {
|
|
||||||
super(
|
|
||||||
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
|
|
||||||
[],
|
|
||||||
"DetectMappingsWithImages",
|
|
||||||
)
|
|
||||||
this._doesImageExist = doesImageExist
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* const context = ConversionContext.test()
|
|
||||||
* const r = new DetectMappingsWithImages(new DoesImageExist(new Set<string>())).convert({
|
|
||||||
* "mappings": [
|
|
||||||
* {
|
|
||||||
* "if": "bicycle_parking=stands",
|
|
||||||
* "then": {
|
|
||||||
* "en": "Staple racks <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "nl": "Nietjes <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "fr": "Arceaux <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "gl": "De roda (Stands) <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "de": "Fahrradbügel <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "hu": "Korlát <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "it": "Archetti <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>",
|
|
||||||
* "zh_Hant": "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
|
|
||||||
* }
|
|
||||||
* }]
|
|
||||||
* }, context);
|
|
||||||
* context.hasErrors() // => true
|
|
||||||
* context.getAll("error").some(msg => msg.message.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
|
|
||||||
*/
|
|
||||||
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
|
|
||||||
if (json.mappings === undefined || json.mappings.length === 0) {
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
const ignoreToken = "ignore-image-in-then"
|
|
||||||
for (let i = 0; i < json.mappings.length; i++) {
|
|
||||||
const mapping = json.mappings[i]
|
|
||||||
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
|
|
||||||
const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? [])
|
|
||||||
const ctx = context.enters("mappings", i)
|
|
||||||
if (images.length > 0) {
|
|
||||||
if (!ignore) {
|
|
||||||
ctx.err(
|
|
||||||
`A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
|
|
||||||
", ",
|
|
||||||
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ctx.info(
|
|
||||||
`Ignored image ${images.join(
|
|
||||||
", ",
|
|
||||||
)} in 'then'-clause of a mapping as this check has been disabled`,
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const image of images) {
|
|
||||||
this._doesImageExist.convert(image, ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (ignore) {
|
|
||||||
ctx.warn(`Unused '${ignoreToken}' - please remove this`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ValidatePossibleLinks extends DesugaringStep<string | Record<string, string>> {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
|
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
|
||||||
|
@ -891,7 +616,7 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckTranslation extends DesugaringStep<Translatable> {
|
export class CheckTranslation extends DesugaringStep<Translatable> {
|
||||||
public static readonly allowUndefined: CheckTranslation = new CheckTranslation(true)
|
public static readonly allowUndefined: CheckTranslation = new CheckTranslation(true)
|
||||||
public static readonly noUndefined: CheckTranslation = new CheckTranslation()
|
public static readonly noUndefined: CheckTranslation = new CheckTranslation()
|
||||||
private readonly _allowUndefined: boolean
|
private readonly _allowUndefined: boolean
|
||||||
|
@ -939,660 +664,6 @@ class CheckTranslation extends DesugaringStep<Translatable> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|
||||||
private readonly _layerConfig: LayerConfigJson
|
|
||||||
|
|
||||||
constructor(layerConfig?: LayerConfigJson) {
|
|
||||||
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
|
|
||||||
this._layerConfig = layerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(
|
|
||||||
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
|
|
||||||
context: ConversionContext,
|
|
||||||
): TagRenderingConfigJson {
|
|
||||||
if (json["special"] !== undefined) {
|
|
||||||
context.err(
|
|
||||||
"Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(json).length === 1 && typeof json["render"] === "string") {
|
|
||||||
context.warn(
|
|
||||||
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
for (const key of ["question", "questionHint", "render"]) {
|
|
||||||
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
|
|
||||||
}
|
|
||||||
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
|
|
||||||
const mapping: MappingConfigJson = json.mappings[i]
|
|
||||||
CheckTranslation.noUndefined.convert(
|
|
||||||
mapping.then,
|
|
||||||
context.enters("mappings", i, "then"),
|
|
||||||
)
|
|
||||||
if (!mapping.if) {
|
|
||||||
console.log(
|
|
||||||
"Checking mappings",
|
|
||||||
i,
|
|
||||||
"if",
|
|
||||||
mapping.if,
|
|
||||||
context.path.join("."),
|
|
||||||
mapping.then,
|
|
||||||
)
|
|
||||||
context.enters("mappings", i, "if").err("No `if` is defined")
|
|
||||||
}
|
|
||||||
if (mapping.addExtraTags) {
|
|
||||||
for (let j = 0; j < mapping.addExtraTags.length; j++) {
|
|
||||||
if (!mapping.addExtraTags[j]) {
|
|
||||||
context
|
|
||||||
.enters("mappings", i, "addExtraTags", j)
|
|
||||||
.err(
|
|
||||||
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const en = mapping?.then?.["en"]
|
|
||||||
if (en && this.detectYesOrNo(en)) {
|
|
||||||
console.log("Found a match with yes or no: ", { en })
|
|
||||||
context
|
|
||||||
.enters("mappings", i, "then")
|
|
||||||
.warn(
|
|
||||||
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (json["group"]) {
|
|
||||||
context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
|
|
||||||
context.err(
|
|
||||||
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
|
|
||||||
context.err("A question is defined, but there is only one option to choose from.")
|
|
||||||
}
|
|
||||||
if (json["questionHint"] && !json["question"]) {
|
|
||||||
context
|
|
||||||
.enter("questionHint")
|
|
||||||
.err(
|
|
||||||
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.icon?.["size"]) {
|
|
||||||
context
|
|
||||||
.enters("icon", "size")
|
|
||||||
.err(
|
|
||||||
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.freeform) {
|
|
||||||
if (json.render === undefined) {
|
|
||||||
context
|
|
||||||
.enter("render")
|
|
||||||
.err(
|
|
||||||
"This tagRendering allows to set a value to key " +
|
|
||||||
json.freeform.key +
|
|
||||||
", but does not define a `render`. Please, add a value here which contains `{" +
|
|
||||||
json.freeform.key +
|
|
||||||
"}`",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const render = new Translation(<any>json.render)
|
|
||||||
for (const ln in render.translations) {
|
|
||||||
if (ln.startsWith("_")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const txt: string = render.translations[ln]
|
|
||||||
if (txt === "") {
|
|
||||||
context.enter("render").err(" Rendering for language " + ln + " is empty")
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
|
|
||||||
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (txt.indexOf("{" + json.freeform.key + ":") >= 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.freeform["type"] === "opening_hours" &&
|
|
||||||
txt.indexOf("{opening_hours_table(") >= 0
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
|
||||||
if (
|
|
||||||
keyFirstArg.some(
|
|
||||||
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
json.freeform["type"] === "wikidata" &&
|
|
||||||
txt.indexOf("{wikipedia(" + json.freeform.key) >= 0
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (json.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
json.freeform["type"] === "wikidata" &&
|
|
||||||
txt.indexOf(`{wikidata_label(${json.freeform.key})`) >= 0
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (json.freeform.key.indexOf("wikidata") >= 0) {
|
|
||||||
context
|
|
||||||
.enter("render")
|
|
||||||
.err(
|
|
||||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
txt.indexOf(json.freeform.key) >= 0 &&
|
|
||||||
txt.indexOf("{" + json.freeform.key + "}") < 0
|
|
||||||
) {
|
|
||||||
context
|
|
||||||
.enter("render")
|
|
||||||
.err(
|
|
||||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
context
|
|
||||||
.enter("render")
|
|
||||||
.err(
|
|
||||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this._layerConfig?.source?.osmTags &&
|
|
||||||
NameSuggestionIndex.supportedTypes().indexOf(json.freeform.key) >= 0
|
|
||||||
) {
|
|
||||||
const tags = TagUtils.TagD(this._layerConfig?.source?.osmTags)?.usedTags()
|
|
||||||
const suggestions = NameSuggestionIndex.getSuggestionsFor(json.freeform.key, tags)
|
|
||||||
if (suggestions === undefined) {
|
|
||||||
context
|
|
||||||
.enters("freeform", "type")
|
|
||||||
.err(
|
|
||||||
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
|
|
||||||
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (json.freeform.type === "nsi") {
|
|
||||||
context
|
|
||||||
.enters("freeform", "type")
|
|
||||||
.warn(
|
|
||||||
"No need to explicitly set type to 'NSI', autodetected based on freeform type",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (json.render && json["question"] && json.freeform === undefined) {
|
|
||||||
context.err(
|
|
||||||
`Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation(
|
|
||||||
json["question"],
|
|
||||||
).textFor("en")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const freeformType = json["freeform"]?.["type"]
|
|
||||||
if (freeformType) {
|
|
||||||
if (Validators.availableTypes.indexOf(freeformType) < 0) {
|
|
||||||
context
|
|
||||||
.enters("freeform", "type")
|
|
||||||
.err(
|
|
||||||
"Unknown type: " +
|
|
||||||
freeformType +
|
|
||||||
"; try one of " +
|
|
||||||
Validators.availableTypes.join(", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.hasErrors()) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* const obj = new MiscTagRenderingChecks()
|
|
||||||
* obj.detectYesOrNo("Yes, this place has") // => true
|
|
||||||
* obj.detectYesOrNo("Yes") // => true
|
|
||||||
* obj.detectYesOrNo("No, this place does not have...") // => true
|
|
||||||
* obj.detectYesOrNo("This place does not have...") // => false
|
|
||||||
*/
|
|
||||||
private detectYesOrNo(en: string): boolean {
|
|
||||||
return en.toLowerCase().match(/^(yes|no)([,:;.?]|$)/) !== null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|
||||||
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
|
|
||||||
super(
|
|
||||||
"Various validation on tagRenderingConfigs",
|
|
||||||
new MiscTagRenderingChecks(layerConfig),
|
|
||||||
new DetectShadowedMappings(layerConfig),
|
|
||||||
|
|
||||||
new DetectMappingsShadowedByCondition(),
|
|
||||||
new DetectConflictingAddExtraTags(),
|
|
||||||
// TODO enable new DetectNonErasedKeysInMappings(),
|
|
||||||
new DetectMappingsWithImages(doesImageExist),
|
|
||||||
new On("render", new ValidatePossibleLinks()),
|
|
||||||
new On("question", new ValidatePossibleLinks()),
|
|
||||||
new On("questionHint", new ValidatePossibleLinks()),
|
|
||||||
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
|
|
||||||
new MiscTagRenderingChecks(layerConfig),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|
||||||
private readonly _isBuiltin: boolean
|
|
||||||
private readonly _doesImageExist: DoesImageExist
|
|
||||||
/**
|
|
||||||
* The paths where this layer is originally saved. Triggers some extra checks
|
|
||||||
*/
|
|
||||||
private readonly _path: string
|
|
||||||
private readonly _studioValidations: boolean
|
|
||||||
private readonly _validatePointRendering = new ValidatePointRendering()
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
path: string,
|
|
||||||
isBuiltin: boolean,
|
|
||||||
doesImageExist: DoesImageExist,
|
|
||||||
studioValidations: boolean,
|
|
||||||
) {
|
|
||||||
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
|
|
||||||
this._path = path
|
|
||||||
this._isBuiltin = isBuiltin
|
|
||||||
this._doesImageExist = doesImageExist
|
|
||||||
this._studioValidations = studioValidations
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
|
||||||
if (json.id === undefined) {
|
|
||||||
context.enter("id").err(`Not a valid layer: id is undefined`)
|
|
||||||
} else {
|
|
||||||
if (json.id?.toLowerCase() !== json.id) {
|
|
||||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
|
||||||
}
|
|
||||||
const layerRegex = /[a-zA-Z][a-zA-Z_0-9]+/
|
|
||||||
if (json.id.match(layerRegex) === null) {
|
|
||||||
context.enter("id").err("Invalid ID. A layer ID should match " + layerRegex.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.source === undefined) {
|
|
||||||
context
|
|
||||||
.enter("source")
|
|
||||||
.err(
|
|
||||||
"No source section is defined; please define one as data is not loaded otherwise",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (json.source === "special" || json.source === "special:library") {
|
|
||||||
} else if (json.source && json.source["osmTags"] === undefined) {
|
|
||||||
context
|
|
||||||
.enters("source", "osmTags")
|
|
||||||
.err(
|
|
||||||
"No osmTags defined in the source section - these should always be present, even for geojson layer",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
|
|
||||||
if (osmTags.isNegative()) {
|
|
||||||
context
|
|
||||||
.enters("source", "osmTags")
|
|
||||||
.err(
|
|
||||||
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
|
||||||
osmTags.asHumanString(false, false, {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.source["geoJsonSource"] !== undefined) {
|
|
||||||
context
|
|
||||||
.enters("source", "geoJsonSource")
|
|
||||||
.err("Use 'geoJson' instead of 'geoJsonSource'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.source["geojson"] !== undefined) {
|
|
||||||
context
|
|
||||||
.enters("source", "geojson")
|
|
||||||
.err("Use 'geoJson' instead of 'geojson' (the J is a capital letter)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.syncSelection !== undefined &&
|
|
||||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
|
||||||
) {
|
|
||||||
context
|
|
||||||
.enter("syncSelection")
|
|
||||||
.err(
|
|
||||||
"Invalid sync-selection: must be one of " +
|
|
||||||
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
|
||||||
" but got '" +
|
|
||||||
json.syncSelection +
|
|
||||||
"'",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (json["pointRenderings"]?.length > 0) {
|
|
||||||
context
|
|
||||||
.enter("pointRenderings")
|
|
||||||
.err("Detected a 'pointRenderingS', it is written singular")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(json.pointRendering?.length > 0) &&
|
|
||||||
json.pointRendering !== null &&
|
|
||||||
json.source !== "special" &&
|
|
||||||
json.source !== "special:library"
|
|
||||||
) {
|
|
||||||
context.enter("pointRendering").err("There are no pointRenderings at all...")
|
|
||||||
}
|
|
||||||
|
|
||||||
json.pointRendering?.forEach((pr, i) =>
|
|
||||||
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (json["mapRendering"]) {
|
|
||||||
context.enter("mapRendering").err("This layer has a legacy 'mapRendering'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.presets?.length > 0) {
|
|
||||||
if (!(json.pointRendering?.length > 0)) {
|
|
||||||
context.enter("presets").warn("A preset is defined, but there is no pointRendering")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.source === "special") {
|
|
||||||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
|
||||||
context.err(
|
|
||||||
"Layer " +
|
|
||||||
json.id +
|
|
||||||
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.hasErrors()) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
|
|
||||||
new On("tagRenderings", new Each(new ValidateTagRenderings(json)))
|
|
||||||
if (json.title === undefined && json.source !== "special:library") {
|
|
||||||
context
|
|
||||||
.enter("title")
|
|
||||||
.err(
|
|
||||||
"This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (json.title === null) {
|
|
||||||
context.info(
|
|
||||||
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Check for multiple, identical builtin questions - usability for studio users
|
|
||||||
const duplicates = Utils.Duplicates(
|
|
||||||
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"),
|
|
||||||
)
|
|
||||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
|
||||||
const tagRendering = json.tagRenderings[i]
|
|
||||||
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
|
|
||||||
context
|
|
||||||
.enters("tagRenderings", i)
|
|
||||||
.err(`This builtin question is used multiple times (${tagRendering})`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json["builtin"] !== undefined) {
|
|
||||||
context.err("This layer hasn't been expanded: " + json)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.minzoom > Constants.minZoomLevelToAddNewPoint) {
|
|
||||||
const c = context.enter("minzoom")
|
|
||||||
const msg = `Minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`
|
|
||||||
if (json.presets?.length > 0) {
|
|
||||||
c.err(msg)
|
|
||||||
} else {
|
|
||||||
c.warn(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// duplicate ids in tagrenderings check
|
|
||||||
const duplicates = Utils.NoNull(
|
|
||||||
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))),
|
|
||||||
)
|
|
||||||
if (duplicates.length > 0) {
|
|
||||||
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
|
|
||||||
context
|
|
||||||
.enter("tagRenderings")
|
|
||||||
.err(
|
|
||||||
"Some tagrenderings have a duplicate id: " +
|
|
||||||
duplicates.join(", ") +
|
|
||||||
"\n" +
|
|
||||||
JSON.stringify(
|
|
||||||
json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) {
|
|
||||||
if (json.deletion.softDeletionTags === undefined) {
|
|
||||||
context
|
|
||||||
.enter("deletion")
|
|
||||||
.warn("No soft-deletion tags in deletion block for layer " + json.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
} catch (e) {
|
|
||||||
context.err("Could not validate layer due to: " + e + e.stack)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._studioValidations) {
|
|
||||||
if (!json.description) {
|
|
||||||
context.enter("description").err("A description is required")
|
|
||||||
}
|
|
||||||
if (!json.name) {
|
|
||||||
context.enter("name").err("A name is required")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isBuiltin) {
|
|
||||||
// Some checks for legacy elements
|
|
||||||
|
|
||||||
if (json["overpassTags"] !== undefined) {
|
|
||||||
context.err(
|
|
||||||
"Layer " +
|
|
||||||
json.id +
|
|
||||||
"still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const forbiddenTopLevel = [
|
|
||||||
"icon",
|
|
||||||
"wayHandling",
|
|
||||||
"roamingRenderings",
|
|
||||||
"roamingRendering",
|
|
||||||
"label",
|
|
||||||
"width",
|
|
||||||
"color",
|
|
||||||
"colour",
|
|
||||||
"iconOverlays",
|
|
||||||
]
|
|
||||||
for (const forbiddenKey of forbiddenTopLevel) {
|
|
||||||
if (json[forbiddenKey] !== undefined)
|
|
||||||
context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
|
|
||||||
}
|
|
||||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
|
||||||
context.err(
|
|
||||||
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.isShown !== undefined &&
|
|
||||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
|
||||||
) {
|
|
||||||
context.warn("Has a tagRendering as `isShown`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this._isBuiltin) {
|
|
||||||
// Check location of layer file
|
|
||||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
|
||||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
|
||||||
context.err(
|
|
||||||
"Layer is in an incorrect place. The path is " +
|
|
||||||
this._path +
|
|
||||||
", but expected " +
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this._isBuiltin) {
|
|
||||||
// Check for correct IDs
|
|
||||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
|
||||||
const emptyIndexes: number[] = []
|
|
||||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
|
||||||
const tagRendering = json.tagRenderings[i]
|
|
||||||
if (tagRendering["id"] === "") {
|
|
||||||
emptyIndexes.push(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context
|
|
||||||
.enter(["tagRenderings", ...emptyIndexes])
|
|
||||||
.err(
|
|
||||||
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
|
|
||||||
",",
|
|
||||||
)}])`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateIds = Utils.Duplicates(
|
|
||||||
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"),
|
|
||||||
)
|
|
||||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
|
||||||
context
|
|
||||||
.enter("tagRenderings")
|
|
||||||
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.description === undefined) {
|
|
||||||
if (typeof json.source === null) {
|
|
||||||
context.err("A priviliged layer must have a description")
|
|
||||||
} else {
|
|
||||||
context.warn("A builtin layer should have a description")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.filter) {
|
|
||||||
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.tagRenderings !== undefined) {
|
|
||||||
new On(
|
|
||||||
"tagRenderings",
|
|
||||||
new Each(new ValidateTagRenderings(json, this._doesImageExist)),
|
|
||||||
).convert(json, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
|
||||||
if (!Array.isArray(json.pointRendering)) {
|
|
||||||
throw (
|
|
||||||
"pointRendering in " +
|
|
||||||
json.id +
|
|
||||||
" is not iterable, it is: " +
|
|
||||||
typeof json.pointRendering
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
|
||||||
const pointRendering = json.pointRendering[i]
|
|
||||||
if (pointRendering.marker === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (const icon of pointRendering?.marker) {
|
|
||||||
const indexM = pointRendering?.marker.indexOf(icon)
|
|
||||||
if (!icon.icon) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (icon.icon["condition"]) {
|
|
||||||
context
|
|
||||||
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
|
||||||
.err(
|
|
||||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.presets !== undefined) {
|
|
||||||
if (typeof json.source === "string") {
|
|
||||||
context.enter("presets").err("A special layer cannot have presets")
|
|
||||||
}
|
|
||||||
// Check that a preset will be picked up by the layer itself
|
|
||||||
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
|
||||||
for (let i = 0; i < json.presets.length; i++) {
|
|
||||||
const preset = json.presets[i]
|
|
||||||
if (!preset) {
|
|
||||||
context.enters("presets", i).err("This preset is undefined")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!preset.tags) {
|
|
||||||
context.enters("presets", i, "tags").err("No tags defined for this preset")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!preset.tags) {
|
|
||||||
context.enters("presets", i, "title").err("No title defined for this preset")
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = new And(preset.tags.map((t) => TagUtils.Tag(t)))
|
|
||||||
const properties = {}
|
|
||||||
for (const tag of tags.asChange({ id: "node/-1" })) {
|
|
||||||
properties[tag.k] = tag.v
|
|
||||||
}
|
|
||||||
const doMatch = baseTags.matchesProperties(properties)
|
|
||||||
if (!doMatch) {
|
|
||||||
context
|
|
||||||
.enters("presets", i, "tags")
|
|
||||||
.err(
|
|
||||||
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
|
||||||
tags.asHumanString(false, false, {}) +
|
|
||||||
"\n The required tags are: " +
|
|
||||||
baseTags.asHumanString(false, false, {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
||||||
private readonly validator: ValidateLayer
|
private readonly validator: ValidateLayer
|
||||||
|
|
||||||
|
@ -1623,7 +694,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
|
export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Various checks for pointRenderings", [], "ValidatePOintRendering")
|
super("Various checks for pointRenderings", [], "ValidatePOintRendering")
|
||||||
}
|
}
|
||||||
|
|
|
@ -451,7 +451,7 @@ export interface LayoutConfigJson {
|
||||||
* ifunset: Write 'change_within_x_m' as usual and if GPS is enabled
|
* ifunset: Write 'change_within_x_m' as usual and if GPS is enabled
|
||||||
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
|
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
|
||||||
*/
|
*/
|
||||||
enableMorePrivacy: boolean
|
enableMorePrivacy?: boolean
|
||||||
/**
|
/**
|
||||||
* question: Should this theme have the cache enabled?
|
* question: Should this theme have the cache enabled?
|
||||||
*
|
*
|
||||||
|
@ -462,4 +462,10 @@ export interface LayoutConfigJson {
|
||||||
* group: hidden
|
* group: hidden
|
||||||
*/
|
*/
|
||||||
enableCache?: true | boolean
|
enableCache?: true | boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set by the preprocessor
|
||||||
|
* group: hidden
|
||||||
|
*/
|
||||||
|
_usedImages?: string[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,10 @@ export default interface PointRenderingConfigJson {
|
||||||
* question: What rotation should be applied on the icon?
|
* question: What rotation should be applied on the icon?
|
||||||
* This is mostly useful for items that face a specific direction, such as surveillance cameras
|
* This is mostly useful for items that face a specific direction, such as surveillance cameras
|
||||||
* This is interpreted as css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
|
* This is interpreted as css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
|
||||||
|
*
|
||||||
|
* If the icon is shown on the projected centerpoint of a way, one can also use `_direction:centerpoint`
|
||||||
|
*
|
||||||
|
* suggestions: return [{if: "value={_direction:centerpoint}deg", then: "Point north if the icon is pointing up"}, {if: "value=calc( {_direction:centerpoint}deg + 90deg)", then: "Point east if the icon is pointing up"}, {if: "value=calc( {_direction:centerpoint}deg + 180deg)", then: "Point south if the icon is pointing up"},{if: "value=calc( {_direction:centerpoint}deg + 270deg)", then: "Point west if the icon is pointing up"}]
|
||||||
* ifunset: Do not rotate
|
* ifunset: Do not rotate
|
||||||
*/
|
*/
|
||||||
rotation?: string | TagRenderingConfigJson
|
rotation?: string | TagRenderingConfigJson
|
||||||
|
|
|
@ -338,12 +338,7 @@ export default class LayoutConfig implements LayoutInformation {
|
||||||
...json,
|
...json,
|
||||||
layers: json.layers.filter((l) => l["id"] !== "favourite"),
|
layers: json.layers.filter((l) => l["id"] !== "favourite"),
|
||||||
}
|
}
|
||||||
const usedImages = new ExtractImages(this.official, undefined)
|
const usedImages =json._usedImages
|
||||||
.convertStrict(
|
|
||||||
jsonNoFavourites,
|
|
||||||
ConversionContext.construct([json.id], ["ExtractImages"])
|
|
||||||
)
|
|
||||||
.flatMap((i) => i.path)
|
|
||||||
usedImages.sort()
|
usedImages.sort()
|
||||||
|
|
||||||
this.usedImages = Utils.Dedup(usedImages)
|
this.usedImages = Utils.Dedup(usedImages)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
Pipe,
|
Pipe,
|
||||||
} from "../../Models/ThemeConfig/Conversion/Conversion"
|
} from "../../Models/ThemeConfig/Conversion/Conversion"
|
||||||
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
||||||
import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
|
import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
||||||
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
|
@ -23,6 +23,7 @@ import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
|
||||||
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
|
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
|
||||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||||
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||||
|
import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme"
|
||||||
|
|
||||||
export interface HighlightedTagRendering {
|
export interface HighlightedTagRendering {
|
||||||
path: ReadonlyArray<string | number>
|
path: ReadonlyArray<string | number>
|
||||||
|
|
|
@ -1493,7 +1493,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SameObject(a: any, b: any) {
|
public static SameObject<T>(a: T, b: T, ignoreKeys?: string[]): boolean {
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue