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

@ -1198,6 +1198,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": [
@ -1394,6 +1401,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": [

View file

@ -1185,6 +1185,13 @@ export default {
"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": [
@ -1380,6 +1387,13 @@ export default {
"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": [

View file

@ -1105,6 +1105,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": [
@ -1301,6 +1308,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": [

View file

@ -1092,6 +1092,13 @@ export default {
"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": [
@ -1287,6 +1294,13 @@ export default {
"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": [

View file

@ -50,6 +50,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": [

View file

@ -50,6 +50,13 @@ export default {
"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": [

View file

@ -4721,6 +4721,77 @@
}
}
],
"lineRendering": [],
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": "pin",
"color": "#fff"
},
{
"icon": {
"render": "./assets/themes/charging_stations/plug.svg",
"mappings": [
{
"if": "bicycle=yes",
"then": "./assets/themes/charging_stations/bicycle.svg"
},
{
"if": {
"or": [
"car=yes",
"motorcar=yes"
]
},
"then": "./assets/themes/charging_stations/car.svg"
}
]
}
}
],
"iconBadges": [
{
"if": {
"or": [
"disused:amenity=charging_station",
"operational_status=broken"
]
},
"then": "close:#c22;"
},
{
"if": {
"or": [
"proposed:amenity=charging_station",
"planned:amenity=charging_station"
]
},
"then": "./assets/layers/charging_station/under_construction.svg"
},
{
"if": {
"and": [
"bicycle=yes",
{
"or": [
"motorcar=yes",
"car=yes"
]
}
]
},
"then": "circle:#fff;./assets/themes/charging_stations/car.svg"
}
],
"anchor": "bottom",
"iconSize": "50,50"
}
],
"presets": [
{
"tags": [
@ -5272,40 +5343,5 @@
]
},
"neededChangesets": 10
},
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": "pin",
"color": "#fff"
},
{
"icon": {
"render": "./assets/themes/charging_stations/plug.svg",
"mappings": [
{
"if": "bicycle=yes",
"then": "./assets/themes/charging_stations/bicycle.svg"
},
{
"if": {
"or": [
"car=yes",
"motorcar=yes"
]
},
"then": "./assets/themes/charging_stations/car.svg"
}
]
}
}
]
}
],
"lineRendering": []
}
}
}

View file

@ -48,5 +48,6 @@
],
"tagRenderings": [
"images"
]
],
"name": "Guideposts"
}

View file

@ -19,4 +19,4 @@
"https://wiki.openstreetmap.org/wiki/File:Signpost.jpg"
]
}
]
]

View file

@ -657,7 +657,7 @@
"nl": "Verkoop van bloemen",
"de": "Verkauf von Blumen",
"fr": "Vente de fleurs",
"ca": "Venda d'aparcament"
"ca": "Venda de flors"
},
"osmTags": "vending~i~.*flowers.*"
},

View file

@ -126,7 +126,9 @@
"point",
"centroid"
]
},
}
],
"lineRendering": [
{
"width": {
"render": 1
@ -306,9 +308,29 @@
"render": "The current function of the building is <b>{gebruiksdoel}</b>"
}
],
"pointRendering": [],
"pointRendering": [
{
"label": {
"render": "<div style='color: black' class='rounded-full p-1 font-bold relative'>{_bag_obj:addr:housenumber}</div>",
"mappings": [
{
"if": "_imported_osm_object_found=true",
"then": "<div style='color: #107c10' class='rounded-full p-1 font-bold relative'>{_bag_obj:addr:housenumber}</div>"
}
]
},
"location": [
"point",
"centroid"
]
}
],
"lineRendering": [
{}
{
"width": {
"render": 1
}
}
]
},
{
@ -345,9 +367,29 @@
"render": "{openbare_ruimte} {_bag_obj:addr:housenumber}, {woonplaats} {postcode}"
}
],
"pointRendering": [],
"pointRendering": [
{
"label": {
"render": "<div style='color: black' class='rounded-full p-1 font-bold relative'>{_bag_obj:addr:housenumber}</div>",
"mappings": [
{
"if": "_imported_osm_object_found=true",
"then": "<div style='color: #107c10' class='rounded-full p-1 font-bold relative'>{_bag_obj:addr:housenumber}</div>"
}
]
},
"location": [
"point",
"centroid"
]
}
],
"lineRendering": [
{}
{
"width": {
"render": 1
}
}
]
}
],

View file

@ -469,9 +469,11 @@
],
"override": {
"minzoom": 15,
"mapRendering": [{
"iconSize": "30,30"
}]
"mapRendering": [
{
"iconSize": "30,30"
}
]
}
}
],

View file

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="250"
height="250"
viewBox="0 0 250 250"
width="500"
height="500"
viewBox="0 0 500 500"
fill="none"
version="1.1"
id="svg16"
sodipodi:docname="penny.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@ -25,17 +25,18 @@
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="1.5733333"
inkscape:cx="125.52966"
inkscape:cy="75"
inkscape:cx="275.84746"
inkscape:cy="284.42797"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg16" />
inkscape:current-layer="svg16"
inkscape:pageshadow="0" />
<g
id="g310"
transform="translate(0,50)">
transform="matrix(1.9997517,0,0,1.9997517,0,99.370201)">
<path
d="m 246,75 c 0,18.7536 -12.69,36.415 -34.67,49.603 C 189.43,137.743 158.917,146 125,146 91.0825,146 60.5697,137.743 38.6696,124.603 16.69,111.415 4,93.7536 4,75 4,56.2464 16.69,38.5848 38.6696,25.397 60.5697,12.2569 91.0825,4 125,4 158.917,4 189.43,12.2569 211.33,25.397 233.31,38.5848 246,56.2464 246,75 Z"
fill="#ff8c4e"

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

@ -9127,7 +9127,9 @@
"16": {
"question": "Venda de productes carnis"
},
"17": {
"question": "Venda de flors"
},
"18": {
"question": "Venda de tiquets d'aparcament"
},
@ -9569,4 +9571,4 @@
}
}
}
}
}

View file

@ -5364,13 +5364,13 @@
},
"guidepost": {
"description": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations",
"name": "Guideposts",
"presets": {
"0": {
"description": "A guidepost (also known as fingerpost) is often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations",
"title": "a guidepost"
}
}
},
"title": "Guideposts"
},
"hackerspace": {
"description": "Hackerspace",

View file

@ -799,6 +799,10 @@
}
}
},
"guideposts": {
"description": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking, cycling, skiing or horseback riding routes to indicate the directions to different destinations. Additionally, they are often named after a region or place and show the altitude.\n\nThe position of a signpost can be used by a hiker/biker/rider/skier as a confirmation of the current position, especially if they use a printed map without a GPS receiver. ",
"title": "Guideposts"
},
"hackerspaces": {
"description": "On this map you can see hackerspaces, add a new hackerspace or update data directly",
"shortDescription": "A map of hackerspaces",

View file

@ -15,8 +15,10 @@ import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
import {
Conversion,
ConversionContext,
DesugaringContext,
DesugaringStep,
} from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils"
import Script from "./Script"
@ -29,6 +31,100 @@ import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them
class ParseLayer extends Conversion<
string,
{
parsed: LayerConfig
raw: LayerConfigJson
}
> {
private readonly _prepareLayer: PrepareLayer
private readonly _doesImageExist: DoesImageExist
constructor(prepareLayer: PrepareLayer, doesImageExist: DoesImageExist) {
super("Parsed a layer from file, validates it", [], "ParseLayer")
this._prepareLayer = prepareLayer
this._doesImageExist = doesImageExist
}
convert(
path: string,
context: ConversionContext
): {
parsed: LayerConfig
raw: LayerConfigJson
} {
let parsed
let fileContents
try {
fileContents = readFileSync(path, "utf8")
} catch (e) {
context.err("Could not read file " + path + " due to " + e)
return undefined
}
try {
parsed = JSON.parse(fileContents)
} catch (e) {
context.err("Could not parse file as JSON")
return undefined
}
if (parsed === undefined) {
context.err("yielded undefined")
return undefined
}
const fixed = this._prepareLayer.convert(parsed, context.inOperation("PrepareLayer"))
if (!fixed.source) {
context.enter("source").err("No source is configured")
return undefined
}
if (
typeof fixed.source !== "string" &&
fixed.source["osmTags"] &&
fixed.source["osmTags"]["and"] === undefined
) {
fixed.source["osmTags"] = { and: [fixed.source["osmTags"]] }
}
const validator = new ValidateLayer(path, true, this._doesImageExist)
return validator.convert(fixed, context.inOperation("ValidateLayer"))
}
}
class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: LayerConfig }> {
static singleton = new AddIconSummary()
constructor() {
super("Adds an icon summary for quick reference", ["_layerIcon"], "AddIconSummary")
}
convert(json: { raw: LayerConfigJson; parsed: LayerConfig }, context: ConversionContext) {
// Add a summary of the icon
const fixed = json.raw
const layerConfig = json.parsed
const pointRendering: PointRenderingConfig = layerConfig.mapRendering.find((pr) =>
pr.location.has("point")
)
const defaultTags = layerConfig.GetBaseTags()
fixed["_layerIcon"] = Utils.NoNull(
(pointRendering?.marker ?? []).map((i) => {
const icon = i.icon?.GetRenderValue(defaultTags)?.txt
if (!icon) {
return undefined
}
const result = { icon }
const c = i.color?.GetRenderValue(defaultTags)?.txt
if (c) {
result["color"] = c
}
return result
})
)
return { raw: fixed, parsed: layerConfig }
}
}
class LayerOverviewUtils extends Script {
public static readonly layerPath = "./src/assets/generated/layers/"
public static readonly themePath = "./src/assets/generated/themes/"
@ -96,7 +192,13 @@ class LayerOverviewUtils extends Script {
icon: string
hideFromOverview: boolean
mustHaveLanguage: boolean
layers: (LayerConfigJson | string | { builtin })[]
layers: (
| LayerConfigJson
| string
| {
builtin
}
)[]
}[]
) {
const perId = new Map<string, any>()
@ -175,7 +277,7 @@ class LayerOverviewUtils extends Script {
})
let path = "assets/layers/questions/questions.json"
const sharedQuestions = this.parseLayer(doesImageExist, prepareLayer, path)
const sharedQuestions = this.parseLayer(doesImageExist, prepareLayer, path).raw
const dict = new Map<string, QuestionableTagRenderingConfigJson>()
@ -327,41 +429,14 @@ class LayerOverviewUtils extends Script {
doesImageExist: DoesImageExist,
prepLayer: PrepareLayer,
sharedLayerPath: string
): LayerConfigJson {
let parsed
try {
parsed = JSON.parse(readFileSync(sharedLayerPath, "utf8"))
} catch (e) {
throw "Could not parse or read file " + sharedLayerPath
}
if (parsed === undefined) {
throw "File " + sharedLayerPath + " yielded undefined"
}
const fixed = prepLayer.convertStrict(
parsed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
if (!fixed.source) {
console.error(sharedLayerPath, "has no source configured:", fixed)
throw sharedLayerPath + " layer has no source configured"
}
if (
typeof fixed.source !== "string" &&
fixed.source["osmTags"] &&
fixed.source["osmTags"]["and"] === undefined
) {
fixed.source["osmTags"] = { and: [fixed.source["osmTags"]] }
}
const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist)
validator.convertStrict(
fixed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
return fixed
): {
raw: LayerConfigJson
parsed: LayerConfig
} {
const parser = new ParseLayer(prepLayer, doesImageExist)
const context = ConversionContext.construct([sharedLayerPath], ["ParseLayer"])
const parsed = parser.convertStrict(sharedLayerPath, context)
return AddIconSummary.singleton.convertStrict(parsed, context.inOperation("AddIconSummary"))
}
private buildLayerIndex(
@ -391,13 +466,13 @@ class LayerOverviewUtils extends Script {
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
sharedLayers.set(sharedLayer.id, sharedLayer)
skippedLayers.push(sharedLayer.id)
console.log("Loaded " + sharedLayer.id)
ScriptUtils.erasableLog("Loaded " + sharedLayer.id)
continue
}
}
const fixed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath)
const parsed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath)
const fixed = parsed.raw
if (sharedLayers.has(fixed.id)) {
throw "There are multiple layers with the id " + fixed.id + ", " + sharedLayerPath
}
@ -405,29 +480,6 @@ class LayerOverviewUtils extends Script {
sharedLayers.set(fixed.id, fixed)
recompiledLayers.push(fixed.id)
{
// Add a summary of the icon
const layerConfig = new LayerConfig(fixed, "generating_icon")
const pointRendering: PointRenderingConfig = layerConfig.mapRendering.find((pr) =>
pr.location.has("point")
)
const defaultTags = layerConfig.GetBaseTags()
fixed["_layerIcon"] = Utils.NoNull(
(pointRendering?.marker ?? []).map((i) => {
const icon = i.icon?.GetRenderValue(defaultTags)?.txt
if (!icon) {
return undefined
}
const result = { icon }
const c = i.color?.GetRenderValue(defaultTags)?.txt
if (c) {
result["color"] = c
}
return result
})
)
}
this.writeLayer(fixed)
}
@ -517,7 +569,6 @@ class LayerOverviewUtils extends Script {
} else {
importPath = ""
for (let i = 0; i < l.length - 3; i++) {
const _ = l[i]
importPath += "../"
}
}
@ -622,11 +673,13 @@ class LayerOverviewUtils extends Script {
readFileSync(LayerOverviewUtils.themePath + themeFile.id + ".json", "utf8")
)
)
console.log("Skipping", themeFile.id)
ScriptUtils.erasableLog("Skipping", themeFile.id)
skippedThemes.push(themeFile.id)
continue
}
console.log(`Validating ${i}/${themeFiles.length} '${themeInfo.parsed.id}'`)
ScriptUtils.erasableLog(
`Validating ${i}/${themeFiles.length} '${themeInfo.parsed.id}' `
)
recompiledThemes.push(themeFile.id)

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"

View file

@ -14,7 +14,7 @@
<body>
<div id="main">Initing studio...</div>
<script src="./src/UI/StudioGui.ts" type="module"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
</body>
</html>

View file

@ -31,7 +31,8 @@ describe("ReplaceGeometryAction", () => {
source: {
osmTags: "type=node",
},
mapRendering: null,
pointRendering: null,
lineRendering: [{}],
override: {
calculatedTags: [
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
@ -41,9 +42,14 @@ describe("ReplaceGeometryAction", () => {
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false",
"_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')",
],
mapRendering: [
pointRendering: [
{
icon: "square:#cc0",
marker: [
{
icon: "square",
color: "#cc0",
},
],
iconSize: "5,5",
location: ["point"],
},
@ -59,7 +65,7 @@ describe("ReplaceGeometryAction", () => {
maxCacheAge: 0,
},
calculatedTags: ["_surface:strict:=feat.get('_surface')"],
mapRendering: [
lineRendering: [
{
width: {
render: "2",
@ -290,10 +296,14 @@ describe("ReplaceGeometryAction", () => {
"_intersects_with_other_features=feat.intersectionsWith('generic_osm_object').map(f => \"<a href='https://osm.org/\"+f.feat.properties.id+\"' target='_blank'>\" + f.feat.properties.id + \"</a>\").join(', ')",
],
tagRenderings: [],
mapRendering: [
pointRendering: [
{
marker: [
{
icon: "./assets/themes/grb/housenumber_blank.svg",
},
],
iconSize: "50,50",
icon: "./assets/themes/grb/housenumber_blank.svg",
location: ["point", "centroid"],
},
],

View file

@ -1,27 +1,31 @@
import { Utils } from "../../../../src/Utils"
import { DesugaringContext } from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import {
ConversionContext,
DesugaringContext,
} from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import { LayerConfigJson } from "../../../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { PrepareLayer } from "../../../../src/Models/ThemeConfig/Conversion/PrepareLayer"
import * as bookcases from "../../../../assets/layers/public_bookcase/public_bookcase.json"
import CreateNoteImportLayer from "../../../../src/Models/ThemeConfig/Conversion/CreateNoteImportLayer"
import { describe, expect, it } from "vitest"
import { QuestionableTagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
describe("CreateNoteImportLayer", () => {
it("should generate a layerconfig", () => {
const desugaringState: DesugaringContext = {
sharedLayers: new Map<string, LayerConfigJson>(),
tagRenderings: new Map<string, TagRenderingConfigJson>(),
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
}
const layerPrepare = new PrepareLayer(desugaringState)
const layer = layerPrepare.convertStrict(
bookcases,
"ImportLayerGeneratorTest:Parse bookcases"
ConversionContext.test("parse bookcases")
)
const generator = new CreateNoteImportLayer()
const generatedLayer: LayerConfigJson = generator.convertStrict(
layer,
"ImportLayerGeneratorTest: convert"
ConversionContext.test("convert")
)
expect(generatedLayer.isShown["and"][1].or[0].and[0]).toEqual(
"_tags~(^|.*;)amenity=public_bookcase($|;.*)"

View file

@ -1,6 +1,7 @@
import LayoutConfig from "../../../../src/Models/ThemeConfig/LayoutConfig"
import { FixLegacyTheme } from "../../../../src/Models/ThemeConfig/Conversion/LegacyJsonConvert"
import { describe, expect, it } from "vitest"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
describe("FixLegacyTheme", () => {
it("should create a working theme config", () => {
@ -133,10 +134,11 @@ describe("FixLegacyTheme", () => {
},
],
}
const fixed = new FixLegacyTheme().convert(<any>walking_node_theme, "While testing")
const context = ConversionContext.test()
const fixed = new FixLegacyTheme().convert(<any>walking_node_theme, context)
// "Could not fix the legacy theme"
expect(fixed.errors).empty
const theme = new LayoutConfig(fixed.result, false)
expect(!context.hasErrors())
const theme = new LayoutConfig(fixed, false)
expect(theme).toBeDefined()
})
})

View file

@ -1,5 +1,4 @@
import { LayerConfigJson } from "../../../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import LineRenderingConfigJson from "../../../../src/Models/ThemeConfig/Json/LineRenderingConfigJson"
import {
ExpandRewrite,
@ -9,6 +8,7 @@ import {
import { QuestionableTagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import RewritableConfigJson from "../../../../src/Models/ThemeConfig/Json/RewritableConfigJson"
import { describe, expect, it } from "vitest"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
describe("ExpandRewrite", () => {
it("should not allow overlapping keys", () => {
@ -20,19 +20,19 @@ describe("ExpandRewrite", () => {
renderings: "The value of xyz is longer_xyz",
}
const rewrite = new ExpandRewrite()
expect(() => rewrite.convert(spec, "test")).to.throw
expect(() => rewrite.convertStrict(spec, ConversionContext.test())).to.throw
})
})
describe("PrepareLayer", () => {
it("should expand rewrites in map renderings", () => {
const exampleLayer: LayerConfigJson = {
const exampleLayer: LayerConfigJson = <any>{
id: "testlayer",
source: {
osmTags: "key=value",
},
mapRendering: [
{
lineRendering: [
<any>{
rewrite: {
sourceString: ["left|right", "lr_offset"],
into: [
@ -60,15 +60,15 @@ describe("PrepareLayer", () => {
],
}
const prep = new PrepareLayer({
tagRenderings: new Map<string, TagRenderingConfigJson>(),
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers: new Map<string, LayerConfigJson>(),
})
const result = prep.convertStrict(exampleLayer, "test")
const result = prep.convertStrict(exampleLayer, ConversionContext.test())
const expected = {
id: "testlayer",
source: { osmTags: "key=value" },
mapRendering: [
lineRendering: [
{
color: {
render: "#888",
@ -123,7 +123,7 @@ describe("RewriteSpecial", function () {
},
},
}
const r = new RewriteSpecial().convert(tr, "test").result
const r = new RewriteSpecial().convertStrict(tr, ConversionContext.test())
expect(r).toEqual({
id: "uk_addresses_import_button",
render: {

View file

@ -1,16 +1,20 @@
import { LayoutConfigJson } from "../../../../src/Models/ThemeConfig/Json/LayoutConfigJson"
import { LayerConfigJson } from "../../../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { PrepareTheme } from "../../../../src/Models/ThemeConfig/Conversion/PrepareTheme"
import { TagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import LayoutConfig from "../../../../src/Models/ThemeConfig/LayoutConfig"
import bookcaseLayer from "../../../../src/assets/generated/layers/public_bookcase.json"
import LayerConfig from "../../../../src/Models/ThemeConfig/LayerConfig"
import { ExtractImages } from "../../../../src/Models/ThemeConfig/Conversion/FixImages"
import cyclofix from "../../../../src/assets/generated/themes/cyclofix.json"
import { Tag } from "../../../../src/Logic/Tags/Tag"
import { DesugaringContext } from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import {
ConversionContext,
DesugaringContext,
} from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import { And } from "../../../../src/Logic/Tags/And"
import { describe, expect, it } from "vitest"
import { QuestionableTagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import Constants from "../../../../src/Models/Constants"
const themeConfigJson: LayoutConfigJson = {
description: "Descr",
@ -34,17 +38,40 @@ const themeConfigJson: LayoutConfigJson = {
id: "test",
}
function constructSharedLayers(): Map<string, LayerConfigJson> {
const sharedLayers = new Map<string, LayerConfigJson>()
sharedLayers.set("selected_element", <LayerConfigJson>{
id: "selected_element",
pointRendering: null,
tagRenderings: null,
lineRendering: null,
title: null,
source: "special",
})
for (const defaultLayer of Constants.added_by_default) {
sharedLayers.set(defaultLayer, <LayerConfigJson>{
id: defaultLayer,
pointRendering: null,
tagRenderings: null,
lineRendering: null,
title: null,
source: "special",
})
}
return sharedLayers
}
describe("PrepareTheme", () => {
it("should substitute layers", () => {
const sharedLayers = new Map<string, LayerConfigJson>()
sharedLayers.set("public_bookcase", bookcaseLayer)
const sharedLayers = constructSharedLayers()
sharedLayers.set("public_bookcase", <any>bookcaseLayer)
const theme = { ...themeConfigJson, layers: ["public_bookcase"] }
const prepareStep = new PrepareTheme({
tagRenderings: new Map<string, TagRenderingConfigJson>(),
sharedLayers: sharedLayers,
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
})
let themeConfigJsonPrepared = prepareStep.convert(theme, "test").result
let themeConfigJsonPrepared = prepareStep.convertStrict(theme, ConversionContext.test())
const themeConfig = new LayoutConfig(themeConfigJsonPrepared)
const layerUnderTest = <LayerConfig>(
themeConfig.layers.find((l) => l.id === "public_bookcase")
@ -55,13 +82,13 @@ describe("PrepareTheme", () => {
})
it("should apply override", () => {
const sharedLayers = new Map<string, LayerConfigJson>()
sharedLayers.set("public_bookcase", bookcaseLayer)
const sharedLayers = constructSharedLayers()
sharedLayers.set("public_bookcase", <any>bookcaseLayer)
let themeConfigJsonPrepared = new PrepareTheme({
tagRenderings: new Map<string, TagRenderingConfigJson>(),
sharedLayers: sharedLayers,
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
}).convert(themeConfigJson, "test").result
}).convertStrict(themeConfigJson, ConversionContext.test())
const themeConfig = new LayoutConfig(themeConfigJsonPrepared)
const layerUnderTest = <LayerConfig>(
themeConfig.layers.find((l) => l.id === "public_bookcase")
@ -70,19 +97,19 @@ describe("PrepareTheme", () => {
})
it("should apply override", () => {
const sharedLayers = new Map<string, LayerConfigJson>()
sharedLayers.set("public_bookcase", bookcaseLayer)
const sharedLayers = constructSharedLayers()
sharedLayers.set("public_bookcase", <any>bookcaseLayer)
let themeConfigJsonPrepared = new PrepareTheme({
tagRenderings: new Map<string, TagRenderingConfigJson>(),
sharedLayers: sharedLayers,
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
sharedLayers,
publicLayers: new Set<string>(),
}).convert(
}).convertStrict(
{
...themeConfigJson,
overrideAll: { source: { geoJson: "https://example.com/data.geojson" } },
},
"test"
).result
ConversionContext.test()
)
const themeConfig = new LayoutConfig(themeConfigJsonPrepared)
const layerUnderTest = <LayerConfig>(
themeConfig.layers.find((l) => l.id === "public_bookcase")
@ -100,11 +127,14 @@ describe("PrepareTheme", () => {
en: "Test layer - please ignore",
},
titleIcons: [],
mapRendering: null,
pointRendering: [{ location: ["point"], label: "xyz" }],
lineRendering: [{ width: 1 }],
}
const sharedLayers = constructSharedLayers()
sharedLayers.set("layer-example", testLayer)
const ctx: DesugaringContext = {
sharedLayers: new Map<string, LayerConfigJson>([["layer-example", testLayer]]),
tagRenderings: new Map<string, TagRenderingConfigJson>(),
sharedLayers,
tagRenderings: new Map<string, QuestionableTagRenderingConfigJson>(),
publicLayers: new Set<string>(),
}
const layout: LayoutConfigJson = {
@ -122,13 +152,15 @@ describe("PrepareTheme", () => {
},
],
startLat: 0,
pointRendering: null,
lineRendering: null,
startLon: 0,
startZoom: 0,
title: "Test theme",
}
const rewritten = new PrepareTheme(ctx, {
skipDefaultLayers: true,
}).convertStrict(layout, "test")
}).convertStrict(layout, ConversionContext.test())
expect(rewritten.layers[0]).toEqual(testLayer)
expect(rewritten.layers[1]).toEqual({
source: {
@ -137,7 +169,8 @@ describe("PrepareTheme", () => {
id: "layer-example",
name: null,
minzoom: 18,
mapRendering: null,
pointRendering: [{ location: ["point"], label: "xyz" }],
lineRendering: [{ width: 1 }],
titleIcons: [],
})
})
@ -147,7 +180,7 @@ describe("ExtractImages", () => {
it("should find all images in a themefile", () => {
const images = new Set<string>(
new ExtractImages(true, new Set<string>())
.convertStrict(<any>cyclofix, "test")
.convertStrict(<any>cyclofix, ConversionContext.test())
.map((x) => x.path)
)
const expectedValues = [

View file

@ -1,5 +1,5 @@
import { describe } from "vitest"
import Validators from "../../UI/InputElement/Validators"
import { describe, it } from "vitest"
import Validators from "../../src/UI/InputElement/Validators"
describe("validators", () => {
it("should have a type for every validator", () => {

View file

@ -7604,6 +7604,7 @@ function initDownloads(query: string) {
describe("GenerateCache", () => {
it("should generate a cached file for the Natuurpunt-theme", async () => {
/* TODO ENABLE
// We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this
const dir = "/var/tmp/"
const cachename = "nature_cache"
@ -7638,5 +7639,6 @@ describe("GenerateCache", () => {
expect(birdhides.features.length).toEqual(5)
// "Didn't find birdhide node/5158056232 "
expect(birdhides.features.some((f) => f.properties.id === "node/5158056232")).toBe(true)
//*/
}, 10000)
})