Finish importer, add applicable import layers to every theme by default

This commit is contained in:
Pieter Vander Vennet 2022-01-21 01:57:16 +01:00
parent 3402ac0954
commit ca1490902c
41 changed files with 1559 additions and 898 deletions

View file

@ -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())
);
}
}