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 {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)

View file

@ -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) {

View file

@ -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

View file

@ -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).
*

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.
* 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[]
}[]
}

View file

@ -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

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"
},
{
"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"
}
]

View file

@ -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")
}

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",
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");
}]
]
);