Reformat all files with prettier

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,42 +1,43 @@
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {DeleteConfigJson} from "./Json/DeleteConfigJson";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { DeleteConfigJson } from "./Json/DeleteConfigJson"
import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils"
export default class DeleteConfig {
public static readonly defaultDeleteReasons : {changesetMessage: string, explanation: Translation} [] = [
public static readonly defaultDeleteReasons: {
changesetMessage: string
explanation: Translation
}[] = [
{
changesetMessage: "testing point",
explanation: Translations.t.delete.reasons.test
explanation: Translations.t.delete.reasons.test,
},
{
changesetMessage:"disused",
explanation: Translations.t.delete.reasons.disused
changesetMessage: "disused",
explanation: Translations.t.delete.reasons.disused,
},
{
changesetMessage: "not found",
explanation: Translations.t.delete.reasons.notFound
explanation: Translations.t.delete.reasons.notFound,
},
{
changesetMessage: "duplicate",
explanation:Translations.t.delete.reasons.duplicate
}
explanation: Translations.t.delete.reasons.duplicate,
},
]
public readonly extraDeleteReasons?: {
explanation: TypedTranslation<object>,
explanation: TypedTranslation<object>
changesetMessage: string
}[]
public readonly nonDeleteMappings?: { if: TagsFilter, then: TypedTranslation<object> }[]
public readonly nonDeleteMappings?: { if: TagsFilter; then: TypedTranslation<object> }[]
public readonly softDeletionTags?: TagsFilter
public readonly neededChangesets?: number
constructor(json: DeleteConfigJson, context: string) {
this.extraDeleteReasons = (json.extraDeleteReasons ?? []).map((reason, i) => {
const ctx = `${context}.extraDeleteReasons[${i}]`
if ((reason.changesetMessage ?? "").length <= 5) {
@ -44,21 +45,23 @@ export default class DeleteConfig {
}
return {
explanation: Translations.T(reason.explanation, ctx + ".explanation"),
changesetMessage: reason.changesetMessage
changesetMessage: reason.changesetMessage,
}
})
this.nonDeleteMappings = (json.nonDeleteMappings??[]).map((nonDelete, i) => {
this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => {
const ctx = `${context}.extraDeleteReasons[${i}]`
return {
if: TagUtils.Tag(nonDelete.if, ctx + ".if"),
then: Translations.T(nonDelete.then, ctx + ".then")
then: Translations.T(nonDelete.then, ctx + ".then"),
}
})
this.softDeletionTags = undefined;
this.softDeletionTags = undefined
if (json.softDeletionTags !== undefined) {
this.softDeletionTags = TagUtils.Tag(json.softDeletionTags, `${context}.softDeletionTags`)
this.softDeletionTags = TagUtils.Tag(
json.softDeletionTags,
`${context}.softDeletionTags`
)
}
if (json["hardDeletionTags"] !== undefined) {
@ -66,6 +69,4 @@ export default class DeleteConfig {
}
this.neededChangesets = json.neededChangesets
}
}
}

View file

@ -1,48 +1,50 @@
import {SpecialVisualization} from "../../UI/SpecialVisualizations";
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
import TagRenderingConfig from "./TagRenderingConfig";
import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions";
import LayerConfig from "./LayerConfig";
import { SpecialVisualization } from "../../UI/SpecialVisualizations"
import { SubstitutedTranslation } from "../../UI/SubstitutedTranslation"
import TagRenderingConfig from "./TagRenderingConfig"
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
import LayerConfig from "./LayerConfig"
export default class DependencyCalculator {
public static GetTagRenderingDependencies(tr: TagRenderingConfig): string[] {
if (tr === undefined) {
throw "Got undefined tag rendering in getTagRenderingDependencies"
}
const deps: string[] = []
// All translated snippets
const parts: string[] = [].concat(...(tr.EnumerateTranslations().map(tr => tr.AllValues())))
const parts: string[] = [].concat(...tr.EnumerateTranslations().map((tr) => tr.AllValues()))
for (const part of parts) {
const specialVizs: { func: SpecialVisualization, args: string[] }[]
= SubstitutedTranslation.ExtractSpecialComponents(part).map(o => o.special)
.filter(o => o?.func?.getLayerDependencies !== undefined)
const specialVizs: { func: SpecialVisualization; args: string[] }[] =
SubstitutedTranslation.ExtractSpecialComponents(part)
.map((o) => o.special)
.filter((o) => o?.func?.getLayerDependencies !== undefined)
for (const specialViz of specialVizs) {
deps.push(...specialViz.func.getLayerDependencies(specialViz.args))
}
}
return deps;
return deps
}
/**
* Returns a set of all other layer-ids that this layer needs to function.
* E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned
*/
public static getLayerDependencies(layer: LayerConfig): { neededLayer: string, reason: string, context?: string, neededBy: string }[] {
const deps: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
public static getLayerDependencies(
layer: LayerConfig
): { neededLayer: string; reason: string; context?: string; neededBy: string }[] {
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string }[] =
[]
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
const preset = layer.presets[i];
preset.preciseInput?.snapToLayers?.forEach(id => {
const preset = layer.presets[i]
preset.preciseInput?.snapToLayers?.forEach((id) => {
deps.push({
neededLayer: id,
reason: "a preset snaps to this layer",
context: "presets[" + i + "]",
neededBy: layer.id
});
neededBy: layer.id,
})
})
}
@ -52,7 +54,7 @@ export default class DependencyCalculator {
neededLayer: dep,
reason: "a tagrendering needs this layer",
context: tr.id,
neededBy: layer.id
neededBy: layer.id,
})
}
}
@ -62,51 +64,53 @@ export default class DependencyCalculator {
type: "Feature",
geometry: {
type: "Point",
coordinates: [0, 0]
coordinates: [0, 0],
},
properties: {
id: "node/1"
}
id: "node/1",
},
}
let currentKey = undefined
let currentLine = undefined
const params: ExtraFuncParams = {
getFeatureById: _ => undefined,
getFeatureById: (_) => undefined,
getFeaturesWithin: (layerId, _) => {
if (layerId === '*') {
if (layerId === "*") {
// This is a wildcard
return []
}
// The important line: steal the dependencies!
deps.push({
neededLayer: layerId, reason: "a calculated tag loads features from this layer",
context: "calculatedTag[" + currentLine + "] which calculates the value for " + currentKey,
neededBy: layer.id
neededLayer: layerId,
reason: "a calculated tag loads features from this layer",
context:
"calculatedTag[" +
currentLine +
"] which calculates the value for " +
currentKey,
neededBy: layer.id,
})
return []
},
memberships: undefined
memberships: undefined,
}
// Init the extra patched functions...
ExtraFunctions.FullPatchFeature(params, obj)
// ... Run the calculated tag code, which will trigger the getFeaturesWithin above...
for (let i = 0; i < layer.calculatedTags.length; i++) {
const [key, code] = layer.calculatedTags[i];
currentLine = i; // Leak the state...
currentKey = key;
const [key, code] = layer.calculatedTags[i]
currentLine = i // Leak the state...
currentKey = key
try {
const func = new Function("feat", "return " + code + ";");
const func = new Function("feat", "return " + code + ";")
const result = func(obj)
obj.properties[key] = JSON.stringify(result);
} catch (e) {
}
obj.properties[key] = JSON.stringify(result)
} catch (e) {}
}
}
return deps
}
}
}

View file

@ -1,31 +1,43 @@
import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson";
import {Translation} from "../../UI/i18n/Translation";
import Translations from "../../UI/i18n/Translations";
import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson"
import { Translation } from "../../UI/i18n/Translation"
import Translations from "../../UI/i18n/Translations"
export default class ExtraLinkConfig {
public readonly icon?: string
public readonly text?: Translation
public readonly href: string
public readonly newTab?: false | boolean
public readonly requirements?: Set<("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")>
public readonly requirements?: Set<
"iframe" | "no-iframe" | "welcome-message" | "no-welcome-message"
>
constructor(configJson: ExtraLinkConfigJson, context) {
this.icon = configJson.icon
this.text = Translations.T(configJson.text, "themes:"+context+".text")
this.text = Translations.T(configJson.text, "themes:" + context + ".text")
this.href = configJson.href
this.newTab = configJson.newTab
this.requirements = new Set(configJson.requirements)
for (let requirement of configJson.requirements) {
if (this.requirements.has(<any>("no-" + requirement))) {
throw "Conflicting requirements found for " + context + ".extraLink: both '" + requirement + "' and 'no-" + requirement + "' found"
throw (
"Conflicting requirements found for " +
context +
".extraLink: both '" +
requirement +
"' and 'no-" +
requirement +
"' found"
)
}
}
if (this.icon === undefined && this.text === undefined) {
throw "At " + context + ".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed"
throw (
"At " +
context +
".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed"
)
}
}
}
}

View file

@ -1,27 +1,27 @@
import {Translation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import FilterConfigJson from "./Json/FilterConfigJson";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {TagConfigJson} from "./Json/TagConfigJson";
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource";
import {FilterState} from "../FilteredLayer";
import {QueryParameters} from "../../Logic/Web/QueryParameters";
import {Utils} from "../../Utils";
import {RegexTag} from "../../Logic/Tags/RegexTag";
import BaseUIElement from "../../UI/BaseUIElement";
import {InputElement} from "../../UI/Input/InputElement";
import { Translation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import FilterConfigJson from "./Json/FilterConfigJson"
import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
import { TagConfigJson } from "./Json/TagConfigJson"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import { FilterState } from "../FilteredLayer"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag"
import BaseUIElement from "../../UI/BaseUIElement"
import { InputElement } from "../../UI/Input/InputElement"
export default class FilterConfig {
public readonly id: string
public readonly options: {
question: Translation;
osmTags: TagsFilter | undefined;
question: Translation
osmTags: TagsFilter | undefined
originalTagsSpec: TagConfigJson
fields: { name: string, type: string }[]
}[];
public readonly defaultSelection? : number
fields: { name: string; type: string }[]
}[]
public readonly defaultSelection?: number
constructor(json: FilterConfigJson, context: string) {
if (json.options === undefined) {
@ -37,99 +37,114 @@ export default class FilterConfig {
if (json.options.map === undefined) {
throw `A filter was given where the options aren't a list at ${context}`
}
this.id = json.id;
let defaultSelection : number = undefined
this.id = json.id
let defaultSelection: number = undefined
this.options = json.options.map((option, i) => {
const ctx = `${context}.options.${i}`;
const question = Translations.T(
option.question,
`${ctx}.question`
);
let osmTags: undefined | TagsFilter = undefined;
const ctx = `${context}.options.${i}`
const question = Translations.T(option.question, `${ctx}.question`)
let osmTags: undefined | TagsFilter = undefined
if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) {
osmTags = TagUtils.Tag(
option.osmTags,
`${ctx}.osmTags`
);
osmTags = TagUtils.Tag(option.osmTags, `${ctx}.osmTags`)
FilterConfig.validateSearch(osmTags, ctx)
}
if (question === undefined) {
throw `Invalid filter: no question given at ${ctx}`
}
const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => {
const fields: { name: string; type: string }[] = (option.fields ?? []).map((f, i) => {
const type = f.type ?? "string"
if (!ValidatedTextField.ForType(type) === undefined) {
throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AvailableTypes()).join(",")}`
throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(
ValidatedTextField.AvailableTypes()
).join(",")}`
}
if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) {
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
}
return {
name: f.name,
type
type,
}
})
for (const field of fields) {
question.OnEveryLanguage((txt, language) => {
if(txt.indexOf("{"+field.name+"}")<0){
throw "Error in filter with fields at "+context+".question."+language+": The question text should contain every field, but it doesn't contain `{"+field+"}`: "+txt
if (txt.indexOf("{" + field.name + "}") < 0) {
throw (
"Error in filter with fields at " +
context +
".question." +
language +
": The question text should contain every field, but it doesn't contain `{" +
field +
"}`: " +
txt
)
}
return txt
})
}
if(option.default){
if(defaultSelection === undefined){
defaultSelection = i;
}else{
if (option.default) {
if (defaultSelection === undefined) {
defaultSelection = i
} else {
throw `Invalid filter: multiple filters are set as default, namely ${i} and ${defaultSelection} at ${context}`
}
}
if(option.osmTags !== undefined){
if (option.osmTags !== undefined) {
FilterConfig.validateSearch(TagUtils.Tag(option.osmTags), ctx)
}
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
});
this.defaultSelection = defaultSelection
return {
question: question,
osmTags: osmTags,
fields,
originalTagsSpec: option.osmTags,
}
})
if (this.options.some(o => o.fields.length > 0) && this.options.length > 1) {
this.defaultSelection = defaultSelection
if (this.options.some((o) => o.fields.length > 0) && this.options.length > 1) {
throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.`
}
if (this.options.length > 1 && this.options[0].osmTags !== undefined) {
throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters"
throw (
"Error in " +
context +
"." +
this.id +
": the first option of a multi-filter should always be the 'reset' option and not have any filters"
)
}
}
private static validateSearch(osmTags: TagsFilter, ctx: string){
osmTags.visit(t => {
private static validateSearch(osmTags: TagsFilter, ctx: string) {
osmTags.visit((t) => {
if (!(t instanceof RegexTag)) {
return;
return
}
if(typeof t.value == "string"){
return;
}
if(t.value.source == '^..*$' || t.value.source == '^[\\s\\S][\\s\\S]*$' /*Compiled regex with 'm'*/){
if (typeof t.value == "string") {
return
}
if(!t.value.ignoreCase) {
throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive`
if (
t.value.source == "^..*$" ||
t.value.source == "^[\\s\\S][\\s\\S]*$" /*Compiled regex with 'm'*/
) {
return
}
if (!t.value.ignoreCase) {
throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive`
}
})
}
public initState(): UIEventSource<FilterState> {
public initState(): UIEventSource<FilterState> {
function reset(state: FilterState): string {
if (state === undefined) {
return ""
@ -138,47 +153,54 @@ export default class FilterConfig {
}
let defaultValue = ""
if(this.options.length > 1){
defaultValue = ""+(this.defaultSelection ?? 0)
}else{
if (this.options.length > 1) {
defaultValue = "" + (this.defaultSelection ?? 0)
} else {
// Only a single option
if(this.defaultSelection === 0){
if (this.defaultSelection === 0) {
defaultValue = "true"
}
}
const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id)
const qp = QueryParameters.GetQueryParameter(
"filter-" + this.id,
defaultValue,
"State of filter " + this.id
)
if (this.options.length > 1) {
// This is a multi-option filter; state should be a number which selects the correct entry
const possibleStates: FilterState [] = this.options.map((opt, i) => ({
const possibleStates: FilterState[] = this.options.map((opt, i) => ({
currentFilter: opt.osmTags,
state: i
state: i,
}))
// We map the query parameter for this case
return qp.sync(str => {
const parsed = Number(str)
if (isNaN(parsed)) {
// Nope, not a correct number!
return undefined
}
return possibleStates[parsed]
}, [], reset)
return qp.sync(
(str) => {
const parsed = Number(str)
if (isNaN(parsed)) {
// Nope, not a correct number!
return undefined
}
return possibleStates[parsed]
},
[],
reset
)
}
const option = this.options[0]
if (option.fields.length > 0) {
return qp.sync(str => {
// There are variables in play!
// str should encode a json-hash
try {
const props = JSON.parse(str)
return qp.sync(
(str) => {
// There are variables in play!
// str should encode a json-hash
try {
const props = JSON.parse(str)
const origTags = option.originalTagsSpec
const rewrittenTags = Utils.WalkJson(origTags,
v => {
const origTags = option.originalTagsSpec
const rewrittenTags = Utils.WalkJson(origTags, (v) => {
if (typeof v !== "string") {
return v
}
@ -186,34 +208,36 @@ export default class FilterConfig {
v = (<string>v).replace("{" + key + "}", props[key])
}
return v
})
const parsed = TagUtils.Tag(rewrittenTags)
return <FilterState>{
currentFilter: parsed,
state: str,
}
)
const parsed = TagUtils.Tag(rewrittenTags)
return <FilterState>{
currentFilter: parsed,
state: str
} catch (e) {
return undefined
}
} catch (e) {
return undefined
}
}, [], reset)
},
[],
reset
)
}
// The last case is pretty boring: it is checked or it isn't
const filterState: FilterState = {
currentFilter: option.osmTags,
state: "true"
state: "true",
}
return qp.sync(
str => {
(str) => {
// Only a single option exists here
if (str === "true") {
return filterState
}
return undefined
}, [],
},
[],
reset
)
}
}
}

View file

@ -1,7 +1,6 @@
import {TagConfigJson} from "./TagConfigJson";
import { TagConfigJson } from "./TagConfigJson"
export interface DeleteConfigJson {
/***
* By default, three reasons to delete a point are shown:
*
@ -21,7 +20,7 @@ export interface DeleteConfigJson {
/**
* The text that will be shown to the user - translatable
*/
explanation: string | any,
explanation: string | any
/**
* The text that will be uploaded into the changeset or will be used in the fixme in case of a soft deletion
* Should be a few words, in english
@ -41,12 +40,12 @@ export interface DeleteConfigJson {
* The tags that will be given to the object.
* This must remove tags so that the 'source/osmTags' won't match anymore
*/
if: TagConfigJson,
if: TagConfigJson
/**
* The human explanation for the options
*/
then: string | any,
}[],
then: string | any
}[]
/**
* In some cases, the contributor is not allowed to delete the current feature (e.g. because it isn't a point, the point is referenced by a relation or the user isn't experienced enough).
@ -67,11 +66,10 @@ export interface DeleteConfigJson {
* }
* ```
*/
softDeletionTags?: TagConfigJson,
softDeletionTags?: TagConfigJson
/***
* By default, the contributor needs 20 previous changesets to delete points edited by others.
* For some small features (e.g. bicycle racks) this is too much and this requirement can be lowered or dropped, which can be done here.
*/
neededChangesets?: number
}
}

View file

@ -1,7 +1,7 @@
export default interface ExtraLinkConfigJson {
icon?: string,
text?: string | any,
href: string,
newTab?: false | boolean,
icon?: string
text?: string | any
href: string
newTab?: false | boolean
requirements?: ("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")[]
}
}

View file

@ -1,10 +1,10 @@
import {TagConfigJson} from "./TagConfigJson";
import { TagConfigJson } from "./TagConfigJson"
export default interface FilterConfigJson {
/**
* An id/name for this filter, used to set the URL parameters
*/
id: string,
id: string
/**
* The options for a filter
* If there are multiple options these will be a list of radio buttons
@ -12,15 +12,15 @@ export default interface FilterConfigJson {
* Filtering is done based on the given osmTags that are compared to the objects in that layer.
*/
options: {
question: string | any;
osmTags?: TagConfigJson,
default?: boolean,
question: string | any
osmTags?: TagConfigJson
default?: boolean
fields?: {
/**
* If name is `search`, use "_first_comment~.*{search}.*" as osmTags
*/
name: string,
name: string
type?: string | "string"
}[]
}[];
}
}[]
}

View file

@ -1,13 +1,13 @@
import {TagConfigJson} from "./TagConfigJson";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import FilterConfigJson from "./FilterConfigJson";
import {DeleteConfigJson} from "./DeleteConfigJson";
import UnitConfigJson from "./UnitConfigJson";
import MoveConfigJson from "./MoveConfigJson";
import PointRenderingConfigJson from "./PointRenderingConfigJson";
import LineRenderingConfigJson from "./LineRenderingConfigJson";
import {QuestionableTagRenderingConfigJson} from "./QuestionableTagRenderingConfigJson";
import RewritableConfigJson from "./RewritableConfigJson";
import { TagConfigJson } from "./TagConfigJson"
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import FilterConfigJson from "./FilterConfigJson"
import { DeleteConfigJson } from "./DeleteConfigJson"
import UnitConfigJson from "./UnitConfigJson"
import MoveConfigJson from "./MoveConfigJson"
import PointRenderingConfigJson from "./PointRenderingConfigJson"
import LineRenderingConfigJson from "./LineRenderingConfigJson"
import { QuestionableTagRenderingConfigJson } from "./QuestionableTagRenderingConfigJson"
import RewritableConfigJson from "./RewritableConfigJson"
/**
* Configuration for a single layer
@ -17,7 +17,7 @@ export interface LayerConfigJson {
* The id of this layer.
* This should be a simple, lowercase, human readable string that is used to identify the layer.
*/
id: string;
id: string
/**
* The name of this layer
@ -31,8 +31,7 @@ export interface LayerConfigJson {
* A description for this layer.
* Shown in the layer selections and in the personel theme
*/
description?: string | any;
description?: string | any
/**
* This determines where the data for the layer is fetched: from OSM or from an external geojson dataset.
@ -42,69 +41,69 @@ export interface LayerConfigJson {
* Every source _must_ define which tags _must_ be present in order to be picked up.
*
*/
source:
({
/**
* Every source must set which tags have to be present in order to load the given layer.
*/
osmTags: TagConfigJson
/**
* The maximum amount of seconds that a tile is allowed to linger in the cache
*/
maxCacheAge?: number
}) &
({
/**
* If set, this custom overpass-script will be used instead of building one by using the OSM-tags.
* Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline.
* _This should be really rare_.
*
* For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible:
* ```
* "source": {
* "overpassScript":
* "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );",
* "osmTags": "access=yes"
* }
* ```
*
*/
overpassScript?: string
} |
{
/**
* The actual source of the data to load, if loaded via geojson.
*
* # A single geojson-file
* source: {geoJson: "https://my.source.net/some-geo-data.geojson"}
* fetches a geojson from a third party source
*
* # A tiled geojson source
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14}
* to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer
*
* Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max}
*/
geoJson: string,
/**
* To load a tiled geojson layer, set the zoomlevel of the tiles
*/
geoJsonZoomLevel?: number,
/**
* Indicates that the upstream geojson data is OSM-derived.
* Useful for e.g. merging or for scripts generating this cache
*/
isOsmCache?: boolean,
/**
* Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this
*/
mercatorCrs?: boolean,
/**
* Some API's have an id-field, but give it a different name.
* Setting this key will rename this field into 'id'
*/
idKey?: string
})
source: {
/**
* Every source must set which tags have to be present in order to load the given layer.
*/
osmTags: TagConfigJson
/**
* The maximum amount of seconds that a tile is allowed to linger in the cache
*/
maxCacheAge?: number
} & (
| {
/**
* If set, this custom overpass-script will be used instead of building one by using the OSM-tags.
* Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline.
* _This should be really rare_.
*
* For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible:
* ```
* "source": {
* "overpassScript":
* "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );",
* "osmTags": "access=yes"
* }
* ```
*
*/
overpassScript?: string
}
| {
/**
* The actual source of the data to load, if loaded via geojson.
*
* # A single geojson-file
* source: {geoJson: "https://my.source.net/some-geo-data.geojson"}
* fetches a geojson from a third party source
*
* # A tiled geojson source
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14}
* to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer
*
* Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max}
*/
geoJson: string
/**
* To load a tiled geojson layer, set the zoomlevel of the tiles
*/
geoJsonZoomLevel?: number
/**
* Indicates that the upstream geojson data is OSM-derived.
* Useful for e.g. merging or for scripts generating this cache
*/
isOsmCache?: boolean
/**
* Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this
*/
mercatorCrs?: boolean
/**
* Some API's have an id-field, but give it a different name.
* Setting this key will rename this field into 'id'
*/
idKey?: string
}
)
/**
*
@ -126,13 +125,13 @@ export interface LayerConfigJson {
* ]
*
*/
calculatedTags?: string[];
calculatedTags?: string[]
/**
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
* Works well together with 'passAllFeatures', to add decoration
*/
doNotDownload?: boolean;
doNotDownload?: boolean
/**
* If set, only features matching this extra tag will be shown.
@ -143,7 +142,7 @@ export interface LayerConfigJson {
*
* The default value is 'yes'
*/
isShown?: TagConfigJson;
isShown?: TagConfigJson
/**
* Advanced option - might be set by the theme compiler
@ -152,30 +151,28 @@ export interface LayerConfigJson {
*/
forceLoad?: false | boolean
/**
* The minimum needed zoomlevel required before loading of the data start
* Default: 0
*/
minzoom?: number;
minzoom?: number
/**
* Indicates if this layer is shown by default;
* can be used to hide a layer from start, or to load the layer but only to show it where appropriate (e.g. for snapping to it)
*/
shownByDefault?: true | boolean;
shownByDefault?: true | boolean
/**
* The zoom level at which point the data is hidden again
* Default: 100 (thus: always visible
*/
minzoomVisible?: number;
minzoomVisible?: number
/**
* The title shown in a popup for elements of this layer.
*/
title?: string | TagRenderingConfigJson;
title?: string | TagRenderingConfigJson
/**
* Small icons shown next to the title.
@ -185,12 +182,23 @@ export interface LayerConfigJson {
*
* Type: icon[]
*/
titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"];
titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]
/**
* Visualisation of the items on the map
*/
mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson | PointRenderingConfigJson | LineRenderingConfigJson[] | PointRenderingConfigJson[]>)[]
mapRendering:
| null
| (
| PointRenderingConfigJson
| LineRenderingConfigJson
| RewritableConfigJson<
| LineRenderingConfigJson
| PointRenderingConfigJson
| LineRenderingConfigJson[]
| PointRenderingConfigJson[]
>
)[]
/**
* If set, this layer will pass all the features it receives onto the next layer.
@ -220,18 +228,18 @@ export interface LayerConfigJson {
*
* Do _not_ indicate 'new': 'add a new shop here' is incorrect, as the shop might have existed forever, it could just be unmapped!
*/
title: string | any,
title: string | any
/**
* The tags to add. It determines the icon too
*/
tags: string[],
tags: string[]
/**
* The _first sentence_ of the description is shown on the button of the `add` menu.
* The full description is shown in the confirmation dialog.
*
* (The first sentence is until the first '.'-character in the description)
*/
description?: string | any,
description?: string | any
/**
* Example images, which show real-life pictures of what such a feature might look like
@ -246,24 +254,32 @@ export interface LayerConfigJson {
*
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
*/
preciseInput?: true | {
/**
* The type of background picture
*/
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string[],
/**
* If specified, these layers will be shown to and the new point will be snapped towards it
*/
snapToLayer?: string | string[],
/**
* If specified, a new point will only be snapped if it is within this range.
* Distance in meter
*
* Default: 10
*/
maxSnapDistance?: number
}
}[],
preciseInput?:
| true
| {
/**
* The type of background picture
*/
preferredBackground:
| "osmbasedmap"
| "photo"
| "historicphoto"
| "map"
| string
| string[]
/**
* If specified, these layers will be shown to and the new point will be snapped towards it
*/
snapToLayer?: string | string[]
/**
* If specified, a new point will only be snapped if it is within this range.
* Distance in meter
*
* Default: 10
*/
maxSnapDistance?: number
}
}[]
/**
* All the tag renderings.
@ -285,19 +301,24 @@ export interface LayerConfigJson {
* This is mainly create questions for a 'left' and a 'right' side of the road.
* These will be grouped and questions will be asked together
*/
tagRenderings?:
(string
| { builtin: string | string[], override: Partial<QuestionableTagRenderingConfigJson> }
| { id: string, builtin: string[], override: Partial<QuestionableTagRenderingConfigJson> }
| QuestionableTagRenderingConfigJson
| (RewritableConfigJson<(string | { builtin: string, override: Partial<QuestionableTagRenderingConfigJson> } | QuestionableTagRenderingConfigJson)[]> & {id: string})
) [],
tagRenderings?: (
| string
| { builtin: string | string[]; override: Partial<QuestionableTagRenderingConfigJson> }
| { id: string; builtin: string[]; override: Partial<QuestionableTagRenderingConfigJson> }
| QuestionableTagRenderingConfigJson
| (RewritableConfigJson<
(
| string
| { builtin: string; override: Partial<QuestionableTagRenderingConfigJson> }
| QuestionableTagRenderingConfigJson
)[]
> & { id: string })
)[]
/**
* All the extra questions for filtering
*/
filter?: (FilterConfigJson) [] | { sameAs: string },
filter?: FilterConfigJson[] | { sameAs: string }
/**
* This block defines under what circumstances the delete dialog is shown for objects of this layer.
@ -435,4 +456,4 @@ export interface LayerConfigJson {
* global: all layers with this ID will be synced accross all themes
*/
syncSelection?: "no" | "local" | "theme-only" | "global"
}
}

View file

@ -1,6 +1,6 @@
import {LayerConfigJson} from "./LayerConfigJson";
import TilesourceConfigJson from "./TilesourceConfigJson";
import ExtraLinkConfigJson from "./ExtraLinkConfigJson";
import { LayerConfigJson } from "./LayerConfigJson"
import TilesourceConfigJson from "./TilesourceConfigJson"
import ExtraLinkConfigJson from "./ExtraLinkConfigJson"
/**
* Defines the entire theme.
@ -15,7 +15,6 @@ import ExtraLinkConfigJson from "./ExtraLinkConfigJson";
* General remark: a type (string | any) indicates either a fixed or a translatable string.
*/
export interface LayoutConfigJson {
/**
* The id of this layout.
*
@ -25,16 +24,16 @@ export interface LayoutConfigJson {
* On official themes, it'll become the name of the page, e.g.
* 'cyclestreets' which become 'cyclestreets.html'
*/
id: string;
id: string
/**
* Who helped to create this theme and should be attributed?
*/
credits?: string;
credits?: string
/**
* Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated.
*
*
* This must be a list of two-letter, lowercase codes which identifies the language, e.g. "en", "nl", ...
*/
mustHaveLanguage?: string[]
@ -42,49 +41,49 @@ export interface LayoutConfigJson {
/**
* The title, as shown in the welcome message and the more-screen.
*/
title: string | any;
title: string | any
/**
* A short description, showed as social description and in the 'more theme'-buttons.
* Note that if this one is not defined, the first sentence of 'description' is used
*/
shortDescription?: string | any;
shortDescription?: string | any
/**
* The description, as shown in the welcome message and the more-screen
*/
description: string | any;
description: string | any
/**
* A part of the description, shown under the login-button.
*/
descriptionTail?: string | any;
descriptionTail?: string | any
/**
* The icon representing this theme.
* Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
*
*
* Type: icon
*/
icon: string;
icon: string
/**
* Link to a 'social image' which is included as og:image-tag on official themes.
* Useful to share the theme on social media.
* See https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$
*
*
* Type: image
*/
socialImage?: string;
socialImage?: string
/**
* Default location and zoom to start.
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
*/
startZoom: number;
startLat: number;
startLon: number;
startZoom: number
startLat: number
startLon: number
/**
* When a query is run, the data within bounds of the visible map is loaded.
@ -93,7 +92,7 @@ export interface LayoutConfigJson {
*
* IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3
*/
widenFactor?: number;
widenFactor?: number
/**
* At low zoom levels, overpass is used to query features.
* At high zoom level, the OSM api is used to fetch one or more BBOX aligning with a slippy tile.
@ -139,12 +138,12 @@ export interface LayoutConfigJson {
*
* In the above scenario, `sometagrendering` will be added at the beginning of the tagrenderings of every layer
*/
overrideAll?: Partial<any | LayerConfigJson>;
overrideAll?: Partial<any | LayerConfigJson>
/**
* The id of the default background. BY default: vanilla OSM
*/
defaultBackgroundId?: string;
defaultBackgroundId?: string
/**
* Define some (overlay) slippy map tilesources
@ -174,7 +173,7 @@ export interface LayoutConfigJson {
* ```
* "layer": {
* "builtin": "nature_reserve",
* "override": {"source":
* "override": {"source":
* {"osmTags": {
* "+and":["operator=Natuurpunt"]
* }
@ -192,122 +191,129 @@ export interface LayoutConfigJson {
* }
*```
*/
layers: (LayerConfigJson | string |
{ builtin: string | string[],
override: any,
/**
* TagRenderings with any of these labels will be removed from the layer.
* Note that the 'id' and 'group' are considered labels too
*/
hideTagRenderingsWithLabels?: string[]})[],
layers: (
| LayerConfigJson
| string
| {
builtin: string | string[]
override: any
/**
* TagRenderings with any of these labels will be removed from the layer.
* Note that the 'id' and 'group' are considered labels too
*/
hideTagRenderingsWithLabels?: string[]
}
)[]
/**
* If defined, data will be clustered.
* Defaults to {maxZoom: 16, minNeeded: 500}
*/
clustering?: {
/**
* All zoom levels above 'maxzoom' are not clustered anymore.
* Defaults to 18
*/
maxZoom?: number,
/**
* The number of elements per tile needed to start clustering
* If clustering is defined, defaults to 250
*/
minNeededElements?: number
} | false,
clustering?:
| {
/**
* All zoom levels above 'maxzoom' are not clustered anymore.
* Defaults to 18
*/
maxZoom?: number
/**
* The number of elements per tile needed to start clustering
* If clustering is defined, defaults to 250
*/
minNeededElements?: number
}
| false
/**
* The URL of a custom CSS stylesheet to modify the layout
*/
customCss?: string;
customCss?: string
/**
* If set to true, this layout will not be shown in the overview with more themes
*/
hideFromOverview?: boolean;
hideFromOverview?: boolean
/**
* If set to true, the basemap will not scroll outside of the area visible on initial zoom.
* If set to [[lon, lat], [lon, lat]], the map will not scroll outside of those bounds.
* Off by default, which will enable panning to the entire world
*/
lockLocation?: [[number, number], [number, number]] | number[][];
lockLocation?: [[number, number], [number, number]] | number[][]
/**
* Adds an additional button on the top-left of the application.
* This can link to an arbitrary location.
*
*
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
*
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
*
*
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
*
*/
extraLink?: ExtraLinkConfigJson
/**
* If set to false, disables logging in.
* The userbadge will be hidden, all login-buttons will be hidden and editing will be disabled
*/
enableUserBadge?: true | boolean;
enableUserBadge?: true | boolean
/**
* If false, hides the tab 'share'-tab in the welcomeMessage
*/
enableShareScreen?: true | boolean;
enableShareScreen?: true | boolean
/**
* Hides the tab with more themes in the welcomeMessage
*/
enableMoreQuests?: true | boolean;
enableMoreQuests?: true | boolean
/**
* If false, the layer selection/filter view will be hidden
* The corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'
*/
enableLayers?: true | boolean;
enableLayers?: true | boolean
/**
* If set to false, hides the search bar
*/
enableSearch?: true | boolean;
enableSearch?: true | boolean
/**
* If set to false, the ability to add new points or nodes will be disabled.
* Editing already existing features will still be possible
*/
enableAddNewPoints?: true | boolean;
enableAddNewPoints?: true | boolean
/**
* If set to false, the 'geolocation'-button will be hidden.
*/
enableGeolocation?: true | boolean;
enableGeolocation?: true | boolean
/**
* Enable switching the backgroundlayer.
* If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well
* If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well
*/
enableBackgroundLayerSelection?: true | boolean;
enableBackgroundLayerSelection?: true | boolean
/**
* If set to true, will show _all_ unanswered questions in a popup instead of just the next one
*/
enableShowAllQuestions?: false | boolean;
enableShowAllQuestions?: false | boolean
/**
* If set to true, download button for the data will be shown (offers downloading as geojson and csv)
*/
enableDownload?: false | boolean;
enableDownload?: false | boolean
/**
* If set to true, exporting a pdf is enabled
*/
enablePdfDownload?: false | boolean;
enablePdfDownload?: false | boolean
/**
* If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),
* these notes will be shown if a relevant layer is present.
*
*
* Default is true for official layers and false for unofficial (sideloaded) layers
*/
enableNoteImports?: true | boolean;
enableNoteImports?: true | boolean
/**
* Set one or more overpass URLs to use for this theme..
*/
overpassUrl?: string | string[];
overpassUrl?: string | string[]
/**
* Set a different timeout for overpass queries - in seconds. Default: 30s
*/
overpassTimeout?: number
}
}

View file

@ -1,4 +1,4 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
/**
* The LineRenderingConfig gives all details onto how to render a single line of a feature.
@ -9,16 +9,15 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
* - The feature is an area
*/
export default interface LineRenderingConfigJson {
/**
* The color for way-elements and SVG-elements.
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
*/
color?: string | TagRenderingConfigJson;
color?: string | TagRenderingConfigJson
/**
* The stroke-width for way-elements
*/
width?: string | number | TagRenderingConfigJson;
width?: string | number | TagRenderingConfigJson
/**
* A dasharray, e.g. "5 6"

View file

@ -9,4 +9,4 @@ export default interface MoveConfigJson {
* Set to false to disable this reason
*/
enableRelocation?: true | boolean
}
}

View file

@ -1,5 +1,5 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {TagConfigJson} from "./TagConfigJson";
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import { TagConfigJson } from "./TagConfigJson"
/**
* The PointRenderingConfig gives all details onto how to render a single point of a feature.
@ -10,7 +10,6 @@ import {TagConfigJson} from "./TagConfigJson";
* - To render something at the centroid of an area, or at the start, end or projected centroid of a way
*/
export default interface PointRenderingConfigJson {
/**
* All the locations that this point should be rendered at.
* Using `location: ["point", "centroid"] will always render centerpoint.
@ -30,7 +29,7 @@ export default interface PointRenderingConfigJson {
* Type: icon
*/
icon?: string | TagRenderingConfigJson;
icon?: string | TagRenderingConfigJson
/**
* A list of extra badges to show next to the icon as small badge
@ -38,26 +37,25 @@ export default interface PointRenderingConfigJson {
*
* Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle
*/
iconBadges?: {
if: TagConfigJson,
iconBadges?: {
if: TagConfigJson
/**
* Badge to show
* Type: icon
*/
then: string | TagRenderingConfigJson
then: string | TagRenderingConfigJson
}[]
/**
* A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ...
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
iconSize?: string | TagRenderingConfigJson
/**
* The rotation of an icon, useful for e.g. directions.
* Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
*/
rotation?: string | TagRenderingConfigJson;
rotation?: string | TagRenderingConfigJson
/**
* A HTML-fragment that is shown below the icon, for example:
* <div style="background: white">{name}</div>
@ -65,5 +63,5 @@ export default interface PointRenderingConfigJson {
* If the icon is undefined, then the label is shown in the center of the feature.
* Note that, if the wayhandling hides the icon then no label is shown as well.
*/
label?: string | TagRenderingConfigJson;
}
label?: string | TagRenderingConfigJson
}

View file

@ -1,33 +1,33 @@
import {TagConfigJson} from "./TagConfigJson";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import { TagConfigJson } from "./TagConfigJson"
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
export interface MappingConfigJson {
/**
* @inheritDoc
*/
if: TagConfigJson,
if: TagConfigJson
/**
* Shown if the 'if is fulfilled
* Type: rendered
*/
then: string | any,
then: string | any
/**
* An extra icon supporting the choice
* Type: icon
*/
icon?: string | {
/**
* The path to the icon
* Type: icon
*/
path: string,
/**
* Size of the image
*/
class: "small" | "medium" | "large" | string
}
icon?:
| string
| {
/**
* The path to the icon
* Type: icon
*/
path: string
/**
* Size of the image
*/
class: "small" | "medium" | "large" | string
}
/**
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
@ -78,7 +78,7 @@ export interface MappingConfigJson {
* {"if":"changing_table:location=female","then":"In the female restroom"},
* {"if":"changing_table:location=male","then":"In the male restroom"},
* {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"},
*
*
* ]
* }
*
@ -89,7 +89,7 @@ export interface MappingConfigJson {
* hideInAnswer: "_country!=be"
* }
*/
hideInAnswer?: boolean | TagConfigJson,
hideInAnswer?: boolean | TagConfigJson
/**
* Only applicable if 'multiAnswer' is set.
* This is for situations such as:
@ -103,7 +103,7 @@ export interface MappingConfigJson {
/**
* If chosen as answer, these tags will be applied as well onto the object.
* Not compatible with multiAnswer.
*
*
* This can be used e.g. to erase other keys which indicate the 'not' value:
*```json
* {
@ -112,13 +112,13 @@ export interface MappingConfigJson {
* "addExtraTags": "not:crossing:marking="
* }
* ```
*
*
*/
addExtraTags?: string[]
/**
* If there are many options, the mappings-radiobuttons will be replaced by an element with a searchfunction
*
*
* Searchterms (per language) allow to easily find an option if there are many options
*/
searchTerms?: Record<string, string[]>
@ -128,7 +128,6 @@ export interface MappingConfigJson {
* Use this sparingly
*/
priorityIf?: TagConfigJson
}
/**
@ -136,19 +135,16 @@ export interface MappingConfigJson {
* If the desired tags are missing and a question is defined, a question will be shown instead.
*/
export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson {
/**
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question.
* If undefined, the question is never asked and this tagrendering is read-only
*/
question?: string | any,
question?: string | any
/**
* Allow freeform text input from the user
*/
freeform?: {
/**
* @inheritDoc
*/
@ -158,7 +154,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
*/
type?: string,
type?: string
/**
* A (translated) text that is shown (as gray text) within the textfield
*/
@ -168,12 +164,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md'
*/
helperArgs?: (string | number | boolean | any)[];
helperArgs?: (string | number | boolean | any)[]
/**
* If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked'
**/
addExtraTags?: string[];
addExtraTags?: string[]
/**
* When set, influences the way a question is asked.
@ -188,15 +184,15 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* Normally undefined (aka do not enter anything)
*/
default?: string
},
}
/**
* If true, use checkboxes instead of radio buttons when asking the question
*/
multiAnswer?: boolean,
multiAnswer?: boolean
/**
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/
mappings?: MappingConfigJson[]
}
}

View file

@ -1,12 +1,12 @@
/**
* Rewrites and multiplies the given renderings of type T.
*
*
* This can be used for introducing many similar questions automatically,
* which also makes translations easier.
*
* (Note that the key does _not_ need to be wrapped in {}.
*
* (Note that the key does _not_ need to be wrapped in {}.
* However, we recommend to use them if the key is used in a translation, as missing keys will be picked up and warned for by the translation scripts)
*
*
* For example:
*
* ```
@ -25,7 +25,7 @@
* }
* ```
* will result in _three_ copies (as the values to rewrite into have three values, namely:
*
*
* [
* {
* # The first pair: key --> X, a|b|c --> 0
@ -37,15 +37,15 @@
* {
* "Z": 2
* }
*
*
* ]
*
*
* @see ExpandRewrite
*/
export default interface RewritableConfigJson<T> {
rewrite: {
sourceString: string[],
sourceString: string[]
into: (string | any)[][]
},
}
renderings: T
}
}

View file

@ -4,7 +4,6 @@
*/
export type TagConfigJson = string | AndTagConfigJson | OrTagConfigJson
/**
* Chain many tags, to match, all of these should be true
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
@ -14,7 +13,7 @@ export type OrTagConfigJson = {
}
/**
* Chain many tags, to match, a single of these should be true
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
*/
export type AndTagConfigJson = {
and: TagConfigJson[]

View file

@ -1,18 +1,17 @@
import {TagConfigJson} from "./TagConfigJson";
import { TagConfigJson } from "./TagConfigJson"
/**
* A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.
* For an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one
*/
export interface TagRenderingConfigJson {
/**
* The id of the tagrendering, should be an unique string.
* Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise.
*
* Use 'questions' to trigger the question box of this group (if a group is defined)
*/
id?: string,
id?: string
/**
* If 'group' is defined on many tagRenderings, these are grouped together when shown. The questions are grouped together as well.
@ -37,15 +36,15 @@ export interface TagRenderingConfigJson {
* Note that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`
* type: rendered
*/
render?: string | any,
render?: string | any
/**
* Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`.
*
* This is useful to ask a follow-up question.
* For example, within toilets, asking _where_ the diaper changing table is is only useful _if_ there is one.
* This can be done by adding `"condition": "changing_table=yes"`
*
*
* A full example would be:
* ```json
* {
@ -78,25 +77,23 @@ export interface TagRenderingConfigJson {
* },
* ```
* */
condition?: TagConfigJson;
condition?: TagConfigJson
/**
* Allow freeform text input from the user
*/
freeform?: {
/**
* If this key is present, then 'render' is used to display the value.
* If this is undefined, the rendering is _always_ shown
*/
key: string,
},
key: string
}
/**
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/
mappings?: {
/**
* If this condition is met, then the text under `then` will be shown.
* If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM.
@ -105,29 +102,30 @@ export interface TagRenderingConfigJson {
*
* This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'}
*/
if: TagConfigJson,
if: TagConfigJson
/**
* If the condition `if` is met, the text `then` will be rendered.
* If not known yet, the user will be presented with `then` as an option
* Type: rendered
*/
then: string | any,
then: string | any
/**
* An icon supporting this mapping; typically shown pretty small
* Type: icon
*/
icon?: string | {
/**
* The path to the icon
* Type: icon
*/
path: string,
/**
* A hint to mapcomplete on how to render this icon within the mapping.
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/
class: "small" | "medium" | "large" | string
}
icon?:
| string
| {
/**
* The path to the icon
* Type: icon
*/
path: string
/**
* A hint to mapcomplete on how to render this icon within the mapping.
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/
class: "small" | "medium" | "large" | string
}
}[]
}

View file

@ -2,19 +2,18 @@
* Configuration for a tilesource config
*/
export default interface TilesourceConfigJson {
/**
* Id of this overlay, used in the URL-parameters to set the state
*/
id: string,
id: string
/**
* The path, where {x}, {y} and {z} will be substituted
*/
source: string,
source: string
/**
* Wether or not this is an overlay. Default: true
*/
isOverlay?: boolean,
isOverlay?: boolean
/**
* How this will be shown in the selection menu.
@ -32,10 +31,8 @@ export default interface TilesourceConfigJson {
*/
maxZoom?: number
/**
* The default state, set to false to hide by default
*/
defaultState: boolean;
}
defaultState: boolean
}

View file

@ -1,33 +1,29 @@
export default interface UnitConfigJson {
/**
* Every key from this list will be normalized.
*
* To render a united value properly, use
*
* To render a united value properly, use
*/
appliesToKey: string[],
appliesToKey: string[]
/**
* If set, invalid values will be erased in the MC application (but not in OSM of course!)
* Be careful with setting this
*/
eraseInvalidValues?: boolean;
eraseInvalidValues?: boolean
/**
* The possible denominations
*/
applicableUnits: DenominationConfigJson[]
}
export interface DenominationConfigJson {
/**
* If this evaluates to true and the value to interpret has _no_ unit given, assumes that this unit is meant.
* Alternatively, a list of country codes can be given where this acts as the default interpretation
*
*
* E.g., a denomination using "meter" would probably set this flag to "true";
* a denomination for "mp/h" will use the condition "_country=gb" to indicate that it is the default in the UK.
*
*
* If none of the units indicate that they are the default, the first denomination will be used instead
*/
useIfNoUnitGiven?: boolean | string[]
@ -42,24 +38,22 @@ export interface DenominationConfigJson {
* The canonical value for this denomination which will be added to the value in OSM.
* e.g. "m" for meters
* If the user inputs '42', the canonical value will be added and it'll become '42m'.
*
*
* Important: often, _no_ canonical values are expected, e.g. in the case of 'maxspeed' where 'km/h' is the default.
* In this case, an empty string should be used
*/
canonicalDenomination: string,
canonicalDenomination: string
/**
* The canonical denomination in the case that the unit is precisely '1'.
* Used for display purposes
*/
canonicalDenominationSingular?: string,
canonicalDenominationSingular?: string
/**
* A list of alternative values which can occur in the OSM database - used for parsing.
*/
alternativeDenomination?: string[],
alternativeDenomination?: string[]
/**
* The value for humans in the dropdown. This should not use abbreviations and should be translated, e.g.
@ -84,6 +78,4 @@ export interface DenominationConfigJson {
* Note that if all values use 'prefix', the dropdown might move to before the text field
*/
prefix?: boolean
}
}

View file

@ -1,86 +1,80 @@
import {Translation} from "../../UI/i18n/Translation";
import SourceConfig from "./SourceConfig";
import TagRenderingConfig from "./TagRenderingConfig";
import PresetConfig, {PreciseInput} from "./PresetConfig";
import {LayerConfigJson} from "./Json/LayerConfigJson";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import FilterConfig from "./FilterConfig";
import {Unit} from "../Unit";
import DeleteConfig from "./DeleteConfig";
import MoveConfig from "./MoveConfig";
import PointRenderingConfig from "./PointRenderingConfig";
import WithContextLoader from "./WithContextLoader";
import LineRenderingConfig from "./LineRenderingConfig";
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import BaseUIElement from "../../UI/BaseUIElement";
import Combine from "../../UI/Base/Combine";
import Title from "../../UI/Base/Title";
import List from "../../UI/Base/List";
import Link from "../../UI/Base/Link";
import {Utils} from "../../Utils";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import Table from "../../UI/Base/Table";
import FilterConfigJson from "./Json/FilterConfigJson";
import {And} from "../../Logic/Tags/And";
import {Overpass} from "../../Logic/Osm/Overpass";
import Constants from "../Constants";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Svg from "../../Svg";
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmTags} from "../OsmFeature";
import { Translation } from "../../UI/i18n/Translation"
import SourceConfig from "./SourceConfig"
import TagRenderingConfig from "./TagRenderingConfig"
import PresetConfig, { PreciseInput } from "./PresetConfig"
import { LayerConfigJson } from "./Json/LayerConfigJson"
import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import FilterConfig from "./FilterConfig"
import { Unit } from "../Unit"
import DeleteConfig from "./DeleteConfig"
import MoveConfig from "./MoveConfig"
import PointRenderingConfig from "./PointRenderingConfig"
import WithContextLoader from "./WithContextLoader"
import LineRenderingConfig from "./LineRenderingConfig"
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import List from "../../UI/Base/List"
import Link from "../../UI/Base/Link"
import { Utils } from "../../Utils"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import Table from "../../UI/Base/Table"
import FilterConfigJson from "./Json/FilterConfigJson"
import { And } from "../../Logic/Tags/And"
import { Overpass } from "../../Logic/Osm/Overpass"
import Constants from "../Constants"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmTags } from "../OsmFeature"
export default class LayerConfig extends WithContextLoader {
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const;
public readonly id: string;
public readonly name: Translation;
public readonly description: Translation;
public readonly source: SourceConfig;
public readonly calculatedTags: [string, string, boolean][];
public readonly doNotDownload: boolean;
public readonly passAllFeatures: boolean;
public readonly isShown: TagsFilter;
public minzoom: number;
public minzoomVisible: number;
public readonly maxzoom: number;
public readonly title?: TagRenderingConfig;
public readonly titleIcons: TagRenderingConfig[];
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const
public readonly id: string
public readonly name: Translation
public readonly description: Translation
public readonly source: SourceConfig
public readonly calculatedTags: [string, string, boolean][]
public readonly doNotDownload: boolean
public readonly passAllFeatures: boolean
public readonly isShown: TagsFilter
public minzoom: number
public minzoomVisible: number
public readonly maxzoom: number
public readonly title?: TagRenderingConfig
public readonly titleIcons: TagRenderingConfig[]
public readonly mapRendering: PointRenderingConfig[]
public readonly lineRendering: LineRenderingConfig[]
public readonly units: Unit[];
public readonly deletion: DeleteConfig | null;
public readonly units: Unit[]
public readonly deletion: DeleteConfig | null
public readonly allowMove: MoveConfig | null
public readonly allowSplit: boolean
public readonly shownByDefault: boolean;
public readonly shownByDefault: boolean
/**
* In seconds
*/
public readonly maxAgeOfCache: number
public readonly presets: PresetConfig[];
public readonly tagRenderings: TagRenderingConfig[];
public readonly filters: FilterConfig[];
public readonly filterIsSameAs: string;
public readonly forceLoad: boolean;
public readonly syncSelection: (typeof LayerConfig.syncSelectionAllowed)[number] // this is a trick to conver a constant array of strings into a type union of these values
public readonly presets: PresetConfig[]
public readonly tagRenderings: TagRenderingConfig[]
public readonly filters: FilterConfig[]
public readonly filterIsSameAs: string
public readonly forceLoad: boolean
public readonly syncSelection: typeof LayerConfig.syncSelectionAllowed[number] // this is a trick to conver a constant array of strings into a type union of these values
constructor(
json: LayerConfigJson,
context?: string,
official: boolean = true
) {
context = context + "." + json.id;
constructor(json: LayerConfigJson, context?: string, official: boolean = true) {
context = context + "." + json.id
const translationContext = "layers:" + json.id
super(json, context)
this.id = json.id;
this.id = json.id
if (typeof json === "string") {
throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})`
}
if (json.id === undefined) {
throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})`
}
@ -89,9 +83,14 @@ export default class LayerConfig extends WithContextLoader {
throw "Layer " + this.id + " does not define a source section (" + context + ")"
}
if (json.source.osmTags === undefined) {
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
throw (
"Layer " +
this.id +
" does not define a osmTags in the source section - these should always be present, even for geojson layers (" +
context +
")"
)
}
if (json.id.toLowerCase() !== json.id) {
@ -102,28 +101,38 @@ export default class LayerConfig extends WithContextLoader {
}
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
if (json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0) {
throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'"
if (
json.syncSelection !== undefined &&
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
) {
throw (
context +
" Invalid sync-selection: must be one of " +
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
" but got '" +
json.syncSelection +
"'"
)
}
this.syncSelection = json.syncSelection ?? "no";
const osmTags = TagUtils.Tag(
json.source.osmTags,
context + "source.osmTags"
);
this.syncSelection = json.syncSelection ?? "no"
const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags")
if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) {
throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + osmTags.asHumanString(false, false, {});
throw (
context +
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
osmTags.asHumanString(false, false, {})
)
}
if (json.source["geoJsonSource"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
throw context + "Use 'geoJson' instead of 'geoJsonSource'"
}
if (json.source["geojson"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"
}
this.source = new SourceConfig(
{
osmTags: osmTags,
@ -132,74 +141,80 @@ export default class LayerConfig extends WithContextLoader {
overpassScript: json.source["overpassScript"],
isOsmCache: json.source["isOsmCache"],
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"]
idKey: json.source["idKey"],
},
Constants.priviliged_layers.indexOf(this.id) > 0,
json.id
);
)
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, translationContext + ".name");
this.allowSplit = json.allowSplit ?? false
this.name = Translations.T(json.name, translationContext + ".name")
if (json.units !== undefined && !Array.isArray(json.units)) {
throw "At " + context + ".units: the 'units'-section should be a list; you probably have an object there"
throw (
"At " +
context +
".units: the 'units'-section should be a list; you probably have an object there"
)
}
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
this.units = (json.units ?? []).map((unitJson, i) =>
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
)
if (json.description !== undefined) {
if (Object.keys(json.description).length === 0) {
json.description = undefined;
json.description = undefined
}
}
this.description = Translations.T(
json.description,
translationContext + ".description"
);
this.description = Translations.T(json.description, translationContext + ".description")
this.calculatedTags = undefined;
this.calculatedTags = undefined
if (json.calculatedTags !== undefined) {
if (!official) {
console.warn(
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
);
)
}
this.calculatedTags = [];
this.calculatedTags = []
for (const kv of json.calculatedTags) {
const index = kv.indexOf("=");
let key = kv.substring(0, index).trim();
const index = kv.indexOf("=")
let key = kv.substring(0, index).trim()
const r = "[a-z_][a-z0-9:]*"
if (key.match(r) === null) {
throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r
throw (
"At " +
context +
" invalid key for calculated tag: " +
key +
"; it should match " +
r
)
}
const isStrict = key.endsWith(':')
const isStrict = key.endsWith(":")
if (isStrict) {
key = key.substr(0, key.length - 1)
}
const code = kv.substring(index + 1);
const code = kv.substring(index + 1)
try {
new Function("feat", "return " + code + ";");
new Function("feat", "return " + code + ";")
} catch (e) {
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
}
this.calculatedTags.push([key, code, isStrict]);
this.calculatedTags.push([key, code, isStrict])
}
}
this.doNotDownload = json.doNotDownload ?? false;
this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom ?? 0;
this.doNotDownload = json.doNotDownload ?? false
this.passAllFeatures = json.passAllFeatures ?? false
this.minzoom = json.minzoom ?? 0
if (json["minZoom"] !== undefined) {
throw "At " + context + ": minzoom is written all lowercase"
}
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
this.shownByDefault = json.shownByDefault ?? true;
this.forceLoad = json.forceLoad ?? false;
this.minzoomVisible = json.minzoomVisible ?? this.minzoom
this.shownByDefault = json.shownByDefault ?? true
this.forceLoad = json.forceLoad ?? false
if (json.presets !== undefined && json.presets?.map === undefined) {
throw "Presets should be a list of items (at " + context + ")"
}
@ -207,23 +222,29 @@ export default class LayerConfig extends WithContextLoader {
let preciseInput: PreciseInput = {
preferredBackground: ["photo"],
snapToLayers: undefined,
maxSnapDistance: undefined
};
maxSnapDistance: undefined,
}
if (pr.preciseInput !== undefined) {
if (pr.preciseInput === true) {
pr.preciseInput = {
preferredBackground: undefined
preferredBackground: undefined,
}
}
let snapToLayers: string[];
let snapToLayers: string[]
if (typeof pr.preciseInput.snapToLayer === "string") {
snapToLayers = [pr.preciseInput.snapToLayer]
} else {
snapToLayers = pr.preciseInput.snapToLayer
}
let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
let preferredBackground: (
| "map"
| "photo"
| "osmbasedmap"
| "historicphoto"
| string
)[]
if (typeof pr.preciseInput.preferredBackground === "string") {
preferredBackground = [pr.preciseInput.preferredBackground]
} else {
@ -232,19 +253,22 @@ export default class LayerConfig extends WithContextLoader {
preciseInput = {
preferredBackground,
snapToLayers,
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10,
}
}
const config: PresetConfig = {
title: Translations.T(pr.title, `${translationContext}.presets.${i}.title`),
tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
description: Translations.T(pr.description, `${translationContext}.presets.${i}.description`),
description: Translations.T(
pr.description,
`${translationContext}.presets.${i}.description`
),
preciseInput: preciseInput,
exampleImages: pr.exampleImages
exampleImages: pr.exampleImages,
}
return config;
});
return config
})
if (json.mapRendering === undefined) {
throw "MapRendering is undefined in " + context
@ -255,41 +279,89 @@ export default class LayerConfig extends WithContextLoader {
this.lineRendering = []
} else {
this.mapRendering = Utils.NoNull(json.mapRendering)
.filter(r => r["location"] !== undefined)
.map((r, i) => new PointRenderingConfig(<PointRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
.filter((r) => r["location"] !== undefined)
.map(
(r, i) =>
new PointRenderingConfig(
<PointRenderingConfigJson>r,
context + ".mapRendering[" + i + "]"
)
)
this.lineRendering = Utils.NoNull(json.mapRendering)
.filter(r => r["location"] === undefined)
.map((r, i) => new LineRenderingConfig(<LineRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
.filter((r) => r["location"] === undefined)
.map(
(r, i) =>
new LineRenderingConfig(
<LineRenderingConfigJson>r,
context + ".mapRendering[" + i + "]"
)
)
const hasCenterRendering = this.mapRendering.some(r => r.location.has("centroid") || r.location.has("start") || r.location.has("end"))
const hasCenterRendering = this.mapRendering.some(
(r) =>
r.location.has("centroid") || r.location.has("start") || r.location.has("end")
)
if (this.lineRendering.length === 0 && this.mapRendering.length === 0) {
throw("The layer " + this.id + " does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'")
} else if (!hasCenterRendering && this.lineRendering.length === 0 && !this.source.geojsonSource?.startsWith("https://api.openstreetmap.org/api/0.6/notes.json")) {
throw "The layer " + this.id + " might not render ways. This might result in dropped information (at " + context + ")"
throw (
"The layer " +
this.id +
" does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'"
)
} else if (
!hasCenterRendering &&
this.lineRendering.length === 0 &&
!this.source.geojsonSource?.startsWith(
"https://api.openstreetmap.org/api/0.6/notes.json"
)
) {
throw (
"The layer " +
this.id +
" might not render ways. This might result in dropped information (at " +
context +
")"
)
}
}
const missingIds = Utils.NoNull(json.tagRenderings)?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["rewrite"] === undefined) ?? [];
const missingIds =
Utils.NoNull(json.tagRenderings)?.filter(
(tr) =>
typeof tr !== "string" &&
tr["builtin"] === undefined &&
tr["id"] === undefined &&
tr["rewrite"] === undefined
) ?? []
if (missingIds?.length > 0 && official) {
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
throw "Missing ids in tagrenderings"
}
this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map((tr, i) => new TagRenderingConfig(<TagRenderingConfigJson>tr, this.id + ".tagRenderings[" + i + "]"))
this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map(
(tr, i) =>
new TagRenderingConfig(
<TagRenderingConfigJson>tr,
this.id + ".tagRenderings[" + i + "]"
)
)
if (json.filter !== undefined && json.filter !== null && json.filter["sameAs"] !== undefined) {
if (
json.filter !== undefined &&
json.filter !== null &&
json.filter["sameAs"] !== undefined
) {
this.filterIsSameAs = json.filter["sameAs"]
this.filters = []
} else {
this.filters = (<FilterConfigJson[]>json.filter ?? []).map((option, i) => {
return new FilterConfig(option, `layers:${this.id}.filter.${i}`)
});
})
}
{
const duplicateIds = Utils.Dupiclates(this.filters.map(f => f.id))
const duplicateIds = Utils.Dupiclates(this.filters.map((f) => f.id))
if (duplicateIds.length > 0) {
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
}
@ -299,46 +371,44 @@ export default class LayerConfig extends WithContextLoader {
throw "Error in " + context + ": use 'filter' instead of 'filters'"
}
this.titleIcons = this.ParseTagRenderings(<TagRenderingConfigJson[]>json.titleIcons, {
readOnlyMode: true,
})
this.titleIcons = this.ParseTagRenderings((<TagRenderingConfigJson[]>json.titleIcons), {
readOnlyMode: true
});
this.title = this.tr("title", undefined)
this.isShown = TagUtils.TagD(json.isShown, context + ".isShown")
this.title = this.tr("title", undefined);
this.isShown = TagUtils.TagD(json.isShown, context+".isShown")
this.deletion = null;
this.deletion = null
if (json.deletion === true) {
json.deletion = {};
json.deletion = {}
}
if (json.deletion !== undefined && json.deletion !== false) {
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`);
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`)
}
this.allowMove = null
if (json.allowMove === false) {
this.allowMove = null;
this.allowMove = null
} else if (json.allowMove === true) {
this.allowMove = new MoveConfig({}, context + ".allowMove")
} else if (json.allowMove !== undefined && json.allowMove !== false) {
this.allowMove = new MoveConfig(json.allowMove, context + ".allowMove")
}
if (json["showIf"] !== undefined) {
throw (
"Invalid key on layerconfig " +
this.id +
": showIf. Did you mean 'isShown' instead?"
);
)
}
}
public defaultIcon(): BaseUIElement | undefined {
if (this.mapRendering === undefined || this.mapRendering === null) {
return undefined;
return undefined
}
const mapRendering = this.mapRendering.filter(r => r.location.has("point"))[0]
const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0]
if (mapRendering === undefined) {
return undefined
}
@ -346,64 +416,104 @@ export default class LayerConfig extends WithContextLoader {
}
public GetBaseTags(): any {
return TagUtils.changeAsProperties(this.source.osmTags.asChange({id: "node/-1"}))
return TagUtils.changeAsProperties(this.source.osmTags.asChange({ id: "node/-1" }))
}
public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map<string, string[]>, dependencies: {
context?: string;
reason: string;
neededLayer: string;
}[] = []
, addedByDefault = false, canBeIncluded = true): BaseUIElement {
const extraProps : (string | BaseUIElement)[] = []
public GenerateDocumentation(
usedInThemes: string[],
layerIsNeededBy?: Map<string, string[]>,
dependencies: {
context?: string
reason: string
neededLayer: string
}[] = [],
addedByDefault = false,
canBeIncluded = true
): BaseUIElement {
const extraProps: (string | BaseUIElement)[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
if (canBeIncluded) {
if (addedByDefault) {
extraProps.push("**This layer is included automatically in every theme. This layer might contain no points**")
extraProps.push(
"**This layer is included automatically in every theme. This layer might contain no points**"
)
}
if (this.shownByDefault === false) {
extraProps.push('This layer is not visible by default and must be enabled in the filter by the user. ')
extraProps.push(
"This layer is not visible by default and must be enabled in the filter by the user. "
)
}
if (this.title === undefined) {
extraProps.push("Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.")
extraProps.push(
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
)
}
if (this.name === undefined && this.shownByDefault === false) {
extraProps.push("This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true")
extraProps.push(
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
)
}
if (this.name === undefined) {
extraProps.push("Not visible in the layer selection by default. If you want to make this layer toggable, override `name`")
extraProps.push(
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
)
}
if (this.mapRendering.length === 0) {
extraProps.push("Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`")
extraProps.push(
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
)
}
if (this.source.geojsonSource !== undefined) {
extraProps.push(
new Combine([
Utils.runningFromConsole ? "<img src='../warning.svg' height='1rem'/>" : undefined,
"This layer is loaded from an external source, namely ",
new FixedUiElement( this.source.geojsonSource).SetClass("code")]));
Utils.runningFromConsole
? "<img src='../warning.svg' height='1rem'/>"
: undefined,
"This layer is loaded from an external source, namely ",
new FixedUiElement(this.source.geojsonSource).SetClass("code"),
])
)
}
} else {
extraProps.push("This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.")
extraProps.push(
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
)
}
let usingLayer: BaseUIElement[] = []
if (usedInThemes?.length > 0 && !addedByDefault) {
usingLayer = [new Title("Themes using this layer", 4),
new List((usedInThemes ?? []).map(id => new Link(id, "https://mapcomplete.osm.be/" + id)))
usingLayer = [
new Title("Themes using this layer", 4),
new List(
(usedInThemes ?? []).map(
(id) => new Link(id, "https://mapcomplete.osm.be/" + id)
)
),
]
}
for (const dep of dependencies) {
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"), " into the layout as it depends on it: ", dep.reason, "(" + dep.context + ")"]))
extraProps.push(
new Combine([
"This layer will automatically load ",
new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"),
" into the layout as it depends on it: ",
dep.reason,
"(" + dep.context + ")",
])
)
}
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
extraProps.push(new Combine(["This layer is needed as dependency for layer", new Link(revDep, "#" + revDep)]))
extraProps.push(
new Combine([
"This layer is needed as dependency for layer",
new Link(revDep, "#" + revDep),
])
)
}
let neededTags: TagsFilter[] = [this.source.osmTags]
@ -411,86 +521,110 @@ export default class LayerConfig extends WithContextLoader {
neededTags = this.source.osmTags["and"]
}
let tableRows = Utils.NoNull(this.tagRenderings.map(tr => tr.FreeformValues())
.map(values => {
if (values == undefined) {
return undefined
}
const embedded: (Link | string)[] = values.values?.map(v => Link.OsmWiki(values.key, v, true).SetClass("mr-2")) ?? ["_no preset options defined, or no values in them_"]
return [
new Combine([
new Link(
Utils.runningFromConsole ? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>" : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values", true
), Link.OsmWiki(values.key)
]).SetClass("flex"),
values.type === undefined ? "Multiple choice" : new Link(values.type, "../SpecialInputElements.md#" + values.type),
new Combine(embedded).SetClass("flex")
];
}))
let tableRows = Utils.NoNull(
this.tagRenderings
.map((tr) => tr.FreeformValues())
.map((values) => {
if (values == undefined) {
return undefined
}
const embedded: (Link | string)[] = values.values?.map((v) =>
Link.OsmWiki(values.key, v, true).SetClass("mr-2")
) ?? ["_no preset options defined, or no values in them_"]
return [
new Combine([
new Link(
Utils.runningFromConsole
? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>"
: Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
true
),
Link.OsmWiki(values.key),
]).SetClass("flex"),
values.type === undefined
? "Multiple choice"
: new Link(values.type, "../SpecialInputElements.md#" + values.type),
new Combine(embedded).SetClass("flex"),
]
})
)
let quickOverview: BaseUIElement = undefined;
let quickOverview: BaseUIElement = undefined
if (tableRows.length > 0) {
quickOverview = new Combine([
new FixedUiElement("Warning: ").SetClass("bold"),
"this quick overview is incomplete",
new Table(["attribute", "type", "values which are supported by this layer"], tableRows).SetClass("zebra-table")
new Table(
["attribute", "type", "values which are supported by this layer"],
tableRows
).SetClass("zebra-table"),
]).SetClass("flex-col flex")
}
let iconImg: BaseUIElement = new FixedUiElement("")
if (Utils.runningFromConsole) {
const icon = this.mapRendering
.filter(mr => mr.location.has("point"))
.map(mr => mr.icon?.render?.txt)
.find(i => i !== undefined)
.filter((mr) => mr.location.has("point"))
.map((mr) => mr.icon?.render?.txt)
.find((i) => i !== undefined)
// This is for the documentation in a markdown-file, so we have to use raw HTML
if (icon !== undefined) {
iconImg = new FixedUiElement(`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `)
iconImg = new FixedUiElement(
`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `
)
}
} else {
iconImg = this.mapRendering
.filter(mr => mr.location.has("point"))
.map(mr => mr.GenerateLeafletStyle(new UIEventSource<OsmTags>({id:"node/-1"}), false, {includeBadges: false}).html)
.find(i => i !== undefined)
.filter((mr) => mr.location.has("point"))
.map(
(mr) =>
mr.GenerateLeafletStyle(
new UIEventSource<OsmTags>({ id: "node/-1" }),
false,
{ includeBadges: false }
).html
)
.find((i) => i !== undefined)
}
let overpassLink: BaseUIElement = undefined;
let overpassLink: BaseUIElement = undefined
if (Constants.priviliged_layers.indexOf(this.id) < 0) {
try {
overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(<TagsFilter>new And(neededTags).optimize()))
overpassLink = new Link(
"Execute on overpass",
Overpass.AsOverpassTurboLink(<TagsFilter>new And(neededTags).optimize())
)
} catch (e) {
console.error("Could not generate overpasslink for " + this.id)
}
}
return new Combine([
new Combine([
new Title(this.id, 1),
iconImg,
this.description,
"\n"
]).SetClass("flex flex-col"),
new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
"flex flex-col"
),
new List(extraProps),
...usingLayer,
new Title("Basic tags for this layer", 2),
"Elements must have the all of following tags to be shown on this layer:",
new List(neededTags.map(t => t.asHumanString(true, false, {}))),
new List(neededTags.map((t) => t.asHumanString(true, false, {}))),
overpassLink,
new Title("Supported attributes", 2),
quickOverview,
...this.tagRenderings.map(tr => tr.GenerateDocumentation())
]).SetClass("flex-col").SetClass("link-underline")
...this.tagRenderings.map((tr) => tr.GenerateDocumentation()),
])
.SetClass("flex-col")
.SetClass("link-underline")
}
public CustomCodeSnippets(): string[] {
if (this.calculatedTags === undefined) {
return [];
return []
}
return this.calculatedTags.map((code) => code[1]);
return this.calculatedTags.map((code) => code[1])
}
AllTagRenderings(): TagRenderingConfig[] {
@ -498,6 +632,6 @@ export default class LayerConfig extends WithContextLoader {
}
public isLeftRightSensitive(): boolean {
return this.lineRendering.some(lr => lr.leftRightSensitive)
return this.lineRendering.some((lr) => lr.leftRightSensitive)
}
}
}

