forked from MapComplete/MapComplete
		
	Refactoring: split tagRenderingConfigJson into a version without and with questions
This commit is contained in:
		
							parent
							
								
									c89bb32c61
								
							
						
					
					
						commit
						9f81628f64
					
				
					 12 changed files with 544 additions and 2348 deletions
				
			
		|  | @ -2,64 +2,89 @@ import {Conversion, DesugaringStep} from "./Conversion"; | |||
| import {LayoutConfigJson} from "../Json/LayoutConfigJson"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import * as metapaths from "../../../assets/layoutconfigmeta.json"; | ||||
| import * as tagrenderingmetapaths from "../../../assets/tagrenderingconfigmeta.json"; | ||||
| import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| 
 | ||||
| export class ExtractImages extends Conversion<LayoutConfigJson, string[]> { | ||||
|     private _isOfficial: boolean; | ||||
|     private _sharedTagRenderings: Map<string, any>; | ||||
| 
 | ||||
|     private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths).filter(mp => mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) | ||||
|     private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths).filter(trpath => trpath.typeHint === "rendered") | ||||
|     private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths) | ||||
|         .filter(mp => (ExtractImages.mightBeTagRendering(mp)) || mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) | ||||
|     private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths) | ||||
| 
 | ||||
| 
 | ||||
|     constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) { | ||||
|         super("Extract all images from a layoutConfig using the meta paths",[],"ExctractImages"); | ||||
|         super("Extract all images from a layoutConfig using the meta paths.",[],"ExctractImages"); | ||||
|         this._isOfficial = isOfficial; | ||||
|         this._sharedTagRenderings = sharedTagRenderings; | ||||
|     } | ||||
|      | ||||
|     public static mightBeTagRendering(metapath: {type: string | string[]}) : boolean{ | ||||
|         if(!Array.isArray(metapath.type)){ | ||||
|             return false | ||||
|         } | ||||
|         return metapath.type.some(t => | ||||
|             t["$ref"] == "#/definitions/TagRenderingConfigJson" ||  t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson") | ||||
|     } | ||||
| 
 | ||||
|     convert(json: LayoutConfigJson, context: string): { result: string[], errors: string[], warnings: string[] } { | ||||
|         const allFoundImages : string[] = [] | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         for (const metapath of ExtractImages.layoutMetaPaths) { | ||||
|             const mightBeTr = Array.isArray(metapath.type) && metapath.type.some(t => t["$ref"] == "#/definitions/TagRenderingConfigJson") | ||||
|             const mightBeTr = ExtractImages.mightBeTagRendering(metapath) | ||||
|             const allRenderedValuesAreImages = metapath.typeHint === "icon" || metapath.typeHint === "image" | ||||
|             const found = Utils.CollectPath(metapath.path, json) | ||||
|             if (mightBeTr) { | ||||
|                 // We might have tagRenderingConfigs containing icons here
 | ||||
|                 for (const el of found) { | ||||
|                     const path = el.path | ||||
|                     const foundImage = el.leaf; | ||||
|                     if (typeof foundImage === "string") { | ||||
|                      if (typeof foundImage === "string") { | ||||
|                          | ||||
|                          if(!allRenderedValuesAreImages){ | ||||
|                              continue | ||||
|                          } | ||||
|                           | ||||
|                         if(foundImage == ""){ | ||||
|                             warnings.push(context+"."+path.join(".")+" Found an empty image") | ||||
|                         } | ||||
|                          | ||||
|                         if(this._sharedTagRenderings?.has(foundImage)){ | ||||
|                             // This is not an image, but a shared tag rendering
 | ||||
|                             // At key positions for checking, they'll be expanded already, so we can safely ignore them here
 | ||||
|                             continue | ||||
|                         } | ||||
|                          | ||||
|                         allFoundImages.push(foundImage) | ||||
|                     } else{ | ||||
|                         // This is a tagRendering where every rendered value might be an icon!
 | ||||
|                         // This is a tagRendering.
 | ||||
|                         // Either every rendered value might be an icon 
 | ||||
|                         // or -in the case of a normal tagrendering- only the 'icons' in the mappings have an icon (or exceptionally an '<img>' tag in the translation
 | ||||
|                         for (const trpath of ExtractImages.tagRenderingMetaPaths) { | ||||
|                             // Inspect all the rendered values
 | ||||
|                             const fromPath = Utils.CollectPath(trpath.path, foundImage) | ||||
|                             const isRendered = trpath.typeHint === "rendered" | ||||
|                             const isImage = trpath.typeHint === "icon" || trpath.typeHint === "image" | ||||
|                             for (const img of fromPath) { | ||||
|                                 if (typeof img.leaf !== "string") { | ||||
|                                     (this._isOfficial ?   errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf)) | ||||
|                                 } | ||||
|                             } | ||||
|                             allFoundImages.push(...fromPath.map(i => i.leaf).filter(i => typeof i=== "string")) | ||||
|                             for (const pathAndImg of fromPath) { | ||||
|                                 if(pathAndImg.leaf === "" || pathAndImg.leaf["path"] == ""){ | ||||
|                                     warnings.push(context+[...path,...pathAndImg.path].join(".")+": Found an empty image at ") | ||||
|                                 if (allRenderedValuesAreImages && isRendered) { | ||||
|                                     // What we found is an image
 | ||||
|                                     if(img.leaf === "" || img.leaf["path"] == ""){ | ||||
|                                         warnings.push(context+[...path,...img.path].join(".")+": Found an empty image at ") | ||||
|                                     }else if(typeof img.leaf !== "string"){ | ||||
|                                         (this._isOfficial ?   errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf)) | ||||
|                                     }else{ | ||||
|                                         allFoundImages.push(img.leaf) | ||||
|                                     } | ||||
|                                 }  | ||||
|                                 if(!allRenderedValuesAreImages && isImage){ | ||||
|                                     // Extract images from the translations
 | ||||
|                                     allFoundImages.push(...(Translations.T(img.leaf, "extract_images from "+img.path.join(".")).ExtractImages(false))) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                     } | ||||
|                     }  | ||||
|                 } | ||||
|             } else { | ||||
|                 for (const foundElement of found) { | ||||
|  | @ -127,7 +152,7 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> { | |||
|             if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") { | ||||
|                 continue | ||||
|             } | ||||
|             const mightBeTr = Array.isArray(metapath.type) && metapath.type.some(t => t["$ref"] == "#/definitions/TagRenderingConfigJson") | ||||
|             const mightBeTr = ExtractImages.mightBeTagRendering(metapath) | ||||
|             Utils.WalkPath(metapath.path, json, (leaf, path) => { | ||||
|                 if (typeof leaf === "string") { | ||||
|                     return replaceString(leaf) | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import ScriptUtils from "../../../scripts/ScriptUtils"; | |||
| import {And} from "../../../Logic/Tags/And"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| import Svg from "../../../Svg"; | ||||
| import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson"; | ||||
| 
 | ||||
| 
 | ||||
| class ValidateLanguageCompleteness extends DesugaringStep<any> { | ||||
|  | @ -237,12 +238,12 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> { | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJson> { | ||||
| export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRenderingConfigJson> { | ||||
|     constructor() { | ||||
|         super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings"); | ||||
|     } | ||||
| 
 | ||||
|     convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[] } { | ||||
|     convert(json: QuestionableTagRenderingConfigJson, context: string): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } { | ||||
|         const errors = [] | ||||
|         const warnings = [] | ||||
|         if (json.mappings === undefined || json.mappings.length === 0) { | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ import UnitConfigJson from "./UnitConfigJson"; | |||
| import MoveConfigJson from "./MoveConfigJson"; | ||||
| import PointRenderingConfigJson from "./PointRenderingConfigJson"; | ||||
| import LineRenderingConfigJson from "./LineRenderingConfigJson"; | ||||
| import {QuestionableTagRenderingConfigJson} from "./QuestionableTagRenderingConfigJson"; | ||||
| import RewritableConfigJson from "./RewritableConfigJson"; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration for a single layer | ||||
|  | @ -40,7 +42,7 @@ export interface LayerConfigJson { | |||
|      * Every source _must_ define which tags _must_ be present in order to be picked up. | ||||
|      * | ||||
|      */ | ||||
|     source:  | ||||
|     source: | ||||
|         ({ | ||||
|             /** | ||||
|              * Every source must set which tags have to be present in order to load the given layer. | ||||
|  | @ -58,40 +60,40 @@ export interface LayerConfigJson { | |||
|             */ | ||||
|             overpassScript?: string | ||||
|         } | | ||||
|         { | ||||
|             /** | ||||
|              * The actual source of the data to load, if loaded via geojson. | ||||
|              * | ||||
|              * # A single geojson-file | ||||
|              * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} | ||||
|              *  fetches a geojson from a third party source | ||||
|              * | ||||
|              * # A tiled geojson source | ||||
|              * source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} | ||||
|              *  to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer | ||||
|              * | ||||
|              * Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max} | ||||
|              */ | ||||
|             geoJson: string, | ||||
|             /** | ||||
|              * To load a tiled geojson layer, set the zoomlevel of the tiles | ||||
|              */ | ||||
|             geoJsonZoomLevel?: number, | ||||
|             /** | ||||
|              * Indicates that the upstream geojson data is OSM-derived. | ||||
|              * Useful for e.g. merging or for scripts generating this cache | ||||
|              */ | ||||
|             isOsmCache?: boolean, | ||||
|             /** | ||||
|              * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true`  in the source for this | ||||
|              */ | ||||
|             mercatorCrs?: boolean, | ||||
|             /** | ||||
|              * Some API's have an id-field, but give it a different name. | ||||
|              * Setting this key will rename this field into 'id' | ||||
|              */ | ||||
|             idKey?: string | ||||
|         }) | ||||
|             { | ||||
|                 /** | ||||
|                  * The actual source of the data to load, if loaded via geojson. | ||||
|                  * | ||||
|                  * # A single geojson-file | ||||
|                  * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} | ||||
|                  *  fetches a geojson from a third party source | ||||
|                  * | ||||
|                  * # A tiled geojson source | ||||
|                  * source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} | ||||
|                  *  to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer | ||||
|                  * | ||||
|                  * Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max} | ||||
|                  */ | ||||
|                 geoJson: string, | ||||
|                 /** | ||||
|                  * To load a tiled geojson layer, set the zoomlevel of the tiles | ||||
|                  */ | ||||
|                 geoJsonZoomLevel?: number, | ||||
|                 /** | ||||
|                  * Indicates that the upstream geojson data is OSM-derived. | ||||
|                  * Useful for e.g. merging or for scripts generating this cache | ||||
|                  */ | ||||
|                 isOsmCache?: boolean, | ||||
|                 /** | ||||
|                  * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true`  in the source for this | ||||
|                  */ | ||||
|                 mercatorCrs?: boolean, | ||||
|                 /** | ||||
|                  * Some API's have an id-field, but give it a different name. | ||||
|                  * Setting this key will rename this field into 'id' | ||||
|                  */ | ||||
|                 idKey?: string | ||||
|             }) | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|  | @ -174,7 +176,9 @@ export interface LayerConfigJson { | |||
|      */ | ||||
|     titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Visualisation of the items on the map | ||||
|      */ | ||||
|     mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson)[] | ||||
| 
 | ||||
|     /** | ||||
|  | @ -260,13 +264,12 @@ export interface LayerConfigJson { | |||
|      * This is mainly create questions for a 'left' and a 'right' side of the road. | ||||
|      * These will be grouped and questions will be asked together | ||||
|      */ | ||||
|     tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson | { | ||||
|         rewrite: { | ||||
|             sourceString: string[], | ||||
|             into: (string | any)[][] | ||||
|         }, | ||||
|         renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] | ||||
|     }) [], | ||||
|     tagRenderings?: | ||||
|         (string | ||||
|             | { builtin: string, override: any } | ||||
|             | QuestionableTagRenderingConfigJson | ||||
|             | RewritableConfigJson<(string | { builtin: string, override: any } | QuestionableTagRenderingConfigJson)[]> | ||||
|             ) [], | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|  | @ -401,7 +404,7 @@ export interface LayerConfigJson { | |||
| 
 | ||||
|     /** | ||||
|      * If set, synchronizes wether or not this layer is selected. | ||||
|      *  | ||||
|      * | ||||
|      * no: Do not sync at all, always revert to default | ||||
|      * local: keep selection on local storage | ||||
|      * theme-only: sync via OSM, but this layer will only be toggled in this theme | ||||
|  |  | |||
|  | @ -1,51 +1,18 @@ | |||
| import {AndOrTagConfigJson} from "./TagConfigJson"; | ||||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||
| 
 | ||||
| /** | ||||
|  * A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. | ||||
|  * A QuestionableTagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. | ||||
|  * If the desired tags are missing and a question is defined, a question will be shown instead. | ||||
|  */ | ||||
| export interface TagRenderingConfigJson { | ||||
| export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson { | ||||
| 
 | ||||
|     /** | ||||
|      * The id of the tagrendering, should be an unique string. | ||||
|      * Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise. | ||||
|      * | ||||
|      * Use 'questions' to trigger the question box of this group (if a group is defined) | ||||
|      */ | ||||
|     id?: string, | ||||
| 
 | ||||
|     /** | ||||
|      * If 'group' is defined on many tagRenderings, these are grouped together when shown. The questions are grouped together as well. | ||||
|      * The first tagRendering of a group will always be a sticky element. | ||||
|      */ | ||||
|     group?: string | ||||
| 
 | ||||
|     /** | ||||
|      * A list of labels. These are strings that are used for various purposes, e.g. to filter them away | ||||
|      */ | ||||
|     labels?: string[] | ||||
| 
 | ||||
|     /** | ||||
|      * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. | ||||
|      * If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value. | ||||
|      * | ||||
|      * Note that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />` | ||||
|      * type: rendered | ||||
|      */ | ||||
|     render?: string | any, | ||||
|      | ||||
|     /** | ||||
|      * If it turns out that this tagRendering doesn't match _any_ value, then we show this question. | ||||
|      * If undefined, the question is never asked and this tagrendering is read-only | ||||
|      */ | ||||
|     question?: string | any, | ||||
| 
 | ||||
|     /** | ||||
|      * Only show this question if the object also matches the following tags. | ||||
|      * | ||||
|      * This is useful to ask a follow-up question. E.g. if there is a diaper table, then ask a follow-up question on diaper tables... | ||||
|      * */ | ||||
|     condition?: AndOrTagConfigJson | string; | ||||
| 
 | ||||
|     /** | ||||
|      * Allow freeform text input from the user | ||||
|  | @ -53,10 +20,10 @@ export interface TagRenderingConfigJson { | |||
|     freeform?: { | ||||
| 
 | ||||
|         /** | ||||
|          * If this key is present, then 'render' is used to display the value. | ||||
|          * If this is undefined, the rendering is _always_ shown | ||||
|          * @inheritDoc | ||||
|          */ | ||||
|         key: string, | ||||
|         key: string | ||||
| 
 | ||||
|         /** | ||||
|          * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... | ||||
|          * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values | ||||
|  | @ -66,7 +33,7 @@ export interface TagRenderingConfigJson { | |||
|          * A (translated) text that is shown (as gray text) within the textfield | ||||
|          */ | ||||
|         placeholder?: string | any | ||||
|          | ||||
| 
 | ||||
|         /** | ||||
|          * Extra parameters to initialize the input helper arguments. | ||||
|          * For semantics, see the 'SpecialInputElements.md' | ||||
|  | @ -104,36 +71,30 @@ export interface TagRenderingConfigJson { | |||
|     mappings?: { | ||||
| 
 | ||||
|         /** | ||||
|          * If this condition is met, then the text under `then` will be shown. | ||||
|          * If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM. | ||||
|          * | ||||
|          * For example: {'if': 'diet:vegetarion=yes', 'then':'A vegetarian option is offered here'} | ||||
|          * | ||||
|          * This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'} | ||||
|          * @inheritDoc | ||||
|          */ | ||||
|         if: AndOrTagConfigJson | string, | ||||
|         /** | ||||
|          * If the condition `if` is met, the text `then` will be rendered. | ||||
|          * If not known yet, the user will be presented with `then` as an option | ||||
|          * Shown if the 'if is fulfilled | ||||
|          * Type: rendered | ||||
|          */ | ||||
|         then: string | any, | ||||
|         /** | ||||
|          * An icon supporting this mapping; typically shown pretty small | ||||
|          * An extra icon supporting the choice | ||||
|          * Type: icon | ||||
|          */ | ||||
|         icon?: string | { | ||||
|             /** | ||||
|              * The path to the icon | ||||
|              * The path to the  icon | ||||
|              * Type: icon | ||||
|              */ | ||||
|             path: string, | ||||
|             /** | ||||
|              * A hint to mapcomplete on how to render this icon within the mapping. | ||||
|              * This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged) | ||||
|              * Size of the image | ||||
|              */ | ||||
|             class: "small" | "medium" | "large" | string | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation). | ||||
|          * | ||||
|  |  | |||
|  | @ -0,0 +1,9 @@ | |||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||
| 
 | ||||
| export default interface RewritableConfigJson<T> { | ||||
|     rewrite: { | ||||
|         sourceString: string[], | ||||
|         into: (string | any)[][] | ||||
|     }, | ||||
|     renderings: T | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import {AndOrTagConfigJson} from "./TagConfigJson"; | |||
| 
 | ||||
| /** | ||||
|  * A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. | ||||
|  * If the desired tags are missing and a question is defined, a question will be shown instead. | ||||
|  * For an _editable_ tagRenerdering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one | ||||
|  */ | ||||
| export interface TagRenderingConfigJson { | ||||
| 
 | ||||
|  | @ -35,13 +35,7 @@ export interface TagRenderingConfigJson { | |||
|     render?: string | any, | ||||
|      | ||||
|     /** | ||||
|      * If it turns out that this tagRendering doesn't match _any_ value, then we show this question. | ||||
|      * If undefined, the question is never asked and this tagrendering is read-only | ||||
|      */ | ||||
|     question?: string | any, | ||||
| 
 | ||||
|     /** | ||||
|      * Only show this question if the object also matches the following tags. | ||||
|      * Only show this tagrendering (or question) if the object also matches the following tags. | ||||
|      * | ||||
|      * This is useful to ask a follow-up question. E.g. if there is a diaper table, then ask a follow-up question on diaper tables... | ||||
|      * */ | ||||
|  | @ -57,47 +51,8 @@ export interface TagRenderingConfigJson { | |||
|          * If this is undefined, the rendering is _always_ shown | ||||
|          */ | ||||
|         key: string, | ||||
|         /** | ||||
|          * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... | ||||
|          * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values | ||||
|          */ | ||||
|         type?: string, | ||||
|         /** | ||||
|          * A (translated) text that is shown (as gray text) within the textfield | ||||
|          */ | ||||
|         placeholder?: string | any | ||||
|          | ||||
|         /** | ||||
|          * Extra parameters to initialize the input helper arguments. | ||||
|          * For semantics, see the 'SpecialInputElements.md' | ||||
|          */ | ||||
|         helperArgs?: (string | number | boolean | any)[]; | ||||
|         /** | ||||
|          * If a value is added with the textfield, these extra tag is addded. | ||||
|          * Useful to add a 'fixme=freeform textfield used - to be checked' | ||||
|          **/ | ||||
|         addExtraTags?: string[]; | ||||
| 
 | ||||
|         /** | ||||
|          * When set, influences the way a question is asked. | ||||
|          * Instead of showing a full-widht text field, the text field will be shown within the rendering of the question. | ||||
|          * | ||||
|          * This combines badly with special input elements, as it'll distort the layout. | ||||
|          */ | ||||
|         inline?: boolean | ||||
| 
 | ||||
|         /** | ||||
|          * default value to enter if no previous tagging is present. | ||||
|          * Normally undefined (aka do not enter anything) | ||||
|          */ | ||||
|         default?: string | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * If true, use checkboxes instead of radio buttons when asking the question | ||||
|      */ | ||||
|     multiAnswer?: boolean, | ||||
| 
 | ||||
|     /** | ||||
|      * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes | ||||
|      */ | ||||
|  | @ -134,82 +89,6 @@ export interface TagRenderingConfigJson { | |||
|              */ | ||||
|             class: "small" | "medium" | "large" | string | ||||
|         } | ||||
|         /** | ||||
|          * In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation). | ||||
|          * | ||||
|          * In the latter case, a correct text should be shown, but only a single, canonical tagging should be selectable by the user. | ||||
|          * In this case, one of the mappings can be hiden by setting this flag. | ||||
|          * | ||||
|          * To demonstrate an example making a default assumption: | ||||
|          * | ||||
|          * mappings: [ | ||||
|          *  { | ||||
|          *      if: "access=", -- no access tag present, we assume accessible | ||||
|          *      then: "Accessible to the general public", | ||||
|          *      hideInAnswer: true | ||||
|          *  }, | ||||
|          *  { | ||||
|          *      if: "access=yes", | ||||
|          *      then: "Accessible to the general public", -- the user selected this, we add that to OSM | ||||
|          *  }, | ||||
|          *  { | ||||
|          *      if: "access=no", | ||||
|          *      then: "Not accessible to the public" | ||||
|          *  } | ||||
|          * ] | ||||
|          * | ||||
|          * | ||||
|          * For example, for an operator, we have `operator=Agentschap Natuur en Bos`, which is often abbreviated to `operator=ANB`. | ||||
|          * Then, we would add two mappings: | ||||
|          * { | ||||
|          *     if: "operator=Agentschap Natuur en Bos" -- the non-abbreviated version which should be uploaded | ||||
|          *     then: "Maintained by Agentschap Natuur en Bos" | ||||
|          * }, | ||||
|          * { | ||||
|          *     if: "operator=ANB", -- we don't want to upload abbreviations | ||||
|          *     then: "Maintained by Agentschap Natuur en Bos" | ||||
|          *     hideInAnswer: true | ||||
|          * } | ||||
|          * | ||||
|          * Hide in answer can also be a tagsfilter, e.g. to make sure an option is only shown when appropriate. | ||||
|          * Keep in mind that this is reverse logic: it will be hidden in the answer if the condition is true, it will thus only show in the case of a mismatch | ||||
|          * | ||||
|          * e.g., for toilets: if "wheelchair=no", we know there is no wheelchair dedicated room. | ||||
|          * For the location of the changing table, the option "in the wheelchair accessible toilet is weird", so we write: | ||||
|          * | ||||
|          * { | ||||
|          *     "question": "Where is the changing table located?" | ||||
|          *     "mappings": [ | ||||
|          *         {"if":"changing_table:location=female","then":"In the female restroom"}, | ||||
|          *        {"if":"changing_table:location=male","then":"In the male restroom"}, | ||||
|          *        {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"}, | ||||
|          *          | ||||
|          *     ] | ||||
|          * } | ||||
|          * | ||||
|          * Also have a look for the meta-tags | ||||
|          * { | ||||
|          *     if: "operator=Agentschap Natuur en Bos", | ||||
|          *     then: "Maintained by Agentschap Natuur en Bos", | ||||
|          *     hideInAnswer: "_country!=be" | ||||
|          * } | ||||
|          */ | ||||
|         hideInAnswer?: boolean | string | AndOrTagConfigJson, | ||||
|         /** | ||||
|          * Only applicable if 'multiAnswer' is set. | ||||
|          * This is for situations such as: | ||||
|          * `accepts:coins=no` where one can select all the possible payment methods. However, we want to make explicit that some options _were not_ selected. | ||||
|          * This can be done with `ifnot` | ||||
|          * Note that we can not explicitly render this negative case to the user, we cannot show `does _not_ accept coins`. | ||||
|          * If this is important to your usecase, consider using multiple radiobutton-fields without `multiAnswer` | ||||
|          */ | ||||
|         ifnot?: AndOrTagConfigJson | string | ||||
| 
 | ||||
|         /** | ||||
|          * If chosen as answer, these tags will be applied as well onto the object. | ||||
|          * Not compatible with multiAnswer | ||||
|          */ | ||||
|         addExtraTags?: string[] | ||||
| 
 | ||||
|     }[] | ||||
| } | ||||
|  | @ -1,6 +1,5 @@ | |||
| import {Translation} from "../../UI/i18n/Translation"; | ||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||
| import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| import {And} from "../../Logic/Tags/And"; | ||||
|  | @ -12,6 +11,7 @@ import Combine from "../../UI/Base/Combine"; | |||
| import Title from "../../UI/Base/Title"; | ||||
| import Link from "../../UI/Base/Link"; | ||||
| import List from "../../UI/Base/List"; | ||||
| import {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson"; | ||||
| 
 | ||||
| /*** | ||||
|  * The parsed version of TagRenderingConfigJSON | ||||
|  | @ -50,7 +50,7 @@ export default class TagRenderingConfig { | |||
|     }[] | ||||
|     public readonly labels: string[] | ||||
| 
 | ||||
|     constructor(json: string | TagRenderingConfigJson, context?: string) { | ||||
|     constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) { | ||||
|         if (json === undefined) { | ||||
|             throw "Initing a TagRenderingConfig with undefined in " + context; | ||||
|         } | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										198
									
								
								assets/questionabletagrenderingconfigmeta.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								assets/questionabletagrenderingconfigmeta.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| [ | ||||
|   { | ||||
|     "path": [], | ||||
|     "type": "object" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "question" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform" | ||||
|     ], | ||||
|     "type": "object" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "type" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "placeholder" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "helperArgs" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "addExtraTags" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "inline" | ||||
|     ], | ||||
|     "type": "boolean" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "default" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "multiAnswer" | ||||
|     ], | ||||
|     "type": "boolean" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "then" | ||||
|     ], | ||||
|     "typeHint": "rendered" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "icon" | ||||
|     ], | ||||
|     "typeHint": "icon", | ||||
|     "type": [ | ||||
|       { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "path": { | ||||
|             "description": "The path to the  icon\nType: icon", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "class": { | ||||
|             "description": "Size of the image", | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "class", | ||||
|           "path" | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         "type": "string" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "icon", | ||||
|       "path" | ||||
|     ], | ||||
|     "typeHint": "icon", | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "icon", | ||||
|       "class" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "hideInAnswer" | ||||
|     ], | ||||
|     "type": [ | ||||
|       { | ||||
|         "$ref": "#/definitions/AndOrTagConfigJson" | ||||
|       }, | ||||
|       { | ||||
|         "type": [ | ||||
|           "string", | ||||
|           "boolean" | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "ifnot" | ||||
|     ], | ||||
|     "type": [ | ||||
|       { | ||||
|         "$ref": "#/definitions/AndOrTagConfigJson" | ||||
|       }, | ||||
|       { | ||||
|         "type": "string" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "addExtraTags" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "id" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "group" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "labels" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "render" | ||||
|     ], | ||||
|     "typeHint": "rendered" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "condition" | ||||
|     ], | ||||
|     "type": [ | ||||
|       { | ||||
|         "$ref": "#/definitions/AndOrTagConfigJson" | ||||
|       }, | ||||
|       { | ||||
|         "type": "string" | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
| ] | ||||
|  | @ -27,11 +27,6 @@ | |||
|     ], | ||||
|     "typeHint": "rendered" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "question" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "condition" | ||||
|  | @ -58,53 +53,6 @@ | |||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "type" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "placeholder" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "helperArgs" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "addExtraTags" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "inline" | ||||
|     ], | ||||
|     "type": "boolean" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "freeform", | ||||
|       "default" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "multiAnswer" | ||||
|     ], | ||||
|     "type": "boolean" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings" | ||||
|  | @ -177,43 +125,5 @@ | |||
|       "class" | ||||
|     ], | ||||
|     "type": "string" | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "hideInAnswer" | ||||
|     ], | ||||
|     "type": [ | ||||
|       { | ||||
|         "$ref": "#/definitions/AndOrTagConfigJson" | ||||
|       }, | ||||
|       { | ||||
|         "type": [ | ||||
|           "string", | ||||
|           "boolean" | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "ifnot" | ||||
|     ], | ||||
|     "type": [ | ||||
|       { | ||||
|         "$ref": "#/definitions/AndOrTagConfigJson" | ||||
|       }, | ||||
|       { | ||||
|         "type": "string" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     "path": [ | ||||
|       "mappings", | ||||
|       "addExtraTags" | ||||
|     ], | ||||
|     "type": "array" | ||||
|   } | ||||
| ] | ||||
|  | @ -6,7 +6,7 @@ interface JsonSchema { | |||
|     description?: string, | ||||
|     type?: string, | ||||
|     properties?: any, | ||||
|     items?: JsonSchema | JsonSchema[], | ||||
|     items?: JsonSchema, | ||||
|     anyOf: JsonSchema[], | ||||
|     enum: JsonSchema[], | ||||
|     "$ref": string | ||||
|  | @ -15,7 +15,6 @@ interface JsonSchema { | |||
| function WalkScheme<T>( | ||||
|     onEach: (schemePart: JsonSchema) => T, | ||||
|     scheme: JsonSchema, | ||||
|     registerSchemePath = false, | ||||
|     fullScheme: JsonSchema & { definitions?: any } = undefined, | ||||
|     path: string[] = [], | ||||
|     isHandlingReference = [] | ||||
|  | @ -24,6 +23,7 @@ function WalkScheme<T>( | |||
|     if (scheme === undefined) { | ||||
|         return [] | ||||
|     } | ||||
|      | ||||
|     if (scheme["$ref"] !== undefined) { | ||||
|         const ref = scheme["$ref"] | ||||
|         const prefix = "#/definitions/" | ||||
|  | @ -35,70 +35,48 @@ function WalkScheme<T>( | |||
|             return; | ||||
|         } | ||||
|         const loadedScheme = fullScheme.definitions[definitionName] | ||||
|         return WalkScheme(onEach, loadedScheme, registerSchemePath, fullScheme, path, [...isHandlingReference, definitionName]); | ||||
|         return WalkScheme(onEach, loadedScheme, fullScheme, path, [...isHandlingReference, definitionName]); | ||||
|     } | ||||
| 
 | ||||
|     fullScheme = fullScheme ?? scheme | ||||
|     var t = onEach(scheme) | ||||
|     if (t !== undefined) { | ||||
|         results.push({ | ||||
|             path: [...path], | ||||
|             path, | ||||
|             t | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|      | ||||
|     function walk(v: JsonSchema, pathPart: string) { | ||||
| 
 | ||||
| 
 | ||||
|     function walk(v: JsonSchema) { | ||||
|         if (v === undefined) { | ||||
|             return | ||||
|         } | ||||
|         if (registerSchemePath) { | ||||
|             path.push("" + pathPart) | ||||
|         } | ||||
|         results.push(...WalkScheme(onEach, v, registerSchemePath, fullScheme, path, isHandlingReference)) | ||||
|         if (registerSchemePath) { | ||||
|             path.pop() | ||||
|         } | ||||
|         results.push(...WalkScheme(onEach, v, fullScheme, path, isHandlingReference)) | ||||
|     } | ||||
| 
 | ||||
|     function walkEach(scheme: JsonSchema[], pathPart: string) { | ||||
|     function walkEach(scheme: JsonSchema[]) { | ||||
|         if (scheme === undefined) { | ||||
|             return | ||||
|         } | ||||
|         if (registerSchemePath) { | ||||
|             path.push("" + pathPart) | ||||
|         } | ||||
|         scheme.forEach((v, i) => walk(v, "" + i)) | ||||
|         if (registerSchemePath) { | ||||
|             path.pop() | ||||
|         } | ||||
|        | ||||
|         scheme.forEach(v => walk(v)) | ||||
|         | ||||
|     } | ||||
| 
 | ||||
|    | ||||
| 
 | ||||
|     { | ||||
|         walkEach(scheme.enum, "enum") | ||||
|         walkEach(scheme.anyOf, "anyOf") | ||||
|         if (scheme.items !== undefined) { | ||||
| 
 | ||||
|             if (scheme.items["forEach"] !== undefined) { | ||||
|                 walkEach(<any>scheme.items, "items") | ||||
|             } else { | ||||
|                 walk(<any>scheme.items, "items") | ||||
|             } | ||||
|         walkEach(scheme.enum) | ||||
|         walkEach(scheme.anyOf) | ||||
|         if (Array.isArray(scheme.items)) { | ||||
|             walkEach(<any>scheme.items) | ||||
|         } else { | ||||
|             walk(<any>scheme.items) | ||||
|         } | ||||
| 
 | ||||
|         if (registerSchemePath) { | ||||
|             path.push("properties") | ||||
|         } | ||||
|         for (const key in scheme.properties) { | ||||
|             const prop = scheme.properties[key] | ||||
|             path.push(key) | ||||
|             results.push(...WalkScheme(onEach, prop, registerSchemePath, fullScheme, path, isHandlingReference)) | ||||
|             path.pop() | ||||
|         } | ||||
|         if (registerSchemePath) { | ||||
|             path.pop() | ||||
|             results.push(...WalkScheme(onEach, prop, fullScheme, [...path, key], isHandlingReference)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -114,14 +92,16 @@ function extractMeta(typename: string, path: string) { | |||
|         const typeHint = schemePart.description.split("\n") | ||||
|             .find(line => line.trim().toLocaleLowerCase().startsWith("type:")) | ||||
|             ?.substr("type:".length)?.trim() | ||||
|         const type = schemePart.type ?? schemePart.anyOf; | ||||
|         const type = schemePart.items?.anyOf ?? schemePart.type ?? schemePart.anyOf; | ||||
|         return {typeHint, type} | ||||
|     }, themeSchema) | ||||
| 
 | ||||
|     writeFileSync("./assets/" + path + ".json", JSON.stringify(withTypes.map(({ | ||||
|                                                                                   path, | ||||
|                                                                                   t | ||||
|                                                                               }) => ({path, ...t})), null, "  ")) | ||||
|     const paths = withTypes.map(({ | ||||
|                                      path, | ||||
|                                      t | ||||
|                                  }) => ({path, ...t})) | ||||
|     writeFileSync("./assets/" + path + ".json", JSON.stringify(paths, null, "  ")) | ||||
|     console.log("Written meta to ./assets/" + path) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -148,6 +128,7 @@ function main() { | |||
| 
 | ||||
|     extractMeta("LayoutConfigJson", "layoutconfigmeta") | ||||
|     extractMeta("TagRenderingConfigJson", "tagrenderingconfigmeta") | ||||
|     extractMeta("QuestionableTagRenderingConfigJson", "questionabletagrenderingconfigmeta") | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -449,7 +449,7 @@ export default class LegacyThemeLoaderSpec extends T { | |||
|                     Assert.equal("https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg", | ||||
|                         fixedMapping) | ||||
|                 }], | ||||
|                 ["Images in 'thens' are detected", () => { | ||||
|                 ["Images in simple mappings are detected", () => { | ||||
|                     const r = new DetectMappingsWithImages().convert({ | ||||
|                         "mappings": [ | ||||
|                             { | ||||
|  | @ -470,7 +470,7 @@ export default class LegacyThemeLoaderSpec extends T { | |||
|                     T.isTrue(errors.length > 0, "No images found"); | ||||
|                     T.isTrue(errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0), "staple.svg not mentioned"); | ||||
|                 }], | ||||
|                 ["Images in 'thens' icons are detected", () => { | ||||
|                 ["Images in 'thens' are detected in QuestionableTagRenderings", () => { | ||||
|                     const r = new ExtractImages(true, new Map<string, any>()).convert(<any>{ | ||||
|                         "layers": [ | ||||
|                             { | ||||
|  | @ -504,6 +504,26 @@ export default class LegacyThemeLoaderSpec extends T { | |||
|                     T.isTrue(images.length > 0, "No images found"); | ||||
|                     T.isTrue(images.findIndex(img => img =="./assets/layers/bike_parking/staple.svg") >= 0, "staple.svg not mentioned"); | ||||
|                     T.isTrue(images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0, "bollard.svg not mentioned"); | ||||
|                 }], | ||||
|                 ["Rotation and colours is not detected as image", () => { | ||||
|                     const r = new ExtractImages(true, new Map<string, any>()).convert(<any>{ | ||||
|                         "layers": [ | ||||
|                             { | ||||
|                                 mapRendering: [ | ||||
|                                     { | ||||
|                                         "location":["point","centroid"], | ||||
|                                         "icon": "pin:black", | ||||
|                                         rotation: 180, | ||||
|                                         iconSize: "40,40,center" | ||||
|                                     } | ||||
|                                 ] | ||||
|                             } | ||||
|                         ] | ||||
|                     }, "test"); | ||||
|                     const images = r.result | ||||
|                     T.isTrue(images.length > 0, "No images found"); | ||||
|                     T.isTrue(images.length < 2, "To much images found: "+images.join(", ")); | ||||
|                     T.isTrue(images[0] === "pin", "pin not mentioned"); | ||||
|                 }] | ||||
|             ] | ||||
|         ); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue