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