View file

@ -1,68 +1,72 @@
import {Translation} from "../../UI/i18n/Translation";
import {LayoutConfigJson} from "./Json/LayoutConfigJson";
import LayerConfig from "./LayerConfig";
import {LayerConfigJson} from "./Json/LayerConfigJson";
import Constants from "../Constants";
import TilesourceConfig from "./TilesourceConfig";
import {ExtractImages} from "./Conversion/FixImages";
import ExtraLinkConfig from "./ExtraLinkConfig";
import { Translation } from "../../UI/i18n/Translation"
import { LayoutConfigJson } from "./Json/LayoutConfigJson"
import LayerConfig from "./LayerConfig"
import { LayerConfigJson } from "./Json/LayerConfigJson"
import Constants from "../Constants"
import TilesourceConfig from "./TilesourceConfig"
import { ExtractImages } from "./Conversion/FixImages"
import ExtraLinkConfig from "./ExtraLinkConfig"
export default class LayoutConfig {
public static readonly defaultSocialImage = "assets/SocialImage.png"
public readonly id: string;
public readonly credits?: string;
public readonly language: string[];
public readonly title: Translation;
public readonly shortDescription: Translation;
public readonly description: Translation;
public readonly descriptionTail?: Translation;
public readonly icon: string;
public readonly socialImage?: string;
public readonly startZoom: number;
public readonly startLat: number;
public readonly startLon: number;
public readonly widenFactor: number;
public readonly defaultBackgroundId?: string;
public layers: LayerConfig[];
public readonly id: string
public readonly credits?: string
public readonly language: string[]
public readonly title: Translation
public readonly shortDescription: Translation
public readonly description: Translation
public readonly descriptionTail?: Translation
public readonly icon: string
public readonly socialImage?: string
public readonly startZoom: number
public readonly startLat: number
public readonly startLon: number
public readonly widenFactor: number
public readonly defaultBackgroundId?: string
public layers: LayerConfig[]
public tileLayerSources: TilesourceConfig[]
public readonly clustering?: {
maxZoom: number,
minNeededElements: number,
};
public readonly hideFromOverview: boolean;
public lockLocation: boolean | [[number, number], [number, number]];
public readonly enableUserBadge: boolean;
public readonly enableShareScreen: boolean;
public readonly enableMoreQuests: boolean;
public readonly enableAddNewPoints: boolean;
public readonly enableLayers: boolean;
public readonly enableSearch: boolean;
public readonly enableGeolocation: boolean;
public readonly enableBackgroundLayerSelection: boolean;
public readonly enableShowAllQuestions: boolean;
public readonly enableExportButton: boolean;
public readonly enablePdfDownload: boolean;
maxZoom: number
minNeededElements: number
}
public readonly hideFromOverview: boolean
public lockLocation: boolean | [[number, number], [number, number]]
public readonly enableUserBadge: boolean
public readonly enableShareScreen: boolean
public readonly enableMoreQuests: boolean
public readonly enableAddNewPoints: boolean
public readonly enableLayers: boolean
public readonly enableSearch: boolean
public readonly enableGeolocation: boolean
public readonly enableBackgroundLayerSelection: boolean
public readonly enableShowAllQuestions: boolean
public readonly enableExportButton: boolean
public readonly enablePdfDownload: boolean
public readonly customCss?: string;
public readonly customCss?: string
public readonly overpassUrl: string[];
public readonly overpassTimeout: number;
public readonly overpassUrl: string[]
public readonly overpassTimeout: number
public readonly overpassMaxZoom: number
public readonly osmApiTileSize: number
public readonly official: boolean;
public readonly official: boolean
public readonly usedImages: string[]
public readonly extraLink?: ExtraLinkConfig
public readonly definedAtUrl?: string;
public readonly definitionRaw?: string;
public readonly definedAtUrl?: string
public readonly definitionRaw?: string
constructor(json: LayoutConfigJson, official = true, options?: {
definedAtUrl?: string,
definitionRaw?: string
}) {
this.official = official;
this.id = json.id;
constructor(
json: LayoutConfigJson,
official = true,
options?: {
definedAtUrl?: string
definitionRaw?: string
}
) {
this.official = official
this.id = json.id
this.definedAtUrl = options?.definedAtUrl
this.definitionRaw = options?.definitionRaw
if (official) {
@ -74,73 +78,108 @@ export default class LayoutConfig {
}
}
const context = this.id
this.credits = json.credits;
this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title));
this.usedImages = Array.from(new ExtractImages(official, undefined).convertStrict(json, "while extracting the images of " + json.id + " " + context ?? "")).sort()
this.credits = json.credits
this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title))
this.usedImages = Array.from(
new ExtractImages(official, undefined).convertStrict(
json,
"while extracting the images of " + json.id + " " + context ?? ""
)
).sort()
{
if (typeof json.title === "string") {
throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${this.id}; the offending object is ${JSON.stringify(json.title)} which is a ${typeof json.title})`
throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${
this.id
}; the offending object is ${JSON.stringify(
json.title
)} which is a ${typeof json.title})`
}
if (this.language.length == 0) {
throw `No languages defined. Define at least one language. (${context}.languages)`
}
if (json.title === undefined) {
throw "Title not defined in " + this.id;
throw "Title not defined in " + this.id
}
if (json.description === undefined) {
throw "Description not defined in " + this.id;
throw "Description not defined in " + this.id
}
if (json.widenFactor <= 0) {
throw "Widenfactor too small, shoud be > 0"
}
if (json.widenFactor > 20) {
throw "Widenfactor is very big, use a value between 1 and 5 (current value is " + json.widenFactor + ") at " + context
throw (
"Widenfactor is very big, use a value between 1 and 5 (current value is " +
json.widenFactor +
") at " +
context
)
}
if (json["hideInOverview"]) {
throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
throw (
"The json for " +
this.id +
" contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
)
}
if (json.layers === undefined) {
throw "Got undefined layers for " + json.id + " at " + context
}
}
this.title = new Translation(json.title, "themes:" + context + ".title");
this.description = new Translation(json.description, "themes:" + context + ".description");
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, "themes:" + context + ".shortdescription");
this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail");
this.icon = json.icon;
this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage;
this.title = new Translation(json.title, "themes:" + context + ".title")
this.description = new Translation(json.description, "themes:" + context + ".description")
this.shortDescription =
json.shortDescription === undefined
? this.description.FirstSentence()
: new Translation(json.shortDescription, "themes:" + context + ".shortdescription")
this.descriptionTail =
json.descriptionTail === undefined
? undefined
: new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail")
this.icon = json.icon
this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage
if (this.socialImage === "") {
if (official) {
throw "Theme " + json.id + " has empty string as social image"
}
}
this.startZoom = json.startZoom;
this.startLat = json.startLat;
this.startLon = json.startLon;
this.widenFactor = json.widenFactor ?? 1.5;
this.startZoom = json.startZoom
this.startLat = json.startLat
this.startLon = json.startLon
this.widenFactor = json.widenFactor ?? 1.5
this.defaultBackgroundId = json.defaultBackgroundId;
this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
this.defaultBackgroundId = json.defaultBackgroundId
this.tileLayerSources = (json.tileLayerSources ?? []).map(
(config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)
)
// At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert
this.layers = json.layers.map(lyrJson => new LayerConfig(<LayerConfigJson>lyrJson, json.id + ".layers." + lyrJson["id"], official));
this.extraLink = new ExtraLinkConfig(json.extraLink ?? {
icon: "./assets/svg/pop-out.svg",
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
newTab: true,
requirements: ["iframe", "no-welcome-message"]
}, context + ".extraLink")
this.layers = json.layers.map(
(lyrJson) =>
new LayerConfig(
<LayerConfigJson>lyrJson,
json.id + ".layers." + lyrJson["id"],
official
)
)
this.extraLink = new ExtraLinkConfig(
json.extraLink ?? {
icon: "./assets/svg/pop-out.svg",
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
newTab: true,
requirements: ["iframe", "no-welcome-message"],
},
context + ".extraLink"
)
this.clustering = {
maxZoom: 16,
minNeededElements: 250,
};
}
if (json.clustering === false) {
this.clustering = {
maxZoom: 0,
minNeededElements: 100000,
};
}
} else if (json.clustering) {
this.clustering = {
maxZoom: json.clustering.maxZoom ?? 18,
@ -148,20 +187,20 @@ export default class LayoutConfig {
}
}
this.hideFromOverview = json.hideFromOverview ?? false;
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined;
this.enableUserBadge = json.enableUserBadge ?? true;
this.enableShareScreen = json.enableShareScreen ?? true;
this.enableMoreQuests = json.enableMoreQuests ?? true;
this.enableLayers = json.enableLayers ?? true;
this.enableSearch = json.enableSearch ?? true;
this.enableGeolocation = json.enableGeolocation ?? true;
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
this.enableExportButton = json.enableDownload ?? false;
this.enablePdfDownload = json.enablePdfDownload ?? false;
this.customCss = json.customCss;
this.hideFromOverview = json.hideFromOverview ?? false
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined
this.enableUserBadge = json.enableUserBadge ?? true
this.enableShareScreen = json.enableShareScreen ?? true
this.enableMoreQuests = json.enableMoreQuests ?? true
this.enableLayers = json.enableLayers ?? true
this.enableSearch = json.enableSearch ?? true
this.enableGeolocation = json.enableGeolocation ?? true
this.enableAddNewPoints = json.enableAddNewPoints ?? true
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false
this.enableExportButton = json.enableDownload ?? false
this.enablePdfDownload = json.enablePdfDownload ?? false
this.customCss = json.customCss
this.overpassUrl = Constants.defaultOverpassUrls
if (json.overpassUrl !== undefined) {
if (typeof json.overpassUrl === "string") {
@ -173,27 +212,27 @@ export default class LayoutConfig {
this.overpassTimeout = json.overpassTimeout ?? 30
this.overpassMaxZoom = json.overpassMaxZoom ?? 16
this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1
}
public CustomCodeSnippets(): string[] {
if (this.official) {
return [];
return []
}
const msg = "<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
const custom = [];
const msg =
"<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
const custom = []
for (const layer of this.layers) {
custom.push(...layer.CustomCodeSnippets().map(code => code + "<br />"))
custom.push(...layer.CustomCodeSnippets().map((code) => code + "<br />"))
}
if (custom.length === 0) {
return custom;
return custom
}
custom.splice(0, 0, msg);
return custom;
custom.splice(0, 0, msg)
return custom
}
public isLeftRightSensitive() {
return this.layers.some(l => l.isLeftRightSensitive())
return this.layers.some((l) => l.isLeftRightSensitive())
}
public getMatchingLayer(tags: any): LayerConfig | undefined {
@ -207,5 +246,4 @@ export default class LayoutConfig {
}
return undefined
}
}
}

View file

@ -1,43 +1,49 @@
import WithContextLoader from "./WithContextLoader";
import TagRenderingConfig from "./TagRenderingConfig";
import {Utils} from "../../Utils";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
import WithContextLoader from "./WithContextLoader"
import TagRenderingConfig from "./TagRenderingConfig"
import { Utils } from "../../Utils"
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"
export default class LineRenderingConfig extends WithContextLoader {
public readonly color: TagRenderingConfig;
public readonly width: TagRenderingConfig;
public readonly dashArray: TagRenderingConfig;
public readonly lineCap: TagRenderingConfig;
public readonly offset: TagRenderingConfig;
public readonly fill: TagRenderingConfig;
public readonly fillColor: TagRenderingConfig;
public readonly color: TagRenderingConfig
public readonly width: TagRenderingConfig
public readonly dashArray: TagRenderingConfig
public readonly lineCap: TagRenderingConfig
public readonly offset: TagRenderingConfig
public readonly fill: TagRenderingConfig
public readonly fillColor: TagRenderingConfig
public readonly leftRightSensitive: boolean
constructor(json: LineRenderingConfigJson, context: string) {
super(json, context)
this.color = this.tr("color", "#0000ff");
this.width = this.tr("width", "7");
this.dashArray = this.tr("dashArray", "");
this.lineCap = this.tr("lineCap", "round");
this.fill = this.tr("fill", undefined);
this.fillColor = this.tr("fillColor", undefined);
this.color = this.tr("color", "#0000ff")
this.width = this.tr("width", "7")
this.dashArray = this.tr("dashArray", "")
this.lineCap = this.tr("lineCap", "round")
this.fill = this.tr("fill", undefined)
this.fillColor = this.tr("fillColor", undefined)
this.leftRightSensitive = json.offset !== undefined && json.offset !== 0 && json.offset !== "0"
this.leftRightSensitive =
json.offset !== undefined && json.offset !== 0 && json.offset !== "0"
this.offset = this.tr("offset", "0");
this.offset = this.tr("offset", "0")
}
public GenerateLeafletStyle(tags: {}):
{ fillColor?: string; color: string; lineCap: string; offset: number; weight: number; dashArray: string; fill?: boolean } {
public GenerateLeafletStyle(tags: {}): {
fillColor?: string
color: string
lineCap: string
offset: number
weight: number
dashArray: string
fill?: boolean
} {
function rendernum(tr: TagRenderingConfig, deflt: number) {
const str = Number(render(tr, "" + deflt));
const n = Number(str);
const str = Number(render(tr, "" + deflt))
const n = Number(str)
if (isNaN(n)) {
return deflt;
return deflt
}
return n;
return n
}
function render(tr: TagRenderingConfig, deflt?: string) {
@ -47,19 +53,17 @@ export default class LineRenderingConfig extends WithContextLoader {
if (tr === undefined) {
return deflt
}
const str = tr?.GetRenderValue(tags)?.txt ?? deflt;
const str = tr?.GetRenderValue(tags)?.txt ?? deflt
if (str === "") {
return deflt
}
return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "");
return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "")
}
const dashArray = render(this.dashArray);
let color = render(this.color, "#00f");
const dashArray = render(this.dashArray)
let color = render(this.color, "#00f")
if (color.startsWith("--")) {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
}
const style = {
@ -67,7 +71,7 @@ export default class LineRenderingConfig extends WithContextLoader {
dashArray,
weight: rendernum(this.width, 5),
lineCap: render(this.lineCap),
offset: rendernum(this.offset, 0)
offset: rendernum(this.offset, 0),
}
const fillStr = render(this.fill, undefined)
@ -80,7 +84,5 @@ export default class LineRenderingConfig extends WithContextLoader {
style["fillColor"] = fillColorStr
}
return style
}
}
}

View file

@ -1,7 +1,6 @@
import MoveConfigJson from "./Json/MoveConfigJson";
import MoveConfigJson from "./Json/MoveConfigJson"
export default class MoveConfig {
public readonly enableImproveAccuracy: boolean
public readonly enableRelocation: boolean
@ -12,6 +11,4 @@ export default class MoveConfig {
throw "At least one default move reason should be allowed (at " + context + ")"
}
}
}
}

View file

@ -1,29 +1,35 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import TagRenderingConfig from "./TagRenderingConfig";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {Utils} from "../../Utils";
import Svg from "../../Svg";
import WithContextLoader from "./WithContextLoader";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Img from "../../UI/Base/Img";
import Combine from "../../UI/Base/Combine";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
import TagRenderingConfig from "./TagRenderingConfig"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { Utils } from "../../Utils"
import Svg from "../../Svg"
import WithContextLoader from "./WithContextLoader"
import { UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../../UI/BaseUIElement"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Img from "../../UI/Base/Img"
import Combine from "../../UI/Base/Combine"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
export default class PointRenderingConfig extends WithContextLoader {
private static readonly allowed_location_codes = new Set<string>([
"point",
"centroid",
"start",
"end",
"projected_centerpoint",
])
public readonly location: Set<
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
>
private static readonly allowed_location_codes = new Set<string>(["point", "centroid", "start", "end","projected_centerpoint"])
public readonly location: Set<"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string>
public readonly icon: TagRenderingConfig;
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];
public readonly iconSize: TagRenderingConfig;
public readonly label: TagRenderingConfig;
public readonly rotation: TagRenderingConfig;
public readonly icon: TagRenderingConfig
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]
public readonly iconSize: TagRenderingConfig
public readonly label: TagRenderingConfig
public readonly rotation: TagRenderingConfig
constructor(json: PointRenderingConfigJson, context: string) {
super(json, context)
@ -34,10 +40,12 @@ export default class PointRenderingConfig extends WithContextLoader {
this.location = new Set(json.location)
this.location.forEach(l => {
this.location.forEach((l) => {
const allowed = PointRenderingConfig.allowed_location_codes
if (!allowed.has(l)) {
throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(allowed).join(", ")} (at ${context}.location)`
throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(
allowed
).join(", ")} (at ${context}.location)`
}
})
@ -46,36 +54,39 @@ export default class PointRenderingConfig extends WithContextLoader {
}
if (this.location.size == 0) {
throw "A pointRendering should have at least one 'location' to defined where it should be rendered. (At " + context + ".location)"
throw (
"A pointRendering should have at least one 'location' to defined where it should be rendered. (At " +
context +
".location)"
)
}
this.icon = this.tr("icon", undefined);
this.icon = this.tr("icon", undefined)
this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => {
let tr: TagRenderingConfig;
if (typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then);
let tr: TagRenderingConfig
if (
typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then)
} else {
tr = new TagRenderingConfig(
overlay.then,
`iconBadges.${i}`
);
tr = new TagRenderingConfig(overlay.then, `iconBadges.${i}`)
}
return {
if: TagUtils.Tag(overlay.if),
then: tr
};
});
then: tr,
}
})
const iconPath = this.icon?.GetRenderValue({id: "node/-1"})?.txt;
const iconPath = this.icon?.GetRenderValue({ id: "node/-1" })?.txt
if (iconPath !== undefined && iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length);
const iconKey = iconPath.substr(Utils.assets_path.length)
if (Svg.All[iconKey] === undefined) {
throw context + ": builtin SVG asset not found: " + iconPath;
throw context + ": builtin SVG asset not found: " + iconPath
}
}
this.iconSize = this.tr("iconSize", "40,40,center");
this.label = this.tr("label", undefined);
this.rotation = this.tr("rotation", "0");
this.iconSize = this.tr("iconSize", "40,40,center")
this.label = this.tr("label", undefined)
this.rotation = this.tr("rotation", "0")
}
/**
@ -84,40 +95,47 @@ export default class PointRenderingConfig extends WithContextLoader {
*/
private static FromHtmlSpec(htmlSpec: string, style: string, isBadge = false): BaseUIElement {
if (htmlSpec === undefined) {
return undefined;
return undefined
}
const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/);
const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/)
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
const svg = (Svg.All[match[1] + ".svg"] as string)
const svg = Svg.All[match[1] + ".svg"] as string
const targetColor = match[2]
const img = new Img(svg
.replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor), true)
.SetStyle(style)
const img = new Img(
svg.replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor),
true
).SetStyle(style)
if (isBadge) {
img.SetClass("badge")
}
return img
} else if (Svg.All[htmlSpec + ".svg"] !== undefined) {
const svg = (Svg.All[htmlSpec + ".svg"] as string)
const img = new Img(svg, true)
.SetStyle(style)
const svg = Svg.All[htmlSpec + ".svg"] as string
const img = new Img(svg, true).SetStyle(style)
if (isBadge) {
img.SetClass("badge")
}
return img
} else {
return new FixedUiElement(`<img src="${htmlSpec}" style="${style}" />`);
return new FixedUiElement(`<img src="${htmlSpec}" style="${style}" />`)
}
}
private static FromHtmlMulti(multiSpec: string, rotation: string, isBadge: boolean, defaultElement: BaseUIElement = undefined) {
private static FromHtmlMulti(
multiSpec: string,
rotation: string,
isBadge: boolean,
defaultElement: BaseUIElement = undefined
) {
if (multiSpec === undefined) {
return defaultElement
}
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`
const htmlDefs = multiSpec.trim()?.split(";") ?? []
const elements = Utils.NoEmpty(htmlDefs).map(def => PointRenderingConfig.FromHtmlSpec(def, style, isBadge))
const elements = Utils.NoEmpty(htmlDefs).map((def) =>
PointRenderingConfig.FromHtmlSpec(def, style, isBadge)
)
if (elements.length === 0) {
return defaultElement
} else {
@ -126,93 +144,95 @@ export default class PointRenderingConfig extends WithContextLoader {
}
public GetBaseIcon(tags?: any): BaseUIElement {
tags = tags ?? {id: "node/-1"}
tags = tags ?? { id: "node/-1" }
let defaultPin: BaseUIElement = undefined
if (this.label === undefined) {
defaultPin = Svg.teardrop_with_hole_green_svg()
}
if(this.icon === undefined){
return defaultPin;
if (this.icon === undefined) {
return defaultPin
}
const rotation = Utils.SubstituteKeys(this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags)
const rotation = Utils.SubstituteKeys(
this.rotation?.GetRenderValue(tags)?.txt ?? "0deg",
tags
)
const htmlDefs = Utils.SubstituteKeys(this.icon?.GetRenderValue(tags)?.txt, tags)
if(htmlDefs === undefined){
if (htmlDefs === undefined) {
// This layer doesn't want to show an icon right now
return undefined
}
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
}
public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
const self = this;
const self = this
if (this.icon === undefined) {
return undefined;
return undefined
}
return new VariableUiElement(tags.map(tags => self.GetBaseIcon(tags))).SetClass("w-full h-full block")
return new VariableUiElement(tags.map((tags) => self.GetBaseIcon(tags))).SetClass(
"w-full h-full block"
)
}
public GenerateLeafletStyle(
tags: UIEventSource<any>,
clickable: boolean,
options?: {
noSize?: false | boolean,
noSize?: false | boolean
includeBadges?: true | boolean
}
):
{
html: BaseUIElement;
iconSize: [number, number];
iconAnchor: [number, number];
popupAnchor: [number, number];
iconUrl: string;
className: string;
} {
): {
html: BaseUIElement
iconSize: [number, number]
iconAnchor: [number, number]
popupAnchor: [number, number]
iconUrl: string
className: string
} {
function num(str, deflt = 40) {
const n = Number(str);
const n = Number(str)
if (isNaN(n)) {
return deflt;
return deflt
}
return n;
return n
}
function render(tr: TagRenderingConfig, deflt?: string) {
if (tags === undefined) {
return deflt
}
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "")
}
const iconSize = render(this.iconSize, "40,40,center").split(",");
const iconSize = render(this.iconSize, "40,40,center").split(",")
const iconW = num(iconSize[0]);
let iconH = num(iconSize[1]);
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
const iconW = num(iconSize[0])
let iconH = num(iconSize[1])
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"
let anchorW = iconW / 2;
let anchorH = iconH / 2;
let anchorW = iconW / 2
let anchorH = iconH / 2
if (mode === "left") {
anchorW = 0;
anchorW = 0
}
if (mode === "right") {
anchorW = iconW;
anchorW = iconW
}
if (mode === "top") {
anchorH = 0;
anchorH = 0
}
if (mode === "bottom") {
anchorH = iconH;
anchorH = iconH
}
const icon = this.GetSimpleIcon(tags)
let badges = undefined;
let badges = undefined
if (options?.includeBadges ?? true) {
badges = this.GetBadges(tags)
}
const iconAndBadges = new Combine([icon, badges])
.SetClass("block relative")
const iconAndBadges = new Combine([icon, badges]).SetClass("block relative")
if (!options?.noSize) {
iconAndBadges.SetStyle(`width: ${iconW}px; height: ${iconH}px`)
@ -221,7 +241,7 @@ export default class PointRenderingConfig extends WithContextLoader {
}
let label = this.GetLabel(tags)
let htmlEl: BaseUIElement;
let htmlEl: BaseUIElement
if (icon === undefined && label === undefined) {
htmlEl = undefined
} else if (icon === undefined) {
@ -238,10 +258,8 @@ export default class PointRenderingConfig extends WithContextLoader {
iconAnchor: [anchorW, anchorH],
popupAnchor: [0, 3 - anchorH],
iconUrl: undefined,
className: clickable
? "leaflet-div-icon"
: "leaflet-div-icon unclickable",
};
className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable",
}
}
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
@ -249,41 +267,46 @@ export default class PointRenderingConfig extends WithContextLoader {
return undefined
}
return new VariableUiElement(
tags.map(tags => {
const badgeElements = this.iconBadges.map(badge => {
tags.map((tags) => {
const badgeElements = this.iconBadges.map((badge) => {
if (!badge.if.matchesProperties(tags)) {
// Doesn't match...
return undefined
}
const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags)
const badgeElement = PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative")
const htmlDefs = Utils.SubstituteKeys(
badge.then.GetRenderValue(tags)?.txt,
tags
)
const badgeElement = PointRenderingConfig.FromHtmlMulti(
htmlDefs,
"0",
true
)?.SetClass("block relative")
if (badgeElement === undefined) {
return undefined;
return undefined
}
return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block")
})
return new Combine(badgeElements).SetClass("inline-flex h-full")
})).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
})
).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
}
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
if (this.label === undefined) {
return undefined;
return undefined
}
const self = this;
return new VariableUiElement(tags.map(tags => {
const label = self.label
?.GetRenderValue(tags)
?.Subs(tags)
?.SetClass("block text-center")
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
}))
const self = this
return new VariableUiElement(
tags.map((tags) => {
const label = self.label
?.GetRenderValue(tags)
?.Subs(tags)
?.SetClass("block text-center")
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
})
)
}
}
}

View file

@ -1,19 +1,19 @@
import {Translation} from "../../UI/i18n/Translation";
import {Tag} from "../../Logic/Tags/Tag";
import { Translation } from "../../UI/i18n/Translation"
import { Tag } from "../../Logic/Tags/Tag"
export interface PreciseInput {
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[],
snapToLayers?: string[],
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
snapToLayers?: string[]
maxSnapDistance?: number
}
export default interface PresetConfig {
title: Translation,
tags: Tag[],
description?: Translation,
exampleImages?: string[],
title: Translation
tags: Tag[]
description?: Translation
exampleImages?: string[]
/**
* If precise input is set, then an extra map is shown in which the user can drag the map to the precise location
*/
preciseInput?: PreciseInput
}
}

View file

@ -1,63 +1,79 @@
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {RegexTag} from "../../Logic/Tags/RegexTag";
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { RegexTag } from "../../Logic/Tags/RegexTag"
export default class SourceConfig {
public readonly osmTags?: TagsFilter
public readonly overpassScript?: string
public geojsonSource?: string
public geojsonZoomLevel?: number
public isOsmCacheLayer: boolean
public readonly mercatorCrs: boolean
public readonly idKey: string
public readonly osmTags?: TagsFilter;
public readonly overpassScript?: string;
public geojsonSource?: string;
public geojsonZoomLevel?: number;
public isOsmCacheLayer: boolean;
public readonly mercatorCrs: boolean;
public readonly idKey : string
constructor(params: {
mercatorCrs?: boolean;
osmTags?: TagsFilter,
overpassScript?: string,
geojsonSource?: string,
isOsmCache?: boolean,
geojsonSourceLevel?: number,
idKey?: string
}, isSpecialLayer: boolean, context?: string) {
let defined = 0;
constructor(
params: {
mercatorCrs?: boolean
osmTags?: TagsFilter
overpassScript?: string
geojsonSource?: string
isOsmCache?: boolean
geojsonSourceLevel?: number
idKey?: string
},
isSpecialLayer: boolean,
context?: string
) {
let defined = 0
if (params.osmTags) {
defined++;
defined++
}
if (params.overpassScript) {
defined++;
defined++
}
if (params.geojsonSource) {
defined++;
defined++
}
if (defined == 0) {
throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify(params)})`
throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify(
params
)})`
}
if (params.isOsmCache && params.geojsonSource == undefined) {
console.error(params)
throw `Source said it is a OSM-cached layer, but didn't define the actual source of the cache (in context ${context})`
}
if (params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined) {
if (!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(toSearch => params.geojsonSource.indexOf(toSearch) > 0)) {
if (
!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(
(toSearch) => params.geojsonSource.indexOf(toSearch) > 0
)
) {
throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})`
}
}
if(params.osmTags !== undefined && !isSpecialLayer){
if (params.osmTags !== undefined && !isSpecialLayer) {
const optimized = params.osmTags.optimize()
if(optimized === false){
throw "Error at "+context+": the specified tags are conflicting with each other: they will never match anything at all"
if (optimized === false) {
throw (
"Error at " +
context +
": the specified tags are conflicting with each other: they will never match anything at all"
)
}
if(optimized === true){
throw "Error at "+context+": the specified tags are very wide: they will always match everything"
if (optimized === true) {
throw (
"Error at " +
context +
": the specified tags are very wide: they will always match everything"
)
}
}
this.osmTags = params.osmTags ?? new RegexTag("id", /.*/);
this.overpassScript = params.overpassScript;
this.geojsonSource = params.geojsonSource;
this.geojsonZoomLevel = params.geojsonSourceLevel;
this.isOsmCacheLayer = params.isOsmCache ?? false;
this.mercatorCrs = params.mercatorCrs ?? false;
this.idKey= params.idKey
this.osmTags = params.osmTags ?? new RegexTag("id", /.*/)
this.overpassScript = params.overpassScript
this.geojsonSource = params.geojsonSource
this.geojsonZoomLevel = params.geojsonSourceLevel
this.isOsmCacheLayer = params.isOsmCache ?? false
this.mercatorCrs = params.mercatorCrs ?? false
this.idKey = params.idKey
}
}
}

View file

@ -1,29 +1,39 @@
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import Translations from "../../UI/i18n/Translations";
import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils";
import {And} from "../../Logic/Tags/And";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {Utils} from "../../Utils";
import {Tag} from "../../Logic/Tags/Tag";
import BaseUIElement from "../../UI/BaseUIElement";
import Combine from "../../UI/Base/Combine";
import Title from "../../UI/Base/Title";
import Link from "../../UI/Base/Link";
import List from "../../UI/Base/List";
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {Paragraph} from "../../UI/Base/Paragraph";
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
export interface Mapping {
readonly if: UploadableTag,
readonly ifnot?: UploadableTag,
readonly then: TypedTranslation<object>,
readonly icon: string,
readonly iconClass: string | "small" | "medium" | "large" | "small-height" | "medium-height" | "large-height",
readonly if: UploadableTag
readonly ifnot?: UploadableTag
readonly then: TypedTranslation<object>
readonly icon: string
readonly iconClass:
| string
| "small"
| "medium"
| "large"
| "small-height"
| "medium-height"
| "large-height"
readonly hideInAnswer: boolean | TagsFilter
readonly addExtraTags: Tag[],
readonly searchTerms?: Record<string, string[]>,
readonly addExtraTags: Tag[]
readonly searchTerms?: Record<string, string[]>
readonly priorityIf?: TagsFilter
}
@ -32,52 +42,49 @@ export interface Mapping {
* Identical data, but with some methods and validation
*/
export default class TagRenderingConfig {
public readonly id: string;
public readonly group: string;
public readonly render?: TypedTranslation<object>;
public readonly question?: TypedTranslation<object>;
public readonly condition?: TagsFilter;
public readonly description?: Translation;
public readonly id: string
public readonly group: string
public readonly render?: TypedTranslation<object>
public readonly question?: TypedTranslation<object>
public readonly condition?: TagsFilter
public readonly description?: Translation
public readonly configuration_warnings: string[] = []
public readonly freeform?: {
readonly key: string,
readonly type: string,
readonly placeholder: Translation,
readonly addExtraTags: UploadableTag[];
readonly inline: boolean,
readonly default?: string,
readonly key: string
readonly type: string
readonly placeholder: Translation
readonly addExtraTags: UploadableTag[]
readonly inline: boolean
readonly default?: string
readonly helperArgs?: (string | number | boolean)[]
};
}
public readonly multiAnswer: boolean;
public readonly multiAnswer: boolean
public readonly mappings?: Mapping[]
public readonly labels: string[]
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context;
throw "Initing a TagRenderingConfig with undefined in " + context
}
if (json === "questions") {
// Very special value
this.render = null;
this.question = null;
this.condition = null;
this.render = null
this.question = null
this.condition = null
this.id = "questions"
this.group = ""
return;
return
}
if (typeof json === "number") {
json = "" + json
}
let translationKey = context;
let translationKey = context
if (json["id"] !== undefined) {
const layerId = context.split(".")[0]
if (json["source"]) {
@ -91,43 +98,54 @@ export default class TagRenderingConfig {
}
}
if (typeof json === "string") {
this.render = Translations.T(json, translationKey + ".render");
this.multiAnswer = false;
return;
this.render = Translations.T(json, translationKey + ".render")
this.multiAnswer = false
return
}
this.id = json.id ?? ""; // Some tagrenderings - especially for the map rendering - don't need an ID
this.id = json.id ?? "" // Some tagrenderings - especially for the map rendering - don't need an ID
if (this.id.match(/^[a-zA-Z0-9 ()?\/=:;,_-]*$/) === null) {
throw "Invalid ID in " + context + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + this.id
throw (
"Invalid ID in " +
context +
": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " +
this.id
)
}
this.group = json.group ?? "";
this.group = json.group ?? ""
this.labels = json.labels ?? []
this.render = Translations.T(json.render, translationKey + ".render");
this.question = Translations.T(json.question, translationKey + ".question");
this.description = Translations.T(json.description, translationKey + ".description");
this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
this.render = Translations.T(json.render, translationKey + ".render")
this.question = Translations.T(json.question, translationKey + ".question")
this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
if (json.freeform) {
if (json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined) {
if (
json.freeform.addExtraTags !== undefined &&
json.freeform.addExtraTags.map === undefined
) {
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
}
const type = json.freeform.type ?? "string"
if (ValidatedTextField.AvailableTypes().indexOf(type) < 0) {
throw "At " + context + ".freeform.type is an unknown type: " + type + "; try one of " + ValidatedTextField.AvailableTypes().join(", ")
throw (
"At " +
context +
".freeform.type is an unknown type: " +
type +
"; try one of " +
ValidatedTextField.AvailableTypes().join(", ")
)
}
let placeholder: Translation = Translations.T(json.freeform.placeholder)
if (placeholder === undefined) {
const typeDescription = <Translation>Translations.t.validation[type]?.description
const key = json.freeform.key;
const key = json.freeform.key
if (typeDescription !== undefined) {
placeholder = typeDescription.OnEveryLanguage(l => key + " (" + l + ")")
placeholder = typeDescription.OnEveryLanguage((l) => key + " (" + l + ")")
} else {
placeholder = Translations.T(key + " (" + type + ")")
}
@ -137,12 +155,13 @@ export default class TagRenderingConfig {
key: json.freeform.key,
type,
placeholder,
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)) ?? [],
addExtraTags:
json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
helperArgs: json.freeform.helperArgs
helperArgs: json.freeform.helperArgs,
}
if (json.freeform["extraTags"] !== undefined) {
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
@ -152,7 +171,6 @@ export default class TagRenderingConfig {
}
if (json.freeform["args"] !== undefined) {
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
}
if (json.freeform.key === "questions") {
@ -161,28 +179,42 @@ export default class TagRenderingConfig {
}
}
if (this.freeform.type !== undefined && ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0) {
const knownKeys = ValidatedTextField.AvailableTypes().join(", ");
if (
this.freeform.type !== undefined &&
ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0
) {
const knownKeys = ValidatedTextField.AvailableTypes().join(", ")
throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
}
if (this.freeform.addExtraTags) {
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
const usedKeys = new And(this.freeform.addExtraTags).usedKeys()
if (usedKeys.indexOf(this.freeform.key) >= 0) {
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`;
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`
}
}
}
this.multiAnswer = json.multiAnswer ?? false
if (json.mappings) {
if (!Array.isArray(json.mappings)) {
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
}
const commonIconSize = Utils.NoNull(json.mappings.map(m => m.icon !== undefined ? m.icon["class"] : undefined))[0] ?? "small"
this.mappings = json.mappings.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, translationKey, context, this.multiAnswer, this.question !== undefined, commonIconSize));
const commonIconSize =
Utils.NoNull(
json.mappings.map((m) => (m.icon !== undefined ? m.icon["class"] : undefined))
)[0] ?? "small"
this.mappings = json.mappings.map((m, i) =>
TagRenderingConfig.ExtractMapping(
m,
i,
translationKey,
context,
this.multiAnswer,
this.question !== undefined,
commonIconSize
)
)
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
@ -196,14 +228,12 @@ export default class TagRenderingConfig {
continue
}
throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!`
}
if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") {
throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together`
}
}
if (this.freeform) {
if (this.render === undefined) {
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
@ -222,21 +252,25 @@ export default class TagRenderingConfig {
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
continue
}
if (this.freeform.type === "opening_hours" && txt.indexOf("{opening_hours_table(") >= 0) {
if (
this.freeform.type === "opening_hours" &&
txt.indexOf("{opening_hours_table(") >= 0
) {
continue
}
if (this.freeform.type === "wikidata" && txt.indexOf("{wikipedia(" + this.freeform.key) >= 0) {
if (
this.freeform.type === "wikidata" &&
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
) {
continue
}
if (this.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) {
continue
}
throw `${context}: The rendering for language ${ln} does not contain the freeform key {${this.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} `
}
}
if (this.render && this.question && this.freeform === undefined) {
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}; the question is ${this.question.txt}`
}
@ -244,7 +278,7 @@ export default class TagRenderingConfig {
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
let keys = []
for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i];
const mapping = this.mappings[i]
if (mapping.if === undefined) {
throw `${context}.mappings[${i}].if is undefined`
}
@ -252,15 +286,17 @@ export default class TagRenderingConfig {
}
keys = Utils.Dedup(keys)
for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i];
const mapping = this.mappings[i]
if (mapping.hideInAnswer) {
continue
}
const usedKeys = mapping.if.usedKeys();
const usedKeys = mapping.if.usedKeys()
for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}`
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
", "
)}, but it should also give a value for ${expectedKey}`
this.configuration_warnings.push(msg)
}
}
@ -272,22 +308,21 @@ export default class TagRenderingConfig {
throw `${context} MultiAnswer is set, but no mappings are defined`
}
let allKeys = [];
let allHaveIfNot = true;
let allKeys = []
let allHaveIfNot = true
for (const mapping of this.mappings) {
if (mapping.hideInAnswer) {
continue;
continue
}
if (mapping.ifnot === undefined) {
allHaveIfNot = false;
allHaveIfNot = false
}
allKeys = allKeys.concat(mapping.if.usedKeys());
allKeys = allKeys.concat(mapping.if.usedKeys())
}
allKeys = Utils.Dedup(allKeys);
allKeys = Utils.Dedup(allKeys)
if (allKeys.length > 1 && !allHaveIfNot) {
throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping`
}
}
}
@ -296,17 +331,24 @@ export default class TagRenderingConfig {
* tr.if // => new Tag("a","b")
* tr.priorityIf // => new Tag("_country","be")
*/
public static ExtractMapping(mapping: MappingConfigJson, i: number, translationKey: string,
context: string,
multiAnswer?: boolean, isQuestionable?: boolean, commonSize: string = "small") {
public static ExtractMapping(
mapping: MappingConfigJson,
i: number,
translationKey: string,
context: string,
multiAnswer?: boolean,
isQuestionable?: boolean,
commonSize: string = "small"
) {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
}
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
mapping
)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
}
@ -315,7 +357,9 @@ export default class TagRenderingConfig {
}
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
mapping
)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
@ -325,18 +369,23 @@ export default class TagRenderingConfig {
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
}
let hideInAnswer: boolean | TagsFilter = false;
let hideInAnswer: boolean | TagsFilter = false
if (typeof mapping.hideInAnswer === "boolean") {
hideInAnswer = mapping.hideInAnswer;
hideInAnswer = mapping.hideInAnswer
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
hideInAnswer = TagUtils.Tag(
mapping.hideInAnswer,
`${context}.mapping[${i}].hideInAnswer`
)
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
)
if (hideInAnswer === true && addExtraTags.length > 0) {
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
}
let icon = undefined;
let icon = undefined
let iconClass = commonSize
if (mapping.icon !== undefined) {
if (typeof mapping.icon === "string" && mapping.icon !== "") {
@ -346,18 +395,22 @@ export default class TagRenderingConfig {
iconClass = mapping.icon["class"] ?? iconClass
}
}
const prioritySearch = mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined;
const prioritySearch =
mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined
const mp = <Mapping>{
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
ifnot:
mapping.ifnot !== undefined
? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`)
: undefined,
then: Translations.T(mapping.then, `${ctx}.then`),
hideInAnswer,
icon,
iconClass,
addExtraTags,
searchTerms: mapping.searchTerms,
priorityIf: prioritySearch
};
priorityIf: prioritySearch,
}
if (isQuestionable) {
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
@ -368,7 +421,7 @@ export default class TagRenderingConfig {
}
}
return mp;
return mp
}
/**
@ -376,15 +429,14 @@ export default class TagRenderingConfig {
* @constructor
*/
public IsKnown(tags: Record<string, string>): boolean {
if (this.condition &&
!this.condition.matchesProperties(tags)) {
if (this.condition && !this.condition.matchesProperties(tags)) {
// Filtered away by the condition, so it is kindof known
return true;
return true
}
if (this.multiAnswer) {
for (const m of this.mappings ?? []) {
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
return true;
return true
}
}
@ -394,15 +446,14 @@ export default class TagRenderingConfig {
return value !== undefined && value !== ""
}
return false
}
if (this.GetRenderValue(tags) !== undefined) {
// This value is known and can be rendered
return true;
return true
}
return false;
return false
}
/**
@ -411,39 +462,49 @@ export default class TagRenderingConfig {
* @param tags
* @constructor
*/
public GetRenderValues(tags: Record<string, string>): { then: Translation, icon?: string, iconClass?: string }[] {
public GetRenderValues(
tags: Record<string, string>
): { then: Translation; icon?: string; iconClass?: string }[] {
if (!this.multiAnswer) {
return [this.GetRenderValueWithImage(tags)]
}
// A flag to check that the freeform key isn't matched multiple times
// A flag to check that the freeform key isn't matched multiple times
// If it is undefined, it is "used" already, or at least we don't have to check for it anymore
let freeformKeyDefined = this.freeform?.key !== undefined;
let freeformKeyDefined = this.freeform?.key !== undefined
let usedFreeformValues = new Set<string>()
// We run over all the mappings first, to check if the mapping matches
const applicableMappings: { then: TypedTranslation<Record<string, string>>, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
if (mapping.if === undefined) {
return mapping;
}
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) {
// THe freeform key is defined: what value does it use though?
// We mark the value to see if we have any leftovers
const value = mapping.if.asChange({}).find(kv => kv.k === this.freeform.key).v
usedFreeformValues.add(value)
const applicableMappings: {
then: TypedTranslation<Record<string, string>>
img?: string
}[] = Utils.NoNull(
(this.mappings ?? [])?.map((mapping) => {
if (mapping.if === undefined) {
return mapping
}
return mapping;
}
return undefined;
}))
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) {
// THe freeform key is defined: what value does it use though?
// We mark the value to see if we have any leftovers
const value = mapping.if
.asChange({})
.find((kv) => kv.k === this.freeform.key).v
usedFreeformValues.add(value)
}
return mapping
}
return undefined
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
const freeformValues = tags[this.freeform.key].split(";")
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
const leftovers = freeformValues.filter((v) => !usedFreeformValues.has(v))
for (const leftover of leftovers) {
applicableMappings.push({
then:
new TypedTranslation<object>(this.render.replace("{" + this.freeform.key + "}", leftover).translations)
then: new TypedTranslation<object>(
this.render.replace("{" + this.freeform.key + "}", leftover).translations
),
})
}
}
@ -451,7 +512,10 @@ export default class TagRenderingConfig {
return applicableMappings
}
public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation<any> | undefined {
public GetRenderValue(
tags: any,
defltValue: any = undefined
): TypedTranslation<any> | undefined {
return this.GetRenderValueWithImage(tags, defltValue)?.then
}
@ -460,7 +524,10 @@ export default class TagRenderingConfig {
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
* @constructor
*/
public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation<any>, icon?: string } | undefined {
public GetRenderValueWithImage(
tags: any,
defltValue: any = undefined
): { then: TypedTranslation<any>; icon?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
return undefined
@ -470,22 +537,23 @@ export default class TagRenderingConfig {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {
return mapping;
return mapping
}
if (mapping.if.matchesProperties(tags)) {
return mapping;
return mapping
}
}
}
if (this.id === "questions" ||
if (
this.id === "questions" ||
this.freeform?.key === undefined ||
tags[this.freeform.key] !== undefined
) {
return {then: this.render}
return { then: this.render }
}
return {then: defltValue};
return { then: defltValue }
}
/**
@ -498,52 +566,57 @@ export default class TagRenderingConfig {
const translations: Translation[] = []
for (const key in this) {
if (!this.hasOwnProperty(key)) {
continue;
continue
}
const o = this[key]
if (o instanceof Translation) {
translations.push(o)
}
}
return translations;
return translations
}
FreeformValues(): { key: string, type?: string, values?: string [] } {
FreeformValues(): { key: string; type?: string; values?: string[] } {
try {
const key = this.freeform?.key
const answerMappings = this.mappings?.filter(m => m.hideInAnswer !== true)
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) {
let values: { k: string, v: string }[][] = Utils.NoNull(answerMappings?.map(m => m.if.asChange({})) ?? [])
let values: { k: string; v: string }[][] = Utils.NoNull(
answerMappings?.map((m) => m.if.asChange({})) ?? []
)
if (values.length === 0) {
return;
return
}
const allKeys = values.map(arr => arr.map(o => o.k))
let common = allKeys[0];
const allKeys = values.map((arr) => arr.map((o) => o.k))
let common = allKeys[0]
for (const keyset of allKeys) {
common = common.filter(item => keyset.indexOf(item) >= 0)
common = common.filter((item) => keyset.indexOf(item) >= 0)
}
const commonKey = common[0]
if (commonKey === undefined) {
return undefined;
return undefined
}
return {
key: commonKey,
values: Utils.NoNull(values.map(arr => arr.filter(item => item.k === commonKey)[0]?.v))
values: Utils.NoNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
),
}
}
let values = Utils.NoNull(answerMappings?.map(m => m.if.asChange({}).filter(item => item.k === key)[0]?.v) ?? [])
let values = Utils.NoNull(
answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? []
)
if (values.length === undefined) {
values = undefined
}
return {
key,
type: this.freeform.type,
values
values,
}
} catch (e) {
console.error("Could not create FreeformValues for tagrendering", this.id)
@ -552,80 +625,93 @@ export default class TagRenderingConfig {
}
GenerateDocumentation(): BaseUIElement {
let withRender: (BaseUIElement | string)[] = [];
let withRender: (BaseUIElement | string)[] = []
if (this.freeform?.key !== undefined) {
withRender = [
`This rendering asks information about the property `,
Link.OsmWiki(this.freeform.key),
new Paragraph(new Combine([
"This is rendered with ",
new FixedUiElement(this.render.txt).SetClass("literalcode bold")
]))
new Paragraph(
new Combine([
"This is rendered with ",
new FixedUiElement(this.render.txt).SetClass("literalcode bold"),
])
),
]
}
let mappings: BaseUIElement = undefined;
let mappings: BaseUIElement = undefined
if (this.mappings !== undefined) {
mappings = new List(
[].concat(...this.mappings.map(m => {
[].concat(
...this.mappings.map((m) => {
const msgs: (string | BaseUIElement)[] = [
new Combine(
[
new FixedUiElement(m.then.txt).SetClass("bold"),
" corresponds with ",
new FixedUiElement( m.if.asHumanString(true, false, {})).SetClass("code")
]
)
new Combine([
new FixedUiElement(m.then.txt).SetClass("bold"),
" corresponds with ",
new FixedUiElement(m.if.asHumanString(true, false, {})).SetClass(
"code"
),
]),
]
if (m.hideInAnswer === true) {
msgs.push(new FixedUiElement("This option cannot be chosen as answer").SetClass("italic"))
msgs.push(
new FixedUiElement(
"This option cannot be chosen as answer"
).SetClass("italic")
)
}
if (m.ifnot !== undefined) {
msgs.push("Unselecting this answer will add " + m.ifnot.asHumanString(true, false, {}))
msgs.push(
"Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {})
)
}
return msgs;
}
))
return msgs
})
)
)
}
let condition: BaseUIElement = undefined
if (this.condition !== undefined && !this.condition?.matchesProperties({})) {
condition = new Combine(["Only visible if ",
new FixedUiElement(this.condition.asHumanString(false, false, {})
).SetClass("code")
, " is shown"])
condition = new Combine([
"Only visible if ",
new FixedUiElement(this.condition.asHumanString(false, false, {})).SetClass("code"),
" is shown",
])
}
let group: BaseUIElement = undefined
if (this.group !== undefined && this.group !== "") {
group = new Combine([
"This tagrendering is part of group ", new FixedUiElement(this.group).SetClass("code")
"This tagrendering is part of group ",
new FixedUiElement(this.group).SetClass("code"),
])
}
let labels: BaseUIElement = undefined
if (this.labels?.length > 0) {
labels = new Combine([
"This tagrendering has labels ",
...this.labels.map(label => new FixedUiElement(label).SetClass("code"))
...this.labels.map((label) => new FixedUiElement(label).SetClass("code")),
]).SetClass("flex")
}
return new Combine([
new Title(this.id, 3),
this.description,
this.question !== undefined ?
new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("font-bold bold")]) :
new FixedUiElement(
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
this.question !== undefined
? new Combine([
"The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
])
: new FixedUiElement(
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
new Combine(withRender),
mappings,
condition,
group,
labels
]).SetClass("flex flex-col");
labels,
]).SetClass("flex flex-col")
}
}
}

View file

@ -1,6 +1,6 @@
import TilesourceConfigJson from "./Json/TilesourceConfigJson";
import Translations from "../../UI/i18n/Translations";
import {Translation} from "../../UI/i18n/Translation";
import TilesourceConfigJson from "./Json/TilesourceConfigJson"
import Translations from "../../UI/i18n/Translations"
import { Translation } from "../../UI/i18n/Translation"
export default class TilesourceConfig {
public readonly source: string
@ -9,21 +9,23 @@ export default class TilesourceConfig {
public readonly name: Translation
public readonly minzoom: number
public readonly maxzoom: number
public readonly defaultState: boolean;
public readonly defaultState: boolean
constructor(config: TilesourceConfigJson, ctx: string = "") {
this.id = config.id
this.source = config.source;
this.isOverlay = config.isOverlay ?? false;
this.source = config.source
this.isOverlay = config.isOverlay ?? false
this.name = Translations.T(config.name)
this.minzoom = config.minZoom ?? 0
this.maxzoom = config.maxZoom ?? 999
this.defaultState = config.defaultState ?? true;
this.defaultState = config.defaultState ?? true
if (this.id === undefined) {
throw "An id is obligated"
}
if (this.minzoom > this.maxzoom) {
throw "Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")"
throw (
"Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")"
)
}
if (this.minzoom < 0) {
throw "minzoom should be > 0 (at " + ctx + ")"
@ -38,5 +40,4 @@ export default class TilesourceConfig {
throw "Disabling an overlay without a name is not possible"
}
}
}
}

View file

@ -1,14 +1,14 @@
import TagRenderingConfig from "./TagRenderingConfig";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import TagRenderingConfig from "./TagRenderingConfig"
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
export default class WithContextLoader {
protected readonly _context: string;
private readonly _json: any;
protected readonly _context: string
private readonly _json: any
constructor(json: any, context: string) {
this._json = json;
this._context = context;
this._json = json
this._context = context
}
/** Given a key, gets the corresponding property from the json (or the default if not found
@ -16,26 +16,20 @@ export default class WithContextLoader {
* The found value is interpreted as a tagrendering and fetched/parsed
* */
public tr(key: string, deflt) {
const v = this._json[key];
const v = this._json[key]
if (v === undefined || v === null) {
if (deflt === undefined) {
return undefined;
return undefined
}
return new TagRenderingConfig(
deflt,
`${this._context}.${key}.default value`
);
return new TagRenderingConfig(deflt, `${this._context}.${key}.default value`)
}
if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering.get(v);
const shared = SharedTagRenderings.SharedTagRendering.get(v)
if (shared) {
return shared;
return shared
}
}
return new TagRenderingConfig(
v,
`${this._context}.${key}`
);
return new TagRenderingConfig(v, `${this._context}.${key}`)
}
/**
@ -48,27 +42,29 @@ export default class WithContextLoader {
/**
* Throw an error if 'question' is defined
*/
readOnlyMode?: boolean,
readOnlyMode?: boolean
requiresId?: boolean
prepConfig?: ((config: TagRenderingConfigJson) => TagRenderingConfigJson)
prepConfig?: (config: TagRenderingConfigJson) => TagRenderingConfigJson
}
): TagRenderingConfig[] {
if (tagRenderings === undefined) {
return [];
return []
}
const context = this._context
options = options ?? {}
if (options.prepConfig === undefined) {
options.prepConfig = c => c
options.prepConfig = (c) => c
}
const renderings: TagRenderingConfig[] = []
for (let i = 0; i < tagRenderings.length; i++) {
const preparedConfig = tagRenderings[i];
const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`);
const preparedConfig = tagRenderings[i]
const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`)
if (options.readOnlyMode && tr.question !== undefined) {
throw "A question is defined for " + `${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label`
throw (
"A question is defined for " +
`${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label`
)
}
if (options.requiresId && tr.id === "") {
throw `${context}.tagrendering[${i}] has an invalid ID - make sure it is defined and not empty`
@ -77,6 +73,6 @@ export default class WithContextLoader {
renderings.push(tr)
}
return renderings;
return renderings
}
}
}