forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue