Refactoring: use more accurate context in conversion, fix tests

This commit is contained in:
Pieter Vander Vennet 2023-10-12 16:55:26 +02:00
parent 86d0de3806
commit f77d99f8ed
43 changed files with 999 additions and 367 deletions

View file

@ -506,7 +506,6 @@ export class OsmConnection {
this.isChecking = true
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
if (self.isLoggedIn.data) {
console.log("Checking for messages")
self.AttemptLogin()
}
})

View file

@ -27,14 +27,14 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
* builtin: ["abc"],
* override: {
* title:{
* _context: "prefix:context.layers.0.override.title"
* _context: "prefix:layers.0.override.title"
* en: "Some title"
* }
* }
@ -57,14 +57,14 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
* tagRenderings:[
* {id: "some-tr",
* question:{
* _context: "prefix:context.layers.0.tagRenderings.some-tr.question"
* _context: "prefix:layers.0.tagRenderings.some-tr.question"
* en:"Question?"
* }
* }
@ -85,7 +85,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
@ -113,7 +113,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* rewritten // => theme
*
*/
@ -139,7 +139,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
}
}
return { ...leaf, _context: this._prefix + context + "." + path.join(".") }
return {
...leaf,
_context: this._prefix + context.path.concat(path).join("."),
}
} else {
return leaf
}

View file

@ -9,17 +9,33 @@ export interface DesugaringContext {
}
export class ConversionContext {
/**
* The path within the data structure where we are currently operating
*/
readonly path: ReadonlyArray<string | number>
/**
* Some information about the current operation
*/
readonly operation: ReadonlyArray<string>
readonly messages: ConversionMessage[] = []
readonly messages: ConversionMessage[]
private constructor(path: ReadonlyArray<string | number>, operation?: ReadonlyArray<string>) {
private constructor(
messages: ConversionMessage[],
path: ReadonlyArray<string | number>,
operation?: ReadonlyArray<string>
) {
this.path = path
this.operation = operation ?? []
// Messages is shared by reference amonst all 'context'-objects for performance
this.messages = messages
}
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([...path], [...operation])
return new ConversionContext([], [...path], [...operation])
}
public static test(msg?: string) {
return new ConversionContext([], msg ? [msg] : [], ["test"])
}
static print(msg: ConversionMessage) {
@ -38,12 +54,7 @@ export class ConversionContext {
msg.context.operation.join(".")
)
} else {
console.log(
" ",
msg.context.path.join("."),
msg.message,
msg.context.operation.join(".")
)
console.log(" ", msg.context.path.join("."), msg.message)
}
}
@ -57,9 +68,9 @@ export class ConversionContext {
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
return new ConversionContext([...this.path, key], this.operation)
return new ConversionContext(this.messages, [...this.path, key], this.operation)
}
return new ConversionContext([...this.path, ...key], this.operation)
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
}
public enters(...key: (string | number)[]) {
@ -67,7 +78,7 @@ export class ConversionContext {
}
public inOperation(key: string) {
return new ConversionContext(this.path, [...this.operation, key])
return new ConversionContext(this.messages, this.path, [...this.operation, key])
}
warn(message: string) {
@ -82,15 +93,19 @@ export class ConversionContext {
this.messages.push({ context: this, level: "information", message })
}
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
return this.messages.filter((m) => m.level === mode)
}
public hasErrors() {
return this.messages?.find((m) => m.level === "error") !== undefined
}
}
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
export interface ConversionMessage {
context: ConversionContext
message: string
level: "debug" | "information" | "warning" | "error"
level: ConversionMsgLevel
}
export abstract class Conversion<TIn, TOut> {
@ -106,7 +121,7 @@ export abstract class Conversion<TIn, TOut> {
public convertStrict(json: TIn, context?: ConversionContext): TOut {
context ??= ConversionContext.construct([], [])
context = context.enter(this.name)
context = context.inOperation(this.name)
const fixed = this.convert(json, context)
for (const msg of context.messages) {
ConversionContext.print(msg)
@ -126,7 +141,7 @@ export abstract class Conversion<TIn, TOut> {
export abstract class DesugaringStep<T> extends Conversion<T, T> {}
class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
private readonly _step0: Conversion<TIn, TInter>
private readonly _step1: Conversion<TInter, TOut>
@ -145,7 +160,7 @@ class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
}
}
class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
export class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
private readonly _f: (t: TIn) => TOut
constructor(f: (t: TIn) => TOut) {
@ -205,14 +220,14 @@ export class On<P, T> extends DesugaringStep<T> {
}
convert(json: T, context: ConversionContext): T {
json = { ...json }
const step = this.step(json)
const key = this.key
const value: P = json[key]
if (value === undefined || value === null) {
return undefined
return json
}
json = { ...json }
const step = this.step(json)
json[key] = step.convert(value, context.enter(key).inOperation("on[" + key + "]"))
return json
}
@ -280,7 +295,7 @@ export class Fuse<T> extends DesugaringStep<T> {
"This fused pipeline of the following steps: " +
steps.map((s) => s.name).join(", "),
Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))),
"Fuse of " + steps.map((s) => s.name).join(", ")
"Fuse(" + steps.map((s) => s.name).join(", ") + ")"
)
this.steps = Utils.NoNull(steps)
}
@ -290,7 +305,7 @@ export class Fuse<T> extends DesugaringStep<T> {
const step = this.steps[i]
try {
const r = step.convert(json, context.inOperation(step.name))
if (r === undefined) {
if (r === undefined || r === null) {
break
}
if (context.hasErrors()) {

View file

@ -33,21 +33,28 @@ export class ExtractImages extends Conversion<
}
public static mightBeTagRendering(metapath: { type?: string | string[] }): boolean {
if (!Array.isArray(metapath.type)) {
if (!metapath.type) {
return false
}
return (
metapath.type?.some(
(t) =>
t !== null &&
(t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson")
) ?? false
let type: any[]
if (!Array.isArray(metapath.type)) {
type = [metapath.type]
} else {
type = metapath.type
}
return type.some(
(t) =>
t !== null &&
(t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/MinimalTagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson" ||
(t["properties"]?.render !== undefined &&
t["properties"]?.mappings !== undefined))
)
}
/**
* const images = new ExtractImages(true, new Map<string, any>()).convert(<any>{
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{
* "layers": [
* {
* tagRenderings: [
@ -75,14 +82,14 @@ export class ExtractImages extends Conversion<
* ]
* }
* ]
* }, "test").result.map(i => i.path);
* }, ConversionContext.test()).map(i => i.path);
* images.length // => 2
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") >= 0 // => true
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0 // => true
*
* // should not pickup rotation, should drop color
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
* }, "test").result
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{"layers": [{"pointRendering": [{"location": ["point", "centroid"],marker: [{"icon": "pin:black"}],rotation: 180,iconSize: "40,40,center"}]}]
* }, ConversionContext.test())
* images.length // => 1
* images[0].path // => "pin"
*
@ -233,9 +240,9 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/verkeerdeborden.json"
* "layers": [
* {
* "mapRendering": [
* "pointRendering": [
* {
* "icon": "./TS_bolt.svg",
* marker: [{"icon": "./TS_bolt.svg"}],
* iconBadges: [{
* if: "id=yes",
* then: {
@ -256,9 +263,9 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* }
* ],
* }
* const fixed = new FixImages(new Set<string>()).convert(<any> theme, "test").result
* fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
* const fixed = new FixImages(new Set<string>()).convert(<any> theme, ConversionContext.test())
* fixed.layers[0]["pointRendering"][0].marker[0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["pointRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
*/
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
let url: URL

View file

@ -11,7 +11,10 @@ import {
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -27,6 +30,7 @@ import ValidationUtils from "./ValidationUtils"
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -157,6 +161,25 @@ class ExpandTagRendering extends Conversion<
}
}
public convert(
spec: string | any,
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
result.push(...stable)
} else {
result.push(tr)
}
}
return result
}
private lookup(name: string): TagRenderingConfigJson[] | undefined {
const direct = this.directLookup(name)
@ -386,25 +409,6 @@ class ExpandTagRendering extends Conversion<
return [tr]
}
public convert(
spec: string | any,
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
result.push(...stable)
} else {
result.push(tr)
}
}
return result
}
}
class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
@ -711,7 +715,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* },
* renderings: "The value of xyz is abc"
* }
* new ExpandRewrite().convertStrict(spec, "test") // => ["The value of X is A", "The value of Y is B", "The value of Z is C"]
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => ["The value of X is A", "The value of Y is B", "The value of Z is C"]
*
* // should rewrite with translations
* const spec = <RewritableConfigJson<any>>{
@ -733,7 +737,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* nl: "De waarde van Y is een andere waarde"
* }
* ]
* new ExpandRewrite().convertStrict(spec, "test") // => expected
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => expected
*/
convert(json: T | RewritableConfigJson<T>, context: ConversionContext): T[] {
if (json === null || json === undefined) {
@ -808,39 +812,38 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* Does the heavy lifting and conversion
*
* // should not do anything if no 'special'-key is present
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, [], "test") // => {"en": "xyz", "nl": "abc"}
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, ConversionContext.test()) // => {"en": "xyz", "nl": "abc"}
*
* // should handle a simple special case
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, [], "test") // => {'*': "{image_carousel()}"}
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, ConversionContext.test()) // => {'*': "{image_carousel()}"}
*
* // should handle special case with a parameter
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, [], "test") // => {'*': "{image_carousel(some_image_key)}"}
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, ConversionContext.test()) // => {'*': "{image_carousel(some_image_key)}"}
*
* // should handle special case with a translated parameter
* const spec = {"special": {"type":"image_upload", "label": {"en": "Add a picture to this object", "nl": "Voeg een afbeelding toe"}}}
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
* r // => {"en": "{image_upload(,Add a picture to this object)}", "nl": "{image_upload(,Voeg een afbeelding toe)}" }
*
* // should handle special case with a prefix and postfix
* const spec = {"special": {"type":"image_upload" }, before: {"en": "PREFIX "}, after: {"en": " POSTFIX", nl: " Achtervoegsel"} }
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
* r // => {"en": "PREFIX {image_upload(,)} POSTFIX", "nl": "PREFIX {image_upload(,)} Achtervoegsel" }
*
* // should warn for unexpected keys
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, errors, "test") // => {'*': "{image_carousel()}"}
* errors // => ["At test: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"]
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, context) // => {'*': "{image_carousel()}"}
* context.getAll("error")[0].message // => "The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"
*
* // should give an error on unknown visualisations
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, errors, "test") // => undefined
* errors.length // => 1
* errors[0].indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, context) // => undefined
* context.getAll("error")[0].message.indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
*
* // should give an error is 'type' is missing
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"]
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {}}, context) // => undefined
* context.getAll("error")[0].message // => "A 'special'-block should define 'type' to indicate which visualisation should be used"
*
*
* // an actual test
@ -858,9 +861,9 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* "en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
* }
* }}
* const errors = []
* RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"}
* errors // => []
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded(special, context) // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"}
* context.getAll("error") // => []
*/
private static convertIfNeeded(
input:
@ -870,8 +873,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
})
| any,
errors: string[],
context: string
context: ConversionContext
): any {
const special = input["special"]
if (special === undefined) {
@ -880,7 +882,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const type = special["type"]
if (type === undefined) {
errors.push(
context.err(
"A 'special'-block should define 'type' to indicate which visualisation should be used"
)
return undefined
@ -893,37 +895,35 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
SpecialVisualizations.specialVisualizations,
(sp) => sp.funcName
)
errors.push(
context.err(
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
)
return undefined
}
errors.push(
...Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `At ${context}: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
)
Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
.forEach((e) => context.err(e))
const argNamesList = vis.args.map((a) => a.name)
const argNames = new Set<string>(argNamesList)
// Check for obsolete and misspelled arguments
errors.push(
...Object.keys(special)
.filter((k) => !argNames.has(k))
.filter((k) => k !== "type" && k !== "before" && k !== "after")
.map((wrongArg) => {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x
)
return `At ${context}: Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
)
Object.keys(special)
.filter((k) => !argNames.has(k))
.filter((k) => k !== "type" && k !== "before" && k !== "after")
.map((wrongArg) => {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x
)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
.forEach((e) => context.err(e))
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
for (const arg of vis.args) {
@ -932,10 +932,8 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
const param = special[arg.name]
if (param === undefined) {
errors.push(
`At ${context}: Obligated parameter '${
arg.name
}' in special rendering of type ${
context.err(
`Obligated parameter '${arg.name}' in special rendering of type ${
vis.funcName
} not found.\n The full special rendering specification is: '${JSON.stringify(
input
@ -1014,7 +1012,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* }
* ]
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]}
* result // => expected
*
@ -1022,7 +1020,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'en': "Some introduction{image_carousel(image)}"}}
* result // => expected
*
@ -1030,12 +1028,11 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, after: {en: "Some footer"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
* result // => expected
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
const errors = []
json = Utils.Clone(json)
const paths: ConfigMeta[] = tagrenderingconfigmeta
for (const path of paths) {
@ -1043,7 +1040,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
continue
}
Utils.WalkPath(path.path, json, (leaf, travelled) =>
RewriteSpecial.convertIfNeeded(leaf, errors, context + ":" + travelled.join("."))
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
)
}
@ -1067,15 +1064,13 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
const iconBadges: {
if: TagConfigJson
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
}[] = []
const errs: string[] = []
const warns: string[] = []
for (let i = 0; i < badgesJson.length; i++) {
const iconBadge: {
if: TagConfigJson
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
} = badgesJson[i]
const expanded = this._expand.convert(
<QuestionableTagRenderingConfigJson>iconBadge.then,
@ -1089,7 +1084,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
iconBadges.push(
...expanded.map((resolved) => ({
if: iconBadge.if,
then: resolved,
then: <MinimalTagRenderingConfigJson>resolved,
}))
)
}
@ -1103,8 +1098,13 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
super(
"Prepares point renderings by expanding 'icon' and 'iconBadges'",
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
"marker",
new Each(
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
)
)
),
new ExpandIconBadges(state, layer)
)
@ -1189,15 +1189,17 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
convert(json: IconConfigJson, context: ConversionContext): IconConfigJson {
const expander = new ExpandTagRendering(this._state, this._layer)
const result: IconConfigJson = { icon: undefined, color: undefined }
const errors: string[] = []
const warnings: string[] = []
if (json.icon && json.icon["builtin"]) {
result.icon = expander.convert(<any>json.icon, context.enter("icon"))[0]
result.icon = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.icon, context.enter("icon"))[0]
)
} else {
result.icon = json.icon
}
if (json.color && json.color["builtin"]) {
result.color = expander.convert(<any>json.color, context.enter("color"))[0]
result.color = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.color, context.enter("color"))[0]
)
} else {
result.color = json.color
}
@ -1217,6 +1219,10 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new AddMiniMap(state),
new AddEditingElements(state),
new SetFullNodeDatabase(),
new On<
(LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
LayerConfigJson
>("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering",
(layer) =>

View file

@ -172,7 +172,13 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName)
if (v === undefined) {
context.err("Default layer " + layerName + " not found")
context.err(
"Default layer " +
layerName +
" not found. " +
state.sharedLayers.size +
" layers are available"
)
continue
}
if (alreadyLoaded.has(v.id)) {

View file

@ -1,4 +1,13 @@
import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
import {
Conversion,
ConversionContext,
DesugaringStep,
Each,
Fuse,
On,
Pipe,
Pure,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
@ -254,7 +263,15 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
super(
"Validates a theme and the contained layers",
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
new On("layers", new Each(new ValidateLayer(undefined, isBuiltin, doesImageExist)))
new On(
"layers",
new Each(
new Pipe(
new ValidateLayer(undefined, isBuiltin, doesImageExist),
new Pure((x) => x.raw)
)
)
)
)
}
}
@ -410,9 +427,10 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* }
* ]
* }
* const r = new DetectShadowedMappings().convert(tr, "test");
* r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value is fully matched by a previous mapping (namely 0)") >= 0 // => true
* const context = ConversionContext.test()
* const r = new DetectShadowedMappings().convert(tr, context);
* context.getAll("error").length // => 1
* context.getAll("error")[0].message.indexOf("The mapping key=value is fully matched by a previous mapping (namely 0)") >= 0 // => true
*
* const tr = {mappings: [
* {
@ -425,9 +443,10 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* }
* ]
* }
* const r = new DetectShadowedMappings().convert(tr, "test");
* r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
* const context = ConversionContext.test()
* const r = new DetectShadowedMappings().convert(tr, context);
* context.getAll("error").length // => 1
* context.getAll("error")[0].message.indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
@ -510,6 +529,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
}
/**
* const context = ConversionContext.test()
* const r = new DetectMappingsWithImages(new DoesImageExist(new Set<string>())).convert({
* "mappings": [
* {
@ -525,9 +545,9 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* "zh_Hant": "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
* }
* }]
* }, "test");
* r.errors.length > 0 // => true
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
* }, context);
* context.hasErrors() // => true
* context.getAll("error").some(msg => msg.message.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
@ -682,7 +702,10 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
}
}
export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
> {
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
@ -698,7 +721,10 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
this._doesImageExist = doesImageExist
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(
json: LayerConfigJson,
context: ConversionContext
): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name)
if (typeof json === "string") {
context.err("This layer hasn't been expanded: " + json)
@ -887,15 +913,27 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
{
const hasCondition = json.pointRendering?.filter(
(mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined
)
if (hasCondition?.length > 0) {
context.err(
"One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
JSON.stringify(hasCondition, null, " ")
)
}
json.pointRendering?.forEach((pointRendering, index) => {
pointRendering?.marker?.forEach((icon, indexM) => {
if (!icon.icon) {
return
}
if (icon.icon["condition"]) {
context
.enters(
"pointRendering",
index,
"marker",
indexM,
"icon",
"condition"
)
.err(
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
)
}
})
})
}
if (json.presets !== undefined) {
@ -927,10 +965,10 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
} catch (e) {
context.err(e)
context.err("Could not validate layer due to: " + e + e.stack)
}
return json
return { raw: json, parsed: layerConfig }
}
}

View file

@ -1,4 +1,4 @@
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./TagRenderingConfigJson"
import { TagConfigJson } from "./TagConfigJson"
export interface IconConfigJson {
@ -7,13 +7,13 @@ export interface IconConfigJson {
* type: icon
* suggestions: return ["pin","square","circle","checkmark","clock","close","crosshair","help","home","invalid","location","location_empty","location_locked","note","resolved","ring","scissors","teardrop","teardrop_with_hole_green","triangle"].map(i => ({if: "value="+i, then: i, icon: i}))
*/
icon: string | TagRenderingConfigJson | { builtin: string; override: any }
icon: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
/**
* question: What colour should the icon be?
* This will only work for the default icons such as `pin`,`circle`,...
* type: color
*/
color?: string | TagRenderingConfigJson | { builtin: string; override: any }
color?: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
}
/**
@ -57,7 +57,7 @@ export default interface PointRenderingConfigJson {
* Badge to show
* Type: icon
*/
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
}[]
/**

View file

@ -1,6 +1,7 @@
import { TagConfigJson } from "./TagConfigJson"
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import type { Translatable } from "./Translatable"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
export interface MappingConfigJson {
/**
@ -244,6 +245,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* ifunset: do not prefill the textfield
*/
default?: string
/**
* question: What values of the freeform key should be interpreted as 'unknown'?
* For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked
* ifunset: The question will be considered answered if any value is set for the key
*/
invalidValues?: string[]
}
/**

View file

@ -42,7 +42,7 @@ export class VariableUiElement extends BaseUIElement {
el.removeChild(el.lastChild)
}
if (contents === undefined) {
if (contents === undefined || contents === null) {
return
}
if (typeof contents === "string") {
@ -54,11 +54,13 @@ export class VariableUiElement extends BaseUIElement {
el.appendChild(c)
}
}
} else {
} else if (contents.ConstructElement) {
const c = contents.ConstructElement()
if (c !== undefined && c !== null) {
el.appendChild(c)
}
} else {
console.error("Could not construct a variable UI element for", contents)
}
})
return el

View file

@ -11,7 +11,8 @@
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
let state = new EditLayerState(layerSchema);
export let initialLayerConfig: Partial<LayerConfigJson> = {}
const messages = state.messages;
export let initialLayerConfig: Partial<LayerConfigJson> = {};
state.configuration.setData(initialLayerConfig);
const configuration = state.configuration;
new LayerStateSender("http://localhost:1235", state);
@ -19,7 +20,7 @@
* Blacklist of regions for the general area tab
* These are regions which are handled by a different tab
*/
const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title","linerendering","pointrendering"];
const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title", "linerendering", "pointrendering"];
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
const perRegion: Record<string, ConfigMeta[]> = {};
@ -49,7 +50,7 @@
<div slot="title1">Information panel (questions and answers)</div>
<div slot="content1">
<Region configs={perRegion["title"]} {state} title="Popup title" />
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents"/>
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
<Region configs={perRegion["editing"]} {state} title="Other editing elements" />
</div>
@ -58,7 +59,7 @@
<Region configs={perRegion["linerendering"]} {state} />
<Region configs={perRegion["pointrendering"]} {state} />
</div>
<div slot="title3">Advanced functionality</div>
<div slot="content3">
<Region configs={perRegion["advanced"]} {state} />
@ -73,6 +74,12 @@
<div class="literal-code">
{JSON.stringify($configuration, null, " ")}
</div>
{#each $messages as message}
<li>
<span class="literal-code">{message.context.path.join(".")}</span>
{message.message}
</li>
{/each}
</div>
</TabbedGroup>

View file

@ -3,6 +3,16 @@ import { ConfigMeta } from "./configMeta"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import {
ConversionContext,
ConversionMessage,
DesugaringContext,
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
/**
* Sends changes back to the server
@ -16,7 +26,7 @@ export class LayerStateSender {
console.log("No id found in layer, not updating")
return
}
const response = await fetch(`${serverLocation}/layers/${id}/${id}.json`, {
const fresponse = await fetch(`${serverLocation}/layers/${id}/${id}.json`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
@ -36,6 +46,7 @@ export default class EditLayerState {
public readonly configuration: UIEventSource<Partial<LayerConfigJson>> = new UIEventSource<
Partial<LayerConfigJson>
>({})
public readonly messages: Store<ConversionMessage[]>
constructor(schema: ConfigMeta[]) {
this.schema = schema
@ -49,6 +60,30 @@ export default class EditLayerState {
this.featureSwitches = {
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
}
let state: DesugaringContext
{
const layers = AllSharedLayers.getSharedLayersConfigs()
const questions = layers.get("questions")
const sharedQuestions = new Map<string, QuestionableTagRenderingConfigJson>()
for (const question of questions.tagRenderings) {
sharedQuestions.set(question["id"], <QuestionableTagRenderingConfigJson>question)
}
state = {
tagRenderings: sharedQuestions,
sharedLayers: layers,
}
}
this.messages = this.configuration.map((config) => {
const context = ConversionContext.construct([], ["prepare"])
const prepare = new Pipe(
new PrepareLayer(state),
new ValidateLayer("dynamic", false, undefined)
)
prepare.convert(<LayerConfigJson>config, context)
console.log(context.messages)
return context.messages
})
console.log("Configuration store:", this.configuration)
}

View file

@ -85,11 +85,11 @@
);
}
const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
let chosenOption: number = writable(defaultOption);
let chosenOption: number = (defaultOption);
const existingValue = state.getCurrentValueFor(path);
console.log("Initial value is", existingValue);
console.log("Initial, existing value for", path.join(".") ,"is", existingValue);
if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) {
tags.setData({ value: "" + existingValue });
} else if (lastIsString && typeof existingValue === "string") {
@ -135,6 +135,8 @@
}
} else if (defaultOption !== undefined) {
tags.setData({ chosen_type_index: "" + defaultOption });
}else{
chosenOption = defaultOption
}
if (hasBooleanOption >= 0 || lastIsString) {
@ -156,8 +158,9 @@
let subpath = path;
console.log("Initial chosen option for",path.join("."),"is", chosenOption);
onDestroy(tags.addCallbackAndRun(tags => {
if (tags["value"] !== "") {
if (tags["value"] !== undefined && tags["value"] !== "") {
chosenOption = undefined;
console.log("Resetting chosenOption as `value` is present in the tags:", tags["value"])
return;
}
const oldOption = chosenOption;
@ -214,4 +217,5 @@
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each}
{/if}
{chosenOption}
</div>

View file

@ -22,7 +22,7 @@ export let state: EditLayerState;
export let schema: ConfigMeta;
export let path: (string | number)[];
let value = state.getCurrentValueFor(path);
let value = state.getCurrentValueFor(path) ;
let mappingsBuiltin: MappingConfigJson[] = [];
for (const tr of questions.tagRenderings) {
@ -65,7 +65,6 @@ function initMappings() {
}
const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && schema.path[0] === "freeform");
console.log("FreeformSchema:", freeformSchema)
</script>
{#if typeof value === "string"}
@ -105,11 +104,5 @@ console.log("FreeformSchema:", freeformSchema)
<Region {state} {path} configs={freeformSchema}/>
<!-- {JSON.stringify(state.getCurrentValueFor(path))} <!-->
</div>
<!--
<Region configs={freeformSchema} {state} path={[...path, "freeform"]} /> -->
{/if}

View file

@ -27,7 +27,7 @@
if (layerId === "") {
return;
}
if (layers.data.has(layerId)) {
if (layers.data?.has(layerId)) {
layerIdFeedback.setData("This id is already used");
}
}, [layers]);
@ -41,6 +41,15 @@
return icon;
}
async function createNewLayer(){
state = "loading"
const id = newLayerId.data
const createdBy = osmConnection.userDetails.data.name
const loaded = await studio.fetchLayer(id, true)
initialLayerConfig = loaded ?? {id, credits: createdBy};
state = "editing_layer"}
let osmConnection = new OsmConnection( new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
@ -91,23 +100,29 @@
{/each}
</div>
{:else if state === "new_layer"}
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} />
<div class="interactive flex m-2 rounded-2xl flex-col p-2">
<h3>Enter the ID for the new layer</h3>
A good ID is:
<ul>
<li>a noun</li>
<li>singular</li>
<li>describes the object</li>
<li>in English</li>
</ul>
<div class="m-2 p-2 w-full">
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()}/>
</div>
{#if $layerIdFeedback !== undefined}
<div class="alert">
{$layerIdFeedback}
</div>
{:else }
<NextButton on:click={async () => {
state = "loading"
const id = newLayerId.data
const createdBy = osmConnection.userDetails.data.name
const loaded = await studio.fetchLayer(id, true)
initialLayerConfig = loaded ?? {id, credits: createdBy};
state = "editing_layer"}}>
Create this layer
<NextButton clss="primary" on:click={() => createNewLayer()}>
Create layer {$newLayerId}
</NextButton>
{/if}
</div>
{:else if state === "loading"}
<div class="w-8 h-8">
<Loading />

View file

@ -12135,6 +12135,13 @@
"default": {
"description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string"
},
"invalidValues": {
"description": "question: What values of the freeform key should be interpreted as 'unknown'?\nFor example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked\nifunset: The question will be considered answered if any value is set for the key",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@ -12982,6 +12989,20 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"tagRenderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"tagRenderings",
@ -14021,6 +14042,21 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"tagRenderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"tagRenderings",
@ -15084,6 +15120,21 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"tagRenderings",
"renderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"tagRenderings",
@ -16165,6 +16216,22 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"tagRenderings",
"renderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"tagRenderings",

View file

@ -692,6 +692,13 @@
"default": {
"description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string"
},
"invalidValues": {
"description": "question: What values of the freeform key should be interpreted as 'unknown'?\nFor example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked\nifunset: The question will be considered answered if any value is set for the key",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@ -13598,6 +13605,13 @@
"default": {
"description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string"
},
"invalidValues": {
"description": "question: What values of the freeform key should be interpreted as 'unknown'?\nFor example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked\nifunset: The question will be considered answered if any value is set for the key",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@ -14472,6 +14486,21 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"tagRenderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -15553,6 +15582,22 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"tagRenderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -16659,6 +16704,22 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"tagRenderings",
"renderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -17782,6 +17843,23 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"tagRenderings",
"renderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -31866,6 +31944,13 @@
"default": {
"description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string"
},
"invalidValues": {
"description": "question: What values of the freeform key should be interpreted as 'unknown'?\nFor example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked\nifunset: The question will be considered answered if any value is set for the key",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@ -32767,6 +32852,22 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"override",
"tagRenderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -33890,6 +33991,23 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"override",
"tagRenderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -35039,6 +35157,23 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"override",
"tagRenderings",
"renderings",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",
@ -36204,6 +36339,24 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"layers",
"override",
"tagRenderings",
"renderings",
"override",
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"layers",

View file

@ -629,6 +629,19 @@
"type": "string",
"description": "This can help people to quickly enter the most common option"
},
{
"path": [
"freeform",
"invalidValues"
],
"required": false,
"hints": {
"question": "What values of the freeform key should be interpreted as 'unknown'?",
"ifunset": "The question will be considered answered if any value is set for the key"
},
"type": "array",
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
},
{
"path": [
"question"