Reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2022-09-08 21:40:48 +02:00
parent e22d189376
commit b541d3eab4
382 changed files with 50893 additions and 35566 deletions

View file

@ -1,13 +1,17 @@
import {DesugaringStep} from "./Conversion";
import {Utils} from "../../../Utils";
import Translations from "../../../UI/i18n/Translations";
import { DesugaringStep } from "./Conversion"
import { Utils } from "../../../Utils"
import Translations from "../../../UI/i18n/Translations"
export class AddContextToTranslations<T> extends DesugaringStep<T> {
private readonly _prefix: string;
private readonly _prefix: string
constructor(prefix = "") {
super("Adds a '_context' to every object that is probably a translation", ["_context"], "AddContextToTranslation");
this._prefix = prefix;
super(
"Adds a '_context' to every object that is probably a translation",
["_context"],
"AddContextToTranslation"
)
this._prefix = prefix
}
/**
@ -21,7 +25,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* }
* }
* ]
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const expected = {
@ -35,10 +39,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* }
* }
* ]
* ]
* }
* rewritten // => expected
*
*
* // should use the ID if one is present instead of the index
* const theme = {
* layers: [
@ -51,7 +55,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* ]
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const expected = {
@ -66,10 +70,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* ]
* ]
* }
* rewritten // => expected
*
*
* // should preserve nulls
* const theme = {
* layers: [
@ -79,7 +83,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* name:null
* }
* }
* ]
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const expected = {
@ -90,11 +94,11 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* name: null
* }
* }
* ]
* ]
* }
* rewritten // => expected
*
*
*
*
* // Should ignore all if '#dont-translate' is set
* const theme = {
* "#dont-translate": "*",
@ -107,43 +111,47 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* }
* }
* ]
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* rewritten // => theme
*
*
*/
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
if(json["#dont-translate"] === "*"){
return {result: json}
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json["#dont-translate"] === "*") {
return { result: json }
}
const result = Utils.WalkJson(json, (leaf, path) => {
if(leaf === undefined || leaf === null){
return leaf
}
if (typeof leaf === "object") {
// follow the path. If we encounter a number, check that there is no ID we can use instead
let breadcrumb = json;
for (let i = 0; i < path.length; i++) {
const pointer = path[i]
breadcrumb = breadcrumb[pointer]
if(pointer.match("[0-9]+") && breadcrumb["id"] !== undefined){
path[i] = breadcrumb["id"]
}
const result = Utils.WalkJson(
json,
(leaf, path) => {
if (leaf === undefined || leaf === null) {
return leaf
}
return {...leaf, _context: this._prefix + context + "." + path.join(".")}
} else {
return leaf
}
}, obj => obj === undefined || obj === null || Translations.isProbablyATranslation(obj))
if (typeof leaf === "object") {
// follow the path. If we encounter a number, check that there is no ID we can use instead
let breadcrumb = json
for (let i = 0; i < path.length; i++) {
const pointer = path[i]
breadcrumb = breadcrumb[pointer]
if (pointer.match("[0-9]+") && breadcrumb["id"] !== undefined) {
path[i] = breadcrumb["id"]
}
}
return { ...leaf, _context: this._prefix + context + "." + path.join(".") }
} else {
return leaf
}
},
(obj) => obj === undefined || obj === null || Translations.isProbablyATranslation(obj)
)
return {
result
};
result,
}
}
}
}

View file

@ -1,37 +1,41 @@
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {Utils} from "../../../Utils";
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
export interface DesugaringContext {
tagRenderings: Map<string, TagRenderingConfigJson>
sharedLayers: Map<string, LayerConfigJson>,
sharedLayers: Map<string, LayerConfigJson>
publicLayers?: Set<string>
}
export abstract class Conversion<TIn, TOut> {
public readonly modifiedAttributes: string[];
public readonly modifiedAttributes: string[]
public readonly name: string
protected readonly doc: string;
protected readonly doc: string
constructor(doc: string, modifiedAttributes: string[] = [], name: string) {
this.modifiedAttributes = modifiedAttributes;
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
this.modifiedAttributes = modifiedAttributes
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ")
this.name = name
}
public static strict<T>(fixed: { errors?: string[], warnings?: string[], information?: string[], result?: T }): T {
fixed.information?.forEach(i => console.log(" ", i))
public static strict<T>(fixed: {
errors?: string[]
warnings?: string[]
information?: string[]
result?: T
}): T {
fixed.information?.forEach((i) => console.log(" ", i))
const yellow = (s) => "\x1b[33m" + s + "\x1b[0m"
const red = s => '\x1b[31m' + s + '\x1b[0m'
fixed.warnings?.forEach(w => console.warn(red(`<!> `), yellow(w)))
const red = (s) => "\x1b[31m" + s + "\x1b[0m"
fixed.warnings?.forEach((w) => console.warn(red(`<!> `), yellow(w)))
if (fixed?.errors !== undefined && fixed?.errors?.length > 0) {
fixed.errors?.forEach(e => console.error(red(`ERR ` + e)))
fixed.errors?.forEach((e) => console.error(red(`ERR ` + e)))
throw "Detected one or more errors, stopping now"
}
return fixed.result;
return fixed.result
}
public convertStrict(json: TIn, context: string): TOut {
@ -39,7 +43,13 @@ export abstract class Conversion<TIn, TOut> {
return DesugaringStep.strict(fixed)
}
public convertJoin(json: TIn, context: string, errors: string[], warnings?: string[], information?: string[]): TOut {
public convertJoin(
json: TIn,
context: string,
errors: string[],
warnings?: string[],
information?: string[]
): TOut {
const fixed = this.convert(json, context)
errors?.push(...(fixed.errors ?? []))
warnings?.push(...(fixed.warnings ?? []))
@ -47,41 +57,41 @@ export abstract class Conversion<TIn, TOut> {
return fixed.result
}
public andThenF<X>(f: (tout:TOut) => X ): Conversion<TIn, X>{
return new Pipe(
this,
new Pure(f)
)
public andThenF<X>(f: (tout: TOut) => X): Conversion<TIn, X> {
return new Pipe(this, new Pure(f))
}
abstract convert(json: TIn, context: string): { result: TOut, errors?: string[], warnings?: string[], information?: string[] }
abstract convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] }
}
export abstract class DesugaringStep<T> extends Conversion<T, T> {
}
export abstract class DesugaringStep<T> extends Conversion<T, T> {}
class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
private readonly _step0: Conversion<TIn, TInter>;
private readonly _step1: Conversion<TInter, TOut>;
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter,TOut>) {
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`);
this._step0 = step0;
this._step1 = step1;
private readonly _step0: Conversion<TIn, TInter>
private readonly _step1: Conversion<TInter, TOut>
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>) {
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`)
this._step0 = step0
this._step1 = step1
}
convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
const r0 = this._step0.convert(json, context);
const {result, errors, information, warnings } = r0;
if(result === undefined && errors.length > 0){
convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
const r0 = this._step0.convert(json, context)
const { result, errors, information, warnings } = r0
if (result === undefined && errors.length > 0) {
return {
...r0,
result: undefined
};
result: undefined,
}
}
const r = this._step1.convert(result, context);
const r = this._step1.convert(result, context)
errors.push(...r.errors)
information.push(...r.information)
warnings.push(...r.warnings)
@ -89,35 +99,44 @@ class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
result: r.result,
errors,
warnings,
information
information,
}
}
}
class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
private readonly _f: (t: TIn) => TOut;
constructor(f: ((t:TIn) => TOut)) {
super("Wrapper around a pure function",[], "Pure");
this._f = f;
private readonly _f: (t: TIn) => TOut
constructor(f: (t: TIn) => TOut) {
super("Wrapper around a pure function", [], "Pure")
this._f = f
}
convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
return {result: this._f(json)};
convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
return { result: this._f(json) }
}
}
export class Each<X, Y> extends Conversion<X[], Y[]> {
private readonly _step: Conversion<X, Y>;
private readonly _step: Conversion<X, Y>
constructor(step: Conversion<X, Y>) {
super("Applies the given step on every element of the list", [], "OnEach(" + step.name + ")");
this._step = step;
super(
"Applies the given step on every element of the list",
[],
"OnEach(" + step.name + ")"
)
this._step = step
}
convert(values: X[], context: string): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
values: X[],
context: string
): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (values === undefined || values === null) {
return {result: undefined}
return { result: undefined }
}
const information: string[] = []
const warnings: string[] = []
@ -132,68 +151,83 @@ export class Each<X, Y> extends Conversion<X[], Y[]> {
result.push(r.result)
}
return {
information, errors, warnings,
result
};
information,
errors,
warnings,
result,
}
}
}
export class On<P, T> extends DesugaringStep<T> {
private readonly key: string;
private readonly step: ((t: T) => Conversion<P, P>);
private readonly key: string
private readonly step: (t: T) => Conversion<P, P>
constructor(key: string, step: Conversion<P, P> | ((t: T )=> Conversion<P, P>)) {
super("Applies " + step.name + " onto property `"+key+"`", [key], `On(${key}, ${step.name})`);
if(typeof step === "function"){
this.step = step;
}else{
this.step = _ => step
constructor(key: string, step: Conversion<P, P> | ((t: T) => Conversion<P, P>)) {
super(
"Applies " + step.name + " onto property `" + key + "`",
[key],
`On(${key}, ${step.name})`
)
if (typeof step === "function") {
this.step = step
} else {
this.step = (_) => step
}
this.key = key;
this.key = key
}
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } {
json = {...json}
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
json = { ...json }
const step = this.step(json)
const key = this.key;
const key = this.key
const value: P = json[key]
if (value === undefined || value === null) {
return { result: json };
return { result: json }
}
const r = step.convert(value, context + "." + key)
json[key] = r.result
return {
...r,
result: json,
};
}
}
}
export class Pass<T> extends Conversion<T, T> {
constructor(message?: string) {
super(message??"Does nothing, often to swap out steps in testing", [], "Pass");
super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass")
}
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
return {
result: json
};
result: json,
}
}
}
export class Concat<X, T> extends Conversion<X[], T[]> {
private readonly _step: Conversion<X, T[]>;
private readonly _step: Conversion<X, T[]>
constructor(step: Conversion<X, T[]>) {
super("Executes the given step, flattens the resulting list", [], "Concat(" + step.name + ")");
this._step = step;
super(
"Executes the given step, flattens the resulting list",
[],
"Concat(" + step.name + ")"
)
this._step = step
}
convert(values: X[], context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
values: X[],
context: string
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (values === undefined || values === null) {
// Move on - nothing to see here!
return {
@ -208,56 +242,68 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
return {
...r,
result: flattened,
};
}
}
}
export class FirstOf<T, X> extends Conversion<T, X>{
private readonly _conversion: Conversion<T, X[]>;
export class FirstOf<T, X> extends Conversion<T, X> {
private readonly _conversion: Conversion<T, X[]>
constructor(conversion: Conversion<T, X[]>) {
super("Picks the first result of the conversion step", [], "FirstOf("+conversion.name+")");
this._conversion = conversion;
super(
"Picks the first result of the conversion step",
[],
"FirstOf(" + conversion.name + ")"
)
this._conversion = conversion
}
convert(json: T, context: string): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
const reslt = this._conversion.convert(json, context);
convert(
json: T,
context: string
): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
const reslt = this._conversion.convert(json, context)
return {
...reslt,
result: reslt.result[0]
};
result: reslt.result[0],
}
}
}
export class Fuse<T> extends DesugaringStep<T> {
private readonly steps: DesugaringStep<T>[];
private readonly steps: DesugaringStep<T>[]
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.name).join(", "),
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes))),
"Fuse of " + steps.map(s => s.name).join(", ")
);
this.steps = Utils.NoNull(steps);
super(
(doc ?? "") +
"This fused pipeline of the following steps: " +
steps.map((s) => s.name).join(", "),
Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))),
"Fuse of " + steps.map((s) => s.name).join(", ")
)
this.steps = Utils.NoNull(steps)
}
convert(json: T, context: string): { result: T; errors: string[]; warnings: string[], information: string[] } {
convert(
json: T,
context: string
): { result: T; errors: string[]; warnings: string[]; information: string[] } {
const errors = []
const warnings = []
const information = []
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
try{
const step = this.steps[i]
try {
let r = step.convert(json, "While running step " + step.name + ": " + context)
errors.push(...r.errors ?? [])
warnings.push(...r.warnings ?? [])
information.push(...r.information ?? [])
errors.push(...(r.errors ?? []))
warnings.push(...(r.warnings ?? []))
information.push(...(r.information ?? []))
json = r.result
if (errors.length > 0) {
break;
break
}
}catch(e){
console.error("Step "+step.name+" failed due to ",e,e.stack);
} catch (e) {
console.error("Step " + step.name + " failed due to ", e, e.stack)
throw e
}
}
@ -265,32 +311,31 @@ export class Fuse<T> extends DesugaringStep<T> {
result: json,
errors,
warnings,
information
};
information,
}
}
}
export class SetDefault<T> extends DesugaringStep<T> {
private readonly value: any;
private readonly key: string;
private readonly _overrideEmptyString: boolean;
private readonly value: any
private readonly key: string
private readonly _overrideEmptyString: boolean
constructor(key: string, value: any, overrideEmptyString = false) {
super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key);
this.key = key;
this.value = value;
this._overrideEmptyString = overrideEmptyString;
super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key)
this.key = key
this.value = value
this._overrideEmptyString = overrideEmptyString
}
convert(json: T, context: string): { result: T } {
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
json = {...json}
json = { ...json }
json[this.key] = this.value
}
return {
result: json
};
result: json,
}
}
}
}

View file

@ -1,27 +1,31 @@
import {Conversion} from "./Conversion";
import LayerConfig from "../LayerConfig";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import Translations from "../../../UI/i18n/Translations";
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
import {Translation, TypedTranslation} from "../../../UI/i18n/Translation";
import { Conversion } from "./Conversion"
import LayerConfig from "../LayerConfig"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import Translations from "../../../UI/i18n/Translations"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
/**
* A closed note is included if it is less then 'n'-days closed
* @private
*/
private readonly _includeClosedNotesDays: number;
private readonly _includeClosedNotesDays: number
constructor(includeClosedNotesDays = 0) {
super([
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
"The import buttons and matches will be based on the presets of the given theme",
].join("\n\n"), [], "CreateNoteImportLayer")
this._includeClosedNotesDays = includeClosedNotesDays;
super(
[
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
"The import buttons and matches will be based on the presets of the given theme",
].join("\n\n"),
[],
"CreateNoteImportLayer"
)
this._includeClosedNotesDays = includeClosedNotesDays
}
convert(layerJson: LayerConfigJson, context: string): { result: LayerConfigJson } {
const t = Translations.t.importLayer;
const t = Translations.t.importLayer
/**
* The note itself will contain `tags=k=v;k=v;k=v;...
@ -35,14 +39,16 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
for (const tag of preset.tags) {
const key = tag.key
const value = tag.value
const condition = "_tags~(^|.*;)" + key + "\=" + value + "($|;.*)"
const condition = "_tags~(^|.*;)" + key + "=" + value + "($|;.*)"
mustMatchAll.push(condition)
}
isShownIfAny.push({and: mustMatchAll})
isShownIfAny.push({ and: mustMatchAll })
}
const pointRenderings = (layerJson.mapRendering ?? []).filter(r => r !== null && r["location"] !== undefined);
const firstRender = <PointRenderingConfigJson>(pointRenderings [0])
const pointRenderings = (layerJson.mapRendering ?? []).filter(
(r) => r !== null && r["location"] !== undefined
)
const firstRender = <PointRenderingConfigJson>pointRenderings[0]
if (firstRender === undefined) {
throw `Layer ${layerJson.id} does not have a pointRendering: ` + context
}
@ -50,7 +56,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
const importButton = {}
{
const translations = trs(t.importButton, {layerId: layer.id, title: layer.presets[0].title})
const translations = trs(t.importButton, {
layerId: layer.id,
title: layer.presets[0].title,
})
for (const key in translations) {
if (key !== "_context") {
importButton[key] = "{" + translations[key] + "}"
@ -70,116 +79,117 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
}
function tr(translation: Translation) {
return {...translation.translations, "_context": translation.context}
return { ...translation.translations, _context: translation.context }
}
function trs<T>(translation: TypedTranslation<T>, subs: T): object {
return {...translation.Subs(subs).translations, "_context": translation.context}
return { ...translation.Subs(subs).translations, _context: translation.context }
}
const result: LayerConfigJson = {
"id": "note_import_" + layer.id,
id: "note_import_" + layer.id,
// By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
"description": trs(t.description, {title: layer.title.render}),
"source": {
"osmTags": {
"and": [
"id~*"
]
description: trs(t.description, { title: layer.title.render }),
source: {
osmTags: {
and: ["id~*"],
},
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + this._includeClosedNotesDays + "&bbox={x_min},{y_min},{x_max},{y_max}",
"geoJsonZoomLevel": 10,
"maxCacheAge": 0
geoJson:
"https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" +
this._includeClosedNotesDays +
"&bbox={x_min},{y_min},{x_max},{y_max}",
geoJsonZoomLevel: 10,
maxCacheAge: 0,
},
"minzoom": Math.min(12, layerJson.minzoom - 2),
"title": {
"render": trs(t.popupTitle, {title})
minzoom: Math.min(12, layerJson.minzoom - 2),
title: {
render: trs(t.popupTitle, { title }),
},
"calculatedTags": [
calculatedTags: [
"_first_comment=feat.get('comments')[0].text.toLowerCase()",
"_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\)?.*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
"_comments_count=feat.get('comments').length",
"_intro=(() => {const lines = feat.get('comments')[0].text.split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
"_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
"_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()",
],
"isShown": {
and:
["_trigger_index~*",
{or: isShownIfAny}
]
isShown: {
and: ["_trigger_index~*", { or: isShownIfAny }],
},
"titleIcons": [
titleIcons: [
{
"render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
}
render: "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>",
},
],
"tagRenderings": [
tagRenderings: [
{
"id": "Intro",
render: "{_intro}"
id: "Intro",
render: "{_intro}",
},
{
"id": "conversation",
"render": "{visualize_note_comments(comments,1)}",
condition: "_comments_count>1"
id: "conversation",
render: "{visualize_note_comments(comments,1)}",
condition: "_comments_count>1",
},
{
"id": "import",
"render": importButton,
condition: "closed_at="
id: "import",
render: importButton,
condition: "closed_at=",
},
{
"id": "close_note_",
"render": embed(
"{close_note(", t.notFound.Subs({title}), ", ./assets/svg/close.svg, id, This feature does not exist, 18)}"),
condition: "closed_at="
id: "close_note_",
render: embed(
"{close_note(",
t.notFound.Subs({ title }),
", ./assets/svg/close.svg, id, This feature does not exist, 18)}"
),
condition: "closed_at=",
},
{
"id": "close_note_mapped",
"render": embed("{close_note(", t.alreadyMapped.Subs({title}), ", ./assets/svg/duplicate.svg, id, Already mapped, 18)}"),
condition: "closed_at="
id: "close_note_mapped",
render: embed(
"{close_note(",
t.alreadyMapped.Subs({ title }),
", ./assets/svg/duplicate.svg, id, Already mapped, 18)}"
),
condition: "closed_at=",
},
{
"id": "handled",
"render": tr(t.importHandled),
condition: "closed_at~*"
id: "handled",
render: tr(t.importHandled),
condition: "closed_at~*",
},
{
"id": "comment",
"render": "{add_note_comment()}"
id: "comment",
render: "{add_note_comment()}",
},
{
"id": "add_image",
"render": "{add_image_to_note()}"
id: "add_image",
render: "{add_image_to_note()}",
},
{
"id": "nearby_images",
render: tr(t.nearbyImagesIntro)
}
id: "nearby_images",
render: tr(t.nearbyImagesIntro),
},
],
"mapRendering": [
mapRendering: [
{
"location": [
"point"
],
"icon": {
"render": "circle:white;help:black",
mappings: [{
if: {or: ["closed_at~*", "_imported=yes"]},
then: "circle:white;checkmark:black"
}]
location: ["point"],
icon: {
render: "circle:white;help:black",
mappings: [
{
if: { or: ["closed_at~*", "_imported=yes"] },
then: "circle:white;checkmark:black",
},
],
},
"iconSize": "40,40,center"
}
]
iconSize: "40,40,center",
},
],
}
return {
result
};
result,
}
}
}
}

View file

@ -1,31 +1,37 @@
import {Conversion, DesugaringStep} from "./Conversion";
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {Utils} from "../../../Utils";
import * as metapaths from "../../../assets/layoutconfigmeta.json";
import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json";
import Translations from "../../../UI/i18n/Translations";
import { Conversion, DesugaringStep } from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils"
import * as metapaths from "../../../assets/layoutconfigmeta.json"
import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"
import Translations from "../../../UI/i18n/Translations"
export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
private _isOfficial: boolean;
private _sharedTagRenderings: Map<string, any>;
private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths)
.filter(mp => (ExtractImages.mightBeTagRendering(mp)) || mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths)
private _isOfficial: boolean
private _sharedTagRenderings: Map<string, any>
private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths).filter(
(mp) =>
ExtractImages.mightBeTagRendering(mp) ||
(mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
)
private static readonly tagRenderingMetaPaths =
tagrenderingmetapaths["default"] ?? tagrenderingmetapaths
constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) {
super("Extract all images from a layoutConfig using the meta paths.",[],"ExctractImages");
this._isOfficial = isOfficial;
this._sharedTagRenderings = sharedTagRenderings;
super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages")
this._isOfficial = isOfficial
this._sharedTagRenderings = sharedTagRenderings
}
public static mightBeTagRendering(metapath: {type: string | string[]}) : boolean{
if(!Array.isArray(metapath.type)){
public static mightBeTagRendering(metapath: { type: string | string[] }): boolean {
if (!Array.isArray(metapath.type)) {
return false
}
return metapath.type.some(t =>
t["$ref"] == "#/definitions/TagRenderingConfigJson" || t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson")
return metapath.type.some(
(t) =>
t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson"
)
}
/**
@ -61,105 +67,131 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
* images.length // => 2
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") // => 0
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") // => 1
*
*
* // should not pickup rotation, should drop color
* const images = new ExtractImages(true, new Map<string, any>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
* }, "test").result
* images.length // => 1
* images[0] // => "pin"
*
*
*/
convert(json: LayoutConfigJson, context: string): { result: string[], errors: string[], warnings: string[] } {
const allFoundImages : string[] = []
convert(
json: LayoutConfigJson,
context: string
): { result: string[]; errors: string[]; warnings: string[] } {
const allFoundImages: string[] = []
const errors = []
const warnings = []
for (const metapath of ExtractImages.layoutMetaPaths) {
const mightBeTr = ExtractImages.mightBeTagRendering(metapath)
const allRenderedValuesAreImages = metapath.typeHint === "icon" || metapath.typeHint === "image"
const allRenderedValuesAreImages =
metapath.typeHint === "icon" || metapath.typeHint === "image"
const found = Utils.CollectPath(metapath.path, json)
if (mightBeTr) {
// We might have tagRenderingConfigs containing icons here
for (const el of found) {
const path = el.path
const foundImage = el.leaf;
if (typeof foundImage === "string") {
if(!allRenderedValuesAreImages){
continue
}
if(foundImage == ""){
warnings.push(context+"."+path.join(".")+" Found an empty image")
const foundImage = el.leaf
if (typeof foundImage === "string") {
if (!allRenderedValuesAreImages) {
continue
}
if(this._sharedTagRenderings?.has(foundImage)){
if (foundImage == "") {
warnings.push(context + "." + path.join(".") + " Found an empty image")
}
if (this._sharedTagRenderings?.has(foundImage)) {
// This is not an image, but a shared tag rendering
// At key positions for checking, they'll be expanded already, so we can safely ignore them here
continue
}
allFoundImages.push(foundImage)
} else{
} else {
// This is a tagRendering.
// Either every rendered value might be an icon
// Either every rendered value might be an icon
// or -in the case of a normal tagrendering- only the 'icons' in the mappings have an icon (or exceptionally an '<img>' tag in the translation
for (const trpath of ExtractImages.tagRenderingMetaPaths) {
// Inspect all the rendered values
const fromPath = Utils.CollectPath(trpath.path, foundImage)
const isRendered = trpath.typeHint === "rendered"
const isImage = trpath.typeHint === "icon" || trpath.typeHint === "image"
const isImage =
trpath.typeHint === "icon" || trpath.typeHint === "image"
for (const img of fromPath) {
if (allRenderedValuesAreImages && isRendered) {
// What we found is an image
if(img.leaf === "" || img.leaf["path"] == ""){
warnings.push(context+[...path,...img.path].join(".")+": Found an empty image at ")
}else if(typeof img.leaf !== "string"){
(this._isOfficial ? errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf))
}else{
if (img.leaf === "" || img.leaf["path"] == "") {
warnings.push(
context +
[...path, ...img.path].join(".") +
": Found an empty image at "
)
} else if (typeof img.leaf !== "string") {
;(this._isOfficial ? errors : warnings).push(
context +
"." +
img.path.join(".") +
": found an image path that is not a string: " +
JSON.stringify(img.leaf)
)
} else {
allFoundImages.push(img.leaf)
}
}
if(!allRenderedValuesAreImages && isImage){
}
if (!allRenderedValuesAreImages && isImage) {
// Extract images from the translations
allFoundImages.push(...(Translations.T(img.leaf, "extract_images from "+img.path.join(".")).ExtractImages(false)))
allFoundImages.push(
...Translations.T(
img.leaf,
"extract_images from " + img.path.join(".")
).ExtractImages(false)
)
}
}
}
}
}
}
} else {
for (const foundElement of found) {
if(foundElement.leaf === ""){
warnings.push(context+"."+foundElement.path.join(".")+" Found an empty image")
if (foundElement.leaf === "") {
warnings.push(
context + "." + foundElement.path.join(".") + " Found an empty image"
)
continue
}
allFoundImages.push(foundElement.leaf)
}
}
}
const splitParts = [].concat(...Utils.NoNull(allFoundImages)
.map(img => img["path"] ?? img)
.map(img => img.split(";")))
.map(img => img.split(":")[0])
.filter(img => img !== "")
return {result: Utils.Dedup(splitParts), errors, warnings};
const splitParts = []
.concat(
...Utils.NoNull(allFoundImages)
.map((img) => img["path"] ?? img)
.map((img) => img.split(";"))
)
.map((img) => img.split(":")[0])
.filter((img) => img !== "")
return { result: Utils.Dedup(splitParts), errors, warnings }
}
}
export class FixImages extends DesugaringStep<LayoutConfigJson> {
private readonly _knownImages: Set<string>;
private readonly _knownImages: Set<string>
constructor(knownImages: Set<string>) {
super("Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL",[],"fixImages");
this._knownImages = knownImages;
super(
"Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL",
[],
"fixImages"
)
this._knownImages = knownImages
}
/**
* If the id is an URL to a json file, replaces "./" in images with the path to the json file
*
*
* const theme = {
* "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/verkeerdeborden.json"
* "layers": [
@ -191,43 +223,50 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
*/
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson, warnings?: string[] } {
let url: URL;
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; warnings?: string[] } {
let url: URL
try {
url = new URL(json.id)
} catch (e) {
// Not a URL, we don't rewrite
return {result: json}
return { result: json }
}
const warnings: string[] = []
const absolute = url.protocol + "//" + url.host
let relative = url.protocol + "//" + url.host + url.pathname
relative = relative.substring(0, relative.lastIndexOf("/"))
const self = this;
if(relative.endsWith("assets/generated/themes")){
warnings.push("Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative")
const self = this
if (relative.endsWith("assets/generated/themes")) {
warnings.push(
"Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative"
)
relative = absolute
}
function replaceString(leaf: string) {
if (self._knownImages.has(leaf)) {
return leaf;
return leaf
}
if(typeof leaf !== "string"){
warnings.push("Found a non-string object while replacing images: "+JSON.stringify(leaf))
return leaf;
if (typeof leaf !== "string") {
warnings.push(
"Found a non-string object while replacing images: " + JSON.stringify(leaf)
)
return leaf
}
if (leaf.startsWith("./")) {
return relative + leaf.substring(1)
}
if (leaf.startsWith("/")) {
return absolute + leaf
}
return leaf;
return leaf
}
json = Utils.Clone(json)
@ -252,21 +291,19 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
if (trpath.typeHint !== "rendered") {
continue
}
Utils.WalkPath(trpath.path, leaf, (rendered => {
Utils.WalkPath(trpath.path, leaf, (rendered) => {
return replaceString(rendered)
}))
})
}
}
return leaf;
return leaf
})
}
return {
warnings,
result: json
};
result: json,
}
}
}
}

