Studio: improvements after user test

This commit is contained in:
Pieter Vander Vennet 2023-11-02 04:35:32 +01:00
parent 449c1adb00
commit e79a0fc81d
59 changed files with 1312 additions and 2920 deletions

View file

@ -1169,7 +1169,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1177,7 +1177,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1379,7 +1387,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1387,7 +1395,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -1156,7 +1156,7 @@ export default {
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1164,7 +1164,15 @@ export default {
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1365,7 +1373,7 @@ export default {
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1373,7 +1381,15 @@ export default {
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -1081,7 +1081,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1089,7 +1089,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1291,7 +1299,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1299,7 +1307,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -1068,7 +1068,7 @@ export default {
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1076,7 +1076,15 @@ export default {
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1277,7 +1285,7 @@ export default {
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -1285,7 +1293,15 @@ export default {
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -21,7 +21,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -29,7 +29,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -21,7 +21,7 @@ export default {
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -29,7 +29,15 @@ export default {
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",

View file

@ -0,0 +1,19 @@
# User Test of the Studio
## Task
Create a simple layer specification using MapComplete studio with 'images', a few questions and an icon. The actual _topic_ of the layer can be chosen by the participant
This participant wanted to create a layer about food_sharing and give_boxes.
## Background info
User has used mapcomplete a few times before but has very little OSM-knowledge.
## Surfaced issues
- [ ] dev.mapcomplete.org crashes
- [x] Switching tagRenderings or creating them sometimes creates a 'null' value which crashes downstream: should be filtered out
- [x] Switching between editing layers does not update the title
- [x] The warning messages don't update when editing
- [x] A questionHint without a question should give an error

View file

@ -308,7 +308,6 @@
"nl": "Aanbod voor kinderen",
"en": "Bikes for children available",
"fr": "Vélos pour enfants disponibles",
"hu": "",
"it": "Sono disponibili biciclette per bambini",
"de": "Fahrräder für Kinder verfügbar",
"ru": "Доступны детские велосипеды",

View file

@ -10,17 +10,7 @@
"es": "Estaciones de carga",
"pl": "Stacje ładowania"
},
"description": {
"en": "A charging station",
"nl": "Oplaadpunten",
"ca": "Una estació de càrrega",
"cs": "Nabíjecí stanice",
"da": "En ladestation",
"de": "Eine Ladestation",
"es": "Una estación de carga",
"fr": "Une station de recharge",
"pl": "Stacja ładowania"
},
"minzoom": 10,
"source": {
"osmTags": {
"and": [
@ -35,7 +25,6 @@
]
}
},
"minzoom": 10,
"title": {
"render": {
"en": "Charging station",
@ -91,113 +80,18 @@
}
]
},
"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"
}
],
"lineRendering": [],
"presets": [
{
"tags": [
"amenity=charging_station",
"motorcar=no",
"bicycle=yes",
"socket:typee=1"
],
"title": {
"en": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)",
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)",
"ca": "una estació de càrrega per a bicicletes elèctriques amb un endoll de paret europeu normal<img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (destinat a carregar bicicletes elèctriques)",
"cs": "nabíjecí stanice pro elektrokola s běžnou evropskou zástrčkou <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (určeno k nabíjení elektrických kol)",
"da": "en ladestation til elektriske cykler med et normalt europæisk vægstik <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (beregnet til opladning af elektriske cykler)",
"de": "eine Ladestation für Elektrofahrräder mit einer normalen europäischen Steckdose <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (zum Laden von Elektrofahrrädern)",
"es": "una estación de carga para bicicletas eléctricas con un enchufe de pared europeo normal <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (pensado para cargar bicicletas eléctricas)"
}
},
{
"tags": [
"amenity=charging_station",
"motorcar=yes",
"bicycle=no"
],
"title": {
"en": "a charging station for cars",
"nl": "een oplaadstation voor elektrische auto's",
"ca": "una estació de càrrega per a cotxes",
"cs": "nabíjecí stanice pro automobily",
"da": "en ladestation til biler",
"de": "Eine Ladestation für Elektrofahrzeuge",
"es": "una estación de carga para coches",
"pl": "stacja ładowania dla samochodów"
}
}
],
"description": {
"en": "A charging station",
"nl": "Oplaadpunten",
"ca": "Una estació de càrrega",
"cs": "Nabíjecí stanice",
"da": "En ladestation",
"de": "Eine Ladestation",
"es": "Una estación de carga",
"fr": "Une station de recharge",
"pl": "Stacja ładowania"
},
"#": "no-question-hint-check",
"tagRenderings": [
"images",
{
@ -5132,6 +5026,113 @@
}
}
],
"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": [
"amenity=charging_station",
"motorcar=no",
"bicycle=yes",
"socket:typee=1"
],
"title": {
"en": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)",
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)",
"ca": "una estació de càrrega per a bicicletes elèctriques amb un endoll de paret europeu normal<img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (destinat a carregar bicicletes elèctriques)",
"cs": "nabíjecí stanice pro elektrokola s běžnou evropskou zástrčkou <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (určeno k nabíjení elektrických kol)",
"da": "en ladestation til elektriske cykler med et normalt europæisk vægstik <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (beregnet til opladning af elektriske cykler)",
"de": "eine Ladestation für Elektrofahrräder mit einer normalen europäischen Steckdose <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (zum Laden von Elektrofahrrädern)",
"es": "una estación de carga para bicicletas eléctricas con un enchufe de pared europeo normal <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (pensado para cargar bicicletas eléctricas)"
}
},
{
"tags": [
"amenity=charging_station",
"motorcar=yes",
"bicycle=no"
],
"title": {
"en": "a charging station for cars",
"nl": "een oplaadstation voor elektrische auto's",
"ca": "una estació de càrrega per a cotxes",
"cs": "nabíjecí stanice pro automobily",
"da": "en ladestation til biler",
"de": "Eine Ladestation für Elektrofahrzeuge",
"es": "una estación de carga para coches",
"pl": "stacja ładowania dla samochodów"
}
}
],
"filter": [
{
"id": "vehicle-type",
@ -5419,19 +5420,6 @@
]
}
],
"deletion": {
"softDeletionTags": {
"and": [
"amenity=",
"disused:amenity=charging_station"
]
},
"neededChangesets": 10
},
"allowMove": {
"enableRelocation": false,
"enableImproveAccuracy": true
},
"units": [
{
"appliesToKey": [
@ -5681,5 +5669,17 @@
"eraseInvalidValues": true
}
],
"#": "no-question-hint-check"
"allowMove": {
"enableRelocation": false,
"enableImproveAccuracy": true
},
"deletion": {
"softDeletionTags": {
"and": [
"amenity=",
"disused:amenity=charging_station"
]
},
"neededChangesets": 10
}
}

View file

@ -32,9 +32,7 @@
"tagRenderings": [
{
"id": "render_crab",
"render": {
"nl": "Volgens het CRAB ligt hier <b>{STRAATNM}</b> {HUISNR} (label: {HNRLABEL})"
}
"render": "Volgens het CRAB ligt hier <b>{STRAATNM}</b> {HUISNR} (label: {HNRLABEL})"
}
]
}

View file

@ -129,7 +129,7 @@
"then": "./assets/layers/id_presets/temaki-hammer_shoe.svg"
}
],
"mappings+":[
"mappings+": [
{
"if": "craft=key_cutter",
"then": "./assets/layers/id_presets/fas-key.svg"
@ -156,7 +156,7 @@
{
"if": {
"or": [
"service:key_cutting=yes",
"service:key_cutting=yes",
"craft=key_cutter"
]
},
@ -399,7 +399,7 @@
{
"id": "key_cutter",
"question": {
"en":"Does this shop offer key cutting?"
"en": "Does this shop offer key cutting?"
},
"mappings": [
{

View file

@ -1,6 +1,7 @@
{
"id": "slow_roads",
"name": {
"en": "Paths, carfree and slow roads",
"nl": "Paadjes, trage wegen en autoluwe straten"
},
"description": {
@ -30,42 +31,51 @@
"minzoom": 16,
"title": {
"render": {
"en": "Slow road",
"nl": "Trage weg"
},
"mappings": [
{
"if": "name~*",
"then": {
"*": "{name}",
"nl": "{name}"
}
},
{
"if": "highway=footway",
"then": {
"en": "Footway",
"nl": "Voetpad"
}
},
{
"if": "highway=cycleway",
"then": {
"en": "Cycleway",
"nl": "Fietspad"
}
},
{
"if": "highway=pedestrian",
"then": {
"en": "Pedestrian street",
"nl": "Voetgangersstraat"
}
},
{
"if": "highway=living_street",
"then": {
"en": "Living street",
"nl": "Woonerf"
}
},
{
"if": "highway=path",
"then": "Klein pad"
"then": {
"en": "Small path",
"nl": "Klein pad"
}
}
]
},
@ -122,7 +132,8 @@
{
"if": "highway=living_street",
"then": {
"nl:": "<div> Dit is een woonerf: <ul><li>Voetgangers mogen hier de volledige breedte van de straat gebruiken</li><li>Gemotoriseerd verkeer mag maximaal <b>20km/h</b> rijden</li></ul></div>"
"en": "This is a living street",
"nl": "<div> Dit is een woonerf: <ul><li>Voetgangers mogen hier de volledige breedte van de straat gebruiken</li><li>Gemotoriseerd verkeer mag maximaal <b>20km/h</b> rijden</li></ul></div>"
},
"icon": {
"path": "./assets/layers/slow_roads/woonerf.svg",
@ -132,30 +143,35 @@
{
"if": "highway=pedestrian",
"then": {
"en": "This is a wide, carfree street",
"nl": "Dit is een brede, autovrije straat"
}
},
{
"if": "highway=footway",
"then": {
"en": "This is a footway",
"nl": "Dit is een voetpaadje"
}
},
{
"if": "highway=path",
"then": {
"en": "This is a small path",
"nl": "Dit is een wegeltje of bospad"
}
},
{
"if": "highway=bridleway",
"then": {
"en": "This is a bridleway",
"nl": "Dit is een ruiterswegel"
}
},
{
"if": "highway=track",
"then": {
"en": "This is a land access road",
"nl": "Dit is een tractorspoor of weg om landbouwgrond te bereikken"
}
}
@ -163,6 +179,7 @@
},
{
"question": {
"en": "What surface does this road have?",
"nl": "Wat is de wegverharding van dit pad?"
},
"render": {
@ -299,7 +316,10 @@
},
{
"id": "slow_road_is_lit",
"question": "Is deze weg 's nachts verlicht?",
"question": {
"en": "Is this road lit at night?",
"nl": "Is deze weg 's nachts verlicht?"
},
"mappings": [
{
"if": "lit=yes",
@ -307,7 +327,10 @@
},
{
"if": "lit=no",
"then": "Niet verlicht"
"then": {
"en": "Not lit",
"nl": "Niet verlicht"
}
}
]
}

View file

@ -461,7 +461,6 @@
"en": "This lamp has 1 fixture",
"nl": "Deze lantaarn heeft 1 lamp",
"de": "Diese Straßenlaterne hat 1 Leuchte",
"es": "",
"ca": "Aquest fanal té 1 aparell",
"cs": "Tato lampa má 1 světlo"
}

View file

@ -11,6 +11,7 @@
"cs": "Stezky"
},
"description": {
"en": "Waymarked trails",
"nl": "Aangeduide wandeltochten"
},
"source": {
@ -103,9 +104,11 @@
},
{
"question": {
"en": "What is the name of this trail?",
"nl": "Wat is de naam van deze wandeling?"
},
"render": {
"en": "This trail is called <b>{name}</b>",
"nl": "Deze wandeling heet <b>{name}</b>"
},
"freeform": {
@ -115,9 +118,11 @@
},
{
"render": {
"en": "This trail is maintained by {operator}",
"nl": "Beheer door {operator}"
},
"question": {
"en": "Who maintains this trail?",
"nl": "Wie beheert deze wandeltocht?"
},
"freeform": {
@ -131,6 +136,7 @@
]
},
"then": {
"en": "This trail is maintained by Natuurpunt",
"nl": "Dit gebied wordt beheerd door Natuurpunt"
},
"icon": {
@ -145,6 +151,7 @@
]
},
"then": {
"en": "This trail is maintained by {operator}",
"nl": "Dit gebied wordt beheerd door {operator}"
},
"hideInAnswer": true,
@ -158,9 +165,11 @@
},
{
"question": {
"en": "What is the reference colour of this trail?",
"nl": "Welke kleur heeft deze wandeling?"
},
"render": {
"en": "The reference colour is {colour}",
"nl": "Deze wandeling heeft kleur {colour}"
},
"freeform": {
@ -221,17 +230,20 @@
},
{
"question": {
"en": "Is this trail wheelchair accessible?",
"nl": "Is deze wandeling toegankelijk met de rolstoel?"
},
"mappings": [
{
"then": {
"en": "This trail is wheelchair-accessible",
"nl": "deze wandeltocht is toegankelijk met de rolstoel"
},
"if": "wheelchair=yes"
},
{
"then": {
"en": "This trail is not wheelchair accessible",
"nl": "deze wandeltocht is niet toegankelijk met de rolstoel"
},
"if": "wheelchair=no"
@ -241,17 +253,20 @@
},
{
"question": {
"en": "Is this trail accessible with a pushchair?",
"nl": "Is deze wandeltocht toegankelijk met de buggy?"
},
"mappings": [
{
"then": {
"en": "This trail is accessible with a pushchair",
"nl": "deze wandeltocht is toegankelijk met de buggy"
},
"if": "pushchair=yes"
},
{
"then": {
"en": "This trail is not accessible with a pushchair",
"nl": "deze wandeltocht is niet toegankelijk met de buggy"
},
"if": "pushchair=no"

View file

@ -8523,7 +8523,8 @@
"2": {
"then": "This shops does not offer key cutting as a service"
}
}
},
"question": "Does this shop offer key cutting?"
},
"organic": {
"mappings": {
@ -8631,7 +8632,38 @@
},
"slow_roads": {
"description": "All carfree roads",
"name": "Paths, carfree and slow roads",
"tagRenderings": {
"explanation": {
"mappings": {
"0": {
"then": "This is a living street"
},
"1": {
"then": "This is a wide, carfree street"
},
"2": {
"then": "This is a footway"
},
"3": {
"then": "This is a small path"
},
"4": {
"then": "This is a bridleway"
},
"5": {
"then": "This is a land access road"
}
}
},
"slow_road_is_lit": {
"mappings": {
"1": {
"then": "Not lit"
}
},
"question": "Is this road lit at night?"
},
"slow_roads-surface": {
"mappings": {
"0": {
@ -8659,8 +8691,29 @@
"then": "The surface is <b>paved</b>"
}
},
"question": "What surface does this road have?",
"render": "The surface is <b>{surface}</b>"
}
},
"title": {
"mappings": {
"1": {
"then": "Footway"
},
"2": {
"then": "Cycleway"
},
"3": {
"then": "Pedestrian street"
},
"4": {
"then": "Living street"
},
"5": {
"then": "Small path"
}
},
"render": "Slow road"
}
},
"speed_camera": {
@ -9696,6 +9749,7 @@
}
},
"trail": {
"description": "Waymarked trails",
"name": "Trails",
"tagRenderings": {
"Color": {
@ -9712,7 +9766,47 @@
"3": {
"then": "Yellow trail"
}
}
},
"question": "What is the reference colour of this trail?",
"render": "The reference colour is {colour}"
},
"Name": {
"question": "What is the name of this trail?",
"render": "This trail is called <b>{name}</b>"
},
"Operator tag": {
"mappings": {
"0": {
"then": "This trail is maintained by Natuurpunt"
},
"1": {
"then": "This trail is maintained by {operator}"
}
},
"question": "Who maintains this trail?",
"render": "This trail is maintained by {operator}"
},
"Wheelchair access": {
"mappings": {
"0": {
"then": "This trail is wheelchair-accessible"
},
"1": {
"then": "This trail is not wheelchair accessible"
}
},
"question": "Is this trail wheelchair accessible?"
},
"pushchair access": {
"mappings": {
"0": {
"then": "This trail is accessible with a pushchair"
},
"1": {
"then": "This trail is not accessible with a pushchair"
}
},
"question": "Is this trail accessible with a pushchair?"
},
"trail-length": {
"render": "The trail is {_length:km} kilometers long"

View file

@ -4246,11 +4246,6 @@
"question": "¿De qué color es la luz que emite esta lámpara?",
"render": "Esta lámpara emite luz {light:colour}"
},
"count": {
"mappings": {
"0": {}
}
},
"direction": {
"question": "¿Hacia donde apunta esta lámpara?",
"render": "Esta lámpara apunta hacia {light:direction}"

View file

@ -340,9 +340,6 @@
"description": "Létesítmény, ahonnan kerékpár kölcsönözhető hosszabb időre",
"tagRenderings": {
"bicycle-library-target-group": {
"mappings": {
"0": {}
},
"question": "Ki kölcsönözhet itt kerékpárt?"
},
"bicycle_library-charge": {

View file

@ -3272,13 +3272,6 @@
"render": "Klok"
}
},
"crab_address": {
"tagRenderings": {
"render_crab": {
"render": "Volgens het CRAB ligt hier <b>{STRAATNM}</b> {HUISNR} (label: {HNRLABEL})"
}
}
},
"crossings": {
"description": "Oversteekplaatsen voor voetgangers en fietsers",
"name": "Oversteekplaatsen",
@ -7631,6 +7624,9 @@
"tagRenderings": {
"explanation": {
"mappings": {
"0": {
"then": "<div> Dit is een woonerf: <ul><li>Voetgangers mogen hier de volledige breedte van de straat gebruiken</li><li>Gemotoriseerd verkeer mag maximaal <b>20km/h</b> rijden</li></ul></div>"
},
"1": {
"then": "Dit is een brede, autovrije straat"
},
@ -7648,6 +7644,14 @@
}
}
},
"slow_road_is_lit": {
"mappings": {
"1": {
"then": "Niet verlicht"
}
},
"question": "Is deze weg 's nachts verlicht?"
},
"slow_roads-surface": {
"mappings": {
"0": {
@ -7695,6 +7699,9 @@
},
"4": {
"then": "Woonerf"
},
"5": {
"then": "Klein pad"
}
},
"render": "Trage weg"

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.34.0",
"version": "0.34.1",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -74,7 +74,7 @@
"lint:prettier": "prettier --check '**/*.ts' '**/*.svelte'",
"format": "prettier --write '**/*.ts' '**/*.svelte'",
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|test\\|studio\\|theme\\|style_test\\|statistics\\|leaderboard).html\" | xargs -r rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs -r rm)",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|test\\|studio\\|theme\\|style_test\\|statistics\\|leaderboard\\).html\" | xargs -r rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs -r rm)",
"generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot",
"weblate-add-upstream": "git remote add weblate-github git@github.com:weblate/MapComplete.git && git remote add weblate-hosted-core https://hosted.weblate.org/git/mapcomplete/core/ && git remote add weblate-hosted-layers https://hosted.weblate.org/git/mapcomplete/layers/",
"weblate-merge": "git remote update weblate-github; git merge weblate-github/weblate-mapcomplete-core weblate-github/weblate-mapcomplete-layers weblate-github/weblate-mapcomplete-layer-translations",

View file

@ -946,6 +946,10 @@ video {
margin-right: 0.75rem;
}
.mt-16 {
margin-top: 4rem;
}
.mr-12 {
margin-right: 3rem;
}
@ -1149,6 +1153,10 @@ video {
max-height: 100vh;
}
.max-h-60 {
max-height: 15rem;
}
.w-full {
width: 100%;
}

View file

@ -28,8 +28,8 @@ BRANCH=`git rev-parse --abbrev-ref HEAD`
echo "The branch name is $BRANCH"
if [ $BRANCH = "develop" ]
then
# SRC_MAPS="--sourcemap"
echo "Source maps are NOT enabled as they consume to much RAM"
SRC_MAPS="--sourcemap"
echo "Source maps are enabled "
fi
if [ $BRANCH = "master" ] || [ $BRANCH = "develop" ]

View file

@ -16,7 +16,6 @@ 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"
@ -30,6 +29,7 @@ import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
import { ConfigMeta } from "../src/UI/Studio/configMeta"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
// 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
@ -767,7 +767,7 @@ class LayerOverviewUtils extends Script {
t.shortDescription ??
new Translation(t.description)
.FirstSentence()
.OnEveryLanguage((s) => parse_html(s).innerText).translations,
.OnEveryLanguage((s) => parse_html(s).textContent).translations,
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
}
})

View file

@ -12,8 +12,8 @@
cp config.json config.json.bu &&
cp ./scripts/hetzner/config.json . && # Copy the config _before_ building, as the config might contain some needed URLs
# npm run reset:layeroverview
# npm run test &&
npm run prepare-deploy &&
npm run test &&
zip dist.zip -r dist/* &&
mv config.json.bu config.json &&
scp ./scripts/hetzner/config/* hetzner:/root/ &&

View file

@ -7,12 +7,12 @@ import {
import Translations from "../src/UI/i18n/Translations"
import { Translation } from "../src/UI/i18n/Translation"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/Conversion"
import themeconfig from "../src/assets/schemas/layoutconfigmeta.json"
import layerconfig from "../src/assets/schemas/layerconfigmeta.json"
import { Utils } from "../src/Utils"
import { ConfigMeta } from "../src/UI/Studio/configMeta"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
/*
* This script reads all theme and layer files and reformats them inplace

View file

@ -118,13 +118,10 @@ export class OsmConnection {
if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data)
const self = this
this.auth.bootstrapToken(
options.oauth_token.data,
(err, result) => {
console.log("Bootstrap token called back", err, result)
self.AttemptLogin()
}
)
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
console.log("Bootstrap token called back", err, result)
self.AttemptLogin()
})
options.oauth_token.setData(undefined)
}
@ -281,20 +278,24 @@ export class OsmConnection {
content?: string,
allowAnonymous: boolean = false
): Promise<string> {
let connection: OSMAuthInstance = this.auth
if(allowAnonymous && !this.auth.authenticated()) {
const possibleResult = await Utils.downloadAdvanced(`${this.Backend()}/api/0.6/${path}`,header, method, content)
if(possibleResult["content"]) {
if (allowAnonymous && !this.auth.authenticated()) {
const possibleResult = await Utils.downloadAdvanced(
`${this.Backend()}/api/0.6/${path}`,
header,
method,
content
)
if (possibleResult["content"]) {
return possibleResult["content"]
}
console.error(possibleResult)
throw "Could not interact with OSM:"+possibleResult["error"]
throw "Could not interact with OSM:" + possibleResult["error"]
}
return new Promise((ok, error) => {
connection.xhr(
<any> {
<any>{
method,
options: {
header,
@ -330,8 +331,12 @@ export class OsmConnection {
return await this.interact(path, "PUT", header, content)
}
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
return await this.interact(path, "GET", header)
public async get(
path: string,
header?: Record<string, string | number>,
allowAnonymous: boolean = false
): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous)
}
public closeNote(id: number | string, text?: string): Promise<void> {
@ -374,9 +379,14 @@ export class OsmConnection {
}
// Lat and lon must be strings for the API to accept it
const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}`
const response = await this.post("notes.json", content, {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}, true)
const response = await this.post(
"notes.json",
content,
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
true
)
const parsed = JSON.parse(response)
console.log("Got result:", parsed)
const id = parsed.properties
@ -519,7 +529,6 @@ export class OsmConnection {
singlepage: !standalone,
auto: true,
})
}
private CheckForMessagesContinuously() {
@ -543,6 +552,29 @@ export class OsmConnection {
})
}
private readonly _userInfoCache: Record<number, any> = {}
public async getInformationAboutUser(id: number): Promise<{
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}> {
if (id === undefined) {
return undefined
}
if (this._userInfoCache[id]) {
return this._userInfoCache[id]
}
const info = await this.get("user/" + id + ".json", { accepts: "application/json" }, true)
const parsed = JSON.parse(info)["user"]
this._userInfoCache[id] = parsed
return parsed
}
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
if (Utils.runningFromConsole) {
return { api: "online", gpx: "online" }

View file

@ -79,6 +79,9 @@ export class Tag extends TagsFilter {
currentProperties?: Record<string, string>
) {
let v = this.value
if (typeof v !== "string") {
v = JSON.stringify(v)
}
if (shorten) {
v = Utils.EllipsesAfter(v, 25)
}

View file

@ -9,6 +9,8 @@ import { Or } from "./Or"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import key_counts from "../../assets/key_totals.json"
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
type Tags = Record<string, string>
export type UploadableTag = Tag | SubstitutingTag | And
@ -475,12 +477,18 @@ export class TagUtils {
* regex.matchesProperties({maxspeed: "50 mph"}) // => true
*/
public static Tag(json: TagConfigJson, context: string = ""): TagsFilter {
public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilter {
try {
return this.ParseTagUnsafe(json, context)
let ctx = typeof context === "string" ? context : context.path.join(".")
return this.ParseTagUnsafe(json, ctx)
} catch (e) {
console.error("Could not parse tag", json, "in context", context, "due to ", e)
throw e
if (typeof context === "string") {
console.error("Could not parse tag", json, "in context", context, "due to ", e)
throw e
} else {
context.err(e)
return undefined
}
}
}

View file

@ -1,6 +1,7 @@
import { ConversionContext, DesugaringStep } from "./Conversion"
import { DesugaringStep } from "./Conversion"
import { Utils } from "../../../Utils"
import Translations from "../../../UI/i18n/Translations"
import { ConversionContext } from "./ConversionContext"
export class AddContextToTranslations<T> extends DesugaringStep<T> {
private readonly _prefix: string

View file

@ -1,6 +1,7 @@
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
export interface DesugaringContext {
tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
@ -8,112 +9,6 @@ export interface DesugaringContext {
publicLayers?: Set<string>
}
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[]
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
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
}
}
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([], [...path], [...operation])
}
public static test(msg?: string) {
return new ConversionContext([], msg ? [msg] : [], ["test"])
}
static print(msg: ConversionMessage) {
const noString = msg.context.path.filter(
(p) => typeof p !== "string" && typeof p !== "number"
)
if (noString.length > 0) {
console.warn("Non-string value in path:", ...noString)
}
if (msg.level === "error") {
console.error(
ConversionContext.red("ERR "),
msg.context.path.join("."),
ConversionContext.red(msg.message),
msg.context.operation.join(".")
)
} else if (msg.level === "warning") {
console.warn(
ConversionContext.red("<!> "),
msg.context.path.join("."),
ConversionContext.yellow(msg.message),
msg.context.operation.join(".")
)
} else {
console.log(" ", msg.context.path.join("."), msg.message)
}
}
private static yellow(s) {
return "\x1b[33m" + s + "\x1b[0m"
}
private static red(s) {
return "\x1b[31m" + s + "\x1b[0m"
}
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
return new ConversionContext(this.messages, [...this.path, key], this.operation)
}
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
}
public enters(...key: (string | number)[]) {
return this.enter(key)
}
public inOperation(key: string) {
return new ConversionContext(this.messages, this.path, [...this.operation, key])
}
warn(message: string) {
this.messages.push({ context: this, level: "warning", message })
}
err(message: string) {
this.messages.push({ context: this, level: "error", message })
}
info(message: string) {
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
}
debug(message: string) {
this.messages.push({ context: this, level: "debug", message })
}
}
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
export interface ConversionMessage {
context: ConversionContext

View file

@ -0,0 +1,116 @@
import { ConversionMessage, ConversionMsgLevel } from "./Conversion"
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[]
private _hasErrors: boolean = false
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
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
}
}
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([], [...path], [...operation])
}
public static test(msg?: string) {
return new ConversionContext([], msg ? [msg] : [], ["test"])
}
static print(msg: ConversionMessage) {
const noString = msg.context.path.filter(
(p) => typeof p !== "string" && typeof p !== "number"
)
if (noString.length > 0) {
console.warn("Non-string value in path:", ...noString)
}
if (msg.level === "error") {
console.error(
ConversionContext.red("ERR "),
msg.context.path.join("."),
ConversionContext.red(msg.message),
msg.context.operation.join(".")
)
} else if (msg.level === "warning") {
console.warn(
ConversionContext.red("<!> "),
msg.context.path.join("."),
ConversionContext.yellow(msg.message),
msg.context.operation.join(".")
)
} else {
console.log(" ", msg.context.path.join("."), msg.message)
}
}
private static yellow(s) {
return "\x1b[33m" + s + "\x1b[0m"
}
private static red(s) {
return "\x1b[31m" + s + "\x1b[0m"
}
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
return new ConversionContext(this.messages, [...this.path, key], this.operation)
}
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
}
public enters(...key: (string | number)[]) {
return this.enter(key)
}
public inOperation(key: string) {
return new ConversionContext(this.messages, this.path, [...this.operation, key])
}
warn(message: string) {
this.messages.push({ context: this, level: "warning", message })
}
err(message: string) {
this._hasErrors = true
this.messages.push({ context: this, level: "error", message })
}
info(message: string) {
this.messages.push({ context: this, level: "information", message })
}
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
return this.messages.filter((m) => m.level === mode)
}
public hasErrors() {
if (this._hasErrors) {
return true
}
const foundErr = this.messages?.find((m) => m.level === "error") !== undefined
this._hasErrors = foundErr
return foundErr
}
debug(message: string) {
this.messages.push({ context: this, level: "debug", message })
}
}

View file

@ -1,8 +1,9 @@
import { Conversion, ConversionContext } from "./Conversion"
import { Conversion } from "./Conversion"
import LayerConfig from "../LayerConfig"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import Translations from "../../../UI/i18n/Translations"
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
import { ConversionContext } from "./ConversionContext"
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
/**

View file

@ -1,4 +1,4 @@
import { Conversion, ConversionContext, DesugaringStep } from "./Conversion"
import { Conversion, DesugaringStep } from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils"
import metapaths from "../../../assets/schemas/layoutconfigmeta.json"
@ -6,6 +6,7 @@ import tagrenderingmetapaths from "../../../assets/schemas/questionabletagrender
import Translations from "../../../UI/i18n/Translations"
import { parse as parse_html } from "node-html-parser"
import { ConversionContext } from "./ConversionContext"
export class ExtractImages extends Conversion<
LayoutConfigJson,

View file

@ -2,8 +2,9 @@ import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
export class UpdateLegacyLayer extends DesugaringStep<
LayerConfigJson | string | { builtin; override }
@ -57,6 +58,9 @@ export class UpdateLegacyLayer extends DesugaringStep<
if (config.tagRenderings !== undefined) {
let i = 0
for (const tagRendering of config.tagRenderings) {
if (!tagRendering) {
continue
}
i++
if (
typeof tagRendering === "string" ||

View file

@ -1,8 +1,6 @@
import {
Cached,
Concat,
Conversion,
ConversionContext,
DesugaringContext,
DesugaringStep,
Each,
@ -32,7 +30,7 @@ import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { j } from "vite-node/types-63205a44"
import { ConversionContext } from "./ConversionContext"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -1192,9 +1190,9 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
}
}
export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) {
const steps = new Fuse<LayerConfigJson>(
super(
"Fully prepares and expands a layer for the LayerConfig.",
new On("tagRenderings", new Each(new RewriteSpecial())),
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
@ -1224,6 +1222,5 @@ export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
),
new ExpandFilter(state)
)
super(steps)
}
}

View file

@ -1,7 +1,6 @@
import {
Concat,
Conversion,
ConversionContext,
DesugaringContext,
DesugaringStep,
Each,
@ -21,6 +20,7 @@ import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import DependencyCalculator from "../DependencyCalculator"
import { AddContextToTranslations } from "./AddContextToTranslations"
import ValidationUtils from "./ValidationUtils"
import { ConversionContext } from "./ConversionContext"
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
private readonly _state: DesugaringContext

View file

@ -1,13 +1,4 @@
import {
Conversion,
ConversionContext,
DesugaringStep,
Each,
Fuse,
On,
Pipe,
Pure,
} from "./Conversion"
import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
@ -29,6 +20,8 @@ import TagRenderingConfig from "../TagRenderingConfig"
import { parse as parse_html } from "node-html-parser"
import PresetConfig from "../PresetConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Translatable } from "../Json/Translatable"
import { ConversionContext } from "./ConversionContext"
class ValidateLanguageCompleteness extends DesugaringStep<any> {
private readonly _languages: string[]
@ -285,7 +278,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
new Each(
new Pipe(
new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true),
new Pure((x) => x.raw)
new Pure((x) => x?.raw)
)
)
)
@ -375,32 +368,34 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
return json
}
const tagRendering = new TagRenderingConfig(json)
try {
const tagRendering = new TagRenderingConfig(json)
const errors = []
for (let i = 0; i < tagRendering.mappings.length; i++) {
const mapping = tagRendering.mappings[i]
if (!mapping.addExtraTags) {
continue
for (let i = 0; i < tagRendering.mappings.length; i++) {
const mapping = tagRendering.mappings[i]
if (!mapping.addExtraTags) {
continue
}
const keysInMapping = new Set(mapping.if.usedKeys())
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
if (duplicateKeys.length > 0) {
context
.enters("mappings", i)
.err(
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
duplicateKeys.join(", ")
)
}
}
const keysInMapping = new Set(mapping.if.usedKeys())
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
if (duplicateKeys.length > 0) {
errors.push(
"At " +
context +
".mappings[" +
i +
"]: AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
duplicateKeys.join(", ")
)
}
return json
} catch (e) {
context.err(e)
return undefined
}
return json
}
}
@ -475,8 +470,8 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
"some_calculated_tag_value_for_" + calculatedTagName
}
const parsedConditions = json.mappings.map((m, i) => {
const ctx = `${context}.mappings[${i}]`
const ifTags = TagUtils.Tag(m.if, ctx)
const c = context.enters("mappings", i)
const ifTags = TagUtils.Tag(m.if, c.enter("if"))
const hideInAnswer = m["hideInAnswer"]
if (hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true) {
let conditionTags = TagUtils.Tag(hideInAnswer)
@ -486,7 +481,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
return ifTags
})
for (let i = 0; i < json.mappings.length; i++) {
if (!parsedConditions[i].isUsableAsAnswer()) {
if (!parsedConditions[i]?.isUsableAsAnswer()) {
// There is no straightforward way to convert this mapping.if into a properties-object, so we simply skip this one
// Yes, it might be shadowed, but running this check is to difficult right now
continue
@ -661,12 +656,57 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
}
}
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
private _options: { noQuestionHintCheck: boolean }
class CheckTranslation extends DesugaringStep<Translatable> {
public static readonly allowUndefined: CheckTranslation = new CheckTranslation(true)
public static readonly noUndefined: CheckTranslation = new CheckTranslation()
private readonly _allowUndefined: boolean
constructor(options: { noQuestionHintCheck: boolean }) {
constructor(allowUndefined: boolean = false) {
super(
"Checks that a translation is valid and internally consistent",
["*"],
"CheckTranslation"
)
this._allowUndefined = allowUndefined
}
convert(json: Translatable, context: ConversionContext): Translatable {
if (json === undefined || json === null) {
if (!this._allowUndefined) {
context.err("Expected a translation, but got " + json)
}
return json
}
if (typeof json === "string") {
return json
}
const keys = Object.keys(json)
if (keys.length === 0) {
context.err("No actual values are given in this translation, it is completely empty")
return json
}
const en = json["en"]
if (!en && json["*"] === undefined) {
const msg = "Received a translation without english version"
context.warn(msg)
}
for (const key of keys) {
const lng = json[key]
if (lng === "") {
context.enter(lng).err("Got an empty string in translation for language " + lng)
}
// TODO validate that all subparts are here
}
return json
}
}
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
constructor() {
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
this._options = options
}
convert(
@ -678,10 +718,42 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
)
}
{
for (const key of ["question", "questionHint", "render"]) {
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
}
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
const mapping = json.mappings[i]
CheckTranslation.noUndefined.convert(
mapping.then,
context.enters("mappings", i, "then")
)
if (!mapping.if) {
context.enters("mappings", i).err("No `if` is defined")
}
}
}
if (json["group"]) {
context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead')
}
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
context.err(
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them"
)
}
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
context.err("A question is defined, but there is only one option to choose from.")
}
if (json["questionHint"] && !json["question"]) {
context
.enter("questionHint")
.err(
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown"
)
}
if (json.freeform) {
if (json.render === undefined) {
context
@ -771,16 +843,13 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
)
}
}
return json
}
}
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
constructor(
layerConfig?: LayerConfigJson,
doesImageExist?: DoesImageExist,
options?: { noQuestionHintCheck: boolean }
) {
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
super(
"Various validation on tagRenderingConfigs",
new DetectShadowedMappings(layerConfig),
@ -790,67 +859,46 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
new On("question", new ValidatePossibleLinks()),
new On("questionHint", new ValidatePossibleLinks()),
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
new MiscTagRenderingChecks(options)
new MiscTagRenderingChecks()
)
}
}
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
> {
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
*/
private readonly _path?: string
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
private readonly _isBuiltin: boolean
private readonly _doesImageExist: DoesImageExist
/**
* The paths where this layer is originally saved. Triggers some extra checks
*/
private readonly _path: string
private readonly _studioValidations: boolean
private _skipDefaultLayers: boolean
constructor(
path: string,
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
constructor(path: string, isBuiltin, doesImageExist, studioValidations) {
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
this._path = path
this._isBuiltin = isBuiltin
this._doesImageExist = doesImageExist
this._studioValidations = studioValidations
this._skipDefaultLayers = skipDefaultLayers
}
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)
return null
}
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
return { parsed: undefined, raw: json }
}
if (typeof json === "string") {
context.err(
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
)
return undefined
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
if (json.id === undefined) {
context.enter("id").err(`Not a valid layer: id is undefined`)
} else {
if (json.id?.toLowerCase() !== json.id) {
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
}
if (json.id?.match(/[a-z0-9-_]/) == null) {
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
}
}
if (json.source === undefined) {
context.enter("source").err("No source section is defined")
context
.enter("source")
.err(
"No source section is defined; please define one as data is not loaded otherwise"
)
} else {
if (json.source === "special" || json.source === "special:library") {
} else if (json.source && json.source["osmTags"] === undefined) {
@ -884,13 +932,6 @@ export class ValidateLayer extends Conversion<
}
}
if (json.id?.toLowerCase() !== json.id) {
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
}
if (json.id?.match(/[a-z0-9-_]/) == null) {
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
}
if (
json.syncSelection !== undefined &&
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
@ -906,27 +947,6 @@ export class ValidateLayer extends Conversion<
)
}
let layerConfig: LayerConfig
try {
layerConfig = new LayerConfig(json, "validation", true)
} catch (e) {
console.error(e)
context.err("Could not parse layer due to:" + e)
return undefined
}
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
const [_, code, __] = layerConfig.calculatedTags[i]
try {
new Function("feat", "return " + code + ";")
} catch (e) {
context
.enters("calculatedTags", i)
.err(
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
)
}
}
if (json.source === "special") {
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
context.err(
@ -937,6 +957,10 @@ export class ValidateLayer extends Conversion<
}
}
if (context.hasErrors()) {
return undefined
}
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
new On("tagRendering", new Each(new ValidateTagRenderings(json)))
if (json.title === undefined && json.source !== "special:library") {
@ -1001,172 +1025,6 @@ export class ValidateLayer extends Conversion<
}
try {
if (this._isBuiltin) {
// Some checks for legacy elements
if (json["overpassTags"] !== undefined) {
context.err(
"Layer " +
json.id +
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
)
}
const forbiddenTopLevel = [
"icon",
"wayHandling",
"roamingRenderings",
"roamingRendering",
"label",
"width",
"color",
"colour",
"iconOverlays",
]
for (const forbiddenKey of forbiddenTopLevel) {
if (json[forbiddenKey] !== undefined)
context.err(
"Layer " + json.id + " still has a forbidden key " + forbiddenKey
)
}
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
context.err(
"Layer " +
json.id +
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
}
if (
json.isShown !== undefined &&
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
) {
context.warn("Has a tagRendering as `isShown`")
}
}
if (this._isBuiltin) {
// Check location of layer file
const expected: string = `assets/layers/${json.id}/${json.id}.json`
if (this._path != undefined && this._path.indexOf(expected) < 0) {
context.err(
"Layer is in an incorrect place. The path is " +
this._path +
", but expected " +
expected
)
}
}
if (this._isBuiltin) {
// Check for correct IDs
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
const emptyIndexes: number[] = []
for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i]
if (tagRendering["id"] === "") {
emptyIndexes.push(i)
}
}
context
.enter(["tagRenderings", ...emptyIndexes])
.err(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
","
)}])`
)
}
const duplicateIds = Utils.Duplicates(
(json.tagRenderings ?? [])
?.map((f) => f["id"])
.filter((id) => id !== "questions")
)
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
context
.enter("tagRenderings")
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
}
if (json.description === undefined) {
if (typeof json.source === null) {
context.err("A priviliged layer must have a description")
} else {
context.warn("A builtin layer should have a description")
}
}
}
if (json.filter) {
new On("filter", new Each(new ValidateFilter())).convert(json, context)
}
if (json.tagRenderings !== undefined) {
new On(
"tagRenderings",
new Each(
new ValidateTagRenderings(json, this._doesImageExist, {
noQuestionHintCheck: json["#"]?.indexOf("no-question-hint-check") >= 0,
})
)
).convert(json, context)
}
if (json.pointRendering !== null && json.pointRendering !== undefined) {
if (!Array.isArray(json.pointRendering)) {
throw (
"pointRendering in " +
json.id +
" is not iterable, it is: " +
typeof json.pointRendering
)
}
for (let i = 0; i < json.pointRendering.length; i++) {
const pointRendering = json.pointRendering[i]
if (pointRendering.marker === undefined) {
continue
}
for (const icon of pointRendering?.marker) {
const indexM = pointRendering?.marker.indexOf(icon)
if (!icon.icon) {
continue
}
if (icon.icon["condition"]) {
context
.enters("pointRendering", i, "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) {
if (typeof json.source === "string") {
context.err("A special layer cannot have presets")
}
// Check that a preset will be picked up by the layer itself
const baseTags = TagUtils.Tag(json.source["osmTags"])
for (let i = 0; i < json.presets.length; i++) {
const preset = json.presets[i]
const tags: { k: string; v: string }[] = new And(
preset.tags.map((t) => TagUtils.Tag(t))
).asChange({ id: "node/-1" })
const properties = {}
for (const tag of tags) {
properties[tag.k] = tag.v
}
const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) {
context
.enters("presets", i, "tags")
.err(
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
JSON.stringify(properties) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
}
}
}
} catch (e) {
context.err("Could not validate layer due to: " + e + e.stack)
}
@ -1180,6 +1038,232 @@ export class ValidateLayer extends Conversion<
}
}
if (this._isBuiltin) {
// Some checks for legacy elements
if (json["overpassTags"] !== undefined) {
context.err(
"Layer " +
json.id +
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
)
}
const forbiddenTopLevel = [
"icon",
"wayHandling",
"roamingRenderings",
"roamingRendering",
"label",
"width",
"color",
"colour",
"iconOverlays",
]
for (const forbiddenKey of forbiddenTopLevel) {
if (json[forbiddenKey] !== undefined)
context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
}
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
context.err(
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
}
if (
json.isShown !== undefined &&
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
) {
context.warn("Has a tagRendering as `isShown`")
}
}
if (this._isBuiltin) {
// Check location of layer file
const expected: string = `assets/layers/${json.id}/${json.id}.json`
if (this._path != undefined && this._path.indexOf(expected) < 0) {
context.err(
"Layer is in an incorrect place. The path is " +
this._path +
", but expected " +
expected
)
}
}
if (this._isBuiltin) {
// Check for correct IDs
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
const emptyIndexes: number[] = []
for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i]
if (tagRendering["id"] === "") {
emptyIndexes.push(i)
}
}
context
.enter(["tagRenderings", ...emptyIndexes])
.err(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
","
)}])`
)
}
const duplicateIds = Utils.Duplicates(
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
)
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
context
.enter("tagRenderings")
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
}
if (json.description === undefined) {
if (typeof json.source === null) {
context.err("A priviliged layer must have a description")
} else {
context.warn("A builtin layer should have a description")
}
}
}
if (json.filter) {
new On("filter", new Each(new ValidateFilter())).convert(json, context)
}
if (json.tagRenderings !== undefined) {
new On(
"tagRenderings",
new Each(new ValidateTagRenderings(json, this._doesImageExist))
).convert(json, context)
}
if (json.pointRendering !== null && json.pointRendering !== undefined) {
if (!Array.isArray(json.pointRendering)) {
throw (
"pointRendering in " +
json.id +
" is not iterable, it is: " +
typeof json.pointRendering
)
}
for (let i = 0; i < json.pointRendering.length; i++) {
const pointRendering = json.pointRendering[i]
if (pointRendering.marker === undefined) {
continue
}
for (const icon of pointRendering?.marker) {
const indexM = pointRendering?.marker.indexOf(icon)
if (!icon.icon) {
continue
}
if (icon.icon["condition"]) {
context
.enters("pointRendering", i, "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) {
if (typeof json.source === "string") {
context.err("A special layer cannot have presets")
}
// Check that a preset will be picked up by the layer itself
const baseTags = TagUtils.Tag(json.source["osmTags"])
for (let i = 0; i < json.presets.length; i++) {
const preset = json.presets[i]
const tags: { k: string; v: string }[] = new And(
preset.tags.map((t) => TagUtils.Tag(t))
).asChange({ id: "node/-1" })
const properties = {}
for (const tag of tags) {
properties[tag.k] = tag.v
}
const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) {
context
.enters("presets", i, "tags")
.err(
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
JSON.stringify(properties) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
}
}
}
return json
}
}
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
> {
private readonly _skipDefaultLayers: boolean
private readonly _prevalidation: PrevalidateLayer
constructor(
path: string,
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
this._prevalidation = new PrevalidateLayer(
path,
isBuiltin,
doesImageExist,
studioValidations
)
this._skipDefaultLayers = skipDefaultLayers
}
convert(
json: LayerConfigJson,
context: ConversionContext
): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name)
if (typeof json === "string") {
context.err(
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
)
return undefined
}
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
return { parsed: undefined, raw: json }
}
this._prevalidation.convert(json, context.inOperation(this._prevalidation.name))
if (context.hasErrors()) {
return undefined
}
let layerConfig: LayerConfig
try {
layerConfig = new LayerConfig(json, "validation", true)
} catch (e) {
context.err("Could not parse layer due to:" + e)
return undefined
}
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
const [_, code, __] = layerConfig.calculatedTags[i]
try {
new Function("feat", "return " + code + ";")
} catch (e) {
context
.enters("calculatedTags", i)
.err(
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
)
}
}
return { raw: json, parsed: layerConfig }
}
}

