Add rewrite of 'special' clauses, various QOLimprovements on import viewer

This commit is contained in:
Pieter Vander Vennet 2022-03-29 00:20:10 +02:00
parent 8df0324572
commit c47a6d5ea7
22 changed files with 597 additions and 155 deletions

View file

@ -42,7 +42,7 @@ export abstract class Conversion<TIn, TOut> {
public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[], information?: string[] } {
if(jsons === undefined || jsons === null){
throw "convertAll received undefined or null - don't do this (at "+context+")"
throw `Detected a bug in the preprocessor pipeline: ${this.name}.convertAll received undefined or null - don't do this (at ${context})`
}
const result = []
const errors = []
@ -72,23 +72,34 @@ 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>;
private _options: { ignoreIfUndefined: boolean };
constructor(key: string, step: DesugaringStep<X>) {
constructor(key: string, step: DesugaringStep<X>, options?: {
ignoreIfUndefined: false | boolean
}) {
super("Applies " + step.name + " onto every object of the list `key`", [key], "OnEvery("+step.name+")");
this.step = step;
this.key = key;
this._options = options;
}
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } {
json = {...json}
const step = this.step
const key = this.key;
const r = step.convertAll((<X[]>json[key]), context + "." + key)
json[key] = r.result
return {
...r,
result: json,
};
if( this._options?.ignoreIfUndefined && json[key] === undefined){
return {
result: json,
};
}else{
const r = step.convertAll((<X[]>json[key]), context + "." + key)
json[key] = r.result
return {
...r,
result: json,
};
}
}
}

View file

@ -1,10 +1,12 @@
import {Conversion, DesugaringContext, Fuse, OnEvery, OnEveryConcat, SetDefault} from "./Conversion";
import {Conversion, DesugaringContext, DesugaringStep, Fuse, OnEvery, OnEveryConcat, 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 RewritableConfigJson from "../Json/RewritableConfigJson";
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
private readonly _state: DesugaringContext;
@ -349,28 +351,168 @@ class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
}
class ExpandRewriteWithFlatten<T> extends Conversion<T | RewritableConfigJson<T | T[]>, T[]> {
private _rewrite = new ExpandRewrite<T>()
/**
* Converts a 'special' translation into a regular translation which uses parameters
* E.g.
*
* const tr = <TagRenderingJson> {
* "special":
* }
*/
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
constructor() {
super("Applies a rewrite, the result is flattened if it is an array", [], "ExpandRewriteWithFlatten");
super("Converts a 'special' translation into a regular translation which uses parameters", ["special"],"RewriteSpecial");
}
convert(json: RewritableConfigJson<T[] | T> | T, context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
return undefined;
/**
* Does the heavy lifting and conversion
*
* // should not do anything if no 'special'-key is present
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, [], "test") // => {"en": "xyz", "nl": "abc"}
*
* // should handle a simple special case
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, [], "test") // => {'*': "{image_carousel()}"}
*
* // should handle special case with a parameter
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, [], "test") // => {'*': "{image_carousel(some_image_key)}"}
*
* // should handle special case with a translated parameter
* const spec = {"special": {"type":"image_upload", "label": {"en": "Add a picture to this object", "nl": "Voeg een afbeelding toe"}}}
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* r // => {"en": "{image_upload(,Add a picture to this object)}", "nl": "{image_upload(,Voeg een afbeelding toe)}" }
*
* // should warn for unexpected keys
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, errors, "test") // => {'*': "{image_carousel()}"}
* errors // => ["At test: Unexpected key in a special block: en"]
*
* // should give an error on unknown visualisations
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, errors, "test") // => undefined
* errors.length // => 1
* errors[0].indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
*
* // should give an error is 'type' is missing
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"]
*/
private static convertIfNeeded(input: (object & {special : {type: string}}) | any, errors: string[], context: string): any {
const special = input["special"]
if(special === undefined){
return input
}
for (const wrongKey of Object.keys(input).filter(k => k !== "special")) {
errors.push(`At ${context}: Unexpected key in a special block: ${wrongKey}`)
}
const type = special["type"]
if(type === undefined){
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)
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`)
return undefined
}
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")
.map(wrongArg => {
const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x)
return `Unexpected argument 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;
}
const param = special[arg.name]
if(param === undefined){
errors.push(`Obligated parameter '${arg.name}' not found`)
}
}
const foundLanguages = new Set<string>()
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)
}
}
if(foundLanguages.size === 0){
const args= argNamesList.map(nm => special[nm] ?? "").join(",")
return {'*': `{${type}(${args})}`
}
}
const result = {}
const languages = Array.from(foundLanguages)
languages.sort()
for (const ln of languages) {
const args = []
for (const argName of argNamesList) {
const v = special[argName] ?? ""
if(Translations.isProbablyATranslation(v)){
args.push(new Translation(v).textFor(ln))
}else{
args.push(v)
}
}
result[ln] = `{${type}(${args.join(",")})}`
}
return result
}
/**
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image" }},
* mappings: [
* {
* if: "other_image_key",
* then: {special: {type: "image_carousel", image_key: "other_image_key"}}
* }
* ]
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]}
* result // => expected
*/
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
for (const path of paths) {
if(path.typeHint !== "rendered"){
continue
}
Utils.WalkPath(path.path, json, ((leaf, travelled) => RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))))
}
return {
result:json,
errors
};
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) {
super(
"Fully prepares and expands a layer for the LayerConfig.",
new OnEvery("tagRenderings", new RewriteSpecial(), {ignoreIfUndefined: true}),
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite(state)),
new OnEveryConcat("tagRenderings", new ExpandTagRendering(state)),
new OnEveryConcat("mapRendering", new ExpandRewrite()),

View file

@ -234,7 +234,7 @@ export interface LayerConfigJson {
/**
* The type of background picture
*/
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string [],
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string[],
/**
* If specified, these layers will be shown to and the new point will be snapped towards it
*/

View file

@ -197,14 +197,14 @@ export default class LayerConfig extends WithContextLoader {
snapToLayers = pr.preciseInput.snapToLayer
}
let preferredBackground: string[]
let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
if (typeof pr.preciseInput.preferredBackground === "string") {
preferredBackground = [pr.preciseInput.preferredBackground]
} else {
preferredBackground = pr.preciseInput.preferredBackground
}
preciseInput = {
preferredBackground: preferredBackground,
preferredBackground,
snapToLayers,
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
}

View file

@ -2,7 +2,7 @@ import {Translation} from "../../UI/i18n/Translation";
import {Tag} from "../../Logic/Tags/Tag";
export interface PreciseInput {
preferredBackground?: string[],
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[],
snapToLayers?: string[],
maxSnapDistance?: number
}