forked from MapComplete/MapComplete
Add rewrite of 'special' clauses, various QOLimprovements on import viewer
This commit is contained in:
parent
8df0324572
commit
c47a6d5ea7
22 changed files with 597 additions and 155 deletions
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue