forked from MapComplete/MapComplete
Refactoring: use more accurate context in conversion, fix tests
This commit is contained in:
parent
86d0de3806
commit
f77d99f8ed
43 changed files with 999 additions and 367 deletions
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}[]
|
||||
|
||||
/**
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue