Merge branch 'develop' into sauna

This commit is contained in:
Pieter Vander Vennet 2025-04-22 01:27:27 +02:00
commit 3bb25b9ed0
64 changed files with 1571 additions and 1297 deletions

View file

@ -0,0 +1,240 @@
{
"id": "adult_changing_table",
"source": {
"osmTags": {
"or": [
"amenity=adult_changing_table",
"changing_table:adult=yes",
"toilets:changing_table:adult=yes",
"toilets:wheelchair:changing_table:adult=yes"
]
}
},
"description": {
"en": "An adult changing table is a bench where adult people can be placed on. They are often used by adults with a severe motoric handicap"
},
"minzoom": 6,
"allowMove": {
"enableRelocation": false,
"enableImproveAccuracy": true
},
"deletion": true,
"name": {
"en": "Adult changing tables",
"nl": "Verzorgingstafels voor volwassenen"
},
"presets": [
{
"title": {
"en": "an adult changing table",
"nl": "een verzorgingstafel voor volwassenen"
},
"tags": [
"amenity=adult_changing_table"
]
}
],
"pointRendering": [
{
"location": [
"centroid",
"point"
],
"marker": [
{
"icon": "circle",
"color": "white"
}
]
}
],
"tagRenderings": [
{
"id": "height",
"labels": [
"relevant_questions"
],
"question": {
"en": "What is the height of the adult changing table?",
"nl": "Hoe hoog is de verzorgingstafel voor volwassenen?"
},
"questionHint": {
"en": "This is measured between the floor and the top of the changing table",
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"mappings": [
{
"if": "height=adjustable",
"then": {
"en": "The changing table is <b>adjustable in height</b>",
"nl": "De verzorgingstafel is <b>in hoogte verstelbaar</b>"
}
}
],
"freeform": {
"key": "height",
"type": "pfloat"
},
"render": {
"en": "The changing table is {canonical(height)} high",
"nl": "De verzorgingstafel is {canonical(height)} hoog"
}
},
{
"labels": [
"relevant_questions"
],
"id": "adult-changing-table-min_height",
"question": {
"en": "What is the lowest height the adult changing table can be moved to?",
"nl": "Wat is de laagste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?"
},
"questionHint": {
"en": "This is measured between the floor and the top of the changing table",
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"freeform": {
"key": "min_height",
"type": "pfloat"
},
"render": {
"en": "The lowest height of the adult changing table is {canonical(min_height)}",
"nl": "De laagste stand van de verzorgingstafel is {canonical(min_height)} hoog"
},
"condition": {
"and": [
"height=adjustable"
]
}
},
{
"labels": [
"relevant_questions"
],
"id": "adult-changing-table-max_height",
"question": {
"en": "What is the highest height the adult changing table can be moved to?",
"nl": "Wat is de hoogste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?"
},
"questionHint": {
"en": "This is measured between the floor and the top of the changing table",
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"freeform": {
"key": "max_height",
"type": "pfloat"
},
"render": {
"en": "The highest height of the adult changing table is {canonical(max_height)}",
"nl": "De hoogste stand van de verzorgingstafel is {canonical(max_height)} hoog"
},
"condition": {
"and": [
"height=adjustable"
]
}
},
{
"labels": [
"relevant_questions"
],
"id": "adult-changing-table-mechanism",
"question": {
"en": "How is the height of the changing table adjusted?",
"nl": "Hoe wordt de hoogte van de verzorgingstafel aangepast?"
},
"mappings": [
{
"if": "height:mechanism=manual",
"then": {
"nl": "De hoogte van de verzorgingstafel wordt <b>met de hand</b> aangepast",
"en": "The height of the adult changing table is adjusted <b>manually</b>"
}
},
{
"if": "height:mechanism=electric",
"then": {
"nl": "De verzorgingstafel wordt <b>door een electrische motor</b> in hoogte versteld",
"en": "The height of the adult changing table is adjusted <b>electrically</b>"
}
}
],
"condition": {
"and": [
"height=adjustable"
]
}
},
{
"labels": [
"relevant_questions"
],
"id": "adult-changing-table-support",
"labels": [
"hidden",
"prefixed",
"adult-changing-table"
],
"question": {
"en": "How is the adult changing table supported?",
"nl": "Hoe is de verschoningstafel in de ruimte geplaatst?"
},
"mappings": [
{
"if": "support=wall_mounted",
"then": {
"en": "The changing table is mounted to the wall",
"nl": "De verschoningstafel voor volwassenen hangt vast aan de muur"
}
},
{
"if": "support=legs",
"then": {
"en": "The changing table stands on table legs",
"nl": "De verschoningstafel voor volwassenen staat op tafelpoten"
}
},
{
"if": "support=wheels",
"then": {
"en": "The changing table stands on table legs <b>with wheels</b> and can be moved",
"nl": "De verschoningstafel voor volwassenen staat op tafelpoten <b>met wielen</b> en kan verplaatst worden"
}
}
]
}
],
"title": {
"en": "Adult changing table",
"nl": "Verzorgingstafel voor volwassenen"
},
"units": [
{
"adult:height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
},
{
"adult:min_height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
},
{
"adult:max_height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
}
]
}

View file

@ -131,6 +131,7 @@
], ],
"tagRenderings": [ "tagRenderings": [
"images", "images",
"opening_hours_24_7",
{ {
"question": { "question": {
"en": "How much does it cost to use the cleaning service?", "en": "How much does it cost to use the cleaning service?",
@ -283,6 +284,24 @@
], ],
"id": "bike_cleaning-charge" "id": "bike_cleaning-charge"
}, },
{
"builtin": "payment-options-split",
"override": {
"condition": "fee=yes"
}
},
{
"builtin": "denominations-coins",
"override": {
"condition": "payment:coins=yes"
}
},
{
"builtin": "denominations-notes",
"override": {
"condition": "payment:notes=yes"
}
},
{ {
"question": { "question": {
"en": "Is this bicycle cleaning service automated?", "en": "Is this bicycle cleaning service automated?",
@ -297,8 +316,8 @@
{ {
"if": "automated=no", "if": "automated=no",
"then": { "then": {
"en": "This is a manual bike washing station", "en": "This is a manual bike washing station - a person still has to point the water hose towards the bicycle",
"nl": "Dit is een handmatig fietsschoonmaakpunt", "nl": "Dit is een handmatig fietsschoonmaakpunt - een persoon moet zelf de waterspuit richten naar de fiets",
"de": "Dies ist eine manuelle Fahrradwaschanlage", "de": "Dies ist eine manuelle Fahrradwaschanlage",
"cs": "Jedná se o ruční mycí stanici kol", "cs": "Jedná se o ruční mycí stanici kol",
"es": "Esta es una estación manual de lavado de bicicletas", "es": "Esta es una estación manual de lavado de bicicletas",
@ -309,8 +328,8 @@
{ {
"if": "automated=yes", "if": "automated=yes",
"then": { "then": {
"en": "This is an automated bike wash", "en": "This is an automated bike wash. Your bicycle is placed in the device and everything happens automatically.",
"nl": "Dit is een automatisch fietsschoonmaakpunt", "nl": "Dit is een automatisch fietsschoonmaakpunt - eens je fiets erin geplaats, wordt alles volledig automatisch proper gemaakt",
"de": "Dies ist eine automatische Fahrradwaschanlage", "de": "Dies ist eine automatische Fahrradwaschanlage",
"cs": "Jedná se o mytí kol bez obsluhy", "cs": "Jedná se o mytí kol bez obsluhy",
"es": "Esta es una estación automática de lavado de bicicletas", "es": "Esta es una estación automática de lavado de bicicletas",

View file

@ -413,10 +413,9 @@
"service:electricity", "service:electricity",
"seating", "seating",
"dog-access", "dog-access",
"internet", "internet-all",
"internet-fee", "reviews",
"internet-ssid", "toilet_at_amenity_lib.all"
"reviews"
], ],
"filter": [ "filter": [
"open_now", "open_now",

View file

@ -0,0 +1,88 @@
{
"id": "excrement_bag_dispenser",
"name": {
"en": "Excrement bag dispensers"
},
"description": {
"en": "Dispensers giving out bags for animal waste"
},
"source": {
"osmTags": {
"and": [
"amenity=vending_machine",
"vending=excrement_bags"
]
}
},
"minzoom": 16,
"title": {
"render": {
"en": "Excrement bag dispenser"
}
},
"tagRenderings": [
{
"id": "fee",
"question": {
"en": "Does it cost money to use this dispenser?"
},
"mappings": [
{
"if": "fee=",
"then": {
"en": "This dispenser probably gives out bags for free."
}
},
{
"if": "fee=yes",
"then": {
"en": "This dispenser give out bags for a fee."
}
},
{
"if": "fee=no",
"then": {
"en": "This dispenser gives out bags for free."
}
}
]
},
"check_date"
],
"presets": [
{
"tags": [
"amenity=vending_machine",
"vending=excrement_bags"
],
"title": {
"en": "an excrement bag dispenser"
},
"description": {
"en": "A stand-alone dispenser giving out bags for animal waste."
}
}
],
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": "square",
"color": "white"
},
{
"icon": "./assets/layers/excrement_bag_dispenser/excrement_bags.svg"
}
],
"iconSize": "30,30"
}
],
"allowMove": {
"enableImproveAccuracy": true,
"enableRelocation": true
}
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path d="M 0,0 C 0,1 0,2 0,3 H 1 V 1 h 12 v 2 h 1 V 0 C 9.3333333,0 4.6666667,0 0,0 Z m 2,2 v 10 l -1,2 c 4,0 8,0 12,0 L 12,12 V 2 C 8.6666667,2 5.3333333,2 2,2 Z m 1,1 h 8 V 4 H 3 Z m 0,2 h 8 c 0,2.5 0,5 0,7.5 0,0.277 -0.223,0.5 -0.5,0.5 h -7 C 3.223,13 3,12.777 3,12.5 3,10 3,7.5 3,5 Z M 7,6 C 7,7 6.7426357,7.5622251 6,8 5.4460001,8 5,8.4460001 5,9 5,9.5539999 5.4460001,10 6,10 H 5 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 h 4 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 H 8 C 8.5539999,10 9,9.5539999 9,9 9,8.4460001 8.5539999,8 8,8 8.000334,7 7.7168863,6.4764131 7,6 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 680 B

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Jérémy Ragusa
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,12 @@
[
{
"path": "excrement_bags.svg",
"license": "CC0-1.0",
"authors": [
"Jérémy Ragusa"
],
"sources": [
"https://github.com/gravitystorm/openstreetmap-carto/blob/master/symbols/amenity/excrement_bags.svg"
]
}
]

View file

@ -1433,7 +1433,8 @@
"dog-access", "dog-access",
"internet", "internet",
"internet-fee", "internet-fee",
"internet-ssid" "internet-ssid",
"toilet_at_amenity_lib.all"
], ],
"filter": [ "filter": [
"open_now", "open_now",

View file

@ -73,8 +73,20 @@
"placeholder": { "placeholder": {
"en": "Name of the mobility hub", "en": "Name of the mobility hub",
"nl": "Naam van de mobiliteitshub" "nl": "Naam van de mobiliteitshub"
},
"addExtraTags": [
"noname="
]
},
"mappings": [
{
"if": "noname=yes",
"then": {
"en": "This mobility hub does not have a name",
"nl": "Deze mobiliteitshub heeft geen naam"
}
} }
} ]
}, },
{ {
"question": { "question": {
@ -90,7 +102,8 @@
"nl": "Netwerk van deze mobiliteitshub" "nl": "Netwerk van deze mobiliteitshub"
}, },
"addExtraTags": [ "addExtraTags": [
"network:wikidata=" "network:wikidata=",
"nonetwork="
] ]
}, },
"render": { "render": {
@ -98,6 +111,16 @@
"nl": "Deze mobiliteitshub hoort bij het netwerk {network}" "nl": "Deze mobiliteitshub hoort bij het netwerk {network}"
}, },
"mappings": [ "mappings": [
{
"if": "nonetwork=yes",
"then": {
"en": "This mobility hub does not belong to a network",
"nl": "Deze mobiliteitshub hoort niet bij een netwerk"
},
"addExtraTags": [
"network:wikidata="
]
},
{ {
"if": "network=Groningen-Drenthe", "if": "network=Groningen-Drenthe",
"then": { "then": {
@ -107,7 +130,8 @@
"hideInAnswer": "_country!=nl", "hideInAnswer": "_country!=nl",
"icon": "./assets/layers/mobility_hub/hub-gd.svg", "icon": "./assets/layers/mobility_hub/hub-gd.svg",
"addExtraTags": [ "addExtraTags": [
"network:wikidata=Q108742233" "network:wikidata=Q108742233",
"nonetwork="
] ]
}, },
{ {
@ -119,7 +143,8 @@
"hideInAnswer": "_country!=be", "hideInAnswer": "_country!=be",
"icon": "./assets/layers/mobility_hub/logo-hoppin.svg", "icon": "./assets/layers/mobility_hub/logo-hoppin.svg",
"addExtraTags": [ "addExtraTags": [
"network:wikidata=Q124310711" "network:wikidata=Q124310711",
"nonetwork="
] ]
}, },
{ {
@ -130,7 +155,8 @@
}, },
"hideInAnswer": "_country!=de", "hideInAnswer": "_country!=de",
"addExtraTags": [ "addExtraTags": [
"network:wikidata=Q110948933" "network:wikidata=Q110948933",
"nonetwork="
] ]
} }
] ]
@ -179,7 +205,8 @@
"nl": "Deze mobiliteitshub is gemarkeerd door een eenvoudig bord met alleen simpele informatie zoals het logo of de naam" "nl": "Deze mobiliteitshub is gemarkeerd door een eenvoudig bord met alleen simpele informatie zoals het logo of de naam"
} }
} }
] ],
"condition": "_geometry:type=Point"
} }
], ],
"lineRendering": [ "lineRendering": [

View file

@ -1827,7 +1827,11 @@
"labels": [ "labels": [
"level" "level"
], ],
"condition": "repeat_on~*", "condition": {
"and": [
"repeat_on~*"
]
},
"render": { "render": {
"en": "Multiple, identical objects can be found on floors {repeat_on}.", "en": "Multiple, identical objects can be found on floors {repeat_on}.",
"nl": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}.", "nl": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}.",
@ -1842,7 +1846,11 @@
"labels": [ "labels": [
"level" "level"
], ],
"condition": "repeat_on=", "condition": {
"and": [
"repeat_on="
]
},
"question": { "question": {
"nl": "Op welke verdieping bevindt dit punt zich?", "nl": "Op welke verdieping bevindt dit punt zich?",
"en": "On what level is this feature located?", "en": "On what level is this feature located?",
@ -2053,7 +2061,11 @@
"pl": "Czy w {title()} wolno palić?" "pl": "Czy w {title()} wolno palić?"
}, },
"#condition": "Based on https://en.wikipedia.org/wiki/List_of_smoking_bans", "#condition": "Based on https://en.wikipedia.org/wiki/List_of_smoking_bans",
"condition": "_country!~al|be", "condition": {
"and": [
"_country!~al|be"
]
},
"mappings": [ "mappings": [
{ {
"if": "smoking=yes", "if": "smoking=yes",
@ -2401,7 +2413,11 @@
"labels": [ "labels": [
"internet-all" "internet-all"
], ],
"condition": "internet_access~.*wlan.*", "condition": {
"and": [
"internet_access~.*wlan.*"
]
},
"question": { "question": {
"en": "What is the network name for the wireless internet access?", "en": "What is the network name for the wireless internet access?",
"nl": "Wat is de netwerknaam voor de draadloze internettoegang?", "nl": "Wat is de netwerknaam voor de draadloze internettoegang?",
@ -3321,6 +3337,29 @@
"freeform": { "freeform": {
"key": "name" "key": "name"
} }
},
{
"id": "has_toilets",
"question": {
"en": "Has {title()} toilets?",
"nl": "Heeft {title()} toiletten?"
},
"mappings": [
{
"if": "toilets=yes",
"then": {
"en": "Has toilets",
"nl": "Heeft toiletten"
}
},
{
"if": "toilets=no",
"then": {
"en": "Has no toilets",
"nl": "Heeft geenad toiletten"
}
}
]
} }
], ],
"allowMove": false, "allowMove": false,

View file

@ -1326,7 +1326,8 @@
} }
}, },
"dog-access", "dog-access",
"description" "description",
"toilet_at_amenity_lib.all"
], ],
"filter": [ "filter": [
{ {

View file

@ -211,7 +211,14 @@
} }
} }
}, },
"level", {
"builtin": "level",
"override": {
"labels+": [
"amenity-no-prefix"
]
}
},
{ {
"question": { "question": {
"en": "Are these toilets publicly accessible?", "en": "Are these toilets publicly accessible?",
@ -247,6 +254,7 @@
"mappings": [ "mappings": [
{ {
"if": "access=yes", "if": "access=yes",
"alsoShowIf": "access=public",
"then": { "then": {
"en": "Public access", "en": "Public access",
"de": "Der Zugang ist öffentlich", "de": "Der Zugang ist öffentlich",
@ -258,7 +266,38 @@
"da": "Offentlig adgang", "da": "Offentlig adgang",
"ca": "Accés públic", "ca": "Accés públic",
"cs": "Veřejný přístup", "cs": "Veřejný přístup",
"uk": "Публічний доступ" "uk": "Публічний доступ",
"en": "Public access",
"de": "Öffentlicher Zugang",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup",
"en": "Public access",
"de": "Der Zugang ist öffentlich",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup",
"sl": "Javno dostopno",
"en": "Public access",
"de": "Öffentlicher Zugang",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup"
} }
}, },
{ {
@ -310,33 +349,19 @@
"cs": "Přístupné, ale pro vstup je třeba požádat o klíč", "cs": "Přístupné, ale pro vstup je třeba požádat o klíč",
"uk": "Доступний, але для входу потрібно попросити ключ" "uk": "Доступний, але для входу потрібно попросити ключ"
} }
},
{
"if": "access=public",
"then": {
"en": "Public access",
"de": "Öffentlicher Zugang",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup"
},
"hideInAnswer": true
} }
], ],
"labels": [ "labels": [
"relevant-questions" "relevant-questions",
"amenity-no-prefix"
], ],
"id": "toilet-access" "id": "toilet-access"
}, },
{ {
"id": "toilets-fee", "id": "toilets-fee",
"labels": [ "labels": [
"relevant-questions" "relevant-questions",
"amenity-no-prefix"
], ],
"condition": { "condition": {
"and": [ "and": [
@ -352,6 +377,8 @@
"da": "Er det gratis at benytte disse toiletter?", "da": "Er det gratis at benytte disse toiletter?",
"ca": "Aquest serveis són gratuïts?", "ca": "Aquest serveis són gratuïts?",
"cs": "Jsou tyto toalety zdarma?", "cs": "Jsou tyto toalety zdarma?",
"es": "¿Son estos baños de uso gratuito?",
"sl": "Ali so ta stranišča brezplačna za uporabo?",
"es": "¿Son estos baños de uso gratuito?" "es": "¿Son estos baños de uso gratuito?"
}, },
"mappings": [ "mappings": [
@ -366,7 +393,9 @@
"es": "Estos son baños de pago", "es": "Estos son baños de pago",
"da": "Det er betalingstoiletter", "da": "Det er betalingstoiletter",
"ca": "Aquests serveis són de pagament", "ca": "Aquests serveis són de pagament",
"cs": "Jedná se o placené toalety" "cs": "Jedná se o placené toalety",
"cs": "Jedná se o placené toalety",
"sl": "To so plačljiva stranišča"
}, },
"if": "fee=yes" "if": "fee=yes"
}, },
@ -381,14 +410,17 @@
"da": "Gratis at bruge", "da": "Gratis at bruge",
"ca": "Gratuït", "ca": "Gratuït",
"cs": "Použití zdarma", "cs": "Použití zdarma",
"es": "De uso gratuito" "es": "De uso gratuito",
"pt": "Grátis para usar",
"sl": "Brezplačna uporaba"
} }
} }
] ]
}, },
{ {
"labels": [ "labels": [
"relevant-questions" "relevant-questions",
"amenity-no-prefix"
], ],
"question": { "question": {
"en": "How much does one have to pay for these toilets?", "en": "How much does one have to pay for these toilets?",
@ -400,7 +432,8 @@
"es": "¿Cuánto hay que pagar por estos baños?", "es": "¿Cuánto hay que pagar por estos baños?",
"da": "Hvor meget skal man betale for disse toiletter?", "da": "Hvor meget skal man betale for disse toiletter?",
"ca": "Quant s'ha de pagar per aquests lavabos?", "ca": "Quant s'ha de pagar per aquests lavabos?",
"cs": "Kolik se za tyto toalety platí?" "cs": "Kolik se za tyto toalety platí?",
"sl": "Koliko je potrebno plačati za ta stranišča?"
}, },
"render": { "render": {
"en": "The fee is {charge}", "en": "The fee is {charge}",
@ -412,7 +445,8 @@
"es": "La tarifa es {charge}", "es": "La tarifa es {charge}",
"da": "Gebyret er {charge}", "da": "Gebyret er {charge}",
"ca": "La taxa és {charge}", "ca": "La taxa és {charge}",
"cs": "Poplatek je {charge}" "cs": "Poplatek je {charge}",
"sl": "Plačilo je {charge}"
}, },
"condition": { "condition": {
"and": [ "and": [
@ -442,7 +476,8 @@
] ]
}, },
"=labels": [ "=labels": [
"relevant-questions" "relevant-questions",
"amenity-no-prefix"
] ]
} }
}, },
@ -454,6 +489,7 @@
"access!=no" "access!=no"
] ]
}, },
"#labels": "NOT included in amenity-no-prefix! The 'amenity' has their own opening hours",
"=labels": [ "=labels": [
"relevant-questions", "relevant-questions",
"no-prefix" "no-prefix"
@ -474,7 +510,8 @@
"id": "toilets-type", "id": "toilets-type",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Which kind of toilets are these?", "en": "Which kind of toilets are these?",
@ -550,7 +587,8 @@
"id": "toilets-disposal", "id": "toilets-disposal",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "How is the waste handled?", "en": "How is the waste handled?",
@ -589,7 +627,8 @@
"id": "gender_segregated", "id": "gender_segregated",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"no-prefix" "no-prefix",
"amenity-no-prefix"
], ],
"question": { "question": {
"en": "Are these toilets gender-segregated?", "en": "Are these toilets gender-segregated?",
@ -636,7 +675,8 @@
"id": "menstrual_products", "id": "menstrual_products",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Are free, menstrual products distributed here?", "en": "Are free, menstrual products distributed here?",
@ -699,7 +739,8 @@
"id": "menstrual_products_location", "id": "menstrual_products_location",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Where are the free menstrual products located?", "en": "Where are the free menstrual products located?",
@ -769,9 +810,11 @@
}, },
{ {
"id": "toilets-changing-table", "id": "toilets-changing-table",
"#labels": "Very weird case: we transfer this as is to the 'amenity'-layer",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"no-prefix" "no-prefix",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Is a changing table (to change diapers) available?", "en": "Is a changing table (to change diapers) available?",
@ -819,7 +862,8 @@
{ {
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"no-prefix" "no-prefix",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Where is the changing table located?", "en": "Where is the changing table located?",
@ -917,7 +961,8 @@
"id": "toilet-supervised", "id": "toilet-supervised",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"no-prefix" "no-prefix",
"amenity-no-prefix"
], ],
"question": { "question": {
"en": "Is this toilets supervised by a person?", "en": "Is this toilets supervised by a person?",
@ -964,7 +1009,8 @@
{ {
"id": "toilet-has-paper", "id": "toilet-has-paper",
"labels": [ "labels": [
"relevant-questions" "relevant-questions",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Does one have to bring their own toilet paper to this toilet?", "en": "Does one have to bring their own toilet paper to this toilet?",
@ -1014,7 +1060,8 @@
{ {
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"id": "toilet-handwashing", "id": "toilet-handwashing",
"question": { "question": {
@ -1060,7 +1107,8 @@
"id": "toilet-drying", "id": "toilet-drying",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Do these toilets have a device to dry your hands?", "en": "Do these toilets have a device to dry your hands?",
@ -1110,12 +1158,23 @@
] ]
} }
}, },
{
"builtin":
"description", "description",
"override": {
"labels": [
"amenity-no-prefix",
"no-prefix",
"relevant-questions"
]
}
},
{ {
"id": "wheelchair-group", "id": "wheelchair-group",
"labels": [ "labels": [
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"render": { "render": {
"special": { "special": {
@ -1131,7 +1190,8 @@
"relevant-questions", "relevant-questions",
"wheelchair", "wheelchair",
"hidden", "hidden",
"no-prefix" "no-prefix",
"amenity-no-prefix"
], ],
"question": { "question": {
"en": "Is there a dedicated toilet for wheelchair users?", "en": "Is there a dedicated toilet for wheelchair users?",
@ -1142,7 +1202,8 @@
"da": "Er der et særligt toilet til kørestolsbrugere?", "da": "Er der et særligt toilet til kørestolsbrugere?",
"ca": "Hi ha un lavabo específic per a usuaris amb cadira de rodes?", "ca": "Hi ha un lavabo específic per a usuaris amb cadira de rodes?",
"cs": "Je zde vyhrazená toaleta pro vozíčkáře?", "cs": "Je zde vyhrazená toaleta pro vozíčkáře?",
"es": "¿Hay un baño dedicado para usuarios de sillas de ruedas?" "es": "¿Hay un baño dedicado para usuarios de sillas de ruedas?",
"sl": "Ali je tu stranišče namenjeno invalidom na vozičku?"
}, },
"mappings": [ "mappings": [
{ {
@ -1171,7 +1232,8 @@
"es": "Sin acceso para sillas de ruedas", "es": "Sin acceso para sillas de ruedas",
"da": "Ingen kørestolsadgang", "da": "Ingen kørestolsadgang",
"ca": "Sense accés per a cadires de rodes", "ca": "Sense accés per a cadires de rodes",
"cs": "Žádný bezbariérový přístup" "cs": "Žádný bezbariérový přístup",
"sl": "Ni dostopno invalidom na vozičku"
} }
}, },
{ {
@ -1205,7 +1267,8 @@
"wheelchair", "wheelchair",
"hidden", "hidden",
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"render": { "render": {
"special": { "special": {
@ -1231,7 +1294,8 @@
"wheelchair", "wheelchair",
"hidden", "hidden",
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"render": { "render": {
"special": { "special": {
@ -1249,7 +1313,8 @@
"labels": [ "labels": [
"hidden", "hidden",
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"render": { "render": {
"en": "Wheelchair accessible toilet", "en": "Wheelchair accessible toilet",
@ -1284,7 +1349,7 @@
] ]
}, },
{ {
"id": "wheelchair-access", "id": "toilet-wheelchair-access",
"question": { "question": {
"en": "Is the wheelchair-accessible toilet locked?", "en": "Is the wheelchair-accessible toilet locked?",
"nl": "Is de rolstoeltoegankelijke toilet op slot?" "nl": "Is de rolstoeltoegankelijke toilet op slot?"
@ -1311,7 +1376,8 @@
"hidden", "hidden",
"wheelchair", "wheelchair",
"relevant-questions", "relevant-questions",
"prefixed" "prefixed",
"amenity-prefixed"
], ],
"mappings": [ "mappings": [
{ {
@ -1429,7 +1495,8 @@
"labels": [ "labels": [
"wheelchair", "wheelchair",
"hidden", "hidden",
"relevant-questions" "relevant-questions",
"amenity-prefixed"
], ],
"render": { "render": {
"special": { "special": {
@ -1439,17 +1506,21 @@
} }
}, },
{ {
"id": "adult-changing-table-title", "builtin": "adult_changing_table.title",
"labels": [ "override": {
"hidden", "labels+": [
"hidden",
"prefixed", "prefixed",
"adult-changing-table" "adult-changing-table",
"amenity-prefixed"
], ],
"render": { "condition": {
"en": "<h3>Adult changing table</h3>", "and": [
"nl": "<h3>Verzorgingstafel voor volwassenen</h3>" "changing_table:adult=yes"
}, ]
"condition": "changing_table:adult=yes" },
"classes": "bold text-lg"
}
}, },
{ {
"id": "adult-changing-table", "id": "adult-changing-table",
@ -1457,7 +1528,8 @@
"prefixed", "prefixed",
"hidden", "hidden",
"relevant-questions", "relevant-questions",
"adult-changing-table" "adult-changing-table",
"amenity-prefixed"
], ],
"question": { "question": {
"en": "Does this toilet have an adult changing table?", "en": "Does this toilet have an adult changing table?",
@ -1482,175 +1554,29 @@
] ]
}, },
{ {
"id": "adult-changing-table-height", "builtin": "adult_changing_table.relevant_questions",
"labels": [ "override": {
"hidden", "labels+": [
"prefixed", "hidden",
"adult-changing-table" "prefixed",
], "adult-changing-table",
"question": { "amenity-prefixed"
"en": "What is the height of the adult changing table?", ],
"nl": "Hoe hoog is de verzorgingstafel voor volwassenen?" "condition": {
}, "and+": [
"questionHint": { "changing_table:adult=yes"
"en": "This is measured between the floor and the top of the changing table", ]
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"mappings": [
{
"if": "changing_table:adult:height=adjustable",
"then": {
"en": "The changing table is <b>adjustable in height</b>",
"nl": "De verzorgingstafel is <b>in hoogte verstelbaar</b>"
}
} }
],
"freeform": {
"key": "changing_table:adult:height",
"type": "pfloat"
}, },
"render": { "prefix": "changing_table:adult"
"en": "The changing table is {canonical(changing_table:adult:height)} high",
"nl": "De verzorgingstafel is {canonical(changing_table:adult:height)} hoog"
},
"condition": {
"and": [
"changing_table:adult=yes"
]
}
},
{
"id": "adult-changing-table-min_height",
"labels": [
"hidden",
"prefixed",
"adult-changing-table"
],
"question": {
"en": "What is the lowest height the adult changing table can be moved to?",
"nl": "Wat is de laagste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?"
},
"questionHint": {
"en": "This is measured between the floor and the top of the changing table",
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"freeform": {
"key": "changing_table:adult:min_height",
"type": "pfloat"
},
"render": {
"en": "The lowest height of the adult changing table is {canonical(changing_table:adult:min_height)}",
"nl": "De laagste stand van de verzorgingstafel is {canonical(changing_table:adult:min_height)} hoog"
},
"condition": {
"and": [
"changing_table:adult:height=adjustable"
]
}
},
{
"id": "adult-changing-table-max_height",
"labels": [
"hidden",
"prefixed",
"adult-changing-table"
],
"question": {
"en": "What is the highest height the adult changing table can be moved to?",
"nl": "Wat is de hoogste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?"
},
"questionHint": {
"en": "This is measured between the floor and the top of the changing table",
"nl": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel"
},
"freeform": {
"key": "changing_table:adult:max_height",
"type": "pfloat"
},
"render": {
"en": "The highest height of the adult changing table is {canonical(changing_table:adult:max_height)}",
"nl": "De hoogste stand van de verzorgingstafel is {canonical(changing_table:adult:max_height)} hoog"
},
"condition": {
"and": [
"changing_table:adult:height=adjustable"
]
}
},
{
"id": "adult-changing-table-mechanism",
"labels": [
"hidden",
"relevant-questions",
"adult-changing-table"
],
"question": {
"en": "How is the height of the changing table adjusted?",
"nl": "Hoe wordt de hoogte van de verzorgingstafel aangepast?"
},
"mappings": [
{
"if": "changing_table:adult:height:mechanism=manual",
"then": {
"nl": "De hoogte van de verzorgingstafel wordt <b>met de hand</b> aangepast",
"en": "The height of the adult changing table is adjusted <b>manually</b>"
}
},
{
"if": "changing_table:adult:height:mechanism=electric",
"then": {
"nl": "De verzorgingstafel wordt <b>door een electrische motor</b> in hoogte versteld",
"en": "The height of the adult changing table is adjusted <b>electrically</b>"
}
}
],
"condition": {
"and": [
"changing_table:adult:height=adjustable"
]
}
},
{
"id": "adult-changing-table-support",
"labels": [
"hidden",
"prefixed",
"adult-changing-table"
],
"question": {
"en": "How is the adult changing table supported?",
"nl": "Hoe is de verschoningstafel in de ruimte geplaatst?"
},
"mappings": [
{
"if": "changing_table:adult:support=wall_mounted",
"then": {
"en": "The changing table is mounted to the wall",
"nl": "De verschoningstafel voor volwassenen hangt vast aan de muur"
}
},
{
"if": "changing_table:adult:support=legs",
"then": {
"en": "The changing table stands on table legs",
"nl": "De verschoningstafel voor volwassenen staat op tafelpoten"
}
},
{
"if": "changing_table:adult:support=wheels",
"then": {
"en": "The changing table stands on table legs <b>with wheels</b> and can be moved",
"nl": "De verschoningstafel voor volwassenen staat op tafelpoten <b>met wielen</b> en kan verplaatst worden"
}
}
]
}, },
{ {
"id": "questions-adult-changing-table", "id": "questions-adult-changing-table",
"labels": [ "labels": [
"hidden", "hidden",
"relevant-questions", "relevant-questions",
"adult-changing-table" "adult-changing-table",
"amenity-prefixed"
], ],
"render": { "render": {
"special": { "special": {
@ -1753,33 +1679,6 @@
"cm" "cm"
] ]
} }
},
{
"changing_table:adult:height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
},
{
"changing_table:adult:min_height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
},
{
"changing_table:adult:max_height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
} }
] ]
} }

View file

@ -140,203 +140,37 @@
], ],
"lineRendering": [], "lineRendering": [],
"tagRenderings": [ "tagRenderings": [
"images",
"level",
{ {
"question": { "id": "images",
"en": "Are these toilets publicly accessible?",
"de": "Ist die Toilette öffentlich zugänglich?",
"nl": "Zijn deze toiletten publiek toegankelijk?",
"fr": "Ces toilettes sont-elles librement accessibles ?",
"ca": "Aquests serveis són d'accés públic?",
"cs": "Jsou tyto toalety veřejně přístupné?",
"sl": "Ali so ta stranišča javno dostopna?",
"es": "¿Son estos baños de acceso público?"
},
"render": { "render": {
"en": "Access is {toilets:access}", "special": {
"de": "Zugang ist {toilets:access}", "before": "{image_carousel(toilets:panoramax;toilets:mapillary;toilets:images)}",
"fr": "L'accès est {toilets:access}", "type": "image_upload",
"nl": "Toegankelijkheid is {toilets:access}", "image_key": "toilets:panoramax",
"it": "L'accesso è {toilets:access}", "label": {
"es": "El acceso es {toilets:access}", "en": "Add a picture of the toilets",
"da": "Adgang er {toilets:access}", "nl": "Voeg een foto van de toiletten toe"
"ca": "L'accés és {toilets:access}", }
"cs": "Přístup je {toilets:access}", }
"sl": "Dostop je {toilets:access}", }
"uk": "Доступ - {toilets:access}" },
}, {
"freeform": { "builtin": "toilet.amenity-no-prefix",
"key": "toilets:access", "prefix": "toilets",
"addExtraTags": [ "override": {
"fixme=the tag toilets:access was filled out by the user and might need refinement" "labels+": [
"relevant_questions"
] ]
}, }
"mappings": [
{
"if": "toilets:access=yes",
"then": {
"en": "Public access",
"de": "Der Zugang ist öffentlich",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup",
"sl": "Javno dostopno"
}
},
{
"if": "toilets:access=customers",
"then": {
"en": "Only access to customers of the amenity",
"de": "Nur Zugang für Kunden der Einrichtung",
"nl": "Enkel toegankelijk voor klanten van de voorziening",
"fr": "Accessibles uniquement au clients du lieu",
"ca": "Només accessible a clients de l'instal·lació",
"cs": "Přístup pouze zákazníkům zařízení občanské vybavenosti",
"sl": "Samo za stranke lokala",
"es": "Solo acceso para clientes del servicio"
}
},
{
"if": "toilets:access=no",
"then": {
"en": "Not accessible, even for customers of the amenity",
"de": "Nicht zugänglich, auch nicht für Kunden der Einrichtung",
"nl": "Niet toegankelijk, ook niet voor klanten van de voorziening",
"fr": "Non accessibles, même pour les clients du lieu",
"ca": "No accessible, inclús per als clients de la instal·lació",
"cs": "Není přístupný, a to ani pro zákazníky občanské vybavenosti",
"sl": "Ni dostopno niti za stranke lokala",
"es": "Inaccesible, incluso para clientes del servicio"
}
},
{
"if": "toilets:access=key",
"then": {
"en": "Accessible, but one has to ask a key to enter",
"de": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen",
"fr": "Accessibles, mais vous devez demander la clé",
"nl": "Toegankelijk na het vragen van de sleutel",
"it": "Accessibile, ma occorre chiedere una chiave per accedere",
"es": "Accesible, pero hay que pedir una llave para entrar",
"da": "Tilgængelig, men man skal bede om en nøgle for at komme ind",
"ca": "Accessible, però hi ha que demanar la clau per a entrar",
"cs": "Přístupné, ale ke vstupu musíte požádat o klíč",
"sl": "Dostopno, a je potrebno vprašati za ključ"
}
},
{
"if": "toilets:access=public",
"then": {
"en": "Public access",
"de": "Öffentlicher Zugang",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico",
"ru": "Свободный доступ",
"es": "Acceso público",
"da": "Offentlig adgang",
"ca": "Accés públic",
"cs": "Veřejný přístup"
},
"hideInAnswer": true
}
],
"id": "toilet-access"
},
{
"id": "toilets-fee",
"condition": "toilets:access!=no",
"question": {
"en": "Are these toilets free to use?",
"de": "Können diese Toiletten kostenlos benutzt werden?",
"fr": "Ces toilettes sont-elles payantes ?",
"nl": "Zijn deze toiletten gratis te gebruiken?",
"it": "Questi servizi igienici sono gratuiti?",
"da": "Er det gratis at benytte disse toiletter?",
"ca": "Aquest serveis són gratuïts?",
"cs": "Jsou tyto toalety zdarma k použití?",
"sl": "Ali so ta stranišča brezplačna za uporabo?",
"es": "¿Son estos baños de uso gratuito?"
},
"mappings": [
{
"then": {
"en": "These are paid toilets",
"de": "Die Nutzung ist gebührenpflichtig",
"fr": "Toilettes payantes",
"nl": "Men moet betalen om deze toiletten te gebruiken",
"ru": "Это платные туалеты",
"it": "Questi servizi igienici sono a pagamento",
"es": "Estos son baños de pago",
"da": "Det er betalingstoiletter",
"ca": "Aquests serveis són de pagament",
"cs": "Jedná se o placené toalety",
"sl": "To so plačljiva stranišča"
},
"if": "toilets:fee=yes"
},
{
"if": "toilets:fee=no",
"then": {
"en": "Free to use",
"de": "Die Nutzung ist kostenlos",
"fr": "Toilettes gratuites",
"nl": "Gratis te gebruiken",
"it": "Gratis",
"da": "Gratis at bruge",
"ca": "Gratuït",
"cs": "Zdarma k použití",
"pt": "Grátis para usar",
"sl": "Brezplačna uporaba",
"es": "De uso gratuito"
}
}
]
},
{
"question": {
"en": "How much does one have to pay for these toilets?",
"de": "Wie viel muss man für die Nutzung bezahlen?",
"fr": "Quel est le prix d'accès de ces toilettes ?",
"nl": "Hoeveel moet men betalen om deze toiletten te gebruiken?",
"it": "Quanto costa l'accesso a questi servizi igienici?",
"ru": "Сколько стоит посещение туалета?",
"es": "¿Cuánto hay que pagar por estos baños?",
"da": "Hvor meget skal man betale for disse toiletter?",
"ca": "Quant s'ha de pagar per aquests lavabos?",
"cs": "Kolik se platí za tyto toalety?",
"sl": "Koliko je potrebno plačati za ta stranišča?"
},
"render": {
"en": "The fee is {toilets:charge}",
"de": "Die Gebühr beträgt {toilets:charge}",
"fr": "Le prix est {toilets:charge}",
"nl": "De toiletten gebruiken kost {toilets:charge}",
"it": "La tariffa è {toilets:charge}",
"ru": "Стоимость {toilets:charge}",
"es": "La tarifa es {toilets:charge}",
"da": "Gebyret er {toilets:charge}",
"ca": "La taxa és {toilets:charge}",
"cs": "Poplatek je {toilets:charge}",
"sl": "Plačilo je {toilets:charge}"
},
"condition": "toilets:fee=yes",
"freeform": {
"key": "toilets:charge",
"type": "string"
},
"id": "toilet-charge"
}, },
{ {
"builtin": "opening_hours", "builtin": "opening_hours",
"override": { "override": {
"condition": "toilets:access!=no", "condition": {
"and+": [
"toilets:access!=no"
]
},
"question": { "question": {
"en": "When is the amenity where these toilets are located open?", "en": "When is the amenity where these toilets are located open?",
"de": "Wann ist der Ort, an dem sich diese Toiletten befinden, geöffnet?", "de": "Wann ist der Ort, an dem sich diese Toiletten befinden, geöffnet?",
@ -348,91 +182,11 @@
} }
}, },
{ {
"id": "toilets-wheelchair", "builtin": "toilet.amenity-prefixed",
"labels": [
"wheelchair",
"hidden"
],
"question": {
"en": "Is there a dedicated toilet for wheelchair users?",
"de": "Können Rollstuhlfahrer die Toilette benutzen?",
"fr": "Y a-t-il des toilettes réservées aux personnes en fauteuil roulant ?",
"nl": "Is er een rolstoeltoegankelijke toilet voorzien?",
"it": "C'è un WC riservato alle persone in sedia a rotelle?",
"da": "Er der et særligt toilet til kørestolsbrugere?",
"ca": "Hi ha un lavabo específic per a usuaris amb cadira de rodes?",
"cs": "Existuje vyhrazená toaleta pro vozíčkáře?",
"sl": "Ali je tu stranišče namenjeno invalidom na vozičku?",
"es": "¿Hay un baño dedicado para usuarios de sillas de ruedas?"
},
"mappings": [
{
"then": {
"en": "There is a dedicated toilet for wheelchair users",
"de": "Rollstuhlfahrer können die Toilette benutzen",
"fr": "Il y a des toilettes réservées pour les personnes à mobilité réduite",
"nl": "Er is een toilet voor rolstoelgebruikers",
"it": "C'è un WC riservato alle persone in sedia a rotelle",
"es": "Hay un baño dedicado para usuarios de sillas de ruedas",
"da": "Der er et særligt toilet til kørestolsbrugere",
"ca": "Hi ha un lavabo dedicat per a usuaris amb cadira de rodes",
"cs": "K dispozici je vyhrazená toaleta pro vozíčkáře"
},
"if": "toilets:wheelchair=yes"
},
{
"if": "toilets:wheelchair=no",
"then": {
"en": "No wheelchair access",
"de": "Rollstuhlfahrer können die Toilette nicht benutzen",
"fr": "Non accessible aux personnes à mobilité réduite",
"nl": "Niet toegankelijk voor rolstoelgebruikers",
"it": "Non accessibile in sedia a rotelle",
"ru": "Недоступно пользователям кресел-колясок",
"es": "Sin acceso para sillas de ruedas",
"da": "Ingen kørestolsadgang",
"ca": "Sense accés per a cadires de rodes",
"cs": "Žádný bezbariérový přístup",
"sl": "Ni dostopno invalidom na vozičku"
}
},
{
"if": "toilets:wheelchair=designated",
"then": {
"en": "There is only a dedicated toilet for wheelchair users",
"nl": "Er is alleen een toilet voor rolstoelgebruikers",
"de": "Es gibt nur eine barrierefreie Toilette für Rollstuhlfahrer",
"da": "Der er kun et særligt toilet til kørestolsbrugere",
"ca": "Sols hi ha un lavabo per a usuaris amb cadira de rodes",
"cs": "K dispozici je pouze vyhrazená toaleta pro vozíčkáře",
"es": "Solo hay un baño dedicado para usuarios de sillas de ruedas"
}
}
]
},
"toilet.prefixed",
{
"id": "questions-wheelchair",
"labels": [
"wheelchair",
"hidden"
],
"render": {
"special": {
"type": "questions",
"labels": "wheelchair",
"show_all": "yes"
}
}
},
{
"builtin": "description",
"override": { "override": {
"render": "{toilets:description}", "labels+": [
"freeform": { "relevant_questions"
"key": "toilets:description", ]
"type": "string"
}
} }
} }
], ],

View file

@ -0,0 +1,95 @@
{
"id": "toilet_at_amenity_lib",
"description": "Special layer which makes it easy to add, as a group, information about toilets to any POI",
"source": "special:library",
"tagRenderings": [
{
"id": "toilets-group",
"labels": [
"all"
],
"render": {
"special": {
"type": "group",
"header": "grouptitle",
"labels": "toilet-questions",
"blacklist": "wheelchair;wheelchair-title;adult-changing-table"
}
}
},
{
"id": "grouptitle",
"labels": [
"all",
"hidden"
],
"icon": "./assets/layers/toilet/toilets.svg",
"render": {
"en": "Toilet information",
"nl": "Informatie over de toiletten"
},
"mappings": [
{
"if": "toilets=no",
"then": {
"en": "Does not have toilets",
"nl": "Heeft geen toiletten"
}
}
]
},
{
"builtin": "has_toilets",
"override": {
"labels+": [
"toilet-questions",
"hidden",
"all"
]
}
},
{
"builtin": "toilet_at_amenity.relevant_questions",
"override": {
"labels+": [
"toilet-questions",
"hidden",
"all"
],
"condition": {
"and+": [
"toilets=yes"
]
}
}
},
{
"id": "toilet-question-box",
"labels": [
"toilet-questions",
"all",
"hidden"
],
"render": {
"special": {
"type": "questions",
"labels": "toilet-questions"
}
}
}
],
"allowMove": false,
"pointRendering": [
{
"location": [
"centroid",
"point"
],
"marker": [
{
"icon": "circle"
}
]
}
]
}

View file

@ -46,6 +46,7 @@
} }
}, },
"recycling", "recycling",
"waste_disposal" "waste_disposal",
"excrement_bag_dispenser"
] ]
} }

View file

@ -60,7 +60,8 @@
"override": { "override": {
"minzoom": 12 "minzoom": 12
} }
} },
"excrement_bag_dispenser"
], ],
"widenFactor": 2 "widenFactor": 2
} }

View file

@ -326,6 +326,7 @@
"openTill": "till", "openTill": "till",
"open_24_7": "Open around the clock", "open_24_7": "Open around the clock",
"open_during_ph": "During a public holiday, it is", "open_during_ph": "During a public holiday, it is",
"open_until": "Closes at {date}",
"opensAt": "from", "opensAt": "from",
"ph_closed": "closed", "ph_closed": "closed",
"ph_not_known": " ", "ph_not_known": " ",

View file

@ -11986,9 +11986,6 @@
}, },
"3": { "3": {
"then": "Accessible, però s'ha de demanar la clau per a entrar" "then": "Accessible, però s'ha de demanar la clau per a entrar"
},
"4": {
"then": "Accés públic"
} }
}, },
"question": "Aquests serveis són d'accés públic?", "question": "Aquests serveis són d'accés públic?",
@ -12135,42 +12132,6 @@
"question": "Quan està oberta la instal·lació on es troben aquests lavabos?" "question": "Quan està oberta la instal·lació on es troben aquests lavabos?"
} }
}, },
"toilet-access": {
"mappings": {
"0": {
"then": "Accés públic"
},
"1": {
"then": "Només accessible a clients de l'instal·lació"
},
"2": {
"then": "No accessible, inclús per als clients de la instal·lació"
},
"3": {
"then": "Accessible, però hi ha que demanar la clau per a entrar"
},
"4": {
"then": "Accés públic"
}
},
"question": "Aquests serveis són d'accés públic?",
"render": "L'accés és {toilets:access}"
},
"toilet-charge": {
"question": "Quant s'ha de pagar per aquests lavabos?",
"render": "La taxa és {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Aquests serveis són de pagament"
},
"1": {
"then": "Gratuït"
}
},
"question": "Aquest serveis són gratuïts?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -10653,9 +10653,6 @@
}, },
"3": { "3": {
"then": "Přístupné, ale pro vstup je třeba požádat o klíč" "then": "Přístupné, ale pro vstup je třeba požádat o klíč"
},
"4": {
"then": "Veřejný přístup"
} }
}, },
"question": "Jsou tyto toalety veřejně přístupné?", "question": "Jsou tyto toalety veřejně přístupné?",
@ -10795,42 +10792,6 @@
"question": "Kdy je otevřeno zařízení, kde se tyto toalety nacházejí?" "question": "Kdy je otevřeno zařízení, kde se tyto toalety nacházejí?"
} }
}, },
"toilet-access": {
"mappings": {
"0": {
"then": "Veřejný přístup"
},
"1": {
"then": "Přístup pouze zákazníkům zařízení občanské vybavenosti"
},
"2": {
"then": "Není přístupný, a to ani pro zákazníky občanské vybavenosti"
},
"3": {
"then": "Přístupné, ale ke vstupu musíte požádat o klíč"
},
"4": {
"then": "Veřejný přístup"
}
},
"question": "Jsou tyto toalety veřejně přístupné?",
"render": "Přístup je {toilets:access}"
},
"toilet-charge": {
"question": "Kolik se platí za tyto toalety?",
"render": "Poplatek je {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Jedná se o placené toalety"
},
"1": {
"then": "Zdarma k použití"
}
},
"question": "Jsou tyto toalety zdarma k použití?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -2210,9 +2210,6 @@
}, },
"3": { "3": {
"then": "Tilgængelig, men man skal bede om en nøgle for at komme ind" "then": "Tilgængelig, men man skal bede om en nøgle for at komme ind"
},
"4": {
"then": "Offentlig adgang"
} }
}, },
"question": "Er disse toiletter offentligt tilgængelige?", "question": "Er disse toiletter offentligt tilgængelige?",
@ -2336,35 +2333,6 @@
} }
}, },
"tagRenderings": { "tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"then": "Offentlig adgang"
},
"3": {
"then": "Tilgængelig, men man skal bede om en nøgle for at komme ind"
},
"4": {
"then": "Offentlig adgang"
}
},
"render": "Adgang er {toilets:access}"
},
"toilet-charge": {
"question": "Hvor meget skal man betale for disse toiletter?",
"render": "Gebyret er {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Det er betalingstoiletter"
},
"1": {
"then": "Gratis at bruge"
}
},
"question": "Er det gratis at benytte disse toiletter?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -11917,7 +11917,7 @@
"toilet-access": { "toilet-access": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Der Zugang ist öffentlich" "then": "Öffentlicher Zugang"
}, },
"1": { "1": {
"then": "Der Zugang ist nur für Kunden" "then": "Der Zugang ist nur für Kunden"
@ -11927,9 +11927,6 @@
}, },
"3": { "3": {
"then": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen" "then": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen"
},
"4": {
"then": "Öffentlicher Zugang"
} }
}, },
"question": "Ist die Toilette öffentlich zugänglich?", "question": "Ist die Toilette öffentlich zugänglich?",
@ -12076,42 +12073,6 @@
"question": "Wann ist der Ort, an dem sich diese Toiletten befinden, geöffnet?" "question": "Wann ist der Ort, an dem sich diese Toiletten befinden, geöffnet?"
} }
}, },
"toilet-access": {
"mappings": {
"0": {
"then": "Der Zugang ist öffentlich"
},
"1": {
"then": "Nur Zugang für Kunden der Einrichtung"
},
"2": {
"then": "Nicht zugänglich, auch nicht für Kunden der Einrichtung"
},
"3": {
"then": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen"
},
"4": {
"then": "Öffentlicher Zugang"
}
},
"question": "Ist die Toilette öffentlich zugänglich?",
"render": "Zugang ist {toilets:access}"
},
"toilet-charge": {
"question": "Wie viel muss man für die Nutzung bezahlen?",
"render": "Die Gebühr beträgt {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Die Nutzung ist gebührenpflichtig"
},
"1": {
"then": "Die Nutzung ist kostenlos"
}
},
"question": "Können diese Toiletten kostenlos benutzt werden?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -42,6 +42,63 @@
"render": "Known address" "render": "Known address"
} }
}, },
"adult_changing_table": {
"description": "An adult changing table is a bench where adult people can be placed on. They are often used by adults with a severe motoric handicap",
"name": "Adult changing tables",
"presets": {
"0": {
"title": "an adult changing table"
}
},
"tagRenderings": {
"adult-changing-table-max_height": {
"question": "What is the highest height the adult changing table can be moved to?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The highest height of the adult changing table is {canonical(max_height)}"
},
"adult-changing-table-mechanism": {
"mappings": {
"0": {
"then": "The height of the adult changing table is adjusted <b>manually</b>"
},
"1": {
"then": "The height of the adult changing table is adjusted <b>electrically</b>"
}
},
"question": "How is the height of the changing table adjusted?"
},
"adult-changing-table-min_height": {
"question": "What is the lowest height the adult changing table can be moved to?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The lowest height of the adult changing table is {canonical(min_height)}"
},
"adult-changing-table-support": {
"mappings": {
"0": {
"then": "The changing table is mounted to the wall"
},
"1": {
"then": "The changing table stands on table legs"
},
"2": {
"then": "The changing table stands on table legs <b>with wheels</b> and can be moved"
}
},
"question": "How is the adult changing table supported?"
},
"height": {
"mappings": {
"0": {
"then": "The changing table is <b>adjustable in height</b>"
}
},
"question": "What is the height of the adult changing table?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The changing table is {canonical(height)} high"
}
},
"title": "Adult changing table"
},
"advertising": { "advertising": {
"description": "We will complete data from advertising features with reference, operator and lit", "description": "We will complete data from advertising features with reference, operator and lit",
"name": "Advertisement", "name": "Advertisement",
@ -1568,10 +1625,10 @@
"automated": { "automated": {
"mappings": { "mappings": {
"0": { "0": {
"then": "This is a manual bike washing station" "then": "This is a manual bike washing station - a person still has to point the water hose towards the bicycle"
}, },
"1": { "1": {
"then": "This is an automated bike wash" "then": "This is an automated bike wash. Your bicycle is placed in the device and everything happens automatically."
} }
}, },
"question": "Is this bicycle cleaning service automated?" "question": "Is this bicycle cleaning service automated?"
@ -1659,6 +1716,9 @@
"10": { "10": {
"then": "A lean-to bracket with possibility to use a lock through eyelet. The seat tube can be held by the stand by an anchor" "then": "A lean-to bracket with possibility to use a lock through eyelet. The seat tube can be held by the stand by an anchor"
}, },
"11": {
"then": "An anchor - a metal loop wide enough for a bike lock attached to a wall, the floor or a boulder."
},
"2": { "2": {
"then": "Wheelbenders / rack" "then": "Wheelbenders / rack"
}, },
@ -7423,10 +7483,21 @@
"description": "Shows the allowed speed for every road", "description": "Shows the allowed speed for every road",
"name": "Maxspeed", "name": "Maxspeed",
"tagRenderings": { "tagRenderings": {
"maxspeed-backward": {
"question": "What is the maximum allowed speed when travelling {direction_absolute(,180)}?",
"render": "The maximum allowed speed when travelling {direction_absolute(,180)} on this road is {canonical(maxspeed:backward)}"
},
"maxspeed-forward": {
"question": "What is the maximum allowed speed when travelling {direction_absolute()}?",
"render": "The maximum allowed speed when travelling {direction_absolute()} on this road is {canonical(maxspeed:forward)}"
},
"maxspeed-maxspeed": { "maxspeed-maxspeed": {
"mappings": { "mappings": {
"0": { "0": {
"then": "This is a living street, which has a maxspeed of 20km/h" "then": "This is a living street, which has a maxspeed of 20km/h"
},
"1": {
"then": "The maximum allowed speed on this road depends on the direction a vehicle goes"
} }
}, },
"question": "What is the legal maximum speed one is allowed to drive on this road?", "question": "What is the legal maximum speed one is allowed to drive on this road?",
@ -12260,54 +12331,6 @@
}, },
"question": "Does this toilet have an adult changing table?" "question": "Does this toilet have an adult changing table?"
}, },
"adult-changing-table-height": {
"mappings": {
"0": {
"then": "The changing table is <b>adjustable in height</b>"
}
},
"question": "What is the height of the adult changing table?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The changing table is {canonical(changing_table:adult:height)} high"
},
"adult-changing-table-max_height": {
"question": "What is the highest height the adult changing table can be moved to?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The highest height of the adult changing table is {canonical(changing_table:adult:max_height)}"
},
"adult-changing-table-mechanism": {
"mappings": {
"0": {
"then": "The height of the adult changing table is adjusted <b>manually</b>"
},
"1": {
"then": "The height of the adult changing table is adjusted <b>electrically</b>"
}
},
"question": "How is the height of the changing table adjusted?"
},
"adult-changing-table-min_height": {
"question": "What is the lowest height the adult changing table can be moved to?",
"questionHint": "This is measured between the floor and the top of the changing table",
"render": "The lowest height of the adult changing table is {canonical(changing_table:adult:min_height)}"
},
"adult-changing-table-support": {
"mappings": {
"0": {
"then": "The changing table is mounted to the wall"
},
"1": {
"then": "The changing table stands on table legs"
},
"2": {
"then": "The changing table stands on table legs <b>with wheels</b> and can be moved"
}
},
"question": "How is the adult changing table supported?"
},
"adult-changing-table-title": {
"render": "<h3>Adult changing table</h3>"
},
"email": { "email": {
"override": { "override": {
"question": "What is the email address one can send to in case of troubles or questions?" "question": "What is the email address one can send to in case of troubles or questions?"
@ -12378,9 +12401,6 @@
}, },
"3": { "3": {
"then": "Accessible, but one has to ask a key to enter" "then": "Accessible, but one has to ask a key to enter"
},
"4": {
"then": "Public access"
} }
}, },
"question": "Are these toilets publicly accessible?", "question": "Are these toilets publicly accessible?",
@ -12614,42 +12634,6 @@
"question": "When is the amenity where these toilets are located open?" "question": "When is the amenity where these toilets are located open?"
} }
}, },
"toilet-access": {
"mappings": {
"0": {
"then": "Public access"
},
"1": {
"then": "Only access to customers of the amenity"
},
"2": {
"then": "Not accessible, even for customers of the amenity"
},
"3": {
"then": "Accessible, but one has to ask a key to enter"
},
"4": {
"then": "Public access"
}
},
"question": "Are these toilets publicly accessible?",
"render": "Access is {toilets:access}"
},
"toilet-charge": {
"question": "How much does one have to pay for these toilets?",
"render": "The fee is {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "These are paid toilets"
},
"1": {
"then": "Free to use"
}
},
"question": "Are these toilets free to use?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -10852,9 +10852,6 @@
}, },
"3": { "3": {
"then": "Accesible, pero hay que pedir una llave para entrar" "then": "Accesible, pero hay que pedir una llave para entrar"
},
"4": {
"then": "Acceso público"
} }
}, },
"question": "¿Son estos baños de acceso público?", "question": "¿Son estos baños de acceso público?",
@ -10994,42 +10991,6 @@
"question": "¿Cuándo está abierto el servicio donde se ubican estos baños?" "question": "¿Cuándo está abierto el servicio donde se ubican estos baños?"
} }
}, },
"toilet-access": {
"mappings": {
"0": {
"then": "Acceso público"
},
"1": {
"then": "Solo acceso para clientes del servicio"
},
"2": {
"then": "Inaccesible, incluso para clientes del servicio"
},
"3": {
"then": "Accesible, pero hay que pedir una llave para entrar"
},
"4": {
"then": "Acceso público"
}
},
"question": "¿Son estos baños de acceso público?",
"render": "El acceso es {toilets:access}"
},
"toilet-charge": {
"question": "¿Cuánto hay que pagar por estos baños?",
"render": "La tarifa es {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Estos son baños de pago"
},
"1": {
"then": "De uso gratuito"
}
},
"question": "¿Son estos baños de uso gratuito?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -6609,7 +6609,7 @@
"toilet-access": { "toilet-access": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Accès public" "then": "Accès publique"
}, },
"1": { "1": {
"then": "Accès réservé aux clients" "then": "Accès réservé aux clients"
@ -6619,9 +6619,6 @@
}, },
"3": { "3": {
"then": "Accessible, mais vous devez demander la clé" "then": "Accessible, mais vous devez demander la clé"
},
"4": {
"then": "Accès publique"
} }
}, },
"question": "Ces toilettes sont-elles accessibles au public ?", "question": "Ces toilettes sont-elles accessibles au public ?",
@ -6748,42 +6745,6 @@
} }
}, },
"tagRenderings": { "tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"then": "Accès publique"
},
"1": {
"then": "Accessibles uniquement au clients du lieu"
},
"2": {
"then": "Non accessibles, même pour les clients du lieu"
},
"3": {
"then": "Accessibles, mais vous devez demander la clé"
},
"4": {
"then": "Accès publique"
}
},
"question": "Ces toilettes sont-elles librement accessibles ?",
"render": "L'accès est {toilets:access}"
},
"toilet-charge": {
"question": "Quel est le prix d'accès de ces toilettes ?",
"render": "Le prix est {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Toilettes payantes"
},
"1": {
"then": "Toilettes gratuites"
}
},
"question": "Ces toilettes sont-elles payantes ?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -2898,9 +2898,6 @@
}, },
"3": { "3": {
"then": "Accessibile, ma occorre chiedere una chiave per accedere" "then": "Accessibile, ma occorre chiedere una chiave per accedere"
},
"4": {
"then": "Accesso pubblico"
} }
}, },
"question": "Questi servizi igienici sono aperti al pubblico?", "question": "Questi servizi igienici sono aperti al pubblico?",
@ -2985,35 +2982,6 @@
}, },
"toilet_at_amenity": { "toilet_at_amenity": {
"tagRenderings": { "tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"then": "Accesso pubblico"
},
"3": {
"then": "Accessibile, ma occorre chiedere una chiave per accedere"
},
"4": {
"then": "Accesso pubblico"
}
},
"render": "L'accesso è {toilets:access}"
},
"toilet-charge": {
"question": "Quanto costa l'accesso a questi servizi igienici?",
"render": "La tariffa è {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Questi servizi igienici sono a pagamento"
},
"1": {
"then": "Gratis"
}
},
"question": "Questi servizi igienici sono gratuiti?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -41,6 +41,62 @@
"render": "Bekend adres" "render": "Bekend adres"
} }
}, },
"adult_changing_table": {
"name": "Verzorgingstafels voor volwassenen",
"presets": {
"0": {
"title": "een verzorgingstafel voor volwassenen"
}
},
"tagRenderings": {
"adult-changing-table-max_height": {
"question": "Wat is de hoogste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De hoogste stand van de verzorgingstafel is {canonical(max_height)} hoog"
},
"adult-changing-table-mechanism": {
"mappings": {
"0": {
"then": "De hoogte van de verzorgingstafel wordt <b>met de hand</b> aangepast"
},
"1": {
"then": "De verzorgingstafel wordt <b>door een electrische motor</b> in hoogte versteld"
}
},
"question": "Hoe wordt de hoogte van de verzorgingstafel aangepast?"
},
"adult-changing-table-min_height": {
"question": "Wat is de laagste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De laagste stand van de verzorgingstafel is {canonical(min_height)} hoog"
},
"adult-changing-table-support": {
"mappings": {
"0": {
"then": "De verschoningstafel voor volwassenen hangt vast aan de muur"
},
"1": {
"then": "De verschoningstafel voor volwassenen staat op tafelpoten"
},
"2": {
"then": "De verschoningstafel voor volwassenen staat op tafelpoten <b>met wielen</b> en kan verplaatst worden"
}
},
"question": "Hoe is de verschoningstafel in de ruimte geplaatst?"
},
"height": {
"mappings": {
"0": {
"then": "De verzorgingstafel is <b>in hoogte verstelbaar</b>"
}
},
"question": "Hoe hoog is de verzorgingstafel voor volwassenen?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De verzorgingstafel is {canonical(height)} hoog"
}
},
"title": "Verzorgingstafel voor volwassenen"
},
"advertising": { "advertising": {
"description": "We vullen de informatie over de advertentie aan met de referentie, de operator en de verlichting", "description": "We vullen de informatie over de advertentie aan met de referentie, de operator en de verlichting",
"name": "Reclame", "name": "Reclame",
@ -1520,10 +1576,10 @@
"automated": { "automated": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Dit is een handmatig fietsschoonmaakpunt" "then": "Dit is een handmatig fietsschoonmaakpunt - een persoon moet zelf de waterspuit richten naar de fiets"
}, },
"1": { "1": {
"then": "Dit is een automatisch fietsschoonmaakpunt" "then": "Dit is een automatisch fietsschoonmaakpunt - eens je fiets erin geplaats, wordt alles volledig automatisch proper gemaakt"
} }
}, },
"question": "Is dit fietsschoonmaakpunt geautomatiseerd?" "question": "Is dit fietsschoonmaakpunt geautomatiseerd?"
@ -1611,6 +1667,9 @@
"10": { "10": {
"then": "Een aanleunbeugel met klem waarbij je de zadelbuis in een anker kan vastklikken. Er is meestal een oog om een slot door te steken" "then": "Een aanleunbeugel met klem waarbij je de zadelbuis in een anker kan vastklikken. Er is meestal een oog om een slot door te steken"
}, },
"11": {
"then": "Een anker - een metalen lus waar een fietsslot door kan en vastgemaakt aan de muur of vloer"
},
"2": { "2": {
"then": "Wielrek/lussen" "then": "Wielrek/lussen"
}, },
@ -9847,54 +9906,6 @@
}, },
"question": "Heeft deze toilet een verzorgingstafel voor volwassenen?" "question": "Heeft deze toilet een verzorgingstafel voor volwassenen?"
}, },
"adult-changing-table-height": {
"mappings": {
"0": {
"then": "De verzorgingstafel is <b>in hoogte verstelbaar</b>"
}
},
"question": "Hoe hoog is de verzorgingstafel voor volwassenen?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De verzorgingstafel is {canonical(changing_table:adult:height)} hoog"
},
"adult-changing-table-max_height": {
"question": "Wat is de hoogste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De hoogste stand van de verzorgingstafel is {canonical(changing_table:adult:max_height)} hoog"
},
"adult-changing-table-mechanism": {
"mappings": {
"0": {
"then": "De hoogte van de verzorgingstafel wordt <b>met de hand</b> aangepast"
},
"1": {
"then": "De verzorgingstafel wordt <b>door een electrische motor</b> in hoogte versteld"
}
},
"question": "Hoe wordt de hoogte van de verzorgingstafel aangepast?"
},
"adult-changing-table-min_height": {
"question": "Wat is de laagste stand waarop de verzorgingstafel voor volwassenen gezet kan worden?",
"questionHint": "Dit wordt gemeten van de vloer tot de bovenkant van de verzorgingstafel",
"render": "De laagste stand van de verzorgingstafel is {canonical(changing_table:adult:min_height)} hoog"
},
"adult-changing-table-support": {
"mappings": {
"0": {
"then": "De verschoningstafel voor volwassenen hangt vast aan de muur"
},
"1": {
"then": "De verschoningstafel voor volwassenen staat op tafelpoten"
},
"2": {
"then": "De verschoningstafel voor volwassenen staat op tafelpoten <b>met wielen</b> en kan verplaatst worden"
}
},
"question": "Hoe is de verschoningstafel in de ruimte geplaatst?"
},
"adult-changing-table-title": {
"render": "<h3>Verzorgingstafel voor volwassenen</h3>"
},
"email": { "email": {
"override": { "override": {
"question": "Naar welk email address kan men sturen voor vragen of om problemen te melden?" "question": "Naar welk email address kan men sturen voor vragen of om problemen te melden?"
@ -9965,9 +9976,6 @@
}, },
"3": { "3": {
"then": "Toegankelijk na het vragen van de sleutel" "then": "Toegankelijk na het vragen van de sleutel"
},
"4": {
"then": "Publiek toegankelijk"
} }
}, },
"question": "Zijn deze toiletten publiek toegankelijk?", "question": "Zijn deze toiletten publiek toegankelijk?",
@ -10196,42 +10204,6 @@
}, },
"name": "Toilet in een voorziening", "name": "Toilet in een voorziening",
"tagRenderings": { "tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"then": "Publiek toegankelijk"
},
"1": {
"then": "Enkel toegankelijk voor klanten van de voorziening"
},
"2": {
"then": "Niet toegankelijk, ook niet voor klanten van de voorziening"
},
"3": {
"then": "Toegankelijk na het vragen van de sleutel"
},
"4": {
"then": "Publiek toegankelijk"
}
},
"question": "Zijn deze toiletten publiek toegankelijk?",
"render": "Toegankelijkheid is {toilets:access}"
},
"toilet-charge": {
"question": "Hoeveel moet men betalen om deze toiletten te gebruiken?",
"render": "De toiletten gebruiken kost {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Men moet betalen om deze toiletten te gebruiken"
},
"1": {
"then": "Gratis te gebruiken"
}
},
"question": "Zijn deze toiletten gratis te gebruiken?"
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -1765,7 +1765,7 @@
} }
} }
}, },
"toilet_at_amenity": { "toilet": {
"tagRenderings": { "tagRenderings": {
"toilets-fee": { "toilets-fee": {
"mappings": { "mappings": {

View file

@ -1909,9 +1909,6 @@
}, },
"2": { "2": {
"then": "Недоступно" "then": "Недоступно"
},
"4": {
"then": "Свободный доступ"
} }
}, },
"question": "Есть ли свободный доступ к этим туалетам?" "question": "Есть ли свободный доступ к этим туалетам?"
@ -1944,27 +1941,6 @@
}, },
"toilet_at_amenity": { "toilet_at_amenity": {
"tagRenderings": { "tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"then": "Свободный доступ"
},
"4": {
"then": "Свободный доступ"
}
}
},
"toilet-charge": {
"question": "Сколько стоит посещение туалета?",
"render": "Стоимость {toilets:charge}"
},
"toilets-fee": {
"mappings": {
"0": {
"then": "Это платные туалеты"
}
}
},
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"1": { "1": {

View file

@ -443,33 +443,17 @@
"description": "Stranišča z vsaj enim invalidom na vozičku dostopnim straniščem" "description": "Stranišča z vsaj enim invalidom na vozičku dostopnim straniščem"
} }
}, },
"title": {
"render": "Stranišče"
}
},
"toilet_at_amenity": {
"tagRenderings": { "tagRenderings": {
"toilet-access": { "toilet-access": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Javno dostopno" "then": "Javno dostopno"
},
"1": {
"then": "Samo za stranke lokala"
},
"2": {
"then": "Ni dostopno niti za stranke lokala"
},
"3": {
"then": "Dostopno, a je potrebno vprašati za ključ"
} }
}, }
"question": "Ali so ta stranišča javno dostopna?",
"render": "Dostop je {toilets:access}"
}, },
"toilet-charge": { "toilet-charge": {
"question": "Koliko je potrebno plačati za ta stranišča?", "question": "Koliko je potrebno plačati za ta stranišča?",
"render": "Plačilo je {toilets:charge}" "render": "Plačilo je {charge}"
}, },
"toilets-fee": { "toilets-fee": {
"mappings": { "mappings": {
@ -481,7 +465,14 @@
} }
}, },
"question": "Ali so ta stranišča brezplačna za uporabo?" "question": "Ali so ta stranišča brezplačna za uporabo?"
}, }
},
"title": {
"render": "Stranišče"
}
},
"toilet_at_amenity": {
"tagRenderings": {
"toilets-wheelchair": { "toilets-wheelchair": {
"mappings": { "mappings": {
"1": { "1": {

View file

@ -2491,9 +2491,6 @@
"override": { "override": {
"question": "Коли відкрито приміщення, де розташовані ці туалети?" "question": "Коли відкрито приміщення, де розташовані ці туалети?"
} }
},
"toilet-access": {
"render": "Доступ - {toilets:access}"
} }
} }
}, },

View file

@ -98,16 +98,15 @@
"reset:translations": "vite-node scripts/generateTranslations.ts -- --ignore-weblate", "reset:translations": "vite-node scripts/generateTranslations.ts -- --ignore-weblate",
"generate:layouts": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayouts.ts", "generate:layouts": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayouts.ts",
"generate:docs": "rm -rf Docs/Themes/* && rm -rf Docs/Layers/* && rm -rf Docs/TagInfo && mkdir Docs/TagInfo && export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateDocs.ts && vite-node scripts/generateTaginfoProjectFiles.ts", "generate:docs": "rm -rf Docs/Themes/* && rm -rf Docs/Layers/* && rm -rf Docs/TagInfo && mkdir Docs/TagInfo && export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateDocs.ts && vite-node scripts/generateTaginfoProjectFiles.ts",
"generate:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateLayerOverview.ts",
"generate:mapcomplete-changes-theme": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts -- --generate-change-map", "generate:mapcomplete-changes-theme": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts -- --generate-change-map",
"refresh:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts -- --force",
"generate:licenses": "vite-node scripts/generateLicenseInfo.ts -- --no-fail", "generate:licenses": "vite-node scripts/generateLicenseInfo.ts -- --no-fail",
"generate:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateLayerOverview.ts",
"prep:layeroverview": "./scripts/initFiles.sh",
"reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview",
"query:licenses": "vite-node scripts/generateLicenseInfo.ts -- --query && npm run generate:licenses", "query:licenses": "vite-node scripts/generateLicenseInfo.ts -- --query && npm run generate:licenses",
"clean:licenses": "find . -type f -name \"*.license\" -exec rm -f {} +", "clean:licenses": "find . -type f -name \"*.license\" -exec rm -f {} +",
"generate:contributor-list": "vite-node scripts/generateContributors.ts", "generate:contributor-list": "vite-node scripts/generateContributors.ts",
"generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak", "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak",
"reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview",
"prep:layeroverview": "./scripts/initFiles.sh",
"generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker", "generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm", "clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",
@ -224,8 +223,7 @@
"latlon2country": "^1.2.7", "latlon2country": "^1.2.7",
"libphonenumber-js": "^1.11.19", "libphonenumber-js": "^1.11.19",
"mangrove-reviews-typescript": "^1.3.1", "mangrove-reviews-typescript": "^1.3.1",
"maplibre": "^0.0.1-security", "maplibre-gl": "^5.1.0",
"maplibre-gl": "^5.1.0 ",
"marked": "^12.0.2", "marked": "^12.0.2",
"monaco-editor": "^0.46.0", "monaco-editor": "^0.46.0",
"mvt-to-geojson": "^0.0.6", "mvt-to-geojson": "^0.0.6",

View file

@ -3093,6 +3093,11 @@ input[type="range"].range-lg::-moz-range-thumb {
border-right-width: 1px; border-right-width: 1px;
} }
.border-x-2 {
border-left-width: 2px;
border-right-width: 2px;
}
.border-y { .border-y {
border-top-width: 1px; border-top-width: 1px;
border-bottom-width: 1px; border-bottom-width: 1px;
@ -5128,6 +5133,10 @@ input[type="range"].range-lg::-moz-range-thumb {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
} }
.\[a-zA-Z0-9\:_-\] {
a-z-a--z0-9: -;
}
.\[a-zA-Z0-9\:_\] { .\[a-zA-Z0-9\:_\] {
a-z-a--z0-9: ; a-z-a--z0-9: ;
} }
@ -5286,6 +5295,11 @@ input[type="text"] {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.border-low-interaction {
border-color: var(--interaction-border);
border-style: dashed;
}
.border-region { .border-region {
border: 2px dashed var(--interactive-background); border: 2px dashed var(--interactive-background);
border-radius: 0.5rem; border-radius: 0.5rem;
@ -5465,6 +5479,10 @@ textarea {
h2.group { h2.group {
/* For flowbite accordions */ /* For flowbite accordions */
margin: 0; margin: 0;
top: 0;
position: -webkit-sticky;
position: sticky;
z-index: 12;
} }
.group button { .group button {

View file

@ -6,13 +6,14 @@ import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import { import {
MappingConfigJson, MappingConfigJson,
QuestionableTagRenderingConfigJson, QuestionableTagRenderingConfigJson
} from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson" import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson"
import { TagUtils } from "../src/Logic/Tags/TagUtils" import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
import * as questions from "../assets/layers/questions/questions.json" import * as questions from "../assets/layers/questions/questions.json"
export class GenerateFavouritesLayer extends Script { export class GenerateFavouritesLayer extends Script {
private readonly layers: LayerConfigJson[] = [] private readonly layers: LayerConfigJson[] = []
@ -202,7 +203,7 @@ export class GenerateFavouritesLayer extends Script {
string, string,
TagRenderingConfigJson[] TagRenderingConfigJson[]
>() >()
const path = "./src/assets/generated/layers/icons.json" const path = "./public/assets/generated/layers/icons.json"
if (existsSync(path)) { if (existsSync(path)) {
const config = <LayerConfigJson>JSON.parse(readFileSync(path, "utf8")) const config = <LayerConfigJson>JSON.parse(readFileSync(path, "utf8"))
for (const tagRendering of config.tagRenderings) { for (const tagRendering of config.tagRenderings) {

View file

@ -9,16 +9,12 @@ import {
DoesImageExist, DoesImageExist,
PrevalidateTheme, PrevalidateTheme,
ValidateLayer, ValidateLayer,
ValidateThemeEnsemble, ValidateThemeEnsemble
} from "../src/Models/ThemeConfig/Conversion/Validation" } from "../src/Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../src/UI/i18n/Translation" import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme" import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
import { import { Conversion, DesugaringContext, DesugaringStep } from "../src/Models/ThemeConfig/Conversion/Conversion"
Conversion,
DesugaringContext,
DesugaringStep,
} from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import Script from "./Script" import Script from "./Script"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
@ -35,6 +31,7 @@ import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { LayerConfigDependencyGraph, LevelInfo } from "../src/Models/ThemeConfig/LayerConfigDependencyGraph"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files. // 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 // It spits out an overview of those to be used to load them
@ -138,8 +135,171 @@ class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: Laye
} }
} }
class LayerBuilder extends Conversion<object, Map<string, LayerConfigJson>> {
private readonly _dependencies: ReadonlyMap<string, string[]>
private readonly _states: Map<string, "clean" | "dirty" | "changed">
private readonly prepareLayer: PrepareLayer
private readonly _levels: LevelInfo[]
private readonly _loadedIds: Set<string> = new Set<string>()
private readonly _layerConfigJsons = new Map<string, LayerConfigJson>
private readonly _desugaringState: DesugaringContext
constructor(
layerConfigJsons: LayerConfigJson[],
dependencies: Map<string, string[]>,
levels: LevelInfo[],
states: Map<string, "clean" | "dirty" | "changed">,
sharedTagRenderings: QuestionableTagRenderingConfigJson[]) {
super("Builds all the layers, writes them to file", [], "LayerBuilder")
this._levels = levels
this._dependencies = dependencies
this._states = states
this._desugaringState = {
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
tagRenderingOrder: sharedTagRenderings.map((tr) => tr.id),
sharedLayers: AllSharedLayers.getSharedLayersConfigs()
}
this.prepareLayer = new PrepareLayer(this._desugaringState)
for (const layerConfigJson of layerConfigJsons) {
this._layerConfigJsons.set(layerConfigJson.id, layerConfigJson)
}
}
public static targetPath(id: string): string {
return `${LayerOverviewUtils.layerPath}${id}.json`
}
public static sourcePath(id: string): string {
return `./assets/layers/${id}/${id}.json`
}
writeLayer(layer: LayerConfigJson) {
if (!existsSync(LayerOverviewUtils.layerPath)) {
mkdirSync(LayerOverviewUtils.layerPath)
}
writeFileSync(
LayerBuilder.targetPath(layer.id),
JSON.stringify(layer, null, " "),
{ encoding: "utf8" }
)
}
public buildLayer(id: string, context: ConversionContext, isLooping: boolean = false): LayerConfigJson {
if (id === "questions") {
return undefined
}
const deps = this._dependencies.get(id)
if (!isLooping) {
// Beware of the looping traps. Bring the leaf to the statue to teleport to "The Lab" (<ref>submachine 4</ref>)
const unbuilt = deps.filter(depId => !this._loadedIds.has(depId))
for (const unbuiltId of unbuilt) {
this.buildLayer(unbuiltId, context)
}
}
context = context.inOperation("building Layer " + id).enters("layer", id)
const config = this._layerConfigJsons.get(id)
const prepped = this.prepareLayer.convert(config, context)
this._loadedIds.add(id)
this._desugaringState.sharedLayers.set(id, prepped)
return prepped
}
private buildLooping(ids: string[], context: ConversionContext) {
const origIds: ReadonlyArray<string> = [...ids]
const deps = this._dependencies
const allDeps = Utils.Dedup([].concat(...ids.map(id => deps.get(id))))
const depsRecord = Utils.asRecord(Array.from(deps.keys()), k =>
deps.get(k).filter(dep => ids.indexOf(dep) >= 0))
const revDeps = Utils.TransposeMap(depsRecord)
for (const someDep of allDeps) {
if (ids.indexOf(someDep) >= 0) {
// BY definition, we _will_ need this dependency
// We add a small stub
this._desugaringState.sharedLayers.set(someDep, {
id: someDep,
pointRendering: [],
tagRenderings: [],
filter: [],
source: "special:stub",
allowMove: true
})
continue
}
// Make sure all are direct dependencies are loaded
if (!this._loadedIds.has(someDep)) {
this.buildLayer(someDep, context)
}
}
while (ids.length > 0) {
const first = ids.pop()
if (first === "questions") {
continue
}
const oldConfig = this._desugaringState.sharedLayers.get(first) ?? this._layerConfigJsons.get(first)
const newConfig = this.buildLayer(first, context.inOperation("resolving a looped dependency"), true)
const isDifferent = JSON.stringify(oldConfig) !== JSON.stringify(newConfig)
if (isDifferent) {
const toRunAgain = revDeps[first] ?? []
for (const id of toRunAgain) {
if (ids.indexOf(id) < 0) {
ids.push(id)
}
}
}
}
for (const id of origIds) {
this.writeLayer(this._desugaringState.sharedLayers.get(id))
}
console.log("Done with the looping layers!")
}
public convert(o, context: ConversionContext):
Map<string, LayerConfigJson> {
for (const level of this._levels
) {
if (level.loop) {
this.buildLooping(level.ids, context)
continue
}
for (let i = 0; i < level.ids.length; i++) {
const id = level.ids[i]
ScriptUtils.erasableLog(`Building level ${i}: validating layer ${i + 1}/${level.ids.length}: ${id}`)
if (id === "questions") {
continue
}
if (this._states.get(id) === "clean") {
const file = readFileSync(LayerBuilder.targetPath(id), "utf-8")
if (file.length > 3) {
try {
const loaded = JSON.parse(file)
this._desugaringState.sharedLayers.set(id, loaded)
continue
} catch (e) {
console.error("Could not load generated layer file for ", id, " building it instead")
}
}
}
const prepped = this.buildLayer(id, context)
this.writeLayer(prepped)
LayerOverviewUtils.extractJavascriptCodeForLayer(prepped)
}
}
context.info("Recompiled " + this._loadedIds.size + " layers")
return this._desugaringState.sharedLayers
}
}
class LayerOverviewUtils extends Script { class LayerOverviewUtils extends Script {
public static readonly layerPath = "./src/assets/generated/layers/" public static readonly layerPath = "./public/assets/generated/layers/"
public static readonly themePath = "./public/assets/generated/themes/" public static readonly themePath = "./public/assets/generated/themes/"
constructor() { constructor() {
@ -190,7 +350,7 @@ class LayerOverviewUtils extends Script {
return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations
} }
shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean { public static shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean {
if (!existsSync(targetfile)) { if (!existsSync(targetfile)) {
return true return true
} }
@ -202,7 +362,6 @@ class LayerOverviewUtils extends Script {
for (const path of sourcefile) { for (const path of sourcefile) {
const hasChange = statSync(path).mtime > targetModified const hasChange = statSync(path).mtime > targetModified
if (hasChange) { if (hasChange) {
console.log("File ", targetfile, " should be updated as ", path, "has been changed")
return true return true
} }
} }
@ -359,17 +518,6 @@ class LayerOverviewUtils extends Script {
) )
} }
writeLayer(layer: LayerConfigJson) {
if (!existsSync(LayerOverviewUtils.layerPath)) {
mkdirSync(LayerOverviewUtils.layerPath)
}
writeFileSync(
`${LayerOverviewUtils.layerPath}${layer.id}.json`,
JSON.stringify(layer, null, " "),
{ encoding: "utf8" }
)
}
static asDict( static asDict(
trs: QuestionableTagRenderingConfigJson[] trs: QuestionableTagRenderingConfigJson[]
): Map<string, QuestionableTagRenderingConfigJson> { ): Map<string, QuestionableTagRenderingConfigJson> {
@ -481,13 +629,6 @@ class LayerOverviewUtils extends Script {
?.split(",") ?? [] ?.split(",") ?? []
) )
const layerWhitelist = new Set(
args
.find((a) => a.startsWith("--layers="))
?.substring("--layers=".length)
?.split(",") ?? []
)
const forceReload = args.some((a) => a == "--force") const forceReload = args.some((a) => a == "--force")
const licensePaths = new Set<string>() const licensePaths = new Set<string>()
@ -495,7 +636,7 @@ class LayerOverviewUtils extends Script {
licensePaths.add(licenses[i].path) licensePaths.add(licenses[i].path)
} }
const doesImageExist = new DoesImageExist(licensePaths, existsSync) const doesImageExist = new DoesImageExist(licensePaths, existsSync)
const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload, layerWhitelist) const sharedLayers = this.buildLayerIndex(doesImageExist)
const priviliged = new Set<string>(Constants.priviliged_layers) const priviliged = new Set<string>(Constants.priviliged_layers)
sharedLayers.forEach((_, key) => { sharedLayers.forEach((_, key) => {
@ -582,9 +723,6 @@ class LayerOverviewUtils extends Script {
) )
} }
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
console.error("This was a bootstrapping-run. Run generate layeroverview again!")
}
} }
private parseLayer( private parseLayer(
@ -606,81 +744,101 @@ class LayerOverviewUtils extends Script {
return { ...result, context } return { ...result, context }
} }
private getAllLayerConfigs(): LayerConfigJson[] {
const allPaths = ScriptUtils.getLayerPaths()
const results: LayerConfigJson[] = []
for (let i = 0; i < allPaths.length; i++) {
const path = allPaths[i]
ScriptUtils.erasableLog(`Parsing layerConfig ${i + 1}/${allPaths.length}: ${path} `)
try {
const data = JSON.parse(readFileSync(path, "utf8"))
results.push(data)
} catch (e) {
throw "Could not parse layer file " + path
}
}
return results
}
private buildLayerIndex( private buildLayerIndex(
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist
forceReload: boolean,
whitelist: Set<string>
): Map<string, LayerConfigJson> { ): Map<string, LayerConfigJson> {
// First, we expand and validate all builtin layers. These are written to src/assets/generated/layers // First, we expand and validate all builtin layers. These are written to src/assets/generated/layers
// At the same time, an index of available layers is built. // At the same time, an index of available layers is built.
console.log("------------- VALIDATING THE BUILTIN QUESTIONS ---------------") const sharedQuestions = this.getSharedTagRenderings(doesImageExist)
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist) const allLayerConfigs = this.getAllLayerConfigs()
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------") const sharedQuestionsDef = allLayerConfigs.find(l => l.id === "questions")
const state: DesugaringContext = { sharedQuestionsDef.tagRenderings = sharedQuestions
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
tagRenderingOrder: sharedTagRenderings.map((tr) => tr.id),
sharedLayers: AllSharedLayers.getSharedLayersConfigs(), const dependencyGraph = LayerConfigDependencyGraph.buildDirectDependencies(allLayerConfigs)
} const levels = LayerConfigDependencyGraph.buildLevels(dependencyGraph)
const sharedLayers = new Map<string, LayerConfigJson>() const layerState = new Map<string, "clean" | "dirty" | "changed">()
const prepLayer = new PrepareLayer(state) console.log("# BUILD PLAN\n\n")
const skippedLayers: string[] = [] for (const levelInfo of levels) {
const recompiledLayers: string[] = [] if (levelInfo.loop) {
let warningCount = 0 console.log(`(LOOP)`)
for (const sharedLayerPath of ScriptUtils.getLayerPaths()) {
if (whitelist.size > 0) {
const idByPath = sharedLayerPath.split("/").at(-1).split(".")[0]
if (!Constants.isPriviliged(idByPath) && !whitelist.has(idByPath)) {
continue
}
} }
{ let allClean = true
const targetPath = for (const id of levelInfo.ids) {
LayerOverviewUtils.layerPath + const deps = dependencyGraph.get(id) ?? []
sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/")) const dirtyDeps = deps.filter(dep => {
if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) { const depState = layerState.get(dep)
try { if (levelInfo.loop && depState === undefined) {
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8")) const depIsClean =
sharedLayers.set(sharedLayer.id, sharedLayer) LayerOverviewUtils.shouldBeUpdated(
skippedLayers.push(sharedLayer.id) LayerBuilder.sourcePath(dep),
continue LayerBuilder.targetPath(dep))
} catch (e) { if (depIsClean) {
throw "Could not parse " + targetPath + " : " + e return false
}
}
return depState !== "clean"
})
if (dirtyDeps.length > 0) {
layerState.set(id, "dirty")
} else {
const sourcePath = `./assets/layers/${id}/${id}.json`
const targetPath = `./public/assets/generated/layers/${id}.json`
if (id === "questions") {
layerState.set(id, "clean")
} else if (LayerOverviewUtils.shouldBeUpdated(sourcePath, targetPath)) {
layerState.set(id, "changed")
} else {
layerState.set(id, "clean")
} }
} }
const state = layerState.get(id)
if (state !== "clean") {
allClean = false
console.log(`- ${id} (${state}; ${dirtyDeps.map(dd => dd + "*").join(", ")})`)
}
} }
if (allClean) {
const parsed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath) console.log("\n")
warningCount += parsed.context.getAll("warning").length
const fixed = parsed.raw
if (sharedLayers.has(fixed.id)) {
throw "There are multiple layers with the id " + fixed.id + ", " + sharedLayerPath
} }
if (parsed.context.hasErrors()) {
throw "Some layers contain errors"
}
sharedLayers.set(fixed.id, fixed)
recompiledLayers.push(fixed.id)
this.writeLayer(fixed)
} }
console.log(
"Recompiled layers " + const builder = new LayerBuilder(allLayerConfigs, dependencyGraph, levels, layerState, sharedQuestions)
recompiledLayers.join(", ") + builder.writeLayer(sharedQuestionsDef)
" and skipped " + const allLayers = builder.convertStrict({}, ConversionContext.construct([], []))
skippedLayers.length + if (layerState.get("usersettings") !== "clean") {
" layers. Detected " + // We always need the calculated tags of 'usersettings', so we export them separately if dirty
warningCount +
" warnings" LayerOverviewUtils.extractJavascriptCodeForLayer(
) allLayers.get("usersettings"),
// We always need the calculated tags of 'usersettings', so we export them separately
this.extractJavascriptCodeForLayer(
state.sharedLayers.get("usersettings"),
"./src/Logic/State/UserSettingsMetaTagging.ts" "./src/Logic/State/UserSettingsMetaTagging.ts"
) )
}
return allLayers
return sharedLayers
} }
/** /**
@ -741,7 +899,7 @@ class LayerOverviewUtils extends Script {
writeFileSync(targetDir + themeFile.id + ".ts", allCode.join("\n")) writeFileSync(targetDir + themeFile.id + ".ts", allCode.join("\n"))
} }
private extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) { public static extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) {
if (!l) { if (!l) {
return // Probably a bootstrapping run return // Probably a bootstrapping run
} }
@ -858,7 +1016,7 @@ class LayerOverviewUtils extends Script {
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false) LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json") ).map((id) => LayerOverviewUtils.layerPath + id + ".json")
if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) { if (!forceReload && !LayerOverviewUtils.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
fixed.set( fixed.set(
themeFile.id, themeFile.id,
JSON.parse( JSON.parse(

View file

@ -5,12 +5,12 @@
mkdir -p ./src/assets/generated/layers mkdir -p ./src/assets/generated/layers
mkdir -p ./public/assets/generated/themes mkdir -p ./public/assets/generated/themes
echo '{"layers": []}' > ./src/assets/generated/known_layers.json echo '{"layers": []}' > ./src/assets/generated/known_layers.json
rm -f ./src/assets/generated/layers/*.json rm -f ./public/assets/generated/layers/*.json
rm -f ./public/assets/generated/themes/*.json rm -f ./public/assets/generated/themes/*.json
cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json echo '{}' > ./public/assets/generated/layers/favourite.json
echo '{}' > ./src/assets/generated/layers/favourite.json echo '{}' > ./public/assets/generated/layers/summary.json
echo '{}' > ./src/assets/generated/layers/summary.json echo '{}' > ./public/assets/generated/layers/last_click.json
echo '{}' > ./src/assets/generated/layers/last_click.json echo '{}' > ./public/assets/generated/layers/search.json
echo '{}' > ./src/assets/generated/layers/search.json echo '[]' > ./public/assets/generated/theme_overview.json
echo '[]' > ./src/assets/generated/theme_overview.json echo '{}' > ./public/assets/generated/layers/geocoded_image.json
echo '{}' > ./src/assets/generated/layers/geocoded_image.json echo '{}' > ./public/assets/generated/layers/usersettings.json

View file

@ -20,8 +20,6 @@ npm run download:editor-layer-index &&
npm run prep:layeroverview && npm run prep:layeroverview &&
npm run generate && # includes a single "refresh:layeroverview". Resetting the files is unnecessary as they are not in there in the first place npm run generate && # includes a single "refresh:layeroverview". Resetting the files is unnecessary as they are not in there in the first place
npm run generate:mapcomplete-changes-theme && npm run generate:mapcomplete-changes-theme &&
npm run refresh:layeroverview && # a second time to propagate all calls
npm run refresh:layeroverview && # a third time to fix some issues with the favourite layer all calls
npm run generate:layouts npm run generate:layouts
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then

View file

@ -1,5 +1,5 @@
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import favourite from "../assets/generated/layers/favourite.json" import favourite from "../../public/assets/generated/layers/favourite.json"
import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson" import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson"
import { AllSharedLayers } from "./AllSharedLayers" import { AllSharedLayers } from "./AllSharedLayers"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"

View file

@ -9,7 +9,7 @@ import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
import licenses from "../assets/generated/license_info.json" import licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
import questions from "../assets/generated/layers/questions.json" import questions from "../../public/assets/generated/layers/questions.json"
import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation" import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion" import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"

View file

@ -2,7 +2,7 @@ import { BBox } from "../BBox"
import { Feature, Geometry } from "geojson" import { Feature, Geometry } from "geojson"
import { DefaultPinIcon } from "../../Models/Constants" import { DefaultPinIcon } from "../../Models/Constants"
import { Store } from "../UIEventSource" import { Store } from "../UIEventSource"
import * as search from "../../assets/generated/layers/search.json" import * as search from "../../../public/assets/generated/layers/search.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"

View file

@ -10,7 +10,7 @@ import translators from "../../assets/translators.json"
import codeContributors from "../../assets/contributors.json" import codeContributors from "../../assets/contributors.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../../src/assets/generated/layers/usersettings.json" import usersettings from "../../../public/assets/generated/layers/usersettings.json"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import LinkToWeblate from "../../UI/Base/LinkToWeblate" import LinkToWeblate from "../../UI/Base/LinkToWeblate"
import FeatureSwitchState from "./FeatureSwitchState" import FeatureSwitchState from "./FeatureSwitchState"

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging { export class ThemeMetaTagging {
public static readonly themeName = "usersettings" public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
feat.properties._description Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
?.at(1) Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
Utils.AddLazyProperty( feat.properties['__current_backgroun'] = 'initial_value'
feat.properties, }
"_d", }
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -0,0 +1,134 @@
import { DesugaringStep } from "./Conversion"
import { ConversionContext } from "./ConversionContext"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { Translatable } from "../Json/Translatable"
import { TagConfigJson } from "../Json/TagConfigJson"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
export default class AddPrefixToTagRenderingConfig extends DesugaringStep<QuestionableTagRenderingConfigJson> {
private readonly _prefix: string
constructor(prefix: string) {
super("Adds `prefix` to _all_ keys. Used to add information about a subamenity withing a bigger amenity (e.g. toilets in a restaurant, a sauna in a water park, ...)", ["*"], "AddPrefixToTagRenderingConfig")
this._prefix = prefix
}
/**
*
* const edit = new AddPrefixToTagRenderingConfig("PREFIX")
* edit.updateString("Some string") // => "Some string"
* edit.updateString("Some string {key0}") // => "Some string {PREFIX:key0}"
*
* // Should prefix a key in a special visualisation
* new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table(opening_hours)}") // => "{opening_hours_table(PREFIX:opening_hours,,)}"
*
* // Should prefix the default key in a special visualisation
* new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table()}") // => "{opening_hours_table(PREFIX:opening_hours,,)}"
*/
private updateString(str: string): string {
const parsed = SpecialVisualizations.constructSpecification(str)
const fixedSpec: string[] = []
for (const spec of parsed) {
if (typeof spec === "string") {
const part = spec.replace(/{([a-zA-Z0-9:_-]+)}/g, `{${this._prefix}:$1}`)
fixedSpec.push(part)
} else {
const newArgs: string[] = []
for (let i = 0; i < spec.func.args.length; i++) {
const argDoc = spec.func.args[i]
const argV = spec.args[i]
if (argDoc.type === "key") {
newArgs.push(this._prefix + ":" + (argV ?? argDoc.defaultValue ?? ""))
} else {
newArgs.push(argV ?? "")
}
}
fixedSpec.push("{" + spec.func.funcName + "(" + newArgs.join(",") + ")}")
}
}
return fixedSpec.join("")
}
private updateTranslatable(val: Translatable | undefined): Translatable | undefined {
if (!val) {
return val
}
if (typeof val === "string") {
return this.updateString(val)
}
const newTranslations: Record<string, string> = {}
for (const lng in val) {
newTranslations[lng] = this.updateString(val[lng])
}
return newTranslations
}
private updateTag(tags: string): string;
private updateTag(tags: TagConfigJson): TagConfigJson;
private updateTag(tags: TagConfigJson): TagConfigJson {
if (!tags) {
return tags
}
if (tags["and"]) {
return { and: this.updateTags(tags["and"]) }
}
if (tags["or"]) {
return { or: this.updateTags(tags["or"]) }
}
return this._prefix + ":" + tags
}
private updateTags(tags: ReadonlyArray<string>): string[] {
return tags?.map(tag => this.updateTag(tag))
}
private updateMapping(mapping: Readonly<MappingConfigJson>): MappingConfigJson {
return {
...mapping,
addExtraTags: this.updateTags(mapping.addExtraTags),
if: this.updateTag(mapping.if),
then: this.updateTranslatable(mapping.then),
alsoShowIf: this.updateTag(mapping.alsoShowIf),
ifnot: this.updateTag(mapping.ifnot),
priorityIf: this.updateTag(mapping.priorityIf),
hideInAnswer: mapping.hideInAnswer === true || mapping.hideInAnswer === false ? mapping.hideInAnswer : this.updateTag(mapping.hideInAnswer)
}
}
public convert(json: Readonly<QuestionableTagRenderingConfigJson>, context: ConversionContext): QuestionableTagRenderingConfigJson {
let freeform = json.freeform
if (freeform) {
const ff = json.freeform
freeform = {
...ff,
key: this._prefix + ":" + ff.key,
addExtraTags: this.updateTags(ff.addExtraTags)
}
}
return <QuestionableTagRenderingConfigJson>{
...json,
id: this._prefix + "_" + json.id,
question: this.updateTranslatable(json.question),
questionHint: this.updateTranslatable(json.questionHint),
render: this.updateTranslatable(<Translatable>json.render),
freeform,
editButtonAriaLabel: json.editButtonAriaLabel,
onSoftDelete: this.updateTags(json.onSoftDelete),
invalidValues: this.updateTag(json.invalidValues),
mappings: json.mappings?.map(mapping => this.updateMapping(mapping)),
condition: this.updateTag(json.condition),
metacondition: json.metacondition, // no update here
filter: json.filter === true, // We break references to filters, as those references won't have the updated tags
_appliedPrefix: this._prefix
}
}
}

View file

@ -239,11 +239,11 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
) )
} }
const layer = this._state.sharedLayers.get(split[0]) const layer = this._state.sharedLayers.get(split[0])
if (layer === undefined) { if (!layer) {
context.err("Layer '" + split[0] + "' not found") context.err("Layer '" + split[0] + "' not found")
} }
const expectedId = split[1] const expectedId = split[1]
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find( const expandedFilter = (<(FilterConfigJson | string)[]>layer?.filter)?.find(
(f) => typeof f !== "string" && f.id === expectedId (f) => typeof f !== "string" && f.id === expectedId
) )
if (expandedFilter === undefined) { if (expandedFilter === undefined) {

View file

@ -6,6 +6,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
import { TagUtils } from "../../../Logic/Tags/TagUtils" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { AddContextToTranslations } from "./AddContextToTranslations" import { AddContextToTranslations } from "./AddContextToTranslations"
import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig"
import { Translatable } from "../Json/Translatable"
export class ExpandTagRendering extends Conversion< export class ExpandTagRendering extends Conversion<
| string | string
@ -208,6 +210,21 @@ export class ExpandTagRendering extends Conversion<
let matchingTrs: (TagRenderingConfigJson & { id: string })[] let matchingTrs: (TagRenderingConfigJson & { id: string })[]
if (id === "*") { if (id === "*") {
matchingTrs = layerTrs matchingTrs = layerTrs
} else if (id === "title") {
const title = layer.title
if (title["render"] || title["mappings"]) {
const titleTr = <TagRenderingConfigJson>layer.title
return [{
...titleTr,
id: layer.id + "_title"
}]
} else {
const transl = <Translatable>layer.title
return [{
render: transl,
id: layer.id + "_title"
}]
}
} else if (id.startsWith("*")) { } else if (id.startsWith("*")) {
const id_ = id.substring(1) const id_ = id.substring(1)
matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0) matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0)
@ -249,6 +266,25 @@ export class ExpandTagRendering extends Conversion<
return undefined return undefined
} }
/**
* Returns a variation of 'tr' where every key has been prefixed by the given 'prefix'-key.
* If the given key is undefined, returns the original tagRendering.
*
* Note: metacondition will _not_ be prefixed
* @param key
* @param tr
* @private
*/
private static applyKeyPrefix(key: string | undefined, tr: Readonly<QuestionableTagRenderingConfigJson>, ctx: ConversionContext): QuestionableTagRenderingConfigJson {
if (key === undefined || key === null) {
return tr
}
if (key.endsWith(":")) {
ctx.err("A 'prefix'-key should not end with a colon. The offending prefix value is: " + key)
}
return new AddPrefixToTagRenderingConfig(key).convert(tr, ctx.enter("prefix"))
}
private convertOnce( private convertOnce(
tr: string | { builtin: string | string[] } | TagRenderingConfigJson, tr: string | { builtin: string | string[] } | TagRenderingConfigJson,
ctx: ConversionContext ctx: ConversionContext
@ -310,6 +346,7 @@ export class ExpandTagRendering extends Conversion<
if ( if (
key === "builtin" || key === "builtin" ||
key === "override" || key === "override" ||
key === "prefix" ||
key === "id" || key === "id" ||
key.startsWith("#") key.startsWith("#")
) { ) {
@ -343,15 +380,10 @@ export class ExpandTagRendering extends Conversion<
Utils.NoNull(Array.from(state.sharedLayers.keys())), Utils.NoNull(Array.from(state.sharedLayers.keys())),
(s) => s (s) => s
) )
if (state.sharedLayers.size === 0) { if (candidates.length === 0) {
ctx.warn( ctx.err("While reusing a tagRendering: " + name + "; no candidates in layer " + layerName)
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found for now, but ignoring as this is a bootstrapping run. "
)
} else { } else {
console.error("Bench was not found...")
ctx.err( ctx.err(
": While reusing tagrendering: " + ": While reusing tagrendering: " +
name + name +
@ -363,22 +395,29 @@ export class ExpandTagRendering extends Conversion<
} }
continue continue
} }
if (layer.source === "special:stub") {
// We are dealing with a looping import, no error is necessary
continue
}
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map( candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
(id) => layerName + "." + id (id) => layerName + "." + id
) )
} }
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i) candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
ctx.err( ctx.err(
"The tagRendering with identifier " + "The tagRendering with identifier " +
name + name +
" was not found.\n\tDid you mean one of " + " was not found.\n\tDid you mean one of " +
candidates.join(", ") + candidates.join(", ") +
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first" "?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first\n" +
"HINT: are you overriding a `condition`? Check that the source condition has the format `condition: {and: [...]}`"
) )
continue continue
} }
for (let foundTr of lookup) { for (let foundTr of lookup) {
foundTr = Utils.Clone(foundTr) foundTr = Utils.Clone(foundTr)
foundTr = ExpandTagRendering.applyKeyPrefix(tr["prefix"], foundTr, ctx)
ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr) ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr)
if (names.length == 1) { if (names.length == 1) {
foundTr["id"] = tr["id"] ?? foundTr["id"] foundTr["id"] = tr["id"] ?? foundTr["id"]

View file

@ -1,18 +1,6 @@
import { import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson" import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -210,7 +198,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (noLabels.length > 1) { if (noLabels.length > 1) {
context.err( context.err(
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this" "Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this. Did you perhaps import all questions from another layer?"
) )
} }
@ -1060,6 +1048,11 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
if (json === undefined || json === null) { if (json === undefined || json === null) {
throw "Error: prepareLayer got null" throw "Error: prepareLayer got null"
} }
if (json.source?.["osmTags"] !== undefined && json.source?.["osmTags"]?.["and"] === undefined) {
json = { ...json }
json.source = <any>{ ...(<object>json.source) }
json.source["osmTags"] = { "and": [json.source["osmTags"]] }
}
return super.convert(json, context) return super.convert(json, context)
} }
} }