View file

@ -198,6 +198,8 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
freeform?: {
/**
* question: What is the name of the attribute that should be written to?
* This is the OpenStreetMap-key that that value will be written to
*
* ifunset: do not offer a freeform textfield as answer option
*/
key: string
@ -215,7 +217,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* A (translated) text that is shown (as gray text) within the textfield
* type: translation
*/
placeholder?: string | any
placeholder?: Translatable
/**
* Extra parameters to initialize the input helper arguments.
@ -259,7 +261,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
*
* ifunset: This tagrendering will be shown if it is known, but cannot be edited by the contributor, effectively resutling in a read-only rendering
*/
question?: string | Translatable
question?: Translatable
/**
* question: Should some extra information be shown to the contributor, alongside the question?
@ -267,7 +269,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* This can give some extra information on what the answer should ook like
* ifunset: No extra hint is given
*/
questionHint?: string | Translatable
questionHint?: Translatable
/**
* A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer

View file

@ -9,7 +9,8 @@ import { Utils } from "../../Utils"
import LanguageUtils from "../../Utils/LanguageUtils"
import { RasterLayerProperties } from "../RasterLayerProperties"
import { ConversionContext } from "./Conversion/Conversion"
import { ConversionContext } from "./Conversion/ConversionContext"
/**
* Minimal information about a theme

View file

@ -240,10 +240,6 @@ export default class TagRenderingConfig {
)
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
}
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
let keys = []
for (let i = 0; i < this.mappings.length; i++) {
@ -315,7 +311,7 @@ export default class TagRenderingConfig {
) {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
throw `Invalid mapping: "if" is not defined`
}
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {

View file

@ -22,11 +22,14 @@
export let highlightedRendering: UIEventSource<string> = undefined;
export let showQuestionIfUnknown: boolean = false;
let editMode = false;
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
export let editMode = !config.IsKnown(tags) || showQuestionIfUnknown;
if (tags) {
onDestroy(
tags.addCallbackAndRunD((tags) => {
editMode = showQuestionIfUnknown && !config.IsKnown(tags);
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags)
})
);
}

View file

@ -132,6 +132,7 @@
function onSave() {
if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event")
return;
}
if (layer === undefined || layer?.source === null) {
@ -197,20 +198,20 @@
</span>
<slot name="upper-right" />
</div>
{#if config.questionhint}
<div>
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full">
<img src="./assets/svg/search.svg" class="h-6 w-6" />

View file

@ -1,20 +1,10 @@
<script lang="ts">
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { createEventDispatcher } from "svelte";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { AllKnownLayouts, AllKnownLayoutsLazy } from "../../Customizations/AllKnownLayouts";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import EditItemButton from "./EditItemButton.svelte";
export let layerIds: { id: string }[];
export let layerIds: { id: string, owner: number }[];
export let category: "layers" | "themes" = "layers";
const dispatch = createEventDispatcher<{ layerSelected: string }>();
function fetchIconDescription(layerId): any {
if(category === "themes"){
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
export let osmConnection: OsmConnection;
</script>
@ -22,12 +12,7 @@
<slot name="title" />
<div class="flex flex-wrap">
{#each Array.from(layerIds) as layer}
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layer.id)} />
</div>
{layer.id}
</NextButton>
<EditItemButton info={layer} {category} {osmConnection} on:layerSelected/>
{/each}
</div>
{/if}

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { createEventDispatcher } from "svelte";
export let info: { id: string, owner: number };
export let category: "layers" | "themes";
export let osmConnection: OsmConnection;
let displayName = UIEventSource.FromPromise(osmConnection.getInformationAboutUser(info.owner)).mapD(response => response.display_name);
let selfId = osmConnection.userDetails.mapD(ud => ud.uid)
function fetchIconDescription(layerId): any {
if (category === "themes") {
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
const dispatch = createEventDispatcher<{ layerSelected: string }>();
</script>
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(info.id)} />
</div>
<b class="px-1"> {info.id}</b>
{#if info.owner && info.owner !== $selfId}
(made by {$displayName ?? info.owner})
{/if}
</NextButton>

View file

@ -21,8 +21,8 @@
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
export let state: EditLayerState;
const messages = state.messages;
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let messages = state.messages;
let hasErrors = messages.mapD((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
const configuration = state.configuration;
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
@ -33,7 +33,7 @@
}
const title: Store<string> = state.getStoreFor(["id"]);
let title: Store<string> = state.getStoreFor(["id"]);
const wl = window.location;
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
@ -53,13 +53,15 @@
let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id);
config = Utils.Clone(config);
config.required = true;
console.log(">>>", config);
config.hints.ifunset = undefined;
return config;
}
let requiredFields = ["id", "name", "description"];
let currentlyMissing = state.configuration.map(config => {
if(!config){
return []
}
const missing = [];
for (const requiredField of requiredFields) {
if (!config[requiredField]) {
@ -160,7 +162,9 @@
</div>
{#if $highlightedItem !== undefined}
<FloatOver on:close={() => highlightedItem.setData(undefined)}>
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
<div class="mt-16">
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
</div>
</FloatOver>
{/if}

View file

@ -3,7 +3,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import {
Conversion,
ConversionContext,
ConversionMessage,
DesugaringContext,
Pipe,
@ -21,6 +20,7 @@ import { Feature, Point } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson"
import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext";
export interface HighlightedTagRendering {
path: ReadonlyArray<string | number>
@ -41,7 +41,9 @@ export abstract class EditJsonState<T> {
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
undefined
)
sendingUpdates = false
private readonly _stores = new Map<string, UIEventSource<any>>()
private boolean
constructor(schema: ConfigMeta[], server: StudioServer, category: "layers" | "themes") {
this.schema = schema
@ -52,7 +54,13 @@ export abstract class EditJsonState<T> {
const layerId = this.getId()
this.configuration
.mapD((config) => JSON.stringify(config, null, " "))
.mapD((config) => {
if (!this.sendingUpdates) {
console.log("Not sending updates yet! Trigger 'startSendingUpdates' first")
return undefined
}
return JSON.stringify(config, null, " ")
})
.stabilized(100)
.addCallbackD(async (config) => {
const id = layerId.data
@ -60,10 +68,17 @@ export abstract class EditJsonState<T> {
console.warn("No id found in layer, not updating")
return
}
await server.update(id, config, category)
await this.server.update(id, config, this.category)
})
}
public startSavingUpdates(enabled = true) {
this.sendingUpdates = enabled
if (enabled) {
this.configuration.ping()
}
}
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
// Walk the path down to see if we find something
let entry = this.configuration.data
@ -96,7 +111,7 @@ export abstract class EditJsonState<T> {
public register(
path: ReadonlyArray<string | number>,
value: Store<any>,
noInitialSync: boolean = false
noInitialSync: boolean = true
): () => void {
const unsync = value.addCallback((v) => {
this.setValueAt(path, v)
@ -260,6 +275,18 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
this.addMissingTagRenderingIds()
this.configuration.addCallbackAndRunD((layer) => {
if (layer.tagRenderings) {
// A bit of cleanup
const lBefore = layer.tagRenderings.length
const cleaned = Utils.NoNull(layer.tagRenderings)
if (cleaned.length != lBefore) {
layer.tagRenderings = cleaned
this.configuration.ping()
}
}
})
}
protected buildValidation(state: DesugaringContext) {
@ -300,6 +327,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
export class EditThemeState extends EditJsonState<LayoutConfigJson> {
constructor(schema: ConfigMeta[], server: StudioServer) {
super(schema, server, "themes")
}
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
return new Pipe(
new PrepareTheme(state),
@ -307,10 +338,6 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
)
}
constructor(schema: ConfigMeta[], server: StudioServer) {
super(schema, server, "themes")
}
protected getId(): Store<string> {
return this.configuration.mapD((config) => config.id)
}

View file

@ -10,8 +10,8 @@
export let state: EditThemeState;
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0);
let config = state.configuration;
const messages = state.messages;
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let messages = state.messages;
let hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let title = state.getStoreFor(["id"]);
const wl = window.location;
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";

View file

@ -5,12 +5,13 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource";
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import * as nmd from "nano-markdown";
import nmd from "nano-markdown";
import type {
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js";
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson";
import FromHtml from "../Base/FromHtml.svelte";
import { Utils } from "../../Utils";
export let state: EditLayerState;
export let path: ReadonlyArray<string | number>;
@ -34,9 +35,15 @@
return [x];
}
});
let configs: Store<TagRenderingConfig[]> = configJson.mapD(configs => configs.map(config => new TagRenderingConfig(config)));
let configs: Store<TagRenderingConfig[]> =configJson.mapD(configs => Utils.NoNull( configs.map(config => {
try{
return new TagRenderingConfig(config);
}catch (e) {
return undefined
}
})));
let id: Store<string> = value.mapD(c => {
if (c.id) {
if (c?.id) {
return c.id;
}
if (typeof c === "string") {
@ -49,6 +56,14 @@
let messages = state.messagesFor(path);
let description = schema.description
if(description){
try{
description = nmd(description)
}catch (e) {
console.error("Could not convert description to markdown", {description})
}
}
</script>
<div class="flex">
@ -63,8 +78,8 @@
{schema.hints.question}
{/if}
</button>
{#if schema.description}
<FromHtml src={nmd(schema.description)} />
{#if description}
<FromHtml src={description} />
{/if}
{#each $messages as message}
<div class="alert">

View file

@ -16,6 +16,7 @@
export let state: EditLayerState;
export let path: (string | number)[] = [];
export let schema: ConfigMeta;
export let startInEditModeIfUnset: boolean = false
let value = new UIEventSource<string | any>(undefined);
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
@ -118,6 +119,7 @@
}
let startValue = state.getCurrentValueFor(path);
const tags = new UIEventSource<Record<string, string>>({ value: startValue });
let startInEditMode = !startValue && startInEditModeIfUnset
try {
onDestroy(state.register(path, tags.map(tgs => {
const v = tgs["value"];
@ -157,7 +159,7 @@
<span class="alert">{err}</span>
{:else}
<div class="w-full flex flex-col">
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
<TagRenderingEditable editMode={startInEditMode} {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
{#if $messages.length > 0}
{#each $messages as msg}
<div class="alert">{msg.message}</div>

View file

@ -149,7 +149,7 @@
}
return tags["value"] === "true";
});
onDestroy(state.register(path, directValue, true));
onDestroy(state.register(path, directValue));
}
let subSchemas: ConfigMeta[] = [];

View file

@ -72,6 +72,7 @@ const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson
const tags = new UIEventSource({ value });
const store = state.getStoreFor(path);
tags.addCallbackAndRunD(tgs => {
store.setData(tgs["value"]);
@ -112,7 +113,7 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
<slot name="upper-right" />
</div>
{#if $allowQuestions}
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
<SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
{/if}
{#each ($mappings ?? []) as mapping, i (mapping)}

View file

@ -24,8 +24,9 @@
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
import type { ConfigMeta } from "./Studio/configMeta";
import EditTheme from "./Studio/EditTheme.svelte";
import * as meta from "../../package.json"
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
export let studioUrl = window.location.hostname === "127.0.0.2" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
let osmConnection = new OsmConnection(new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
@ -61,18 +62,22 @@
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
const version = meta.version
async function editLayer(event: Event) {
const layerId: {owner: number, id: string} = event.detail;
state = "loading";
editLayerState.startSavingUpdates(false)
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner));
editLayerState.startSavingUpdates()
state = "editing_layer";
}
async function editTheme(event: Event) {
const id : {id: string, owner: number} = event.detail;
state = "loading";
editThemeState.startSavingUpdates(false)
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner));
editThemeState.startSavingUpdates()
state = "editing_theme";
}
@ -153,6 +158,7 @@
Show the introduction again
</NextButton>
</div>
<span class="subtle">MapComplete version {version}</span>
</div>
{:else if state === "edit_layer"}
@ -160,14 +166,14 @@
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
<h2>Choose a layer to edit</h2>
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
<ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}>
<h3 slot="title">Your layers</h3>
</ChooseLayerToEdit>
<h3>Layers by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
<ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} />
<h3>Official layers by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialLayers} on:layerSelected={editLayer} />
<ChooseLayerToEdit {osmConnection} layerIds={$officialLayers} on:layerSelected={editLayer} />
</div>
{:else if state === "edit_theme"}
@ -175,13 +181,13 @@
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
<h2>Choose a theme to edit</h2>
<ChooseLayerToEdit layerIds={$selfThemes} on:layerSelected={editTheme}>
<ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}>
<h3 slot="title">Your themes</h3>
</ChooseLayerToEdit>
<h3>Themes by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherThemes} on:layerSelected={editTheme} />
<ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} />
<h3>Official themes by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialThemes} on:layerSelected={editTheme} />
<ChooseLayerToEdit {osmConnection} layerIds={$officialThemes} on:layerSelected={editTheme} />
</div>
{:else if state === "loading"}

View file

@ -59,7 +59,6 @@ export class Translation extends BaseUIElement {
"Constructing a translation, but the object containing translations is empty " +
(context ?? "No context given")
)
throw `Constructing a translation, but the object containing translations is empty (${context})`
}
}

View file

@ -9816,556 +9816,7 @@
"required": false,
"hints": {
"question": "Should the created point be snapped to a line layer?",
"suggestions": [
{
"if": "value=address",
"then": "address - Addresses"
},
{
"if": "value=advertising",
"then": "advertising - We will complete data from advertising features with reference, operator and lit"
},
{
"if": "value=ambulancestation",
"then": "ambulancestation - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
},
{
"if": "value=animal_shelter",
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
},
{
"if": "value=artwork",
"then": "artwork - An open map of statues, busts, graffitis and other artwork all over the world"
},
{
"if": "value=atm",
"then": "atm - ATMs to withdraw money"
},
{
"if": "value=bank",
"then": "bank - A financial institution to deposit money"
},
{
"if": "value=barrier",
"then": "barrier - Obstacles while cycling, such as bollards and cycle barriers"
},
{
"if": "value=bench",
"then": "bench - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
},
{
"if": "value=bench_at_pt",
"then": "bench_at_pt - A layer showing all public-transport-stops which do have a bench"
},
{
"if": "value=bicycle_library",
"then": "bicycle_library - A facility where bicycles can be lent for longer period of times"
},
{
"if": "value=bicycle_rental",
"then": "bicycle_rental - Bicycle rental stations"
},
{
"if": "value=bicycle_tube_vending_machine",
"then": "bicycle_tube_vending_machine - A layer showing vending machines for bicycle tubes (either purpose-built bicycle tube vending machines or classical vending machines with bicycle tubes and optionally additional bicycle related objects such as lights, gloves, locks, …)"
},
{
"if": "value=bike_cafe",
"then": "bike_cafe - A bike café is a café geared towards cyclists, for example with services such as a pump, with lots of bicycle-related decoration, …"
},
{
"if": "value=bike_cleaning",
"then": "bike_cleaning - A layer showing facilities where one can clean their bike"
},
{
"if": "value=bike_parking",
"then": "bike_parking - A layer showing where you can park your bike"
},
{
"if": "value=bike_repair_station",
"then": "bike_repair_station - A layer showing bicycle pumps and bicycle repair tool stands"
},
{
"if": "value=bike_shop",
"then": "bike_shop - A shop specifically selling bicycles or related items"
},
{
"if": "value=bike_themed_object",
"then": "bike_themed_object - A layer with bike-themed objects but who don't match any other layer"
},
{
"if": "value=binocular",
"then": "binocular - Binoculars"
},
{
"if": "value=birdhide",
"then": "birdhide - A birdhide"
},
{
"if": "value=cafe_pub",
"then": "cafe_pub - A layer showing cafés and pubs where one can gather around a drink. The layer asks for some relevant questions"
},
{
"if": "value=car_rental",
"then": "car_rental - Places where you can rent a car"
},
{
"if": "value=charging_station",
"then": "charging_station - A charging station"
},
{
"if": "value=climbing",
"then": "climbing - A dummy layer which contains tagrenderings, shared among the climbing layers"
},
{
"if": "value=climbing_area",
"then": "climbing_area - An area where climbing is possible, e.g. a crag, site, boulder, … Contains aggregation of routes"
},
{
"if": "value=climbing_club",
"then": "climbing_club - A climbing club or organisation"
},
{
"if": "value=climbing_gym",
"then": "climbing_gym - A climbing gym"
},
{
"if": "value=climbing_opportunity",
"then": "climbing_opportunity - Fallback layer with items on which climbing _might_ be possible. It is loaded when zoomed in a lot, to prevent duplicate items to be added"
},
{
"if": "value=climbing_route",
"then": "climbing_route - A single climbing route and its properties. Some properties are derived from the containing features"
},
{
"if": "value=clock",
"then": "clock - Layer with public clocks"
},
{
"if": "value=conflation",
"then": "conflation - If the import-button moves OSM points, the imported way points or conflates, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme."
},
{
"if": "value=crab_address",
"then": "crab_address - Address data for Flanders by the governement, suited for import into OpenStreetMap. Datadump from 2021-10-26. This layer contains only visualisation logic. Import buttons should be added via an override. Note that HNRLABEL contains the original value, whereas _HNRLABEL contains a slightly cleaned version"
},
{
"if": "value=crossings",
"then": "crossings - Crossings for pedestrians and cyclists"
},
{
"if": "value=current_view",
"then": "current_view - A meta-layer which contains one single feature, namely the bounding box of the current map view. This can be used to trigger special actions. If a popup is defined for this layer, this popup will be accessible via an extra button on screen.\n\nThe icon on the button is the default icon of the layer, but can be customized by detecting 'button=yes'."
},
{
"if": "value=cycleways_and_roads",
"then": "cycleways_and_roads - All infrastructure that someone can cycle over, accompanied with questions about this infrastructure"
},
{
"if": "value=defibrillator",
"then": "defibrillator - A layer showing defibrillators which can be used in case of emergency. This contains public defibrillators, but also defibrillators which might need staff to fetch the actual device"
},
{
"if": "value=dentist",
"then": "dentist - This layer shows dentist offices"
},
{
"if": "value=direction",
"then": "direction - This layer visualizes directions"
},
{
"if": "value=doctors",
"then": "doctors - This layer shows doctor offices"
},
{
"if": "value=dogpark",
"then": "dogpark - A layer showing dogparks, which are areas where dog are allowed to run without a leash"
},
{
"if": "value=drinking_water",
"then": "drinking_water - A layer showing drinking water fountains"
},
{
"if": "value=elevator",
"then": "elevator - This layer show elevators and asks for operational status and elevator dimensions. Useful for wheelchair accessibility information"
},
{
"if": "value=elongated_coin",
"then": "elongated_coin - Layer showing penny presses."
},
{
"if": "value=entrance",
"then": "entrance - A layer showing entrances and offering capabilities to survey some advanced data which is important for e.g. wheelchair users (but also bicycle users, people who want to deliver, …)"
},
{
"if": "value=etymology",
"then": "etymology - All objects which have an etymology known"
},
{
"if": "value=extinguisher",
"then": "extinguisher - Map layer to show fire extinguishers."
},
{
"if": "value=filters",
"then": "filters - This layer acts as library for common filters"
},
{
"if": "value=fire_station",
"then": "fire_station - Map layer to show fire stations."
},
{
"if": "value=fitness_centre",
"then": "fitness_centre - Layer showing fitness centres"
},
{
"if": "value=fitness_station",
"then": "fitness_station - Find a fitness station near you, and add missing ones."
},
{
"if": "value=fixme",
"then": "fixme - OSM objects that likely need to be fixed, based on a FIXME tag."
},
{
"if": "value=food",
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
},
{
"if": "value=ghost_bike",
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
},
{
"if": "value=governments",
"then": "governments - This layer show governmental buildings. It was setup as commissioned layer for the client of OSOC '22"
},
{
"if": "value=gps_location",
"then": "gps_location - Meta layer showing the current location of the user. Add this to your theme and override the icon to change the appearance of the current location. The object will always have `id=gps` and will have _all_ the properties included in the [`Coordinates`-object](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates) (except latitude and longitude) returned by the browser, such as `speed`, `altitude`, `heading`, ...."
},
{
"if": "value=gps_location_history",
"then": "gps_location_history - Meta layer which contains the previous locations of the user as single points. This is mainly for technical reasons, e.g. to keep match the distance to the modified object"
},
{
"if": "value=gps_track",
"then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track."
},
{
"if": "value=guidepost",
"then": "guidepost - 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"
},
{
"if": "value=hackerspace",
"then": "hackerspace - Hackerspace"
},
{
"if": "value=home_location",
"then": "home_location - Meta layer showing the home location of the user. The home location can be set in the [profile settings](https://www.openstreetmap.org/profile/edit) of OpenStreetMap."
},
{
"if": "value=hospital",
"then": "hospital - A layer showing hospital grounds"
},
{
"if": "value=hotel",
"then": "hotel - Layer showing all hotels"
},
{
"if": "value=hydrant",
"then": "hydrant - Map layer to show fire hydrants."
},
{
"if": "value=ice_cream",
"then": "ice_cream - A place where ice cream is sold over the counter"
},
{
"if": "value=icons",
"then": "icons - A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
},
{
"if": "value=id_presets",
"then": "id_presets - Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset.<tagrendering>"
},
{
"if": "value=import_candidate",
"then": "import_candidate - Layer used as template in the importHelper"
},
{
"if": "value=indoors",
"then": "indoors - Basic indoor mapping: shows room outlines"
},
{
"if": "value=information_board",
"then": "information_board - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
},
{
"if": "value=kerbs",
"then": "kerbs - A layer showing kerbs."
},
{
"if": "value=kindergarten_childcare",
"then": "kindergarten_childcare - Shows kindergartens and preschools. Both are grouped in one layer, as they are regularly confused with each other"
},
{
"if": "value=last_click",
"then": "last_click - This layer defines how to render the 'last click'-location. By default, it will show a marker with the possibility to add a new point (if there are some presets) and/or to add a new note (if the 'note' layer attribute is set). If none are possible, this layer won't show up"
},
{
"if": "value=map",
"then": "map - A map, meant for tourists which is permanently installed in the public space"
},
{
"if": "value=maproulette",
"then": "maproulette - Layer showing all tasks in MapRoulette"
},
{
"if": "value=maproulette_challenge",
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
},
{
"if": "value=maxspeed",
"then": "maxspeed - Shows the allowed speed for every road"
},
{
"if": "value=memorial",
"then": "memorial - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
},
{
"if": "value=named_streets",
"then": "named_streets - Hidden layer with all streets which have a name. Useful to detect addresses"
},
{
"if": "value=nature_reserve",
"then": "nature_reserve - A nature reserve is an area where nature can take its course"
},
{
"if": "value=note",
"then": "note - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
},
{
"if": "value=observation_tower",
"then": "observation_tower - Towers with a panoramic view"
},
{
"if": "value=osm_community_index",
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
},
{
"if": "value=parcel_lockers",
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
},
{
"if": "value=parking",
"then": "parking - A layer showing car parkings"
},
{
"if": "value=parking_spaces",
"then": "parking_spaces - Layer showing individual parking spaces."
},
{
"if": "value=parking_ticket_machine",
"then": "parking_ticket_machine - Layer with parking ticket machines to pay for parking."
},
{
"if": "value=pedestrian_path",
"then": "pedestrian_path - Pedestrian footpaths, especially used for indoor navigation and snapping entrances to this layer"
},
{
"if": "value=pharmacy",
"then": "pharmacy - A layer showing pharmacies, which (probably) dispense prescription drugs"
},
{
"if": "value=physiotherapist",
"then": "physiotherapist - This layer shows physiotherapists"
},
{
"if": "value=picnic_table",
"then": "picnic_table - The layer showing picnic tables"
},
{
"if": "value=play_forest",
"then": "play_forest - Een speelbos is een vrij toegankelijke zone in een bos"
},
{
"if": "value=playground",
"then": "playground - Playgrounds"
},
{
"if": "value=postboxes",
"then": "postboxes - The layer showing postboxes."
},
{
"if": "value=postoffices",
"then": "postoffices - A layer showing post offices."
},
{
"if": "value=public_bookcase",
"then": "public_bookcase - A streetside cabinet with books, accessible to anyone"
},
{
"if": "value=questions",
"then": "questions - Special library layer which does not need a '.questions'-prefix before being imported"
},
{
"if": "value=railway_platforms",
"then": "railway_platforms - Find every platform in the station, and the train routes that use them."
},
{
"if": "value=rainbow_crossings",
"then": "rainbow_crossings - A layer showing pedestrian crossings with rainbow paintings"
},
{
"if": "value=range",
"then": "range - Meta-layer, simply showing a bbox in red"
},
{
"if": "value=reception_desk",
"then": "reception_desk - A layer showing where the reception desks are and which asks some accessibility information"
},
{
"if": "value=recycling",
"then": "recycling - A layer with recycling containers and centres"
},
{
"if": "value=route_marker",
"then": "route_marker - Route markers are small markers often found along official hiking/cycling/riding/skiing routes to indicate the direction of the route."
},
{
"if": "value=school",
"then": "school - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
},
{
"if": "value=selected_element",
"then": "selected_element - Highlights the currently selected element. Override this layer to have different colors"
},
{
"if": "value=shelter",
"then": "shelter - Layer showing shelter structures"
},
{
"if": "value=shops",
"then": "shops - A shop"
},
{
"if": "value=shower",
"then": "shower - A layer showing (public) showers"
},
{
"if": "value=slow_roads",
"then": "slow_roads - All carfree roads"
},
{
"if": "value=speed_camera",
"then": "speed_camera - Layer showing speed cameras"
},
{
"if": "value=speed_display",
"then": "speed_display - Layer showing speed displays that alert drivers of their speed."
},
{
"if": "value=split_point",
"then": "split_point - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
},
{
"if": "value=split_road",
"then": "split_road - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
},
{
"if": "value=sport_pitch",
"then": "sport_pitch - A sport pitch"
},
{
"if": "value=sports_centre",
"then": "sports_centre - Indoor and outdoor sports centres can be found on this layer"
},
{
"if": "value=stairs",
"then": "stairs - Layer showing stairs and escalators"
},
{
"if": "value=street_lamps",
"then": "street_lamps - A layer showing street lights"
},
{
"if": "value=surveillance_camera",
"then": "surveillance_camera - This layer shows surveillance cameras and allows a contributor to update information and add new cameras"
},
{
"if": "value=tertiary_education",
"then": "tertiary_education - Layer with all tertiary education institutes (ISCED:2011 levels 6,7 and 8)"
},
{
"if": "value=ticket_machine",
"then": "ticket_machine - Find ticket machines for public transport tickets"
},
{
"if": "value=ticket_validator",
"then": "ticket_validator - Find ticket validators to validate public transport tickets"
},
{
"if": "value=toilet",
"then": "toilet - A layer showing (public) toilets"
},
{
"if": "value=toilet_at_amenity",
"then": "toilet_at_amenity - A layer showing (public) toilets located at different places."
},
{
"if": "value=trail",
"then": "trail - Aangeduide wandeltochten"
},
{
"if": "value=transit_routes",
"then": "transit_routes - Layer showing bus lines"
},
{
"if": "value=transit_stops",
"then": "transit_stops - Layer showing different types of transit stops."
},
{
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
},
{
"if": "value=vending_machine",
"then": "vending_machine - Layer showing vending machines"
},
{
"if": "value=veterinary",
"then": "veterinary - A layer showing veterinarians"
},
{
"if": "value=viewpoint",
"then": "viewpoint - A nice viewpoint or nice view. Ideal to add an image if no other category fits"
},
{
"if": "value=village_green",
"then": "village_green - A layer showing village-green (which are communal green areas, but not quite parks)"
},
{
"if": "value=visitor_information_centre",
"then": "visitor_information_centre - A visitor center offers information about a specific attraction or place of interest where it is located."
},
{
"if": "value=walls_and_buildings",
"then": "walls_and_buildings - Special builtin layer providing all walls and buildings. This layer is useful in presets for objects which can be placed against walls (e.g. AEDs, postboxes, entrances, addresses, surveillance cameras, …). This layer is invisible by default and not toggleable by the user."
},
{
"if": "value=waste_basket",
"then": "waste_basket - This is a public waste basket, thrash can, where you can throw away your thrash."
},
{
"if": "value=waste_disposal",
"then": "waste_disposal - Waste Disposal Bin, medium to large bin for disposal of (household) waste"
},
{
"if": "value=windturbine",
"then": "windturbine - Modern windmills generating electricity"
}
]
"suggestions": []
},
"type": "array",
"description": "If specified, these layers will be shown in the precise location picker and the new point will be snapped towards it.\nFor example, this can be used to snap against `walls_and_buildings` (e.g. to attach a defibrillator, an entrance, an artwork, ... to the wall)\nor to snap an obstacle (such as a bollard) to the `cycleways_and_roads`."
@ -10418,7 +9869,7 @@
"type": "object",
"properties": {
"key": {
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
"type": "string"
},
"type": {
@ -10426,7 +9877,15 @@
"type": "string"
},
"placeholder": {
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -11150,7 +10609,7 @@
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": ""
"description": "This is the OpenStreetMap-key that that value will be written to"
},
{
"path": [
@ -11255,6 +10714,14 @@
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "A (translated) text that is shown (as gray text) within the textfield"
},
{
@ -12277,7 +11744,7 @@
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": ""
"description": "This is the OpenStreetMap-key that that value will be written to"
},
{
"path": [
@ -12384,6 +11851,14 @@
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "A (translated) text that is shown (as gray text) within the textfield"
},
{
@ -13437,7 +12912,7 @@
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": ""
"description": "This is the OpenStreetMap-key that that value will be written to"
},
{
"path": [
@ -13544,6 +13019,14 @@
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "A (translated) text that is shown (as gray text) within the textfield"
},
{
@ -14609,7 +14092,7 @@
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": ""
"description": "This is the OpenStreetMap-key that that value will be written to"
},
{
"path": [
@ -14718,6 +14201,14 @@
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "A (translated) text that is shown (as gray text) within the textfield"
},
{

File diff suppressed because it is too large Load diff

View file

@ -477,7 +477,7 @@
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": ""
"description": "This is the OpenStreetMap-key that that value will be written to"
},
{
"path": [
@ -580,6 +580,14 @@
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "A (translated) text that is shown (as gray text) within the textfield"
},
{

View file

@ -1,8 +1,5 @@
import { Utils } from "../../../../src/Utils"
import {
ConversionContext,
DesugaringContext,
} from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import { 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"
@ -10,6 +7,7 @@ import * as bookcases from "../../../../assets/layers/public_bookcase/public_boo
import CreateNoteImportLayer from "../../../../src/Models/ThemeConfig/Conversion/CreateNoteImportLayer"
import { describe, expect, it } from "vitest"
import { QuestionableTagRenderingConfigJson } from "../../../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/ConversionContext"
describe("CreateNoteImportLayer", () => {
it("should generate a layerconfig", () => {

View file

@ -1,7 +1,8 @@
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"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/ConversionContext"
describe("FixLegacyTheme", () => {
it("should create a working theme config", () => {

View file

@ -8,7 +8,8 @@ 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"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/ConversionContext"
describe("ExpandRewrite", () => {
it("should not allow overlapping keys", () => {

View file

@ -7,14 +7,12 @@ 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 {
ConversionContext,
DesugaringContext,
} from "../../../../src/Models/ThemeConfig/Conversion/Conversion"
import { 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"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/ConversionContext"
const themeConfigJson: LayoutConfigJson = {
description: "Descr",