View file

@ -1,42 +1,51 @@
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {Utils} from "../../../Utils";
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {DesugaringStep, Each, Fuse, On} from "./Conversion";
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
export class UpdateLegacyLayer extends DesugaringStep<
LayerConfigJson | string | { builtin; override }
> {
constructor() {
super("Updates various attributes from the old data format to the new to provide backwards compatibility with the formats",
super(
"Updates various attributes from the old data format to the new to provide backwards compatibility with the formats",
["overpassTags", "source.osmtags", "tagRenderings[*].id", "mapRendering"],
"UpdateLegacyLayer");
"UpdateLegacyLayer"
)
}
convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
convert(
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
const warnings = []
if (typeof json === "string" || json["builtin"] !== undefined) {
// Reuse of an already existing layer; return as-is
return {result: json, errors: [], warnings: []}
return { result: json, errors: [], warnings: [] }
}
let config = {...json};
let config = { ...json }
if (config["overpassTags"]) {
config.source = config.source ?? {
osmTags: config["overpassTags"]
osmTags: config["overpassTags"],
}
config.source.osmTags = config["overpassTags"]
delete config["overpassTags"]
}
if (config.tagRenderings !== undefined) {
let i = 0;
let i = 0
for (const tagRendering of config.tagRenderings) {
i++;
if (typeof tagRendering === "string" || tagRendering["builtin"] !== undefined || tagRendering["rewrite"] !== undefined) {
i++
if (
typeof tagRendering === "string" ||
tagRendering["builtin"] !== undefined ||
tagRendering["rewrite"] !== undefined
) {
continue
}
if (tagRendering["id"] === undefined) {
if (tagRendering["#"] !== undefined) {
tagRendering["id"] = tagRendering["#"]
delete tagRendering["#"]
@ -49,7 +58,6 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
}
}
if (config.mapRendering === undefined) {
config.mapRendering = []
// This is a legacy format, lets create a pointRendering
@ -59,14 +67,13 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
location = ["point", "centroid"]
}
if (config["icon"] ?? config["label"] !== undefined) {
const pointConfig = {
icon: config["icon"],
iconBadges: config["iconOverlays"],
label: config["label"],
iconSize: config["iconSize"],
location,
rotation: config["rotation"]
rotation: config["rotation"],
}
config.mapRendering.push(pointConfig)
}
@ -75,19 +82,20 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
const lineRenderConfig = <LineRenderingConfigJson>{
color: config["color"],
width: config["width"],
dashArray: config["dashArray"]
dashArray: config["dashArray"],
}
if (Object.keys(lineRenderConfig).length > 0) {
config.mapRendering.push(lineRenderConfig)
}
}
if (config.mapRendering.length === 0) {
throw "Could not convert the legacy theme into a new theme: no renderings defined for layer " + config.id
throw (
"Could not convert the legacy theme into a new theme: no renderings defined for layer " +
config.id
)
}
}
delete config["color"]
delete config["width"]
delete config["dashArray"]
@ -100,7 +108,7 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
delete config["wayHandling"]
delete config["hideUnderlayingFeaturesMinPercentage"]
for (const mapRenderingElement of (config.mapRendering ?? [])) {
for (const mapRenderingElement of config.mapRendering ?? []) {
if (mapRenderingElement["iconOverlays"] !== undefined) {
mapRenderingElement["iconBadges"] = mapRenderingElement["iconOverlays"]
}
@ -115,34 +123,37 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
return {
result: config,
errors: [],
warnings
};
warnings,
}
}
}
class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme");
super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme")
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const oldThemeConfig = {...json}
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const oldThemeConfig = { ...json }
if (oldThemeConfig.socialImage === "") {
delete oldThemeConfig.socialImage
}
if (oldThemeConfig["roamingRenderings"] !== undefined) {
if (oldThemeConfig["roamingRenderings"].length == 0) {
delete oldThemeConfig["roamingRenderings"]
} else {
return {
result: null,
errors: [context + ": The theme contains roamingRenderings. These are not supported anymore"],
warnings: []
errors: [
context +
": The theme contains roamingRenderings. These are not supported anymore",
],
warnings: [],
}
}
}
@ -152,8 +163,12 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
delete oldThemeConfig["version"]
if (oldThemeConfig["maintainer"] !== undefined) {
console.log("Maintainer: ", oldThemeConfig["maintainer"], "credits: ", oldThemeConfig["credits"])
console.log(
"Maintainer: ",
oldThemeConfig["maintainer"],
"credits: ",
oldThemeConfig["credits"]
)
if (oldThemeConfig.credits === undefined) {
oldThemeConfig["credits"] = oldThemeConfig["maintainer"]
delete oldThemeConfig["maintainer"]
@ -167,7 +182,7 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
return {
errors: [],
warnings: [],
result: oldThemeConfig
result: oldThemeConfig,
}
}
}
@ -178,8 +193,6 @@ export class FixLegacyTheme extends Fuse<LayoutConfigJson> {
"Fixes a legacy theme to the modern JSON format geared to humans. Syntactic sugars are kept (i.e. no tagRenderings are expandend, no dependencies are automatically gathered)",
new UpdateLegacyTheme(),
new On("layers", new Each(new UpdateLegacyLayer()))
);
)
}
}

View file

@ -1,50 +1,74 @@
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault} from "./Conversion";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {Utils} from "../../../Utils";
import RewritableConfigJson from "../Json/RewritableConfigJson";
import SpecialVisualizations from "../../../UI/SpecialVisualizations";
import Translations from "../../../UI/i18n/Translations";
import {Translation} from "../../../UI/i18n/Translation";
import {
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import Translations from "../../../UI/i18n/Translations"
import { Translation } from "../../../UI/i18n/Translation"
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
import {AddContextToTranslations} from "./AddContextToTranslations";
import { AddContextToTranslations } from "./AddContextToTranslations"
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
private readonly _state: DesugaringContext;
private readonly _self: LayerConfigJson;
class ExpandTagRendering extends Conversion<
string | TagRenderingConfigJson | { builtin: string | string[]; override: any },
TagRenderingConfigJson[]
> {
private readonly _state: DesugaringContext
private readonly _self: LayerConfigJson
private readonly _options: {
/* If true, will copy the 'osmSource'-tags into the condition */
applyCondition?: true | boolean;
};
constructor(state: DesugaringContext, self: LayerConfigJson, options?: { applyCondition?: true | boolean;}) {
super("Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question", [], "ExpandTagRendering");
this._state = state;
this._self = self;
this._options = options;
applyCondition?: true | boolean
}
convert(json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
constructor(
state: DesugaringContext,
self: LayerConfigJson,
options?: { applyCondition?: true | boolean }
) {
super(
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question",
[],
"ExpandTagRendering"
)
this._state = state
this._self = self
this._options = options
}
convert(
json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any },
context: string
): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
return {
result: this.convertUntilStable(json, warnings, errors, context),
errors, warnings
};
errors,
warnings,
}
}
private lookup(name: string): TagRenderingConfigJson[] {
const state = this._state;
const state = this._state
if (state.tagRenderings.has(name)) {
return [state.tagRenderings.get(name)]
}
if (name.indexOf(".") < 0) {
return undefined;
return undefined
}
const spl = name.split(".");
const spl = name.split(".")
let layer = state.sharedLayers.get(spl[0])
if (spl[0] === this._self.id) {
layer = this._self
@ -54,29 +78,30 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
return undefined
}
const id = spl[1];
const id = spl[1]
const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined)
const layerTrs = <TagRenderingConfigJson[]>(
layer.tagRenderings.filter((tr) => tr["id"] !== undefined)
)
let matchingTrs: TagRenderingConfigJson[]
if (id === "*") {
matchingTrs = layerTrs
} else if (id.startsWith("*")) {
const id_ = id.substring(1)
matchingTrs = layerTrs.filter(tr => tr.group === id_ || tr.labels?.indexOf(id_) >= 0)
matchingTrs = layerTrs.filter((tr) => tr.group === id_ || tr.labels?.indexOf(id_) >= 0)
} else {
matchingTrs = layerTrs.filter(tr => tr.id === id)
matchingTrs = layerTrs.filter((tr) => tr.id === id)
}
const contextWriter = new AddContextToTranslations<TagRenderingConfigJson>("layers:")
for (let i = 0; i < matchingTrs.length; i++) {
let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i]);
if(this._options?.applyCondition){
let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i])
if (this._options?.applyCondition) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
if (found.condition === undefined) {
found.condition = layer.source.osmTags
} else {
found.condition = {and: [found.condition, layer.source.osmTags]}
found.condition = { and: [found.condition, layer.source.osmTags] }
}
}
@ -87,29 +112,37 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
if (matchingTrs.length !== 0) {
return matchingTrs
}
return undefined;
return undefined
}
private convertOnce(tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
private convertOnce(
tr: string | any,
warnings: string[],
errors: string[],
ctx: string
): TagRenderingConfigJson[] {
const state = this._state
if (tr === "questions") {
return [{
id: "questions"
}]
return [
{
id: "questions",
},
]
}
if (typeof tr === "string") {
const lookup = this.lookup(tr);
const lookup = this.lookup(tr)
if (lookup === undefined) {
const isTagRendering = ctx.indexOf("On(mapRendering") < 0
if (isTagRendering) {
warnings.push(ctx + "A literal rendering was detected: " + tr)
}
return [{
render: tr,
id: tr.replace(/[^a-zA-Z0-9]/g, "")
}]
return [
{
render: tr,
id: tr.replace(/[^a-zA-Z0-9]/g, ""),
},
]
}
return lookup
}
@ -121,10 +154,22 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
}
for (const key of Object.keys(tr)) {
if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) {
if (
key === "builtin" ||
key === "override" ||
key === "id" ||
key.startsWith("#")
) {
continue
}
errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr))
errors.push(
"At " +
ctx +
": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
key +
"` was found. This won't be picked up! The full object is: " +
JSON.stringify(tr)
)
}
const trs: TagRenderingConfigJson[] = []
@ -136,21 +181,50 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
const [layerName, search] = name.split(".")
let layer = state.sharedLayers.get(layerName)
if (layerName === this._self.id) {
layer = this._self;
layer = this._self
}
if (layer === undefined) {
const candidates = Utils.sortedByLevenshteinDistance(layerName, Array.from(state.sharedLayers.keys()), s => s)
const candidates = Utils.sortedByLevenshteinDistance(
layerName,
Array.from(state.sharedLayers.keys()),
(s) => s
)
if (state.sharedLayers.size === 0) {
warnings.push(ctx + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", "))
warnings.push(
ctx +
": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found. Maybe you meant on of " +
candidates.slice(0, 3).join(", ")
)
} else {
errors.push(ctx + ": While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", "))
errors.push(
ctx +
": While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found. Maybe you meant on of " +
candidates.slice(0, 3).join(", ")
)
}
continue
}
candidates = Utils.NoNull(layer.tagRenderings.map(tr => tr["id"])).map(id => layerName + "." + id)
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
(id) => layerName + "." + id
)
}
candidates = Utils.sortedByLevenshteinDistance(name, candidates, i => i);
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + candidates.join(", ") + "?")
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
errors.push(
ctx +
": The tagRendering with identifier " +
name +
" was not found.\n\tDid you mean one of " +
candidates.join(", ") +
"?"
)
continue
}
for (let foundTr of lookup) {
@ -159,36 +233,44 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
trs.push(foundTr)
}
}
return trs;
return trs
}
return [tr]
}
private convertUntilStable(spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
const trs = this.convertOnce(spec, warnings, errors, ctx);
private convertUntilStable(
spec: string | any,
warnings: string[],
errors: string[],
ctx: string
): TagRenderingConfigJson[] {
const trs = this.convertOnce(spec, warnings, errors, ctx)
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convertUntilStable(tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
const stable = this.convertUntilStable(
tr,
warnings,
errors,
ctx + "(RECURSIVE RESOLVE)"
)
result.push(...stable)
} else {
result.push(tr)
}
}
return result;
return result
}
}
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
constructor() {
super("Applies a rewrite", [], "ExpandRewrite");
super("Applies a rewrite", [], "ExpandRewrite")
}
/**
* Used for left|right group creation and replacement.
* Every 'keyToRewrite' will be replaced with 'target' recursively. This substitution will happen in place in the object 'tr'
@ -210,7 +292,6 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
const targetIsTranslation = Translations.isProbablyATranslation(target)
function replaceRecursive(obj: string | any, target) {
if (obj === keyToRewrite) {
return target
}
@ -224,11 +305,11 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
}
if (Array.isArray(obj)) {
// This is a list of items
return obj.map(o => replaceRecursive(o, target))
return obj.map((o) => replaceRecursive(o, target))
}
if (typeof obj === "object") {
obj = {...obj}
obj = { ...obj }
const isTr = targetIsTranslation && Translations.isProbablyATranslation(obj)
@ -257,7 +338,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* sourceString: ["xyz","abc"],
* into: [
* ["X", "A"],
* ["Y", "B"],
* ["Y", "B"],
* ["Z", "C"]],
* },
* renderings: "The value of xyz is abc"
@ -286,25 +367,27 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* ]
* new ExpandRewrite().convertStrict(spec, "test") // => expected
*/
convert(json: T | RewritableConfigJson<T>, context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: T | RewritableConfigJson<T>,
context: string
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json === null || json === undefined) {
return {result: []}
return { result: [] }
}
if (json["rewrite"] === undefined) {
// not a rewrite
return {result: [(<T>json)]}
return { result: [<T>json] }
}
const rewrite = <RewritableConfigJson<T>>json;
const rewrite = <RewritableConfigJson<T>>json
const keysToRewrite = rewrite.rewrite
const ts: T[] = []
{// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered
{
// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered
for (let i = 0; i < keysToRewrite.sourceString.length; i++) {
const guard = keysToRewrite.sourceString[i];
const guard = keysToRewrite.sourceString[i]
for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) {
const toRewrite = keysToRewrite.sourceString[j]
if (toRewrite.indexOf(guard) >= 0) {
@ -314,12 +397,12 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
}
}
{// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case
{
// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case
for (let i = 0; i < rewrite.rewrite.into.length; i++) {
const into = keysToRewrite.into[i]
if (into.length !== rewrite.rewrite.sourceString.length) {
throw `${context}.into.${i} Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values`
}
}
}
@ -327,17 +410,15 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
for (let i = 0; i < keysToRewrite.into.length; i++) {
let t = Utils.Clone(rewrite.renderings)
for (let j = 0; j < keysToRewrite.sourceString.length; j++) {
const key = keysToRewrite.sourceString[j];
const key = keysToRewrite.sourceString[j]
const target = keysToRewrite.into[i][j]
t = ExpandRewrite.RewriteParts(key, target, t)
}
ts.push(t)
}
return {result: ts};
return { result: ts }
}
}
/**
@ -345,7 +426,11 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
*/
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
constructor() {
super("Converts a 'special' translation into a regular translation which uses parameters", ["special"], "RewriteSpecial");
super(
"Converts a 'special' translation into a regular translation which uses parameters",
["special"],
"RewriteSpecial"
)
}
/**
@ -406,7 +491,11 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"}
* errors // => []
*/
private static convertIfNeeded(input: (object & { special: { type: string } }) | any, errors: string[], context: string): any {
private static convertIfNeeded(
input: (object & { special: { type: string } }) | any,
errors: string[],
context: string
): any {
const special = input["special"]
if (special === undefined) {
return input
@ -414,37 +503,55 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const type = special["type"]
if (type === undefined) {
errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used")
errors.push(
"A 'special'-block should define 'type' to indicate which visualisation should be used"
)
return undefined
}
const vis = SpecialVisualizations.specialVisualizations.find(sp => sp.funcName === type)
const vis = SpecialVisualizations.specialVisualizations.find((sp) => sp.funcName === type)
if (vis === undefined) {
const options = Utils.sortedByLevenshteinDistance(type, SpecialVisualizations.specialVisualizations, sp => sp.funcName)
errors.push(`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`)
const options = Utils.sortedByLevenshteinDistance(
type,
SpecialVisualizations.specialVisualizations,
(sp) => sp.funcName
)
errors.push(
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
)
return undefined
}
errors.push(...
Array.from(Object.keys(input)).filter(k => k !== "special" && k !== "before" && k !== "after")
.map(k => {
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`;
}))
errors.push(
...Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
)
const argNamesList = vis.args.map(a => a.name)
const argNamesList = vis.args.map((a) => a.name)
const argNames = new Set<string>(argNamesList)
// Check for obsolete and misspelled arguments
errors.push(...Object.keys(special)
.filter(k => !argNames.has(k))
.filter(k => k !== "type" && k !== "before" && k !== "after")
.map(wrongArg => {
const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${argNamesList.join(", ")}`;
}))
errors.push(
...Object.keys(special)
.filter((k) => !argNames.has(k))
.filter((k) => k !== "type" && k !== "before" && k !== "after")
.map((wrongArg) => {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x
)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
)
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
for (const arg of vis.args) {
if (arg.required !== true) {
continue;
continue
}
const param = special[arg.name]
if (param === undefined) {
@ -453,9 +560,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
const foundLanguages = new Set<string>()
const translatedArgs = argNamesList.map(nm => special[nm])
.filter(v => v !== undefined)
.filter(v => Translations.isProbablyATranslation(v))
const translatedArgs = argNamesList
.map((nm) => special[nm])
.filter((v) => v !== undefined)
.filter((v) => Translations.isProbablyATranslation(v))
for (const translatedArg of translatedArgs) {
for (const ln of Object.keys(translatedArg)) {
foundLanguages.add(ln)
@ -473,9 +581,9 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
if (foundLanguages.size === 0) {
const args = argNamesList.map(nm => special[nm] ?? "").join(",")
const args = argNamesList.map((nm) => special[nm] ?? "").join(",")
return {
'*': `{${type}(${args})}`
"*": `{${type}(${args})}`,
}
}
@ -487,16 +595,16 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
for (const argName of argNamesList) {
let v = special[argName] ?? ""
if (Translations.isProbablyATranslation(v)) {
v = new Translation(v).textFor(ln)
}
v = new Translation(v).textFor(ln)
}
if (typeof v === "string") {
const txt = v.replace(/,/g, "&COMMA")
const txt = v
.replace(/,/g, "&COMMA")
.replace(/\{/g, "&LBRACE")
.replace(/}/g, "&RBRACE")
.replace(/\(/g, "&LPARENS")
.replace(/\)/g, '&RPARENS')
.replace(/\)/g, "&RPARENS")
args.push(txt)
} else if (typeof v === "object") {
args.push(JSON.stringify(v))
@ -506,7 +614,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
const beforeText = before?.textFor(ln) ?? ""
const afterText = after?.textFor(ln) ?? ""
result[ln] = `${beforeText}{${type}(${args.map(a => a).join(",")})}${afterText}`
result[ln] = `${beforeText}{${type}(${args.map((a) => a).join(",")})}${afterText}`
}
return result
}
@ -541,23 +649,33 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
* result // => expected
*/
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors = []
json = Utils.Clone(json)
const paths: { path: string[], type?: any, typeHint?: string }[] = tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
const paths: { path: string[]; type?: any; typeHint?: string }[] =
tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
for (const path of paths) {
if (path.typeHint !== "rendered") {
continue
}
Utils.WalkPath(path.path, json, ((leaf, travelled) => RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))))
Utils.WalkPath(path.path, json, (leaf, travelled) =>
RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))
)
}
return {
result: json,
errors
};
errors,
}
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
@ -566,11 +684,22 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
"Fully prepares and expands a layer for the LayerConfig.",
new On("tagRenderings", new Each(new RewriteSpecial())),
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("tagRenderings", layer => new Concat(new ExpandTagRendering(state, layer))),
new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("mapRendering", layer => new Each(new On("icon", new FirstOf(new ExpandTagRendering(state, layer, {applyCondition: false}))))),
new On(
"mapRendering",
(layer) =>
new Each(
new On(
"icon",
new FirstOf(
new ExpandTagRendering(state, layer, { applyCondition: false })
)
)
)
),
new SetDefault("titleIcons", ["defaults"]),
new On("titleIcons", layer => new Concat(new ExpandTagRendering(state, layer)))
);
new On("titleIcons", (layer) => new Concat(new ExpandTagRendering(state, layer)))
)
}
}
}

View file

@ -1,42 +1,59 @@
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault} from "./Conversion";
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {PrepareLayer} from "./PrepareLayer";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {Utils} from "../../../Utils";
import Constants from "../../Constants";
import CreateNoteImportLayer from "./CreateNoteImportLayer";
import LayerConfig from "../LayerConfig";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
import DependencyCalculator from "../DependencyCalculator";
import {AddContextToTranslations} from "./AddContextToTranslations";
import {
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { PrepareLayer } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import Constants from "../../Constants"
import CreateNoteImportLayer from "./CreateNoteImportLayer"
import LayerConfig from "../LayerConfig"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { SubstitutedTranslation } from "../../../UI/SubstitutedTranslation"
import DependencyCalculator from "../DependencyCalculator"
import { AddContextToTranslations } from "./AddContextToTranslations"
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
private readonly _state: DesugaringContext;
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
private readonly _state: DesugaringContext
constructor(
state: DesugaringContext,
) {
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", [], "SubstituteLayer");
this._state = state;
constructor(state: DesugaringContext) {
super(
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form",
[],
"SubstituteLayer"
)
this._state = state
}
convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], information?: string[] } {
convert(
json: string | LayerConfigJson,
context: string
): { result: LayerConfigJson[]; errors: string[]; information?: string[] } {
const errors = []
const information = []
const state = this._state
function reportNotFound(name: string) {
const knownLayers = Array.from(state.sharedLayers.keys())
const withDistance = knownLayers.map(lname => [lname, Utils.levenshteinDistance(name, lname)])
const withDistance = knownLayers.map((lname) => [
lname,
Utils.levenshteinDistance(name, lname),
])
withDistance.sort((a, b) => a[1] - b[1])
const ids = withDistance.map(n => n[0])
const ids = withDistance.map((n) => n[0])
// Known builtin layers are "+.join(",")+"\n For more information, see "
errors.push(`${context}: The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}?
For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`)
}
if (typeof json === "string") {
const found = state.sharedLayers.get(json)
if (found === undefined) {
@ -48,7 +65,7 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
}
return {
result: [found],
errors
errors,
}
}
@ -65,49 +82,80 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
reportNotFound(name)
continue
}
if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) {
errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`)
if (
json["override"]["tagRenderings"] !== undefined &&
(found["tagRenderings"] ?? []).length > 0
) {
errors.push(
`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
)
}
try {
Utils.Merge(json["override"], found);
Utils.Merge(json["override"], found)
layers.push(found)
} catch (e) {
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
errors.push(
`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"]
)}`
)
}
if (json["hideTagRenderingsWithLabels"]) {
const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
// These labels caused at least one deletion
const usedLabels: Set<string> = new Set<string>();
const usedLabels: Set<string> = new Set<string>()
const filtered = []
for (const tr of found.tagRenderings) {
const labels = tr["labels"]
if (labels !== undefined) {
const forbiddenLabel = labels.findIndex(l => hideLabels.has(l))
const forbiddenLabel = labels.findIndex((l) => hideLabels.has(l))
if (forbiddenLabel >= 0) {
usedLabels.add(labels[forbiddenLabel])
information.push(context + ": Dropping tagRendering " + tr["id"] + " as it has a forbidden label: " + labels[forbiddenLabel])
information.push(
context +
": Dropping tagRendering " +
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel]
)
continue
}
}
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its id is a forbidden label")
information.push(
context +
": Dropping tagRendering " +
tr["id"] +
" as its id is a forbidden label"
)
continue
}
if (hideLabels.has(tr["group"])) {
usedLabels.add(tr["group"])
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its group `" + tr["group"] + "` is a forbidden label")
information.push(
context +
": Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
)
continue
}
filtered.push(tr)
}
const unused = Array.from(hideLabels).filter(l => !usedLabels.has(l))
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
if (unused.length > 0) {
errors.push("This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + unused.join(", ") + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore")
errors.push(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
)
}
found.tagRenderings = filtered
}
@ -115,33 +163,38 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
return {
result: layers,
errors,
information
information,
}
}
return {
result: [json],
errors
};
errors,
}
}
}
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
private _state: DesugaringContext;
private _state: DesugaringContext
constructor(state: DesugaringContext) {
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"], "AddDefaultLayers");
this._state = state;
super(
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
["layers"],
"AddDefaultLayers"
)
this._state = state
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
const state = this._state
json.layers = [...json.layers]
const alreadyLoaded = new Set(json.layers.map(l => l["id"]))
const alreadyLoaded = new Set(json.layers.map((l) => l["id"]))
for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName)
@ -150,7 +203,13 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
continue
}
if (alreadyLoaded.has(v.id)) {
warnings.push("Layout " + context + " already has a layer with name " + v.id + "; skipping inclusion of this builtin layer")
warnings.push(
"Layout " +
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer"
)
continue
}
json.layers.push(v)
@ -159,34 +218,43 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
return {
result: json,
errors,
warnings
};
warnings,
}
}
}
class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"], "AddImportLayers");
super(
"For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)",
["layers"],
"AddImportLayers"
)
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[], warnings?: string[] } {
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
if (!(json.enableNoteImports ?? true)) {
return {
warnings: ["Not creating a note import layers for theme "+json.id+" as they are disabled"],
result: json
};
warnings: [
"Not creating a note import layers for theme " +
json.id +
" as they are disabled",
],
result: json,
}
}
const errors = []
json = {...json}
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers;
json = { ...json }
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers
json.layers = [...json.layers]
const creator = new CreateNoteImportLayer()
for (let i1 = 0; i1 < allLayers.length; i1++) {
const layer = allLayers[i1];
const layer = allLayers[i1]
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
// Priviliged layers are skipped
continue
@ -204,12 +272,14 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
if (layer.presets === undefined || layer.presets.length == 0) {
// A preset is needed to be able to generate a new point
continue;
continue
}
try {
const importLayerResult = creator.convert(layer, context + ".(noteimportlayer)[" + i1 + "]")
const importLayerResult = creator.convert(
layer,
context + ".(noteimportlayer)[" + i1 + "]"
)
if (importLayerResult.result !== undefined) {
json.layers.push(importLayerResult.result)
}
@ -220,18 +290,21 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
return {
errors,
result: json
};
result: json,
}
}
}
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
private readonly _state: DesugaringContext;
private readonly _state: DesugaringContext
constructor(state: DesugaringContext,) {
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"], "AddMiniMap");
this._state = state;
constructor(state: DesugaringContext) {
super(
"Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap",
["tagRenderings"],
"AddMiniMap"
)
this._state = state
}
/**
@ -249,72 +322,94 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
* AddMiniMap.hasMinimap({render: "Some random value {minimap}"}) // => false
*/
static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
const translations: any[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
const translations: any[] = Utils.NoNull([
renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then),
])
for (let translation of translations) {
if (typeof translation == "string") {
translation = {"*": translation}
translation = { "*": translation }
}
for (const key in translation) {
if (!translation.hasOwnProperty(key)) {
continue
}
const template = translation[key]
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
const hasMiniMap = parts
.filter((part) => part.special !== undefined)
.some((special) => special.special.func.funcName === "minimap")
if (hasMiniMap) {
return true;
return true
}
}
}
return false;
return false
}
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
const state = this._state;
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)) ?? true
const state = this._state
const hasMinimap =
layerConfig.tagRenderings?.some((tr) =>
AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)
) ?? true
if (!hasMinimap) {
layerConfig = {...layerConfig}
layerConfig = { ...layerConfig }
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("questions"))
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
return {
result: layerConfig
};
result: layerConfig,
}
}
}
class AddContextToTransltionsInLayout extends DesugaringStep <LayoutConfigJson> {
class AddContextToTransltionsInLayout extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too", ["_context"], "AddContextToTranlationsInLayout");
super(
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
["_context"],
"AddContextToTranlationsInLayout"
)
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:")
return conversion.convert(json, json.id);
return conversion.convert(json, json.id)
}
}
class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", ["overrideAll", "layers"], "ApplyOverrideAll");
super(
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
["overrideAll", "layers"],
"ApplyOverrideAll"
)
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const overrideAll = json.overrideAll;
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const overrideAll = json.overrideAll
if (overrideAll === undefined) {
return {result: json, warnings: [], errors: []}
return { result: json, warnings: [], errors: [] }
}
json = {...json}
json = { ...json }
delete json.overrideAll
const newLayers = []
@ -325,157 +420,215 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
}
json.layers = newLayers
return {result: json, warnings: [], errors: []};
return { result: json, warnings: [], errors: [] }
}
}
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext;
private readonly _state: DesugaringContext
constructor(state: DesugaringContext,) {
constructor(state: DesugaringContext) {
super(
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)
Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature.
Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers.
Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too.
`, ["layers"], "AddDependencyLayersToTheme");
this._state = state;
`,
["layers"],
"AddDependencyLayersToTheme"
)
this._state = state
}
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string):
{config: LayerConfigJson, reason: string}[] {
const dependenciesToAdd: {config: LayerConfigJson, reason: string}[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id));
private static CalculateDependencies(
alreadyLoaded: LayerConfigJson[],
allKnownLayers: Map<string, LayerConfigJson>,
themeId: string
): { config: LayerConfigJson; reason: string }[] {
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l.id))
// Verify cross-dependencies
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
let unmetDependencies: {
neededLayer: string
neededBy: string
reason: string
context?: string
}[] = []
do {
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
const dependencies: {
neededLayer: string
reason: string
context?: string
neededBy: string
}[] = []
for (const layerConfig of alreadyLoaded) {
try {
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig, themeId+"(dependencies)"))
const layerDeps = DependencyCalculator.getLayerDependencies(
new LayerConfig(layerConfig, themeId + "(dependencies)")
)
dependencies.push(...layerDeps)
} catch (e) {
console.error(e)
throw "Detecting layer dependencies for " + layerConfig.id + " failed due to " + e
throw (
"Detecting layer dependencies for " + layerConfig.id + " failed due to " + e
)
}
}
for (const dependency of dependencies) {
if (loadedLayerIds.has(dependency.neededLayer)) {
// We mark the needed layer as 'mustLoad'
alreadyLoaded.find(l => l.id === dependency.neededLayer).forceLoad = true
alreadyLoaded.find((l) => l.id === dependency.neededLayer).forceLoad = true
}
}
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
// Their existence is checked elsewhere, so this is fine
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
unmetDependencies = dependencies.filter((dep) => !loadedLayerIds.has(dep.neededLayer))
for (const unmetDependency of unmetDependencies) {
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
continue
}
const dep = Utils.Clone(allKnownLayers.get(unmetDependency.neededLayer))
const reason = "This layer is needed by " + unmetDependency.neededBy +" because " +
unmetDependency.reason + " (at " + unmetDependency.context + ")";
const reason =
"This layer is needed by " +
unmetDependency.neededBy +
" because " +
unmetDependency.reason +
" (at " +
unmetDependency.context +
")"
if (dep === undefined) {
const message =
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
reason,
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
]
throw message.join("\n\t");
const message = [
"Loading a dependency failed: layer " +
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
reason,
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
]
throw message.join("\n\t")
}
dep.forceLoad = true;
dep.passAllFeatures = true;
dep.description = reason;
dep.forceLoad = true
dep.passAllFeatures = true
dep.description = reason
dependenciesToAdd.unshift({
config: dep,
reason
reason,
})
loadedLayerIds.add(dep.id);
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
loadedLayerIds.add(dep.id)
unmetDependencies = unmetDependencies.filter(
(d) => d.neededLayer !== unmetDependency.neededLayer
)
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd
}
convert(theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; information: string[] } {
convert(
theme: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; information: string[] } {
const state = this._state
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
const information = [];
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers; // Layers should be expanded at this point
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings
const information = []
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers // Layers should be expanded at this point
knownTagRenderings.forEach((value, key) => {
value.id = key;
value.id = key
})
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(
layers,
allKnownLayers,
theme.id
)
for (const dependency of dependencies) {
}
if (dependencies.length > 0) {
for (const dependency of dependencies) {
information.push(context + ": added " + dependency.config.id + " to the theme. "+dependency.reason)
information.push(
context +
": added " +
dependency.config.id +
" to the theme. " +
dependency.reason
)
}
}
layers.unshift(...dependencies.map(l => l.config));
layers.unshift(...dependencies.map((l) => l.config))
return {
result: {
...theme,
layers: layers
layers: layers,
},
information
};
information,
}
}
}
class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext;
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme");
this._state = state;
super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme")
this._state = state
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (json.id !== "personal") {
return {result: json}
return { result: json }
}
// The only thing this _really_ does, is adding the layer-ids into 'layers'
// All other preparations are done by the 'override-all'-block in personal.json
json.layers = Array.from(this._state.sharedLayers.keys())
.filter(l => Constants.priviliged_layers.indexOf(l) < 0)
.filter(l => this._state.publicLayers.has(l))
return {result: json, information: [
"The personal theme has "+json.layers.length+" public layers"
]};
.filter((l) => Constants.priviliged_layers.indexOf(l) < 0)
.filter((l) => this._state.publicLayers.has(l))
return {
result: json,
information: ["The personal theme has " + json.layers.length + " public layers"],
}
}
}
class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Generates a warning if a theme uses an unsubstituted layer", ["layers"], "WarnForUnsubstitutedLayersInTheme");
super(
"Generates a warning if a theme uses an unsubstituted layer",
["layers"],
"WarnForUnsubstitutedLayersInTheme"
)
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (json.hideFromOverview === true) {
return {result: json}
return { result: json }
}
const warnings = []
for (const layer of json.layers) {
@ -490,21 +643,28 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
continue
}
const wrn = "The theme " + json.id + " has an inline layer: " + layer["id"] + ". This is discouraged."
const wrn =
"The theme " +
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
warnings.push(wrn)
}
return {
result: json,
warnings
};
warnings,
}
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
constructor(state: DesugaringContext, options?: {
skipDefaultLayers: false | boolean
}) {
constructor(
state: DesugaringContext,
options?: {
skipDefaultLayers: false | boolean
}
) {
super(
"Fully prepares and expands a theme",
@ -519,10 +679,12 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
new ApplyOverrideAll(),
// And then we prepare all the layers _again_ in case that an override all contained unexpanded tagrenderings!
new On("layers", new Each(new PrepareLayer(state))),
options?.skipDefaultLayers ? new Pass("AddDefaultLayers is disabled due to the set flag") : new AddDefaultLayers(state),
options?.skipDefaultLayers
? new Pass("AddDefaultLayers is disabled due to the set flag")
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new On("layers", new Each(new AddMiniMap(state)))
);
)
}
}
}

View file

@ -1,96 +1,119 @@
import {DesugaringStep, Each, Fuse, On} from "./Conversion";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import LayerConfig from "../LayerConfig";
import {Utils} from "../../../Utils";
import Constants from "../../Constants";
import {Translation} from "../../../UI/i18n/Translation";
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import LayoutConfig from "../LayoutConfig";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {TagUtils} from "../../../Logic/Tags/TagUtils";
import {ExtractImages} from "./FixImages";
import ScriptUtils from "../../../scripts/ScriptUtils";
import {And} from "../../../Logic/Tags/And";
import Translations from "../../../UI/i18n/Translations";
import Svg from "../../../Svg";
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson";
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
import Constants from "../../Constants"
import { Translation } from "../../../UI/i18n/Translation"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import LayoutConfig from "../LayoutConfig"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExtractImages } from "./FixImages"
import ScriptUtils from "../../../scripts/ScriptUtils"
import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
import Svg from "../../../Svg"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
class ValidateLanguageCompleteness extends DesugaringStep<any> {
private readonly _languages: string[];
private readonly _languages: string[]
constructor(...languages: string[]) {
super("Checks that the given object is fully translated in the specified languages", [], "ValidateLanguageCompleteness");
this._languages = languages ?? ["en"];
super(
"Checks that the given object is fully translated in the specified languages",
[],
"ValidateLanguageCompleteness"
)
this._languages = languages ?? ["en"]
}
convert(obj: any, context: string): { result: LayerConfig; errors: string[] } {
const errors = []
const translations = Translation.ExtractAllTranslationsFrom(
obj
)
const translations = Translation.ExtractAllTranslationsFrom(obj)
for (const neededLanguage of this._languages) {
translations
.filter(t => t.tr.translations[neededLanguage] === undefined && t.tr.translations["*"] === undefined)
.forEach(missing => {
errors.push(context + "A theme should be translation-complete for " + neededLanguage + ", but it lacks a translation for " + missing.context + ".\n\tThe known translation is " + missing.tr.textFor('en'))
.filter(
(t) =>
t.tr.translations[neededLanguage] === undefined &&
t.tr.translations["*"] === undefined
)
.forEach((missing) => {
errors.push(
context +
"A theme should be translation-complete for " +
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en")
)
})
}
return {
result: obj,
errors
};
errors,
}
}
}
export class DoesImageExist extends DesugaringStep<string> {
private readonly _knownImagePaths: Set<string>
private readonly doesPathExist: (path: string) => boolean = undefined
private readonly _knownImagePaths: Set<string>;
private readonly doesPathExist: (path: string) => boolean = undefined;
constructor(knownImagePaths: Set<string>, checkExistsSync: (path: string) => boolean = undefined) {
super("Checks if an image exists", [], "DoesImageExist");
this._knownImagePaths = knownImagePaths;
this.doesPathExist = checkExistsSync;
constructor(
knownImagePaths: Set<string>,
checkExistsSync: (path: string) => boolean = undefined
) {
super("Checks if an image exists", [], "DoesImageExist")
this._knownImagePaths = knownImagePaths
this.doesPathExist = checkExistsSync
}
convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
image: string,
context: string
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
const errors = []
const warnings = []
const information = []
if (image.indexOf("{") >= 0) {
information.push("Ignoring image with { in the path: " + image)
return {result: image}
return { result: image }
}
if (image === "assets/SocialImage.png") {
return {result: image}
return { result: image }
}
if (image.match(/[a-z]*/)) {
if (Svg.All[image + ".svg"] !== undefined) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
return {result: image};
return { result: image }
}
}
if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) {
errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`)
errors.push(
`Image with path ${image} not found or not attributed; it is used in ${context}`
)
} else if (!this.doesPathExist(image)) {
errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`)
errors.push(
`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`
)
} else {
errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`)
errors.push(
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
)
}
}
return {
result: image,
errors, warnings, information
errors,
warnings,
information,
}
}
}
class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
@ -98,20 +121,28 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
* The paths where this layer is originally saved. Triggers some extra checks
* @private
*/
private readonly _path?: string;
private readonly _isBuiltin: boolean;
private _sharedTagRenderings: Map<string, any>;
private readonly _validateImage: DesugaringStep<string>;
private readonly _path?: string
private readonly _isBuiltin: boolean
private _sharedTagRenderings: Map<string, any>
private readonly _validateImage: DesugaringStep<string>
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme");
this._validateImage = doesImageExist;
this._path = path;
this._isBuiltin = isBuiltin;
this._sharedTagRenderings = sharedTagRenderings;
constructor(
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings: Map<string, any>
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
this._validateImage = doesImageExist
this._path = path
this._isBuiltin = isBuiltin
this._sharedTagRenderings = sharedTagRenderings
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } {
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
const errors = []
const warnings = []
const information = []
@ -119,55 +150,77 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const theme = new LayoutConfig(json, true)
{
// Legacy format checks
// Legacy format checks
if (this._isBuiltin) {
if (json["units"] !== undefined) {
errors.push("The theme " + json.id + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ")
errors.push(
"The theme " +
json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
)
}
if (json["roamingRenderings"] !== undefined) {
errors.push("Theme " + json.id + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead")
errors.push(
"Theme " +
json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
)
}
}
}
{
// Check images: are they local, are the licenses there, is the theme icon square, ...
const images = new ExtractImages(this._isBuiltin, this._sharedTagRenderings).convertStrict(json, "validation")
const remoteImages = images.filter(img => img.indexOf("http") == 0)
const images = new ExtractImages(
this._isBuiltin,
this._sharedTagRenderings
).convertStrict(json, "validation")
const remoteImages = images.filter((img) => img.indexOf("http") == 0)
for (const remoteImage of remoteImages) {
errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.")
errors.push(
"Found a remote image: " +
remoteImage +
" in theme " +
json.id +
", please download it."
)
}
for (const image of images) {
this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information)
this._validateImage.convertJoin(
image,
context === undefined ? "" : ` in a layer defined in the theme ${context}`,
errors,
warnings,
information
)
}
if (json.icon.endsWith(".svg")) {
try {
ScriptUtils.ReadSvgSync(json.icon, svg => {
const width: string = svg.$.width;
const height: string = svg.$.height;
ScriptUtils.ReadSvgSync(json.icon, (svg) => {
const width: string = svg.$.width
const height: string = svg.$.height
if (width !== height) {
const e = `the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` +
` Width = ${width} height = ${height}`;
(json.hideFromOverview ? warnings : errors).push(e)
const e =
`the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` +
` Width = ${width} height = ${height}`
;(json.hideFromOverview ? warnings : errors).push(e)
}
const w = parseInt(width);
const w = parseInt(width)
const h = parseInt(height)
if (w < 370 || h < 370) {
const e: string = [
`the icon for theme ${json.id} is too small. Please rescale the icon at ${json.icon}`,
`Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`,
` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`,
].join("\n");
(json.hideFromOverview ? warnings : errors).push(e)
].join("\n")
;(json.hideFromOverview ? warnings : errors).push(e)
}
})
} catch (e) {
console.error("Could not read " + json.icon + " due to " + e)
}
}
}
try {
@ -175,36 +228,53 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
errors.push("Theme ids should be in lowercase, but it is " + theme.id)
}
const filename = this._path.substring(this._path.lastIndexOf("/") + 1, this._path.length - 5)
const filename = this._path.substring(
this._path.lastIndexOf("/") + 1,
this._path.length - 5
)
if (theme.id !== filename) {
errors.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + this._path + ")")
errors.push(
"Theme ids should be the same as the name.json, but we got id: " +
theme.id +
" and filename " +
filename +
" (" +
this._path +
")"
)
}
this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information);
const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"]))
this._validateImage.convertJoin(
theme.icon,
context + ".icon",
errors,
warnings,
information
)
const dups = Utils.Dupiclates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) {
errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`)
errors.push(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
)
}
if (json["mustHaveLanguage"] !== undefined) {
const checked = new ValidateLanguageCompleteness(...json["mustHaveLanguage"])
.convert(theme, theme.id)
const checked = new ValidateLanguageCompleteness(
...json["mustHaveLanguage"]
).convert(theme, theme.id)
errors.push(...checked.errors)
}
if (!json.hideFromOverview && theme.id !== "personal") {
// 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") {
warnings.push(`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`)
warnings.push(
`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
const checked = new ValidateLanguageCompleteness("en")
.convert(theme, theme.id)
const checked = new ValidateLanguageCompleteness("en").convert(theme, theme.id)
errors.push(...checked.errors)
}
} catch (e) {
errors.push(e)
}
@ -213,61 +283,86 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
result: json,
errors,
warnings,
information
};
information,
}
}
}
export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
super("Validates a theme and the contained layers",
constructor(
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings: Map<string, any>
) {
super(
"Validates a theme and the contained layers",
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
new On("layers", new Each(new ValidateLayer(undefined, false, doesImageExist)))
);
)
}
}
class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Checks that an 'overrideAll' does not override a single override", [], "OverrideShadowingCheck");
super(
"Checks that an 'overrideAll' does not override a single override",
[],
"OverrideShadowingCheck"
)
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
const overrideAll = json.overrideAll;
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
const overrideAll = json.overrideAll
if (overrideAll === undefined) {
return {result: json}
return { result: json }
}
const errors = []
const withOverride = json.layers.filter(l => l["override"] !== undefined)
const withOverride = json.layers.filter((l) => l["override"] !== undefined)
for (const layer of withOverride) {
for (const key in overrideAll) {
if(key.endsWith("+") || key.startsWith("+")){
if (key.endsWith("+") || key.startsWith("+")) {
// This key will _add_ to the list, not overwrite it - so no warning is needed
continue
}
if (layer["override"][key] !== undefined || layer["override"]["=" + key] !== undefined) {
const w = "The override of layer " + JSON.stringify(layer["builtin"]) + " has a shadowed property: " + key + " is overriden by overrideAll of the theme";
if (
layer["override"][key] !== undefined ||
layer["override"]["=" + key] !== undefined
) {
const w =
"The override of layer " +
JSON.stringify(layer["builtin"]) +
" has a shadowed property: " +
key +
" is overriden by overrideAll of the theme"
errors.push(w)
}
}
}
return {result: json, errors}
return { result: json, errors }
}
}
class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Miscelleanous checks on the theme", [], "MiscThemesChecks");
super("Miscelleanous checks on the theme", [], "MiscThemesChecks")
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const warnings = []
const errors = []
if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) {
@ -279,29 +374,27 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
return {
result: json,
warnings,
errors
};
errors,
}
}
}
export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
constructor() {
super("Various consistency checks on the raw JSON",
super(
"Various consistency checks on the raw JSON",
new MiscThemeChecks(),
new OverrideShadowingCheck()
);
)
}
}
export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRenderingConfigJson> {
private readonly _calculatedTagNames: string[];
private readonly _calculatedTagNames: string[]
constructor(layerConfig?: LayerConfigJson) {
super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings");
this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig);
super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings")
this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig)
}
/**
@ -309,14 +402,17 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc:=js()"]}) // => ["_abc"]
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
*/
private static extractCalculatedTagNames(layerConfig?: LayerConfigJson | { calculatedTags: string [] }) {
return layerConfig?.calculatedTags?.map(ct => {
if (ct.indexOf(':=') >= 0) {
return ct.split(':=')[0]
}
return ct.split("=")[0]
}) ?? []
private static extractCalculatedTagNames(
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
) {
return (
layerConfig?.calculatedTags?.map((ct) => {
if (ct.indexOf(":=") >= 0) {
return ct.split(":=")[0]
}
return ct.split("=")[0]
}) ?? []
)
}
/**
@ -352,20 +448,28 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
* r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
*/
convert(json: QuestionableTagRenderingConfigJson, context: string): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
convert(
json: QuestionableTagRenderingConfigJson,
context: string
): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
const errors = []
const warnings = []
if (json.mappings === undefined || json.mappings.length === 0) {
return {result: json}
return { result: json }
}
const defaultProperties = {}
for (const calculatedTagName of this._calculatedTagNames) {
defaultProperties[calculatedTagName] = "some_calculated_tag_value_for_" + calculatedTagName
defaultProperties[calculatedTagName] =
"some_calculated_tag_value_for_" + calculatedTagName
}
const parsedConditions = json.mappings.map((m, i) => {
const ctx = `${context}.mappings[${i}]`
const ifTags = TagUtils.Tag(m.if, ctx);
if (m.hideInAnswer !== undefined && m.hideInAnswer !== false && m.hideInAnswer !== true) {
const ifTags = TagUtils.Tag(m.if, ctx)
if (
m.hideInAnswer !== undefined &&
m.hideInAnswer !== false &&
m.hideInAnswer !== true
) {
let conditionTags = TagUtils.Tag(m.hideInAnswer)
// Merge the condition too!
return new And([conditionTags, ifTags])
@ -378,19 +482,29 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
// Yes, it might be shadowed, but running this check is to difficult right now
continue
}
const keyValues = parsedConditions[i].asChange(defaultProperties);
const keyValues = parsedConditions[i].asChange(defaultProperties)
const properties = {}
keyValues.forEach(({k, v}) => {
keyValues.forEach(({ k, v }) => {
properties[k] = v
})
for (let j = 0; j < i; j++) {
const doesMatch = parsedConditions[j].matchesProperties(properties)
if (doesMatch && json.mappings[j].hideInAnswer === true && json.mappings[i].hideInAnswer !== true) {
warnings.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`)
if (
doesMatch &&
json.mappings[j].hideInAnswer === true &&
json.mappings[i].hideInAnswer !== true
) {
warnings.push(
`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
)
} else if (doesMatch) {
// The current mapping is shadowed!
errors.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
The mapping ${parsedConditions[i].asHumanString(false, false, {})} is fully matched by a previous mapping (namely ${j}), which matches:
The mapping ${parsedConditions[i].asHumanString(
false,
false,
{}
)} is fully matched by a previous mapping (namely ${j}), which matches:
${parsedConditions[j].asHumanString(false, false, {})}.
To fix this problem, you can try to:
@ -404,23 +518,26 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
`)
}
}
}
return {
errors,
warnings,
result: json
};
result: json,
}
}
}
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
private readonly _doesImageExist: DoesImageExist;
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;
super(
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
[],
"DetectMappingsWithImages"
)
this._doesImageExist = doesImageExist
}
/**
@ -443,31 +560,44 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* r.errors.length > 0 // => true
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } {
convert(
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
if (json.mappings === undefined || json.mappings.length === 0) {
return {result: json}
return { result: 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}.mappings[${i}]`
if (images.length > 0) {
if (!ignore) {
errors.push(`${ctx}: 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`)
errors.push(
`${ctx}: 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 {
information.push(`${ctx}: Ignored image ${images.join(", ")} in 'then'-clause of a mapping as this check has been disabled`)
information.push(
`${ctx}: Ignored image ${images.join(
", "
)} in 'then'-clause of a mapping as this check has been disabled`
)
for (const image of images) {
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information);
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information)
}
}
} else if (ignore) {
warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`)
@ -478,17 +608,18 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
errors,
warnings,
information,
result: json
};
result: json,
}
}
}
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
super("Various validation on tagRenderingConfigs",
super(
"Various validation on tagRenderingConfigs",
new DetectShadowedMappings(layerConfig),
new DetectMappingsWithImages(doesImageExist)
);
)
}
}
@ -497,36 +628,45 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
* The paths where this layer is originally saved. Triggers some extra checks
* @private
*/
private readonly _path?: string;
private readonly _isBuiltin: boolean;
private readonly _doesImageExist: DoesImageExist;
private readonly _path?: string
private readonly _isBuiltin: boolean
private readonly _doesImageExist: DoesImageExist
constructor(path: string, isBuiltin: boolean, doesImageExist: DoesImageExist) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer");
this._path = path;
this._isBuiltin = isBuiltin;
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
this._path = path
this._isBuiltin = isBuiltin
this._doesImageExist = doesImageExist
}
convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } {
convert(
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors: string[]; warnings?: string[]; information?: string[] } {
const errors = []
const warnings = []
const information = []
context = "While validating a layer: "+context
context = "While validating a layer: " + context
if (typeof json === "string") {
errors.push(context + ": This layer hasn't been expanded: " + json)
return {
result: null,
errors
errors,
}
}
if(json.tagRenderings !== undefined && json.tagRenderings.length > 0){
if(json.title === undefined){
errors.push(context + ": 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.tagRenderings !== undefined && json.tagRenderings.length > 0) {
if (json.title === undefined) {
errors.push(
context +
": 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){
information.push(context + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.")
if (json.title === null) {
information.push(
context +
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
)
}
}
@ -534,20 +674,28 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
errors.push(context + ": This layer hasn't been expanded: " + json)
return {
result: null,
errors
errors,
}
}
if(json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints ){
(json.presets?.length > 0 ? errors : warnings).push(`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} 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.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints) {
;(json.presets?.length > 0 ? errors : warnings).push(
`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} 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`
)
}
{
// duplicate ids in tagrenderings check
const duplicates = Utils.Dedup(Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map(tr => tr["id"]))))
.filter(dupl => dupl !== "questions")
const duplicates = Utils.Dedup(
Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
).filter((dupl) => dupl !== "questions")
if (duplicates.length > 0) {
errors.push("At " + context + ": some tagrenderings have a duplicate id: " + duplicates.join(", "))
errors.push(
"At " +
context +
": some tagrenderings have a duplicate id: " +
duplicates.join(", ")
)
}
}
@ -556,18 +704,46 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
// Some checks for legacy elements
if (json["overpassTags"] !== undefined) {
errors.push("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)")
errors.push(
"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"]
const forbiddenTopLevel = [
"icon",
"wayHandling",
"roamingRenderings",
"roamingRendering",
"label",
"width",
"color",
"colour",
"iconOverlays",
]
for (const forbiddenKey of forbiddenTopLevel) {
if (json[forbiddenKey] !== undefined)
errors.push(context + ": layer " + json.id + " still has a forbidden key " + forbiddenKey)
errors.push(
context +
": layer " +
json.id +
" still has a forbidden key " +
forbiddenKey
)
}
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
errors.push(context + ": layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'")
errors.push(
context +
": layer " +
json.id +
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
}
if(json.isShown !== undefined && (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)){
if (
json.isShown !== undefined &&
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
) {
warnings.push(context + " has a tagRendering as `isShown`")
}
}
@ -575,83 +751,109 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
// Check location of layer file
const expected: string = `assets/layers/${json.id}/${json.id}.json`
if (this._path != undefined && this._path.indexOf(expected) < 0) {
errors.push("Layer is in an incorrect place. The path is " + this._path + ", but expected " + expected)
errors.push(
"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"] === "")) {
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
const emptyIndexes: number[] = []
for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i];
const tagRendering = json.tagRenderings[i]
if (tagRendering["id"] === "") {
emptyIndexes.push(i)
}
}
errors.push(`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(",")}])`)
errors.push(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(
","
)}])`
)
}
const duplicateIds = Utils.Dupiclates((json.tagRenderings ?? [])?.map(f => f["id"]).filter(id => id !== "questions"))
const duplicateIds = Utils.Dupiclates(
(json.tagRenderings ?? [])
?.map((f) => f["id"])
.filter((id) => id !== "questions")
)
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
errors.push(`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`)
errors.push(
`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`
)
}
if (json.description === undefined) {
if (Constants.priviliged_layers.indexOf(json.id) >= 0) {
errors.push(
context + ": A priviliged layer must have a description"
)
errors.push(context + ": A priviliged layer must have a description")
} else {
warnings.push(
context + ": A builtin layer should have a description"
)
warnings.push(context + ": A builtin layer should have a description")
}
}
}
if (json.tagRenderings !== undefined) {
const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this._doesImageExist))).convert(json, context)
const r = new On(
"tagRenderings",
new Each(new ValidateTagRenderings(json, this._doesImageExist))
).convert(json, context)
warnings.push(...(r.warnings ?? []))
errors.push(...(r.errors ?? []))
information.push(...(r.information ?? []))
}
{
const hasCondition = json.mapRendering?.filter(mr => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined)
if(hasCondition?.length > 0){
errors.push("At "+context+":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n"+JSON.stringify(hasCondition, null, " "))
const hasCondition = json.mapRendering?.filter(
(mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined
)
if (hasCondition?.length > 0) {
errors.push(
"At " +
context +
":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
JSON.stringify(hasCondition, null, " ")
)
}
}
if (json.presets !== undefined) {
// 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];
const tags: { k: string, v: string }[] = new And(preset.tags.map(t => TagUtils.Tag(t))).asChange({id: "node/-1"})
const preset = json.presets[i]
const tags: { k: string; v: string }[] = new And(
preset.tags.map((t) => TagUtils.Tag(t))
).asChange({ id: "node/-1" })
const properties = {}
for (const tag of tags) {
properties[tag.k] = tag.v
}
const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) {
errors.push(context + ".presets[" + i + "]: 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: " + JSON.stringify(properties) + "\n The required tags are: " + baseTags.asHumanString(false, false, {}))
errors.push(
context +
".presets[" +
i +
"]: 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: " +
JSON.stringify(properties) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
}
}
}
} catch (e) {
errors.push(e)
}
return {
result: json,
errors,
warnings,
information
};
information,
}
}
}
}