forked from MapComplete/MapComplete
Finish importer, add applicable import layers to every theme by default
This commit is contained in:
parent
3402ac0954
commit
ca1490902c
41 changed files with 1559 additions and 898 deletions
171
Models/ThemeConfig/Conversion/Conversion.ts
Normal file
171
Models/ThemeConfig/Conversion/Conversion.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
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>
|
||||
}
|
||||
|
||||
export abstract class Conversion<TIn, TOut> {
|
||||
public readonly modifiedAttributes: string[];
|
||||
protected readonly doc: string;
|
||||
|
||||
constructor(doc: string, modifiedAttributes: string[] = []) {
|
||||
this.modifiedAttributes = modifiedAttributes;
|
||||
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
|
||||
}
|
||||
|
||||
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
|
||||
if (fixed?.errors?.length > 0) {
|
||||
throw fixed.errors.join("\n");
|
||||
}
|
||||
fixed.warnings?.forEach(w => console.warn(w))
|
||||
return fixed.result;
|
||||
}
|
||||
|
||||
public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut {
|
||||
const fixed = this.convert(state, json, context)
|
||||
return DesugaringStep.strict(fixed)
|
||||
}
|
||||
|
||||
abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] }
|
||||
|
||||
public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } {
|
||||
const result = []
|
||||
const errors = []
|
||||
const warnings = []
|
||||
for (let i = 0; i < jsons.length; i++) {
|
||||
const json = jsons[i];
|
||||
const r = this.convert(state, json, context + "[" + i + "]")
|
||||
result.push(r.result)
|
||||
errors.push(...r.errors)
|
||||
warnings.push(...r.warnings)
|
||||
}
|
||||
return {
|
||||
result,
|
||||
errors,
|
||||
warnings
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class DesugaringStep<T> extends Conversion<T, T> {
|
||||
}
|
||||
|
||||
export class OnEvery<X, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
private readonly step: DesugaringStep<X>;
|
||||
|
||||
constructor(key: string, step: DesugaringStep<X>) {
|
||||
super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]);
|
||||
this.step = step;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
json = {...json}
|
||||
const step = this.step
|
||||
const key = this.key;
|
||||
const r = step.convertAll(state, (<X[]>json[key]), context + "." + key)
|
||||
json[key] = r.result
|
||||
return {
|
||||
result: json,
|
||||
errors: r.errors,
|
||||
warnings: r.warnings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class OnEveryConcat<X, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
private readonly step: Conversion<X, X[]>;
|
||||
|
||||
constructor(key: string, step: Conversion<X, X[]>) {
|
||||
super(`Applies ${step.constructor.name} onto every object of the list \`${key}\`. The results are concatenated and used as new list`, [key]);
|
||||
this.step = step;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
json = {...json}
|
||||
const step = this.step
|
||||
const key = this.key;
|
||||
const values = json[key]
|
||||
if (values === undefined) {
|
||||
// Move on - nothing to see here!
|
||||
return {
|
||||
result: json,
|
||||
errors: [],
|
||||
warnings: []
|
||||
}
|
||||
}
|
||||
const r = step.convertAll(state, (<X[]>values), context + "." + key)
|
||||
const vals: X[][] = r.result
|
||||
json[key] = [].concat(...vals)
|
||||
return {
|
||||
result: json,
|
||||
errors: r.errors,
|
||||
warnings: r.warnings
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class Fuse<T> extends 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.constructor.name).join(", "),
|
||||
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes)))
|
||||
);
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
for (let i = 0; i < this.steps.length; i++) {
|
||||
const step = this.steps[i];
|
||||
let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")")
|
||||
errors.push(...r.errors)
|
||||
warnings.push(...r.warnings)
|
||||
json = r.result
|
||||
if (errors.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class SetDefault<T> extends DesugaringStep<T> {
|
||||
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");
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this._overrideEmptyString = overrideEmptyString;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
|
||||
json = {...json}
|
||||
json[this.key] = this.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [], warnings: [],
|
||||
result: json
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,29 +1,69 @@
|
|||
import {Conversion, DesugaringContext} from "./LegacyJsonConvert";
|
||||
import {Conversion, DesugaringContext} from "./Conversion";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
|
||||
|
||||
export default class CreateNoteImportLayer extends Conversion<LayerConfig, LayerConfigJson> {
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
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"), [])
|
||||
this._includeClosedNotesDays = includeClosedNotesDays;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, layer: LayerConfig, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
convert(state: DesugaringContext, layerJson: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const t = Translations.t.importLayer;
|
||||
|
||||
const possibleTags: TagsFilter[] = layer.presets.map(p => new And(p.tags))
|
||||
/**
|
||||
* The note itself will contain `tags=k=v;k=v;k=v;...
|
||||
* This must be matched with a regex.
|
||||
* This is a simple JSON-object as how it'll be put into the layerConfigJson directly
|
||||
*/
|
||||
const isShownIfAny : any[] = []
|
||||
const layer = new LayerConfig(layerJson, "while constructing a note-import layer")
|
||||
for (const preset of layer.presets) {
|
||||
const mustMatchAll = []
|
||||
for (const tag of preset.tags) {
|
||||
const key = tag.key
|
||||
const value = tag.value
|
||||
const condition = "_tags~(^|.*;)"+key+"\="+value+"($|;.*)"
|
||||
mustMatchAll.push(condition)
|
||||
}
|
||||
isShownIfAny.push({and:mustMatchAll})
|
||||
}
|
||||
|
||||
const pointRenderings = (layerJson.mapRendering??[]).filter(r => r!== null && r["location"] !== undefined);
|
||||
const firstRender = <PointRenderingConfigJson>(pointRenderings [0])
|
||||
const icon = firstRender.icon
|
||||
const iconBadges = []
|
||||
if(icon !== undefined){
|
||||
iconBadges.push({
|
||||
if: {and:[]},
|
||||
then:icon
|
||||
})
|
||||
}
|
||||
|
||||
const importButton = {}
|
||||
{
|
||||
const translations = t.importButton.Subs({layerId: layer.id, title: layer.presets[0].title}).translations
|
||||
for (const key in translations) {
|
||||
importButton[key] = "{"+translations[key]+"}"
|
||||
}
|
||||
}
|
||||
|
||||
const result : LayerConfigJson = {
|
||||
"id": "note_import_"+layer.id,
|
||||
"name": t.layerName.Subs({title: layer.title.render}).translations,
|
||||
// By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
|
||||
"description": t.description.Subs({title: layer.title.render}).translations,
|
||||
"source": {
|
||||
"osmTags": {
|
||||
|
@ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
"id~*"
|
||||
]
|
||||
},
|
||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"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": 10,
|
||||
"minzoom": 12,
|
||||
"title": {
|
||||
"render": t.popupTitle.Subs({title: layer.presets[0].title}).translations
|
||||
},
|
||||
"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] })()",
|
||||
"_intro:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.map(l => l == '' ? '<br/>' : l).join('');})()",
|
||||
"_tags:=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
|
||||
"_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.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
|
||||
"_tags=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
|
||||
],
|
||||
"isShown": {
|
||||
"render": "no",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "comments!~.*https://mapcomplete.osm.be.*",
|
||||
"then":"no"
|
||||
},
|
||||
{
|
||||
"if": {and:
|
||||
["_trigger_index~*",
|
||||
{or: possibleTags.map(tf => tf.AsJson())}
|
||||
{or: isShownIfAny}
|
||||
]},
|
||||
"then": "yes"
|
||||
}
|
||||
|
@ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "conversation",
|
||||
"render": "{visualize_note_comments(comments,1)}"
|
||||
},
|
||||
{
|
||||
"id": "Intro",
|
||||
"render": "{_intro}"
|
||||
},
|
||||
{
|
||||
"id": "conversation",
|
||||
"render": "{visualize_note_comments(comments,1)}",
|
||||
condition: "_comments_count>1"
|
||||
},
|
||||
{
|
||||
"id": "import",
|
||||
"render": "{import_button(public_bookcase, _tags, There might be a public bookcase here,./assets/svg/addSmall.svg,,,id)}"
|
||||
"render": importButton,
|
||||
condition: "closed_at="
|
||||
},
|
||||
{
|
||||
"id": "close_note_",
|
||||
"render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}"
|
||||
"render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}",
|
||||
condition: "closed_at="
|
||||
},
|
||||
{
|
||||
"id": "close_note_mapped",
|
||||
"render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}"
|
||||
"render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}",
|
||||
condition: "closed_at="
|
||||
},
|
||||
{
|
||||
"id": "handled",
|
||||
"render": t.importHandled.translations,
|
||||
condition: "closed_at~*"
|
||||
},
|
||||
{
|
||||
"id": "comment",
|
||||
|
@ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
{
|
||||
"id": "add_image",
|
||||
"render": "{add_image_to_note()}"
|
||||
},
|
||||
{
|
||||
id:"alltags",
|
||||
render:"{all_tags()}"
|
||||
}
|
||||
],
|
||||
"mapRendering": [
|
||||
|
@ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
"centroid"
|
||||
],
|
||||
"icon": {
|
||||
"render": "teardrop:#3333cc"
|
||||
"render": "circle:white;help:black",
|
||||
mappings:[{
|
||||
if: {or:["closed_at~*","_imported=yes"]},
|
||||
then:"circle:white;checkmark:black"
|
||||
}]
|
||||
},
|
||||
"iconSize": "40,40,bottom"
|
||||
iconBadges,
|
||||
"iconSize": "40,40,center"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,439 +1,12 @@
|
|||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import DependencyCalculator from "../DependencyCalculator";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import LayoutConfig from "../LayoutConfig";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import Constants from "../../Constants";
|
||||
import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts";
|
||||
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
|
||||
|
||||
export interface DesugaringContext {
|
||||
tagRenderings: Map<string, TagRenderingConfigJson>
|
||||
sharedLayers: Map<string, LayerConfigJson>
|
||||
}
|
||||
|
||||
export abstract class Conversion<TIn, TOut> {
|
||||
public readonly modifiedAttributes: string[];
|
||||
protected readonly doc: string;
|
||||
|
||||
constructor(doc: string, modifiedAttributes: string[] = []) {
|
||||
this.modifiedAttributes = modifiedAttributes;
|
||||
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
|
||||
}
|
||||
|
||||
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
|
||||
if (fixed?.errors?.length > 0) {
|
||||
throw fixed.errors.join("\n");
|
||||
}
|
||||
fixed.warnings?.forEach(w => console.warn(w))
|
||||
return fixed.result;
|
||||
}
|
||||
|
||||
public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut {
|
||||
const fixed = this.convert(state, json, context)
|
||||
return DesugaringStep.strict(fixed)
|
||||
}
|
||||
|
||||
abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] }
|
||||
|
||||
public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } {
|
||||
const result = []
|
||||
const errors = []
|
||||
const warnings = []
|
||||
for (let i = 0; i < jsons.length; i++) {
|
||||
const json = jsons[i];
|
||||
const r = this.convert(state, json, context + "[" + i + "]")
|
||||
result.push(r.result)
|
||||
errors.push(...r.errors)
|
||||
warnings.push(...r.warnings)
|
||||
}
|
||||
return {
|
||||
result,
|
||||
errors,
|
||||
warnings
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class DesugaringStep<T> extends Conversion<T, T> {
|
||||
}
|
||||
|
||||
class OnEvery<X, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
private readonly step: DesugaringStep<X>;
|
||||
|
||||
constructor(key: string, step: DesugaringStep<X>) {
|
||||
super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]);
|
||||
this.step = step;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
json = {...json}
|
||||
const step = this.step
|
||||
const key = this.key;
|
||||
const r = step.convertAll(state, (<X[]>json[key]), context + "." + key)
|
||||
json[key] = r.result
|
||||
return {
|
||||
result: json,
|
||||
errors: r.errors,
|
||||
warnings: r.warnings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OnEveryConcat<X, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
private readonly step: Conversion<X, X[]>;
|
||||
|
||||
constructor(key: string, step: Conversion<X, X[]>) {
|
||||
super(`Applies ${step.constructor.name} onto every object of the list \`${key}\``, [key]);
|
||||
this.step = step;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
json = {...json}
|
||||
const step = this.step
|
||||
const key = this.key;
|
||||
const values = json[key]
|
||||
if (values === undefined) {
|
||||
// Move on - nothing to see here!
|
||||
return {
|
||||
result: json,
|
||||
errors: [],
|
||||
warnings: []
|
||||
}
|
||||
}
|
||||
const r = step.convertAll(state, (<X[]>values), context + "." + key)
|
||||
const vals: X[][] = r.result
|
||||
json[key] = [].concat(...vals)
|
||||
return {
|
||||
result: json,
|
||||
errors: r.errors,
|
||||
warnings: r.warnings
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Fuse<T> extends 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.constructor.name).join(", "),
|
||||
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes)))
|
||||
);
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
for (let i = 0; i < this.steps.length; i++) {
|
||||
const step = this.steps[i];
|
||||
let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")")
|
||||
errors.push(...r.errors)
|
||||
warnings.push(...r.warnings)
|
||||
json = r.result
|
||||
if (errors.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this tag rendering has a minimap in some language.
|
||||
* Note: this minimap can be hidden by conditions
|
||||
*/
|
||||
private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
|
||||
const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
|
||||
for (const translation of translations) {
|
||||
for (const key in translation.translations) {
|
||||
if (!translation.translations.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
const template = translation.translations[key]
|
||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||
if (hasMiniMap) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
|
||||
|
||||
|
||||
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson> tr)) ?? true
|
||||
if (!hasMinimap) {
|
||||
layerConfig = {...layerConfig}
|
||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
|
||||
}
|
||||
|
||||
return {
|
||||
errors:[],
|
||||
warnings: [],
|
||||
result: layerConfig
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
|
||||
constructor() {
|
||||
super("Converts a tagRenderingSpec into the full tagRendering", []);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
|
||||
return {
|
||||
result: this.convertUntilStable(state, json, warnings, errors, context),
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] {
|
||||
if (state.tagRenderings.has(name)) {
|
||||
return [state.tagRenderings.get(name)]
|
||||
}
|
||||
if (name.indexOf(".") >= 0) {
|
||||
const spl = name.split(".");
|
||||
const layer = state.sharedLayers.get(spl[0])
|
||||
if (spl.length === 2 && layer !== undefined) {
|
||||
const id = spl[1];
|
||||
|
||||
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_)
|
||||
} else {
|
||||
matchingTrs = layerTrs.filter(tr => tr.id === id)
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < matchingTrs.length; i++) {
|
||||
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
|
||||
const found = Utils.Clone(matchingTrs[i]);
|
||||
if (found.condition === undefined) {
|
||||
found.condition = layer.source.osmTags
|
||||
} else {
|
||||
found.condition = {and: [found.condition, layer.source.osmTags]}
|
||||
}
|
||||
matchingTrs[i] = found
|
||||
}
|
||||
|
||||
if (matchingTrs.length !== 0) {
|
||||
return matchingTrs
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
if (tr === "questions") {
|
||||
return [{
|
||||
id: "questions"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
if (typeof tr === "string") {
|
||||
const lookup = this.lookup(state, tr);
|
||||
if (lookup !== undefined) {
|
||||
return lookup
|
||||
}
|
||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
||||
return [{
|
||||
render: tr,
|
||||
id: tr.replace(/![a-zA-Z0-9]/g, "")
|
||||
}]
|
||||
}
|
||||
|
||||
if (tr["builtin"] !== undefined) {
|
||||
let names = tr["builtin"]
|
||||
if (typeof names === "string") {
|
||||
names = [names]
|
||||
}
|
||||
|
||||
for (const key of Object.keys(tr)) {
|
||||
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))
|
||||
}
|
||||
|
||||
const trs: TagRenderingConfigJson[] = []
|
||||
for (const name of names) {
|
||||
const lookup = this.lookup(state, name)
|
||||
if (lookup === undefined) {
|
||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
|
||||
continue
|
||||
}
|
||||
for (let foundTr of lookup) {
|
||||
foundTr = Utils.Clone<any>(foundTr)
|
||||
Utils.Merge(tr["override"] ?? {}, foundTr)
|
||||
trs.push(foundTr)
|
||||
}
|
||||
}
|
||||
return trs;
|
||||
}
|
||||
|
||||
return [tr]
|
||||
}
|
||||
|
||||
private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(state, spec, warnings, errors, ctx);
|
||||
|
||||
const result = []
|
||||
for (const tr of trs) {
|
||||
if (tr["builtin"] !== undefined) {
|
||||
const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
|
||||
result.push(...stable)
|
||||
} else {
|
||||
result.push(tr)
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandGroupRewrite extends Conversion<{
|
||||
rewrite: {
|
||||
sourceString: string,
|
||||
into: string[]
|
||||
}[],
|
||||
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
|
||||
} | TagRenderingConfigJson, TagRenderingConfigJson[]> {
|
||||
|
||||
|
||||
private static expandSubTagRenderings = new ExpandTagRendering()
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"Converts a rewrite config for tagRenderings into the expanded form"
|
||||
);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json:
|
||||
{
|
||||
rewrite:
|
||||
{ sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
||||
} | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
|
||||
if (json["rewrite"] === undefined) {
|
||||
return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []}
|
||||
}
|
||||
let config = <{
|
||||
rewrite:
|
||||
{ sourceString: string; into: string[] }[];
|
||||
renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
||||
}>json;
|
||||
|
||||
|
||||
const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context);
|
||||
const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result);
|
||||
const errors = subRenderingsRes.errors;
|
||||
const warnings = subRenderingsRes.warnings;
|
||||
|
||||
|
||||
const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>()
|
||||
|
||||
// The actual rewriting
|
||||
for (const rewrite of config.rewrite) {
|
||||
const source = rewrite.sourceString;
|
||||
for (const target of rewrite.into) {
|
||||
const groupName = target;
|
||||
const trs: TagRenderingConfigJson[] = []
|
||||
|
||||
for (const tr of subRenderings) {
|
||||
trs.push(this.prepConfig(source, target, tr))
|
||||
}
|
||||
if (rewrittenPerGroup.has(groupName)) {
|
||||
rewrittenPerGroup.get(groupName).push(...trs)
|
||||
|
||||
} else {
|
||||
rewrittenPerGroup.set(groupName, trs)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add questions box for this category
|
||||
rewrittenPerGroup.forEach((group, groupName) => {
|
||||
group.push(<TagRenderingConfigJson>{
|
||||
id: "questions",
|
||||
group: groupName
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
rewrittenPerGroup.forEach((group, _) => {
|
||||
group.forEach(tr => {
|
||||
if (tr.id === undefined || tr.id === "") {
|
||||
errors.push("A tagrendering has an empty ID after expanding the tag")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
result: [].concat(...Array.from(rewrittenPerGroup.values())),
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
/* Used for left|right group creation and replacement */
|
||||
private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
|
||||
|
||||
function replaceRecursive(transl: string | any) {
|
||||
if (typeof transl === "string") {
|
||||
return transl.replace(keyToRewrite, target)
|
||||
}
|
||||
if (transl.map !== undefined) {
|
||||
return transl.map(o => replaceRecursive(o))
|
||||
}
|
||||
transl = {...transl}
|
||||
for (const key in transl) {
|
||||
transl[key] = replaceRecursive(transl[key])
|
||||
}
|
||||
return transl
|
||||
}
|
||||
|
||||
const orig = tr;
|
||||
tr = replaceRecursive(tr)
|
||||
|
||||
tr.id = target + "-" + orig.id
|
||||
tr.group = target
|
||||
return tr
|
||||
}
|
||||
}
|
||||
import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion";
|
||||
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
|
||||
|
@ -822,235 +395,3 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
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)", ["layers"]);
|
||||
}
|
||||
|
||||
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] {
|
||||
const dependenciesToAdd: LayerConfigJson[] = []
|
||||
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 }[] = []
|
||||
do {
|
||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
|
||||
for (const layerConfig of alreadyLoaded) {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
|
||||
dependencies.push(...layerDeps)
|
||||
}
|
||||
|
||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
||||
// Their existance is checked elsewhere, so this is fine
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
|
||||
continue
|
||||
}
|
||||
const dep = allKnownLayers.get(unmetDependency.neededLayer)
|
||||
if (dep === undefined) {
|
||||
const message =
|
||||
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
|
||||
"This layer is needed by " + unmetDependency.neededBy,
|
||||
unmetDependency.reason + " (at " + unmetDependency.context + ")",
|
||||
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
|
||||
|
||||
]
|
||||
throw message.join("\n\t");
|
||||
}
|
||||
dependenciesToAdd.unshift(dep)
|
||||
loadedLayerIds.add(dep.id);
|
||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
||||
}
|
||||
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
return dependenciesToAdd;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
|
||||
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const layers: LayerConfigJson[] = <LayerConfigJson[]> theme.layers; // Layers should be expanded at this point
|
||||
|
||||
knownTagRenderings.forEach((value, key) => {
|
||||
value.id = key;
|
||||
})
|
||||
|
||||
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
|
||||
if (dependencies.length > 0) {
|
||||
|
||||
warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed")
|
||||
}
|
||||
layers.unshift(...dependencies);
|
||||
|
||||
return {
|
||||
result: {
|
||||
...theme,
|
||||
layers: layers
|
||||
},
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SetDefault<T> extends DesugaringStep<T> {
|
||||
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");
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this._overrideEmptyString = overrideEmptyString;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
||||
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
|
||||
json = {...json}
|
||||
json[this.key] = this.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [], warnings: [],
|
||||
result: json
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()),
|
||||
new OnEveryConcat("tagRenderings", new ExpandTagRendering()),
|
||||
new SetDefault("titleIcons", ["defaults"]),
|
||||
new OnEveryConcat("titleIcons", new ExpandTagRendering())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
|
||||
constructor() {
|
||||
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
if (typeof json === "string") {
|
||||
const found = state.sharedLayers.get(json)
|
||||
if (found === undefined) {
|
||||
return {
|
||||
result: null,
|
||||
errors: [context + ": The layer with name " + json + " was not found as a builtin layer"],
|
||||
warnings
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: [found],
|
||||
errors, warnings
|
||||
}
|
||||
}
|
||||
|
||||
if (json["builtin"] !== undefined) {
|
||||
let names = json["builtin"]
|
||||
if (typeof names === "string") {
|
||||
names = [names]
|
||||
}
|
||||
const layers = []
|
||||
for (const name of names) {
|
||||
const found = Utils.Clone(state.sharedLayers.get(name))
|
||||
if (found === undefined) {
|
||||
errors.push(context + ": The layer with name " + json + " was not found as a builtin layer")
|
||||
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.`)
|
||||
}
|
||||
try {
|
||||
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"],)}`)
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: layers,
|
||||
errors, warnings
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
result: [json],
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
json.layers = [...json.layers]
|
||||
|
||||
if (json.id === "personal") {
|
||||
json.layers = []
|
||||
for (const publicLayer of AllKnownLayouts.AllPublicLayers()) {
|
||||
const id = publicLayer.id
|
||||
const config = state.sharedLayers.get(id)
|
||||
if(Constants.added_by_default.indexOf(id) >= 0){
|
||||
continue;
|
||||
}
|
||||
if(config === undefined){
|
||||
// This is a layer which is coded within a public theme, not as separate .json
|
||||
continue
|
||||
}
|
||||
json.layers.push(config)
|
||||
}
|
||||
const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id)
|
||||
publicIds.map(id => state.sharedLayers.get(id))
|
||||
}
|
||||
|
||||
for (const layerName of Constants.added_by_default) {
|
||||
const v = state.sharedLayers.get(layerName)
|
||||
if (v === undefined) {
|
||||
errors.push("Default layer " + layerName + " not found")
|
||||
}
|
||||
json.layers.push(v)
|
||||
}
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Fully prepares and expands a theme",
|
||||
new OnEveryConcat("layers", new SubstituteLayer()),
|
||||
new SetDefault("socialImage", "assets/SocialImage.png", true),
|
||||
new AddDefaultLayers(),
|
||||
new AddDependencyLayersToTheme(),
|
||||
new OnEvery("layers", new PrepareLayer()),
|
||||
new OnEvery("layers", new AddMiniMap())
|
||||
);
|
||||
}
|
||||
}
|
252
Models/ThemeConfig/Conversion/PrepareLayer.ts
Normal file
252
Models/ThemeConfig/Conversion/PrepareLayer.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
import {Conversion, DesugaringContext, Fuse, OnEveryConcat, SetDefault} from "./Conversion";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
|
||||
constructor() {
|
||||
super("Converts a tagRenderingSpec into the full tagRendering", []);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
|
||||
return {
|
||||
result: this.convertUntilStable(state, json, warnings, errors, context),
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] {
|
||||
if (state.tagRenderings.has(name)) {
|
||||
return [state.tagRenderings.get(name)]
|
||||
}
|
||||
if (name.indexOf(".") >= 0) {
|
||||
const spl = name.split(".");
|
||||
const layer = state.sharedLayers.get(spl[0])
|
||||
if (spl.length === 2 && layer !== undefined) {
|
||||
const id = spl[1];
|
||||
|
||||
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_)
|
||||
} else {
|
||||
matchingTrs = layerTrs.filter(tr => tr.id === id)
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < matchingTrs.length; i++) {
|
||||
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
|
||||
const found = Utils.Clone(matchingTrs[i]);
|
||||
if (found.condition === undefined) {
|
||||
found.condition = layer.source.osmTags
|
||||
} else {
|
||||
found.condition = {and: [found.condition, layer.source.osmTags]}
|
||||
}
|
||||
matchingTrs[i] = found
|
||||
}
|
||||
|
||||
if (matchingTrs.length !== 0) {
|
||||
return matchingTrs
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
if (tr === "questions") {
|
||||
return [{
|
||||
id: "questions"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
if (typeof tr === "string") {
|
||||
const lookup = this.lookup(state, tr);
|
||||
if (lookup !== undefined) {
|
||||
return lookup
|
||||
}
|
||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
||||
return [{
|
||||
render: tr,
|
||||
id: tr.replace(/![a-zA-Z0-9]/g, "")
|
||||
}]
|
||||
}
|
||||
|
||||
if (tr["builtin"] !== undefined) {
|
||||
let names = tr["builtin"]
|
||||
if (typeof names === "string") {
|
||||
names = [names]
|
||||
}
|
||||
|
||||
for (const key of Object.keys(tr)) {
|
||||
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))
|
||||
}
|
||||
|
||||
const trs: TagRenderingConfigJson[] = []
|
||||
for (const name of names) {
|
||||
const lookup = this.lookup(state, name)
|
||||
if (lookup === undefined) {
|
||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
|
||||
continue
|
||||
}
|
||||
for (let foundTr of lookup) {
|
||||
foundTr = Utils.Clone<any>(foundTr)
|
||||
Utils.Merge(tr["override"] ?? {}, foundTr)
|
||||
trs.push(foundTr)
|
||||
}
|
||||
}
|
||||
return trs;
|
||||
}
|
||||
|
||||
return [tr]
|
||||
}
|
||||
|
||||
private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(state, spec, warnings, errors, ctx);
|
||||
|
||||
const result = []
|
||||
for (const tr of trs) {
|
||||
if (tr["builtin"] !== undefined) {
|
||||
const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
|
||||
result.push(...stable)
|
||||
} else {
|
||||
result.push(tr)
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandGroupRewrite extends Conversion<{
|
||||
rewrite: {
|
||||
sourceString: string,
|
||||
into: string[]
|
||||
}[],
|
||||
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
|
||||
} | TagRenderingConfigJson, TagRenderingConfigJson[]> {
|
||||
|
||||
|
||||
private static expandSubTagRenderings = new ExpandTagRendering()
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"Converts a rewrite config for tagRenderings into the expanded form"
|
||||
);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json:
|
||||
{
|
||||
rewrite:
|
||||
{ sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
||||
} | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
|
||||
if (json["rewrite"] === undefined) {
|
||||
return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []}
|
||||
}
|
||||
let config = <{
|
||||
rewrite:
|
||||
{ sourceString: string; into: string[] }[];
|
||||
renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
||||
}>json;
|
||||
|
||||
|
||||
const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context);
|
||||
const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result);
|
||||
const errors = subRenderingsRes.errors;
|
||||
const warnings = subRenderingsRes.warnings;
|
||||
|
||||
|
||||
const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>()
|
||||
|
||||
// The actual rewriting
|
||||
for (const rewrite of config.rewrite) {
|
||||
const source = rewrite.sourceString;
|
||||
for (const target of rewrite.into) {
|
||||
const groupName = target;
|
||||
const trs: TagRenderingConfigJson[] = []
|
||||
|
||||
for (const tr of subRenderings) {
|
||||
trs.push(this.prepConfig(source, target, tr))
|
||||
}
|
||||
if (rewrittenPerGroup.has(groupName)) {
|
||||
rewrittenPerGroup.get(groupName).push(...trs)
|
||||
|
||||
} else {
|
||||
rewrittenPerGroup.set(groupName, trs)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add questions box for this category
|
||||
rewrittenPerGroup.forEach((group, groupName) => {
|
||||
group.push(<TagRenderingConfigJson>{
|
||||
id: "questions",
|
||||
group: groupName
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
rewrittenPerGroup.forEach((group, _) => {
|
||||
group.forEach(tr => {
|
||||
if (tr.id === undefined || tr.id === "") {
|
||||
errors.push("A tagrendering has an empty ID after expanding the tag")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
result: [].concat(...Array.from(rewrittenPerGroup.values())),
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
/* Used for left|right group creation and replacement */
|
||||
private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
|
||||
|
||||
function replaceRecursive(transl: string | any) {
|
||||
if (typeof transl === "string") {
|
||||
return transl.replace(keyToRewrite, target)
|
||||
}
|
||||
if (transl.map !== undefined) {
|
||||
return transl.map(o => replaceRecursive(o))
|
||||
}
|
||||
transl = {...transl}
|
||||
for (const key in transl) {
|
||||
transl[key] = replaceRecursive(transl[key])
|
||||
}
|
||||
return transl
|
||||
}
|
||||
|
||||
const orig = tr;
|
||||
tr = replaceRecursive(tr)
|
||||
|
||||
tr.id = target + "-" + orig.id
|
||||
tr.group = target
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()),
|
||||
new OnEveryConcat("tagRenderings", new ExpandTagRendering()),
|
||||
new SetDefault("titleIcons", ["defaults"]),
|
||||
new OnEveryConcat("titleIcons", new ExpandTagRendering())
|
||||
);
|
||||
}
|
||||
}
|
316
Models/ThemeConfig/Conversion/PrepareTheme.ts
Normal file
316
Models/ThemeConfig/Conversion/PrepareTheme.ts
Normal file
|
@ -0,0 +1,316 @@
|
|||
import {Conversion, DesugaringContext, DesugaringStep, Fuse, OnEvery, OnEveryConcat, 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 {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts";
|
||||
import CreateNoteImportLayer from "./CreateNoteImportLayer";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
|
||||
import DependencyCalculator from "../DependencyCalculator";
|
||||
|
||||
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
|
||||
constructor() {
|
||||
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
if (typeof json === "string") {
|
||||
const found = state.sharedLayers.get(json)
|
||||
if (found === undefined) {
|
||||
return {
|
||||
result: null,
|
||||
errors: [context + ": The layer with name " + json + " was not found as a builtin layer"],
|
||||
warnings
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: [found],
|
||||
errors, warnings
|
||||
}
|
||||
}
|
||||
|
||||
if (json["builtin"] !== undefined) {
|
||||
let names = json["builtin"]
|
||||
if (typeof names === "string") {
|
||||
names = [names]
|
||||
}
|
||||
const layers = []
|
||||
for (const name of names) {
|
||||
const found = Utils.Clone(state.sharedLayers.get(name))
|
||||
if (found === undefined) {
|
||||
errors.push(context + ": The layer with name " + json + " was not found as a builtin layer")
|
||||
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.`)
|
||||
}
|
||||
try {
|
||||
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"],)}`)
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: layers,
|
||||
errors, warnings
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
result: [json],
|
||||
errors, warnings
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
json.layers = [...json.layers]
|
||||
|
||||
if (json.id === "personal") {
|
||||
json.layers = []
|
||||
for (const publicLayer of AllKnownLayouts.AllPublicLayers()) {
|
||||
const id = publicLayer.id
|
||||
const config = state.sharedLayers.get(id)
|
||||
if (Constants.added_by_default.indexOf(id) >= 0) {
|
||||
continue;
|
||||
}
|
||||
if (config === undefined) {
|
||||
// This is a layer which is coded within a public theme, not as separate .json
|
||||
continue
|
||||
}
|
||||
json.layers.push(config)
|
||||
}
|
||||
const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id)
|
||||
publicIds.map(id => state.sharedLayers.get(id))
|
||||
}
|
||||
|
||||
for (const layerName of Constants.added_by_default) {
|
||||
const v = state.sharedLayers.get(layerName)
|
||||
if (v === undefined) {
|
||||
errors.push("Default layer " + layerName + " not found")
|
||||
}
|
||||
json.layers.push(v)
|
||||
}
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
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"]);
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
|
||||
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];
|
||||
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
|
||||
// Priviliged layers are skipped
|
||||
continue
|
||||
}
|
||||
|
||||
if (layer.source["geoJson"] !== undefined) {
|
||||
// Layer which don't get their data from OSM are skipped
|
||||
continue
|
||||
}
|
||||
|
||||
if (layer.title === undefined || layer.name === undefined) {
|
||||
// Anonymous layers and layers without popup are skipped
|
||||
continue
|
||||
}
|
||||
|
||||
if (layer.presets === undefined || layer.presets.length == 0) {
|
||||
// A preset is needed to be able to generate a new point
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const importLayerResult = creator.convert(state, layer, context + ".(noteimportlayer)[" + i1 + "]")
|
||||
errors.push(...importLayerResult.errors)
|
||||
warnings.push(...importLayerResult.warnings)
|
||||
if (importLayerResult.result !== undefined) {
|
||||
warnings.push("Added an import layer to theme " + json.id + ", namely " + importLayerResult.result.id)
|
||||
json.layers.push(importLayerResult.result)
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push("Could not generate an import-layer for " + layer.id + " due to " + e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
errors,
|
||||
warnings,
|
||||
result: json
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this tag rendering has a minimap in some language.
|
||||
* Note: this minimap can be hidden by conditions
|
||||
*/
|
||||
private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
|
||||
const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
|
||||
for (const translation of translations) {
|
||||
for (const key in translation.translations) {
|
||||
if (!translation.translations.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
const template = translation.translations[key]
|
||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||
if (hasMiniMap) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
|
||||
|
||||
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)) ?? true
|
||||
if (!hasMinimap) {
|
||||
layerConfig = {...layerConfig}
|
||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
result: layerConfig
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
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)", ["layers"]);
|
||||
}
|
||||
|
||||
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] {
|
||||
const dependenciesToAdd: LayerConfigJson[] = []
|
||||
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 }[] = []
|
||||
do {
|
||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
|
||||
for (const layerConfig of alreadyLoaded) {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
|
||||
dependencies.push(...layerDeps)
|
||||
}
|
||||
|
||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
||||
// Their existance is checked elsewhere, so this is fine
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
|
||||
continue
|
||||
}
|
||||
const dep = allKnownLayers.get(unmetDependency.neededLayer)
|
||||
if (dep === undefined) {
|
||||
const message =
|
||||
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
|
||||
"This layer is needed by " + unmetDependency.neededBy,
|
||||
unmetDependency.reason + " (at " + unmetDependency.context + ")",
|
||||
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
|
||||
|
||||
]
|
||||
throw message.join("\n\t");
|
||||
}
|
||||
dependenciesToAdd.unshift(dep)
|
||||
loadedLayerIds.add(dep.id);
|
||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
||||
}
|
||||
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
return dependenciesToAdd;
|
||||
}
|
||||
|
||||
convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
|
||||
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers; // Layers should be expanded at this point
|
||||
|
||||
knownTagRenderings.forEach((value, key) => {
|
||||
value.id = key;
|
||||
})
|
||||
|
||||
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
|
||||
if (dependencies.length > 0) {
|
||||
|
||||
warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed")
|
||||
}
|
||||
layers.unshift(...dependencies);
|
||||
|
||||
return {
|
||||
result: {
|
||||
...theme,
|
||||
layers: layers
|
||||
},
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Fully prepares and expands a theme",
|
||||
new OnEveryConcat("layers", new SubstituteLayer()),
|
||||
new SetDefault("socialImage", "assets/SocialImage.png", true),
|
||||
new AddDefaultLayers(),
|
||||
new AddDependencyLayersToTheme(),
|
||||
new OnEvery("layers", new PrepareLayer()),
|
||||
new AddImportLayers(),
|
||||
new OnEvery("layers", new AddMiniMap())
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue