Refactoring: split tagRenderingConfigJson into a version without and with questions

This commit is contained in:
pietervdvn 2022-02-28 17:17:38 +01:00
parent c89bb32c61
commit 9f81628f64
12 changed files with 544 additions and 2348 deletions

View file

@ -2,64 +2,89 @@ import {Conversion, DesugaringStep} from "./Conversion";
import {LayoutConfigJson} from "../Json/LayoutConfigJson"; import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import * as metapaths from "../../../assets/layoutconfigmeta.json"; 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[]> { export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
private _isOfficial: boolean; private _isOfficial: boolean;
private _sharedTagRenderings: Map<string, any>; 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 layoutMetaPaths = (metapaths["default"] ?? metapaths)
private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths).filter(trpath => trpath.typeHint === "rendered") .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>) { 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._isOfficial = isOfficial;
this._sharedTagRenderings = sharedTagRenderings; 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[] } { convert(json: LayoutConfigJson, context: string): { result: string[], errors: string[], warnings: string[] } {
const allFoundImages : string[] = [] const allFoundImages : string[] = []
const errors = [] const errors = []
const warnings = [] const warnings = []
for (const metapath of ExtractImages.layoutMetaPaths) { 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) const found = Utils.CollectPath(metapath.path, json)
if (mightBeTr) { if (mightBeTr) {
// We might have tagRenderingConfigs containing icons here // We might have tagRenderingConfigs containing icons here
for (const el of found) { for (const el of found) {
const path = el.path const path = el.path
const foundImage = el.leaf; const foundImage = el.leaf;
if (typeof foundImage === "string") { if (typeof foundImage === "string") {
if(!allRenderedValuesAreImages){
continue
}
if(foundImage == ""){ if(foundImage == ""){
warnings.push(context+"."+path.join(".")+" Found an empty image") warnings.push(context+"."+path.join(".")+" Found an empty image")
} }
if(this._sharedTagRenderings?.has(foundImage)){ if(this._sharedTagRenderings?.has(foundImage)){
// This is not an image, but a shared tag rendering // 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 continue
} }
allFoundImages.push(foundImage) allFoundImages.push(foundImage)
} else{ } 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) { for (const trpath of ExtractImages.tagRenderingMetaPaths) {
// Inspect all the rendered values
const fromPath = Utils.CollectPath(trpath.path, foundImage) 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) { for (const img of fromPath) {
if (typeof img.leaf !== "string") { if (allRenderedValuesAreImages && isRendered) {
(this._isOfficial ? errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf)) // What we found is an image
} if(img.leaf === "" || img.leaf["path"] == ""){
} warnings.push(context+[...path,...img.path].join(".")+": Found an empty image at ")
allFoundImages.push(...fromPath.map(i => i.leaf).filter(i => typeof i=== "string")) }else if(typeof img.leaf !== "string"){
for (const pathAndImg of fromPath) { (this._isOfficial ? errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf))
if(pathAndImg.leaf === "" || pathAndImg.leaf["path"] == ""){ }else{
warnings.push(context+[...path,...pathAndImg.path].join(".")+": Found an empty image at ") 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 { } else {
for (const foundElement of found) { for (const foundElement of found) {
@ -127,7 +152,7 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") { if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") {
continue 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) => { Utils.WalkPath(metapath.path, json, (leaf, path) => {
if (typeof leaf === "string") { if (typeof leaf === "string") {
return replaceString(leaf) return replaceString(leaf)

View file

@ -13,6 +13,7 @@ import ScriptUtils from "../../../scripts/ScriptUtils";
import {And} from "../../../Logic/Tags/And"; import {And} from "../../../Logic/Tags/And";
import Translations from "../../../UI/i18n/Translations"; import Translations from "../../../UI/i18n/Translations";
import Svg from "../../../Svg"; import Svg from "../../../Svg";
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson";
class ValidateLanguageCompleteness extends DesugaringStep<any> { 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() { constructor() {
super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings"); 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 errors = []
const warnings = [] const warnings = []
if (json.mappings === undefined || json.mappings.length === 0) { if (json.mappings === undefined || json.mappings.length === 0) {

View file

@ -6,6 +6,8 @@ import UnitConfigJson from "./UnitConfigJson";
import MoveConfigJson from "./MoveConfigJson"; import MoveConfigJson from "./MoveConfigJson";
import PointRenderingConfigJson from "./PointRenderingConfigJson"; import PointRenderingConfigJson from "./PointRenderingConfigJson";
import LineRenderingConfigJson from "./LineRenderingConfigJson"; import LineRenderingConfigJson from "./LineRenderingConfigJson";
import {QuestionableTagRenderingConfigJson} from "./QuestionableTagRenderingConfigJson";
import RewritableConfigJson from "./RewritableConfigJson";
/** /**
* Configuration for a single layer * 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. * 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. * 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 overpassScript?: string
} | } |
{ {
/** /**
* The actual source of the data to load, if loaded via geojson. * The actual source of the data to load, if loaded via geojson.
* *
* # A single geojson-file * # A single geojson-file
* source: {geoJson: "https://my.source.net/some-geo-data.geojson"} * source: {geoJson: "https://my.source.net/some-geo-data.geojson"}
* fetches a geojson from a third party source * fetches a geojson from a third party source
* *
* # A tiled geojson source * # A tiled geojson source
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} * 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 * 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} * 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, geoJson: string,
/** /**
* To load a tiled geojson layer, set the zoomlevel of the tiles * To load a tiled geojson layer, set the zoomlevel of the tiles
*/ */
geoJsonZoomLevel?: number, geoJsonZoomLevel?: number,
/** /**
* Indicates that the upstream geojson data is OSM-derived. * Indicates that the upstream geojson data is OSM-derived.
* Useful for e.g. merging or for scripts generating this cache * Useful for e.g. merging or for scripts generating this cache
*/ */
isOsmCache?: boolean, isOsmCache?: boolean,
/** /**
* Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this
*/ */
mercatorCrs?: boolean, mercatorCrs?: boolean,
/** /**
* Some API's have an id-field, but give it a different name. * Some API's have an id-field, but give it a different name.
* Setting this key will rename this field into 'id' * Setting this key will rename this field into 'id'
*/ */
idKey?: string idKey?: string
}) })
/** /**
* *
@ -174,7 +176,9 @@ export interface LayerConfigJson {
*/ */
titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]; titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"];
/**
* Visualisation of the items on the map
*/
mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson)[] 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. * 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 * These will be grouped and questions will be asked together
*/ */
tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson | { tagRenderings?:
rewrite: { (string
sourceString: string[], | { builtin: string, override: any }
into: (string | any)[][] | QuestionableTagRenderingConfigJson
}, | RewritableConfigJson<(string | { builtin: string, override: any } | QuestionableTagRenderingConfigJson)[]>
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] ) [],
}) [],
/** /**
@ -401,7 +404,7 @@ export interface LayerConfigJson {
/** /**
* If set, synchronizes wether or not this layer is selected. * If set, synchronizes wether or not this layer is selected.
* *
* no: Do not sync at all, always revert to default * no: Do not sync at all, always revert to default
* local: keep selection on local storage * local: keep selection on local storage
* theme-only: sync via OSM, but this layer will only be toggled in this theme * theme-only: sync via OSM, but this layer will only be toggled in this theme

View file

@ -1,51 +1,18 @@
import {AndOrTagConfigJson} from "./TagConfigJson"; 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. * 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 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 * If undefined, the question is never asked and this tagrendering is read-only
*/ */
question?: string | any, 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 * Allow freeform text input from the user
@ -53,10 +20,10 @@ export interface TagRenderingConfigJson {
freeform?: { freeform?: {
/** /**
* If this key is present, then 'render' is used to display the value. * @inheritDoc
* If this is undefined, the rendering is _always_ shown
*/ */
key: string, key: string
/** /**
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values * 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 * A (translated) text that is shown (as gray text) within the textfield
*/ */
placeholder?: string | any placeholder?: string | any
/** /**
* Extra parameters to initialize the input helper arguments. * Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md' * For semantics, see the 'SpecialInputElements.md'
@ -104,36 +71,30 @@ export interface TagRenderingConfigJson {
mappings?: { mappings?: {
/** /**
* If this condition is met, then the text under `then` will be shown. * @inheritDoc
* 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}'}
*/ */
if: AndOrTagConfigJson | string, if: AndOrTagConfigJson | string,
/** /**
* If the condition `if` is met, the text `then` will be rendered. * Shown if the 'if is fulfilled
* If not known yet, the user will be presented with `then` as an option
* Type: rendered * Type: rendered
*/ */
then: string | any, then: string | any,
/** /**
* An icon supporting this mapping; typically shown pretty small * An extra icon supporting the choice
* Type: icon * Type: icon
*/ */
icon?: string | { icon?: string | {
/** /**
* The path to the icon * The path to the icon
* Type: icon * Type: icon
*/ */
path: string, path: string,
/** /**
* A hint to mapcomplete on how to render this icon within the mapping. * Size of the image
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/ */
class: "small" | "medium" | "large" | string 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 some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
* *

View file

@ -0,0 +1,9 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
export default interface RewritableConfigJson<T> {
rewrite: {
sourceString: string[],
into: (string | any)[][]
},
renderings: T
}

View file

@ -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. * 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 { export interface TagRenderingConfigJson {
@ -35,13 +35,7 @@ export interface TagRenderingConfigJson {
render?: string | any, render?: string | any,
/** /**
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question. * Only show this tagrendering (or question) if the object also matches the following tags.
* 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... * 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 * If this is undefined, the rendering is _always_ shown
*/ */
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
*/
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 * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/ */
@ -134,82 +89,6 @@ export interface TagRenderingConfigJson {
*/ */
class: "small" | "medium" | "large" | string 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[]
}[] }[]
} }

View file

@ -1,6 +1,5 @@
import {Translation} from "../../UI/i18n/Translation"; import {Translation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils"; import {TagUtils} from "../../Logic/Tags/TagUtils";
import {And} from "../../Logic/Tags/And"; import {And} from "../../Logic/Tags/And";
@ -12,6 +11,7 @@ import Combine from "../../UI/Base/Combine";
import Title from "../../UI/Base/Title"; import Title from "../../UI/Base/Title";
import Link from "../../UI/Base/Link"; import Link from "../../UI/Base/Link";
import List from "../../UI/Base/List"; import List from "../../UI/Base/List";
import {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
/*** /***
* The parsed version of TagRenderingConfigJSON * The parsed version of TagRenderingConfigJSON
@ -50,7 +50,7 @@ export default class TagRenderingConfig {
}[] }[]
public readonly labels: string[] public readonly labels: string[]
constructor(json: string | TagRenderingConfigJson, context?: string) { constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
if (json === undefined) { if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context; throw "Initing a TagRenderingConfig with undefined in " + context;
} }

File diff suppressed because it is too large Load diff

View 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"
}
]
}
]

View file

@ -27,11 +27,6 @@
], ],
"typeHint": "rendered" "typeHint": "rendered"
}, },
{
"path": [
"question"
]
},
{ {
"path": [ "path": [
"condition" "condition"
@ -58,53 +53,6 @@
], ],
"type": "string" "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": [ "path": [
"mappings" "mappings"
@ -177,43 +125,5 @@
"class" "class"
], ],
"type": "string" "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"
} }
] ]

View file

@ -6,7 +6,7 @@ interface JsonSchema {
description?: string, description?: string,
type?: string, type?: string,
properties?: any, properties?: any,
items?: JsonSchema | JsonSchema[], items?: JsonSchema,
anyOf: JsonSchema[], anyOf: JsonSchema[],
enum: JsonSchema[], enum: JsonSchema[],
"$ref": string "$ref": string
@ -15,7 +15,6 @@ interface JsonSchema {
function WalkScheme<T>( function WalkScheme<T>(
onEach: (schemePart: JsonSchema) => T, onEach: (schemePart: JsonSchema) => T,
scheme: JsonSchema, scheme: JsonSchema,
registerSchemePath = false,
fullScheme: JsonSchema & { definitions?: any } = undefined, fullScheme: JsonSchema & { definitions?: any } = undefined,
path: string[] = [], path: string[] = [],
isHandlingReference = [] isHandlingReference = []
@ -24,6 +23,7 @@ function WalkScheme<T>(
if (scheme === undefined) { if (scheme === undefined) {
return [] return []
} }
if (scheme["$ref"] !== undefined) { if (scheme["$ref"] !== undefined) {
const ref = scheme["$ref"] const ref = scheme["$ref"]
const prefix = "#/definitions/" const prefix = "#/definitions/"
@ -35,70 +35,48 @@ function WalkScheme<T>(
return; return;
} }
const loadedScheme = fullScheme.definitions[definitionName] 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 fullScheme = fullScheme ?? scheme
var t = onEach(scheme) var t = onEach(scheme)
if (t !== undefined) { if (t !== undefined) {
results.push({ results.push({
path: [...path], path,
t t
}) })
} }
function walk(v: JsonSchema, pathPart: string) { function walk(v: JsonSchema) {
if (v === undefined) { if (v === undefined) {
return return
} }
if (registerSchemePath) { results.push(...WalkScheme(onEach, v, fullScheme, path, isHandlingReference))
path.push("" + pathPart)
}
results.push(...WalkScheme(onEach, v, registerSchemePath, fullScheme, path, isHandlingReference))
if (registerSchemePath) {
path.pop()
}
} }
function walkEach(scheme: JsonSchema[], pathPart: string) { function walkEach(scheme: JsonSchema[]) {
if (scheme === undefined) { if (scheme === undefined) {
return return
} }
if (registerSchemePath) {
path.push("" + pathPart) scheme.forEach(v => walk(v))
}
scheme.forEach((v, i) => walk(v, "" + i))
if (registerSchemePath) {
path.pop()
}
} }
{ {
walkEach(scheme.enum, "enum") walkEach(scheme.enum)
walkEach(scheme.anyOf, "anyOf") walkEach(scheme.anyOf)
if (scheme.items !== undefined) { if (Array.isArray(scheme.items)) {
walkEach(<any>scheme.items)
if (scheme.items["forEach"] !== undefined) { } else {
walkEach(<any>scheme.items, "items") walk(<any>scheme.items)
} else {
walk(<any>scheme.items, "items")
}
} }
if (registerSchemePath) {
path.push("properties")
}
for (const key in scheme.properties) { for (const key in scheme.properties) {
const prop = scheme.properties[key] const prop = scheme.properties[key]
path.push(key) results.push(...WalkScheme(onEach, prop, fullScheme, [...path, key], isHandlingReference))
results.push(...WalkScheme(onEach, prop, registerSchemePath, fullScheme, path, isHandlingReference))
path.pop()
}
if (registerSchemePath) {
path.pop()
} }
} }
@ -114,14 +92,16 @@ function extractMeta(typename: string, path: string) {
const typeHint = schemePart.description.split("\n") const typeHint = schemePart.description.split("\n")
.find(line => line.trim().toLocaleLowerCase().startsWith("type:")) .find(line => line.trim().toLocaleLowerCase().startsWith("type:"))
?.substr("type:".length)?.trim() ?.substr("type:".length)?.trim()
const type = schemePart.type ?? schemePart.anyOf; const type = schemePart.items?.anyOf ?? schemePart.type ?? schemePart.anyOf;
return {typeHint, type} return {typeHint, type}
}, themeSchema) }, themeSchema)
writeFileSync("./assets/" + path + ".json", JSON.stringify(withTypes.map(({ const paths = withTypes.map(({
path, path,
t t
}) => ({path, ...t})), null, " ")) }) => ({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("LayoutConfigJson", "layoutconfigmeta")
extractMeta("TagRenderingConfigJson", "tagrenderingconfigmeta") extractMeta("TagRenderingConfigJson", "tagrenderingconfigmeta")
extractMeta("QuestionableTagRenderingConfigJson", "questionabletagrenderingconfigmeta")
} }

View file

@ -449,7 +449,7 @@ export default class LegacyThemeLoaderSpec extends T {
Assert.equal("https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg", Assert.equal("https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg",
fixedMapping) fixedMapping)
}], }],
["Images in 'thens' are detected", () => { ["Images in simple mappings are detected", () => {
const r = new DetectMappingsWithImages().convert({ const r = new DetectMappingsWithImages().convert({
"mappings": [ "mappings": [
{ {
@ -470,7 +470,7 @@ export default class LegacyThemeLoaderSpec extends T {
T.isTrue(errors.length > 0, "No images found"); 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"); 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>{ const r = new ExtractImages(true, new Map<string, any>()).convert(<any>{
"layers": [ "layers": [
{ {
@ -504,6 +504,26 @@ export default class LegacyThemeLoaderSpec extends T {
T.isTrue(images.length > 0, "No images found"); 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/staple.svg") >= 0, "staple.svg not mentioned");
T.isTrue(images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0, "bollard.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");
}] }]
] ]
); );