View file

@ -1,14 +1,4 @@
import { import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { ThemeConfigJson } from "../Json/ThemeConfigJson" import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { PrepareLayer, RewriteSpecial } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -163,9 +153,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l)) const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
if (unused.length > 0) { if (unused.length > 0) {
context.err( context.err(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + `You are attempting to import layer '${found.id}' in this theme. This layer import specifies that certain tagrenderings have to be removed based on forbidden ids and/or labels. One or more of these forbidden ids did not match any tagRenderings and caused no deletions: ${unused.join(", ")}
unused.join(", ") + This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore`
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
) )
} }
found.tagRenderings = filtered found.tagRenderings = filtered

View file

@ -78,6 +78,7 @@ export interface LayerConfigJson {
| undefined | undefined
| "special" | "special"
| "special:library" | "special:library"
| "special:stub" // only used when building looping imports
| { | {
/** /**
* question: Which tags must be present on the feature to show it in this layer? * question: Which tags must be present on the feature to show it in this layer?
@ -422,8 +423,15 @@ export interface LayerConfigJson {
| string | string
| { | {
id?: string id?: string
/**
* Special value: "<layerid>.title" will return the layer's title for an element
*/
builtin: string | string[] builtin: string | string[]
override: Partial<QuestionableTagRenderingConfigJson> override: Partial<QuestionableTagRenderingConfigJson>,
/**
* Add this prefix to all keys. This is applied _before_ the override, thus keys added in 'override' will not be prefixed
*/
prefix?: string
} }
| QuestionableTagRenderingConfigJson | QuestionableTagRenderingConfigJson
| (RewritableConfigJson< | (RewritableConfigJson<

View file

@ -289,7 +289,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* Extra arguments to configure the input element * Extra arguments to configure the input element
* group: hidden * group: hidden
*/ */
helperArgs: any helperArgs?: any
} }
/** /**

View file

@ -225,7 +225,9 @@ export interface TagRenderingConfigJson {
classes?: string classes?: string
/** /**
* This tagRendering can introduce this builtin filter * If 'true', then a filter is automatically created for this tagFilter.
* If one or more strings are given, these strings are interpreted as filter-ids and will summon this filter.
* For example, the "opening_hours"-question will also add the filter "filters.open_now"
*/ */
filter?: string[] | true filter?: string[] | true

View file

@ -0,0 +1,91 @@
import { LayerConfigJson } from "./Json/LayerConfigJson"
export interface LevelInfo {
ids: string[],
loop?: boolean
}
export class LayerConfigDependencyGraph {
/**
* Calculates the dependencies for the given layer
* @param layerconfig
*/
public static getLayerImports(layerconfig: LayerConfigJson): string[] {
const defaultImports: ReadonlyArray<string> = ["questions", "filters","icons"]
if (defaultImports.indexOf(layerconfig.id) >= 0) {
return []
}
const importedTrs: string[] = []
for (const tr of layerconfig.tagRenderings ?? []) {
if (typeof tr === "string") {
importedTrs.push(tr)
} else if (tr["builtin"] !== undefined) {
const builtin = <string | string[]>tr["builtin"]
if (typeof builtin === "string") {
importedTrs.push(builtin)
} else {
importedTrs.push(...builtin)
}
}
}
const imports = new Set<string>(defaultImports)
for (const importValue of importedTrs) {
if (importValue.indexOf(".") < 0) {
continue
}
const [layer, _] = importValue.split(".")
imports.add(layer)
}
return Array.from(imports)
}
public static buildDirectDependencies(layers: LayerConfigJson[]) {
const dependsOn = new Map<string, string[]>()
for (const layer of layers) {
const layerDependsOn = LayerConfigDependencyGraph.getLayerImports(layer)
dependsOn.set(layer.id, layerDependsOn)
}
return dependsOn
}
public static buildLevels(dependsOn: Map<string, string[]>): LevelInfo[]{
const levels: LevelInfo[] = []
const seenIds = new Set<string>()
while (Array.from(dependsOn.keys()).length > 0) {
const currentLevel: LevelInfo = {
ids: <string[]>[],
}
levels.push(currentLevel)
for (const layerId of dependsOn.keys()) {
const dependencies = dependsOn.get(layerId)
if (dependencies.length === 0) {
currentLevel.ids.push(layerId)
seenIds.add(layerId)
}
}
const newDependsOn = new Map<string, string[]>()
for (const layerId of dependsOn.keys()) {
if (seenIds.has(layerId)) {
continue
}
const dependencies = dependsOn.get(layerId)
newDependsOn.set(layerId, dependencies.filter(d => !seenIds.has(d)))
}
const oldSize = dependsOn.size
if(oldSize === newDependsOn.size){
// We detected a loop.
currentLevel.loop = true
const allLayers =Array.from(newDependsOn.keys())
currentLevel.ids.push(...allLayers )
allLayers.forEach(l => seenIds.add(l))
}
dependsOn = newDependsOn
}
return levels
}
}

View file

@ -11,14 +11,14 @@ import MetaTagging from "../../Logic/MetaTagging"
import FilteredLayer from "../FilteredLayer" import FilteredLayer from "../FilteredLayer"
import LayerConfig from "../ThemeConfig/LayerConfig" import LayerConfig from "../ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson"
import last_click_layerconfig from "../../assets/generated/layers/last_click.json" import last_click_layerconfig from "../../../public/assets/generated/layers/last_click.json"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import summaryLayer from "../../assets/generated/layers/summary.json" import summaryLayer from "../../../public/assets/generated/layers/summary.json"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource" import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
import { import {
SummaryTileSource, SummaryTileSource,
SummaryTileSourceRewriter, SummaryTileSourceRewriter
} from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" } from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions" import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
@ -195,7 +195,6 @@ export class WithSpecialLayers extends WithChangesState {
| "range" // handled by UserMapFeatureSwitchState | "range" // handled by UserMapFeatureSwitchState
| "selected_element" // handled by this.drawSelectedElement | "selected_element" // handled by this.drawSelectedElement
> >
const empty = []
/** /**
* A listing which maps the layerId onto the featureSource * A listing which maps the layerId onto the featureSource
*/ */

View file

@ -26,7 +26,7 @@
import SelectedElementView from "./SelectedElementView.svelte" import SelectedElementView from "./SelectedElementView.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../assets/generated/layers/usersettings.json" import usersettings from "../../../public/assets/generated/layers/usersettings.json"
import UserRelatedState from "../../Logic/State/UserRelatedState" import UserRelatedState from "../../Logic/State/UserRelatedState"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray" import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import DownloadPanel from "../DownloadFlow/DownloadPanel.svelte" import DownloadPanel from "../DownloadFlow/DownloadPanel.svelte"

View file

@ -3,7 +3,7 @@
export let expanded = false export let expanded = false
export let noBorder = false export let noBorder = false
export let contentClass = noBorder ? "normal-background" : "low-interaction rounded-b p-2" export let contentClass = noBorder ? "normal-background" : "low-interaction rounded-b p-2 border-x-2 border-b-2 border-dashed border-low-interaction"
let defaultClass: string = undefined let defaultClass: string = undefined
if (noBorder) { if (noBorder) {
defaultClass = "unstyled w-full flex-grow" defaultClass = "unstyled w-full flex-grow"
@ -11,7 +11,8 @@
</script> </script>
<Accordion> <Accordion>
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black" {defaultClass}> <AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black"
{defaultClass}>
<span slot="header" class={!noBorder ? "w-full p-2 text-base" : "w-full"}> <span slot="header" class={!noBorder ? "w-full p-2 text-base" : "w-full"}>
<slot name="header" /> <slot name="header" />
</span> </span>

View file

@ -19,7 +19,7 @@
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer" import ShowDataLayer from "../Map/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as geocoded_image from "../../assets/generated/layers/geocoded_image.json" import * as geocoded_image from "../../../public/assets/generated/layers/geocoded_image.json"
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"

View file

@ -160,7 +160,7 @@ export class OH {
} }
for (let i = newList.length - 1; i >= 0 && doAddEntry; i--) { for (let i = newList.length - 1; i >= 0 && doAddEntry; i--) {
let guard = newList[i] const guard = newList[i]
if (maybeAdd.weekday != guard.weekday) { if (maybeAdd.weekday != guard.weekday) {
// Not the same day // Not the same day
continue continue
@ -236,9 +236,8 @@ export class OH {
/** /**
* Gives the number of hours since the start of day. * Gives the number of hours since the start of day.
* E.g. *
* startTime({startHour: 9, startMinuts: 15}) == 9.25 * // OH.startTime({startHour: 9, startMinutes: 15}) // => 9.25
* @param oh
*/ */
public static startTime(oh: OpeningHour): number { public static startTime(oh: OpeningHour): number {
return oh.startHour + oh.startMinutes / 60 return oh.startHour + oh.startMinutes / 60
@ -346,8 +345,8 @@ export class OH {
const split = rule.trim().replace(/, */g, ",").split(" ") const split = rule.trim().replace(/, */g, ",").split(" ")
if (split.length == 1) { if (split.length == 1) {
// First, try to parse this rule as a rule without weekdays // First, try to parse this rule as a rule without weekdays
let timeranges = OH.ParseHhmmRanges(rule) const timeranges = OH.ParseHhmmRanges(rule)
let weekdays = [0, 1, 2, 3, 4, 5, 6] const weekdays = [0, 1, 2, 3, 4, 5, 6]
return OH.multiply(weekdays, timeranges) return OH.multiply(weekdays, timeranges)
} }
@ -450,7 +449,7 @@ export class OH {
return ohs return ohs
} }
/* /**
This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs. This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs.
E.g. E.g.
Monday, some business is opened from 9:00 till 17:00 Monday, some business is opened from 9:00 till 17:00
@ -458,6 +457,11 @@ Tuesday from 9:30 till 18:00
Wednesday from 9:30 till 12:30 Wednesday from 9:30 till 12:30
This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00 This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00
This list will be sorted This list will be sorted
const startDate = new Date(2025,4,20,10,0,0)
const endDate = new Date(2025,4,20,17,0,0)
const changes = OH.allChangeMoments([[{isOpen: true, isSpecial: false, comment: "", startDate, endDate}]])
changes // => [[36000,61200], ["10:00", "17:00"]]
*/ */
public static allChangeMoments( public static allChangeMoments(
ranges: { ranges: {
@ -483,8 +487,7 @@ This list will be sorted
startOfDay.setHours(0, 0, 0, 0) startOfDay.setHours(0, 0, 0, 0)
// The number of seconds since the start of the day // The number of seconds since the start of the day
// @ts-ignore const changeMoment: number = (range.startDate.getTime() - startOfDay.getTime()) / 1000
const changeMoment: number = (range.startDate - startOfDay) / 1000
if (changeHours.indexOf(changeMoment) < 0) { if (changeHours.indexOf(changeMoment) < 0) {
changeHours.push(changeMoment) changeHours.push(changeMoment)
changeHourText.push( changeHourText.push(
@ -493,8 +496,7 @@ This list will be sorted
} }
// The number of seconds till between the start of the day and closing // The number of seconds till between the start of the day and closing
// @ts-ignore const changeMomentEnd: number = (range.endDate.getTime() - startOfDay.getTime()) / 1000
let changeMomentEnd: number = (range.endDate - startOfDay) / 1000
if (changeMomentEnd >= 24 * 60 * 60) { if (changeMomentEnd >= 24 * 60 * 60) {
if (extrachangeHours.indexOf(changeMomentEnd) < 0) { if (extrachangeHours.indexOf(changeMomentEnd) < 0) {
extrachangeHours.push(changeMomentEnd) extrachangeHours.push(changeMomentEnd)
@ -665,13 +667,18 @@ This list will be sorted
*/ */
/** /**
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week * Constructs the opening-ranges for either this week, or for next week if there are no more openings this week.
* Note: 'today' is mostly used for testing
*
* const oh = new opening_hours("mar 15 - oct 15")
* const ranges = OH.createRangesForApplicableWeek(oh, new Date(2025,4,20,10,0,0))
* ranges // => {ranges: [[],[],[],[],[],[],[]], startingMonday: new Date(2025,4,18,24,0,0)}
*/ */
public static createRangesForApplicableWeek(oh: opening_hours): { public static createRangesForApplicableWeek(oh: opening_hours, today?: Date): {
ranges: OpeningRange[][] ranges: OpeningRange[][]
startingMonday: Date startingMonday: Date
} { } {
const today = new Date() today ??= new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
const lastMonday = OH.getMondayBefore(today) const lastMonday = OH.getMondayBefore(today)
const nextSunday = new Date(lastMonday) const nextSunday = new Date(lastMonday)
@ -699,7 +706,7 @@ This list will be sorted
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) { public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
const monday = openingRanges[startday] const monday = openingRanges[startday]
for (let i = startday + 1; i <= endday; i++) { for (let i = startday + 1; i <= endday; i++) {
let weekday = openingRanges[i] const weekday = openingRanges[i]
if (weekday.length !== monday.length) { if (weekday.length !== monday.length) {
return false return false
} }
@ -831,12 +838,12 @@ This list will be sorted
} }
return [parsed] return [parsed]
} else if (split.length == 2) { } else if (split.length == 2) {
let start = OH.ParseWeekday(split[0]) const start = OH.ParseWeekday(split[0])
let end = OH.ParseWeekday(split[1]) const end = OH.ParseWeekday(split[1])
if ((start ?? null) === null || (end ?? null) === null) { if ((start ?? null) === null || (end ?? null) === null) {
return null return null
} }
let range = [] const range = []
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
range.push(i) range.push(i)
} }
@ -847,8 +854,8 @@ This list will be sorted
} }
private static ParseWeekdayRanges(weekdays: string): number[] { private static ParseWeekdayRanges(weekdays: string): number[] {
let ranges = [] const ranges = []
let split = weekdays.split(",") const split = weekdays.split(",")
for (const weekday of split) { for (const weekday of split) {
const parsed = OH.ParseWeekdayRange(weekday) const parsed = OH.ParseWeekdayRange(weekday)
if (parsed === undefined || parsed === null) { if (parsed === undefined || parsed === null) {
@ -1054,7 +1061,7 @@ export class ToTextualDescription {
languages[supportedLanguage] = "{a}. {b}" languages[supportedLanguage] = "{a}. {b}"
} }
} }
let chainer = new TypedTranslation<{ a; b }>(languages) const chainer = new TypedTranslation<{ a; b }>(languages)
let tr = trs[0] let tr = trs[0]
for (let i = 1; i < trs.length; i++) { for (let i = 1; i < trs.length; i++) {
tr = chainer.PartialSubsTr("a", tr).PartialSubsTr("b", trs[i]) tr = chainer.PartialSubsTr("a", tr).PartialSubsTr("b", trs[i])

View file

@ -7,7 +7,7 @@ import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Table from "../Base/Table" import Table from "../Base/Table"
import { Translation } from "../i18n/Translation" import { Translation, TypedTranslation } from "../i18n/Translation"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
import opening_hours from "opening_hours" import opening_hours from "opening_hours"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
@ -73,7 +73,7 @@ export default class OpeningHoursVisualization extends Toggle {
ranges: OpeningRange[][], ranges: OpeningRange[][],
lastMonday: Date lastMonday: Date
): BaseUIElement { ): BaseUIElement {
// First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special // First, a small sanity check. The business might be permanently closed, 24/7 opened or be another special case
if (ranges.some((range) => range.length > 0)) { if (ranges.some((range) => range.length > 0)) {
// The normal case: we have items for the coming days // The normal case: we have items for the coming days
return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday) return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday)
@ -98,8 +98,7 @@ export default class OpeningHoursVisualization extends Toggle {
const today = new Date() const today = new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
// @ts-ignore const todayIndex = Math.ceil((today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24))
const todayIndex = Math.ceil((today - rangeStart) / (1000 * 60 * 60 * 24))
// By default, we always show the range between 8 - 19h, in order to give a stable impression // By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed // Ofc, a bigger range is used if needed
const earliestOpen = Math.min(8 * 60 * 60, ...changeHours) const earliestOpen = Math.min(8 * 60 * 60, ...changeHours)
@ -193,11 +192,9 @@ export default class OpeningHoursVisualization extends Toggle {
const startOfDay: Date = new Date(range.startDate) const startOfDay: Date = new Date(range.startDate)
startOfDay.setHours(0, 0, 0, 0) startOfDay.setHours(0, 0, 0, 0)
// @ts-ignore const startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen
// prettier-ignore // prettier-ignore
// @ts-ignore const width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / (latestclose - earliestOpen)
const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen);
const startPercentage = (100 * startpoint) / availableArea const startPercentage = (100 * startpoint) / availableArea
return new FixedUiElement(textToShow) return new FixedUiElement(textToShow)
.SetStyle(`left:${startPercentage}%; width:${width}%`) .SetStyle(`left:${startPercentage}%; width:${width}%`)
@ -236,7 +233,7 @@ export default class OpeningHoursVisualization extends Toggle {
changeHourText: string[], changeHourText: string[],
earliestOpen: number earliestOpen: number
): [BaseUIElement, string] { ): [BaseUIElement, string] {
let header: BaseUIElement[] = [] const header: BaseUIElement[] = []
header.push( header.push(
...OpeningHoursVisualization.CreateLinesAtChangeHours( ...OpeningHoursVisualization.CreateLinesAtChangeHours(
@ -249,7 +246,7 @@ export default class OpeningHoursVisualization extends Toggle {
let showHigher = false let showHigher = false
let showHigherUsed = false let showHigherUsed = false
for (let i = 0; i < changeHours.length; i++) { for (let i = 0; i < changeHours.length; i++) {
let changeMoment = changeHours[i] const changeMoment = changeHours[i]
const offset = (100 * (changeMoment - earliestOpen)) / availableArea const offset = (100 * (changeMoment - earliestOpen)) / availableArea
if (offset < 0 || offset > 100) { if (offset < 0 || offset > 100) {
continue continue
@ -285,21 +282,23 @@ export default class OpeningHoursVisualization extends Toggle {
/* /*
* Visualizes any special case: e.g. not open for a long time, 24/7 open, ... * Visualizes any special case: e.g. not open for a long time, 24/7 open, ...
* */ * */
private static ShowSpecialCase(oh: any) { private static ShowSpecialCase(oh: opening_hours) {
const opensAtDate = oh.getNextChange() const nextChange = oh.getNextChange()
if (opensAtDate === undefined) { if (nextChange !== undefined) {
const comm = oh.getComment() ?? oh.getUnknown() const nowOpen = oh.getState(new Date())
if (!!comm) { const t = Translations.t.general.opening_hours
return new FixedUiElement(comm) const tr: TypedTranslation<{ date }> = nowOpen ? t.open_until : t.closed_until
} const date = nextChange.toLocaleString()
return tr.Subs({ date })
if (oh.getState()) {
return Translations.t.general.opening_hours.open_24_7.Clone()
}
return Translations.t.general.opening_hours.closed_permanently.Clone()
} }
return Translations.t.general.opening_hours.closed_until.Subs({ const comment = oh.getComment() ?? oh.getUnknown()
date: opensAtDate.toLocaleString(), if (typeof comment === "string") {
}) return new FixedUiElement(comment)
}
if (oh.getState()) {
return Translations.t.general.opening_hours.open_24_7.Clone()
}
return Translations.t.general.opening_hours.closed_permanently.Clone()
} }
} }

View file

@ -13,6 +13,7 @@
export let selectedElement: Feature export let selectedElement: Feature
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let labels: string[] export let labels: string[]
export let blacklist: string[]
export let header: string export let header: string
export let layer: LayerConfig export let layer: LayerConfig
@ -22,11 +23,15 @@
} }
let tagRenderings: TagRenderingConfig[] = [] let tagRenderings: TagRenderingConfig[] = []
let seenIds = new Set<string>() let seenIds = new Set<string>()
let blacklistSet = new Set(blacklist)
for (const label of labels) { for (const label of labels) {
for (const tr of layer.tagRenderings) { for (const tr of layer.tagRenderings) {
if (seenIds.has(tr.id)) { if (seenIds.has(tr.id)) {
continue continue
} }
if (blacklistSet.has(tr.id) || tr.labels.some(l => blacklistSet.has(l))) {
continue
}
if (label === tr.id || tr.labels.some((l) => l === label)) { if (label === tr.id || tr.labels.some((l) => l === label)) {
tagRenderings.push(tr) tagRenderings.push(tr)
seenIds.add(tr.id) seenIds.add(tr.id)

View file

@ -226,7 +226,9 @@
</button> </button>
{/if} {/if}
{#if $debug} {#if $debug}
Skipped questions are {Array.from($skippedQuestions).join(", ")} <span class="subtle">
DBG:Skipped questions are {Array.from($skippedQuestions).join(", ")}
</span>
{/if} {/if}
</div> </div>
</LoginToggle> </LoginToggle>

View file

@ -14,7 +14,7 @@ import NearbyImagesCollapsed from "../Image/NearbyImagesCollapsed.svelte"
class NearbyImageVis implements SpecialVisualizationSvelte { class NearbyImageVis implements SpecialVisualizationSvelte {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ args = [
{ {
name: "mode", name: "mode",
defaultValue: "closed", defaultValue: "closed",
@ -26,7 +26,7 @@ class NearbyImageVis implements SpecialVisualizationSvelte {
doc: "If 'readonly' or 'yes', will not show the 'link'-button", doc: "If 'readonly' or 'yes', will not show the 'link'-button",
}, },
] ]
group: "images" group = "images"
docs = docs =
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images" funcName = "nearby_images"
@ -65,6 +65,7 @@ export class ImageVisualisations {
args: [ args: [
{ {
name: "image_key", name: "image_key",
type: "key",
defaultValue: AllImageProviders.defaultKeys.join(";"), defaultValue: AllImageProviders.defaultKeys.join(";"),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ", doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
}, },
@ -95,6 +96,7 @@ export class ImageVisualisations {
needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls], needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls],
args: [ args: [
{ {
type: "key",
name: "image_key", name: "image_key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "panoramax", defaultValue: "panoramax",

View file

@ -186,6 +186,11 @@ export default class TagrenderingManipulationSpecialVisualisations {
name: "labels", name: "labels",
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion", doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion",
}, },
{
name: "blacklist",
required: false,
doc: "A `;`-separated list of either identifiers or label names. Matching tagrenderings will _not_ be included, even if they are in `labels`"
}
], ],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -194,8 +199,9 @@ export default class TagrenderingManipulationSpecialVisualisations {
selectedElement: Feature, selectedElement: Feature,
layer: LayerConfig layer: LayerConfig
): SvelteUIElement { ): SvelteUIElement {
const [header, labelsStr] = argument const [header, labelsStr, blacklistStr] = argument
const labels = labelsStr.split(";").map((x) => x.trim()) const labels = labelsStr.split(";").map((x) => x.trim())
const blacklist = blacklistStr?.split(";")?.map(x => x.trim()) ?? []
return new SvelteUIElement(GroupedView, { return new SvelteUIElement(GroupedView, {
state, state,
tags, tags,
@ -203,6 +209,7 @@ export default class TagrenderingManipulationSpecialVisualisations {
layer, layer,
header, header,
labels, labels,
blacklist
}) })
}, },
}, },

View file

@ -1,11 +1,7 @@
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import { import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties" import { ExportableMap, MapProperties } from "../Models/MapProperties"
@ -100,7 +96,8 @@ export interface SpecialVisualization {
name: string name: string
defaultValue?: string defaultValue?: string
doc: string doc: string
required?: false | boolean required?: false | boolean,
type?: "key" | string
}[] }[]
readonly getLayerDependencies?: (argument: string[]) => string[] readonly getLayerDependencies?: (argument: string[]) => string[]
@ -115,7 +112,7 @@ export interface SpecialVisualization {
): BaseUIElement ): BaseUIElement
} }
export interface SpecialVisualizationSvelte { export interface SpecialVisualizationSvelte extends SpecialVisualization {
readonly funcName: string readonly funcName: string
readonly docs: string readonly docs: string
/** /**
@ -133,7 +130,8 @@ export interface SpecialVisualizationSvelte {
name: string name: string
defaultValue?: string defaultValue?: string
doc: string doc: string
required?: false | boolean required?: false | boolean,
type?: "key" | string
}[] }[]
readonly getLayerDependencies?: (argument: string[]) => string[] readonly getLayerDependencies?: (argument: string[]) => string[]

View file

@ -257,6 +257,7 @@ export default class SpecialVisualizations {
{ {
name: "key", name: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
type: "key",
doc: "The tagkey from which the table is constructed.", doc: "The tagkey from which the table is constructed.",
}, },
{ {
@ -284,6 +285,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
doc: "The tagkey from which the opening hours are read.", doc: "The tagkey from which the opening hours are read.",
}, },
@ -324,6 +326,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The key of the tag to give the canonical text for", doc: "The key of the tag to give the canonical text for",
required: true, required: true,
}, },
@ -412,6 +415,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The attribute to interpret as json", doc: "The attribute to interpret as json",
defaultValue: "value", defaultValue: "value",
}, },
@ -463,6 +467,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
defaultValue: "value", defaultValue: "value",
doc: "The key to look for the tags", doc: "The key to look for the tags",
}, },
@ -470,9 +475,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[]
feature: Feature,
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "value" const key = argument[0] ?? "value"
return new VariableUiElement( return new VariableUiElement(
@ -508,8 +511,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature }) return new SvelteUIElement(DirectionIndicator, { state, feature })
}, },
@ -520,6 +522,7 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
type: "key",
doc: "The attribute containing the degrees", doc: "The attribute containing the degrees",
defaultValue: "_direction:centerpoint", defaultValue: "_direction:centerpoint",
}, },

View file

@ -466,8 +466,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (v !== undefined && v !== null) { if (v !== undefined && v !== null) {
if (v["toISOString"] != undefined) { if (v["toISOString"] != undefined) {
// This is a date, probably the timestamp of the object // This is a date, probably the timestamp of the object
// @ts-ignore const date: Date = v
const date: Date = el
v = date.toISOString() v = date.toISOString()
} }

View file

@ -165,6 +165,11 @@ input[type="text"] {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.border-low-interaction {
border-color: var(--interaction-border);
border-style: dashed;
}
.border-region { .border-region {
border: 2px dashed var(--interactive-background); border: 2px dashed var(--interactive-background);
border-radius: 0.5rem; border-radius: 0.5rem;
@ -356,6 +361,9 @@ textarea {
h2.group { h2.group {
/* For flowbite accordions */ /* For flowbite accordions */
margin: 0; margin: 0;
top: 0;
position: sticky;
z-index: 12;
} }
.group button { .group button {